Skip to content

aggregator(charges-producer): a trailing or flag-like --start/--end/--out value is silently replaced by the defaults — the producer overwrites the live candidates file for the wrong period and exits 0 #3333

Open

Description

@dezren39

aggregator(charges-producer): a trailing or flag-like --start/--end/--out value is silently replaced by the defaults — the producer overwrites the live candidates file for the wrong period and exits 0

Summary

pm-stack/aggregator/scripts/produce-charge-candidates.ts reads its three
value-taking flags through argValue() (:52-55), which returns
process.argv[i + 1] — or undefined when the flag is last. In main()
(:252-257) every result is combined with ?? defaults:

const start = argValue("--start") ?? isoDay(<first of current month>);
const end   = argValue("--end")   ?? isoDay(now);
const outPath = argValue("--out") ?? process.env.CHARGE_CANDIDATES_PATH ??
  join(homedir(), "workspace/goals/keep-the-four-repos-healthy/hidden_files/charges-candidates.json");

So a flag that is present but missing its value is indistinguishable from a
flag that was never passed: no error, no warning, exit 0. There is also no
validation that --start/--end are real YYYY-MM-DD dates, or that
start <= end. The candidates file is the live input the fleet-feed collector
consumes (collect-fleet-feed.ts: charges-candidates.json → matcher →
charges/chargesAttention), and this producer is run by hand (#2205: it "only
refreshes when an agent runs it by hand"), so a typo lands silently in the
one file that matters.

Evidence (verified this run, main d64e83c1)

Laptop probe ~/git/fleet/probe-2026-10-31-laneA-chargeargs/ — the real
script (sha256 999f2dd904924bc718aa278f209441656e85da05db75aa33c6a95e6d8b0eefe3,
identical on the sandbox main checkout and the laptop checkout), Bun 1.4.2,
with PLAID_CLI pointed at a stub that logs its arguments and returns one
valid transaction, and CHARGE_CANDIDATES_PATH pointed at a probe-local
default file:

Case Result
control: --skip-gmail --start 2026-09-01 --end 2026-09-30 --out <probe>/control.json exit 0; control.json written with period 2026-09-01..2026-09-30; stub logged --start-date 2026-09-01 --end-date 2026-09-30; default file untouched
trailing --out: ... --start 2026-09-01 --end 2026-09-30 --out exit 0; silently wrote the default candidates path (wrote .../default-out.json) — in a real run that is the live hidden_files/charges-candidates.json, overwritten when the operator asked for a custom file and forgot its value
trailing --start: --skip-gmail --end 2026-09-30 --out <probe>/trailstart.json --start exit 0; period became 2026-11-01..2026-09-30 (start after end); the stub was queried with --start-date 2026-11-01 --end-date 2026-09-30, and the inverted period was persisted into the written file
flag-like --out value: --start 2026-09-01 --end 2026-09-30 --out --skip-gmail exit 0; wrote a file literally named --skip-gmail in the working directory (producer: wrote --skip-gmail), and the same token also switched Gmail off
unvalidated date: --skip-gmail --start banana --end 2026-09-30 --out <probe>/banana.json exit 0; stub queried with --start-date banana; the written file's period.start is the string "banana"

Why it matters

The failure is silent in exactly the dangerous direction: the operator
believes they produced a custom period into a scratch file, while the
script actually (a) regenerates the current month into the live
candidates file, or (b) persists a candidates file whose declared period is
inverted or not a date at all, which the collector then matches and surfaces
as if it were the requested period. Nothing in the output line looks wrong —
it prints the substituted values as if they were requested.

Distinct from: #3295 (append-usage-snapshots --by voiding observedBy),
#3330 (backfill --dir crash / flag-named directory), #3285 (compact
--months 0) — same unchecked-argv family, different script, different
failure: here the ?? fallback masks the missing value instead of
crashing or misdirecting, and the target of the silent write is the live
charges pipeline input. Also distinct from #3224/#3120/#3121 (amount/subject
parsing inside this same producer) and #2448 (a garbled Gmail date throwing
inside gmailCandidates).

Acceptance criteria

  • --start, --end, or --out with no following value, or with a
    following token that starts with --, prints a usage/error message
    naming the flag and its missing value to stderr and exits 2 — no Plaid or
    Gmail call is made and no file is written (in particular the default
    candidates file is untouched).
  • --start / --end values that are not real calendar dates in
    YYYY-MM-DD form, or a start after end, exit 2 with an error naming
    the offending value — nothing is queried or written.
  • Valid explicit flags behave exactly as today (same period, same output
    file, same summary line); the no-flag defaults (current month, env /
    default candidates path) are unchanged.
  • A regression test (or script-level harness case, e.g. with a stub
    PLAID_CLI) covers the trailing-flag, flag-like-value, and
    invalid/inverted-date cases.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Adopted as Phase 2 of design #3352 (corollary 3: present-but-valueless is not absent). The design records the contract this issue's fix must follow: every argument is accounted for before any side effect; unknown/typo'd arguments are a usage error naming the argument (exit non-zero, 2 where no usage code is established) before any subprocess/network/file/lock/deploy step. Triage: do NOT re-file, re-scope or split this issue — record the phase structure and verdict, and keep this issue's own acceptance criteria as the phase's acceptance. Design doc: files/designs/code-operator-argument-contract.md (keep-the-four-repos-healthy goal workspace).

  4. added
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  5. 20 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off — PR opened

  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done — PR opened, not merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions