Skip to content

fix(pm): check-half-states answers --help and refuses an unknown option before any board read - #18430

Merged
os-elon-musk merged 3 commits into
mainfrom
claude/issue-18369-check-half-states-help-argv
Sep 16, 2026
Merged

os-elon-musk merged 3 commits into
mainfrom
claude/issue-18369-check-half-states-help-argv

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18369

node scripts/pm/check-half-states.mjs --help printed nothing and started the full
board sweep. So did --totally-bogus-flag-xyz. argv was read here but never
validated: the only membership tests were includes('--self-test') /
includes('--probe'), and parseOutputOptions matched two prefixes and dropped
every other token silently.

The unanswered --help is the cheap half. The expensive half is the mistyped
real flag — --format markdown with a space, or a --repo=… this tool has
never had — which was ignored, so the sweep ran with the default format
against the default board and produced a wrong-target reading that is
indistinguishable from the one the caller asked for.

What changed

One roster (CLI_FLAGS / CLI_VALUE_OPTIONS) feeds both the refusal and the
printed USAGE, so the two cannot drift. Inside the existing
if (isMain) entry guard, before the SWEEP_REPO / CLOSED_FLOOR guards,
before the proxy re-exec and before any request:

  • --help / -h writes USAGE to stdout and exits 0.
  • Any token outside the roster is refused by name and exits 2.

The refusal wording mirrors git-history.mjs (check-half-states: unknown option --foo).
The exit code does not: this file's exit vocabulary is pinned by its own
header at 0 / 2 / 3, and all three of its pre-existing bad-usage exits are 2, so a
fourth code would make "bad usage" two numbers inside one tool. That is a
deliberate deviation from the card's "matching git-history.mjs's existing
spelling", which the dispatch order anticipated.

--help writes through writeSync(1, …) rather than console.log because the
next statement is process.exit, and stdout to a pipe is asynchronous on some
platforms — the self-test reads it through one.

The roster, measured from the file rather than the docs

Enumerated by grepping process.argv, parseOutputOptions, rearmThroughProxy
and every double-dash literal in the file, then cross-checked against every
spelled invocation in .github/, scripts/, package.json and .claude/.

token who honours it status
--self-test entry includes() honoured, unchanged
--probe entry includes() honoured, unchanged
--format=FMT parseOutputOptions honoured, unchanged (the VALUE is still that function's judgement, not the roster's)
--provenance=TEXT parseOutputOptions honoured, unchanged
--help, -h new new

Nothing was dropped. Three tokens the dispatch order listed as needing to keep
working do not exist in this file and never did — --repo=, --closed-floor
and --use-env-proxy as a script argument:

  • the swept board and the closed floor are resolved from the environment
    (resolveSweepRepo reads PM_SWEEP_REPO then GITHUB_REPOSITORY;
    resolveClosureFloor reads PM_SWEEP_CLOSED_FLOOR). A --repo= on the command
    line was silently dropped and the default board swept — the card's own
    wrong-target scenario, now refused by name and pinned.
  • --use-env-proxy is node's flag, read at process start, so it belongs in
    NODE_OPTIONS or before the script path. Every invocation in the repo spells it
    that way. After the script path it did nothing; it is now refused, and USAGE
    carries the spelling that works. The rearmThroughProxy re-exec is untouched
    and still passes the flag as an execArgv.

There are no positional arguments and never were, so a bare owner/name on the
command line is refused with a sentence naming where the board actually comes from.

Measurements (pinned to 6bd81fa, the final commit)

Wall time is not the acceptance criterion — "zero HTTP requests" is — so both legs
were run with globalThis.fetch replaced by a trap that exits 97 on any
attempt, under env -u HTTPS_PROXY -u https_proxy -u NODE_OPTIONS -u GITHUB_TOKEN -u GH_TOKEN
(the proxy is cleared so no re-exec can escape the trap).

invocation before (f04be62) after (6bd81fa)
--help exit 97, 1 fetch attempt, 0 B stdout exit 0, 0 fetch attempts, 1290 B of usage on stdout, 84 ms
--totally-bogus-flag-xyz exit 97, 1 fetch attempt, byte-identical to --help exit 2, 0 fetch attempts, refused by name on stderr, 79 ms
no arguments (control) exit 97, 1 fetch attempt exit 97, 1 fetch attempt — unchanged, which is what proves the trap is armed and the two rows above mean "no request", not "no trap"

Without the trap, in this container's real environment:

$ node scripts/pm/check-half-states.mjs --help            ; echo $?
0     (94 ms, 1290 bytes of usage)
$ node scripts/pm/check-half-states.mjs --totally-bogus-flag-xyz ; echo $?
check-half-states: unknown option --totally-bogus-flag-xyz
2     (91 ms)

Leg 3 — a valid invocation is untouched, and the evidence is structural
rather than behavioural: outside the new block the entire diff is one line, the
node:fs import gaining writeSync. if (isMain) { is unchanged and the whole
body under it is byte-identical (git diff -U0 against the merge base shows
exactly one deleted line). Behaviourally, --probe still exits 0 after its
proxy re-exec, and a live --format=markdown --provenance=… run still completed
and rendered the markdown body, exit 0, report-only.

Self-test: 4,270 cases before, 4,303 after (+33), all green, 1.5 s.
Twenty pure cases on refuseUnknownArgs and roster/usage parity, plus thirteen
spawned entry runs under the fetch trap — including the default-sweep control.

Ablation, from the committed state: forcing the --help branch false turns five
cases red and no others (--help exits 0 (got 97, want 0), --help issues ZERO requests (got true, want false), the stdout pin, the -h pin, and
--help answers even when the sweep target is malformed (got 2, want 0) — the
mutant falls into the SWEEP_REPO guard, which is the ordering that case exists
to pin). Restored byte-identically (git hash-object matched HEAD), and the
self-test re-run green. No dist/ is in the resolution path — the self-test
spawns SELF_PATH directly — so there is no build step to invalidate the reading.

⚠️ This file is now 1,653 bytes from a cliff, and the cliff misdirects

check-closing-keyword-parity's sweep skips any tracked file over 2 MiB
(statSync(...).size > 2 * 1024 * 1024). This file was 2,082,119 bytes
15,033 under. The first draft of this change added 15,213 and the gate went red
with:

scripts/pm/check-half-states.mjs is registered as carrying the closing-keyword
grammar but no longer matches the sweep. Stale registry entries verify nothing --
remove it, or fix the signature.

The registry entry is correct and the signature is untouched; the file had simply
become invisible to the sweep. Measured, not inferred: appending 260 lines of
pure comment to the otherwise unmodified file reproduces that exact failure, and
restoring it byte-for-byte clears it. The prescribed remedy would delete a live
registry row and silently retire a real parity check.

This change was refitted to 13,380 bytes, leaving 1,653 bytes of headroom.
Anyone else editing this file should know the wall is there. Filed as a finding.

Changeset

skip-changeset: scripts/pm/** is repo tooling under the root workspace, which
is private: true and ships no files[]. Nothing published moves.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
derived 40 commands from the real change set. All 40 were run on 6bd81fa and
reconciled:

Run reconciliation — 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run,
  0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).

Every one exited 0, including the two that judged the first draft
(check-closing-keyword-parity and check:entry-guard). The derived list was
identical at dispatch time, after the refit, and after merging main.

origin/main was merged before this PR opened (25 files, none of them this one,
no deferred regeneration recorded). No package source moved, so no package
test/typecheck is owed; scripts/** carries no typecheck script.

Lint is a full-population reading, not a narrowed one:
pnpm exec eslint . --no-inline-config --format json6,792 files linted from
eslint's own config, 0 errors, 0 warnings, exit 0, 85 s.

Acceptance notes

  • This file's --self-test is COUNT-shaped — one printed total, no
    SELF_TEST_BATTERIES roster and no per-battery floor, which AGENTS.md's
    "Writing a --self-test" section requires of a new one. It is already recorded
    as COUNT | HELD in docs/audits/2026-09-self-test-shape-census.md. The new
    cases were added in the shape the file already has; converting a 4,300-case
    battery to the roster shape is not this card, and would not fit the byte budget
    above. Noted, not filed — the census page is who picks this up.
  • lint.yml's comment on this step still says "no network, ~0.05 s"; the
    self-test measured 1.13 s before this change and 1.53 s after. Prose drift in a
    file outside this card's surface. Noted, not filed — whoever next edits that
    step picks it up.
  • The family disagrees on the bad-usage exit code: post-stamped.mjs uses 1,
    label-write.mjs uses 2, git-history.mjs exits 1 from usage(). This change
    follows the file it is in. Observation, not a defect. Noted, not filed — nobody picks this up; there is nothing to pick up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt


Generated by Claude Code

…on before any board read

argv was never validated. The only membership tests were
`includes('--self-test')` / `includes('--probe')`, and `parseOutputOptions`
matched two prefixes and dropped every other token silently — so `--help` and a
flag that certainly does not exist behaved identically: both fell through into
the full sweep, a multi-page, rate-limit-spending read of a live board, with no
usage string anywhere in the file to print instead.

The expensive half is the mistyped REAL flag: `--format markdown` with a space,
or a `--repo=…` this tool has never had, was ignored and the sweep then ran with
the default format against the default board — a wrong-target reading
indistinguishable from the one the caller asked for.

One roster (`CLI_FLAGS` / `CLI_VALUE_OPTIONS`) feeds both the refusal and the
printed `USAGE`, so the two cannot drift. `--help`/`-h` is answered before the
`SWEEP_REPO` / `CLOSED_FLOOR` guards, before the proxy re-exec and before any
request; an unrecognised token is refused by name in `git-history.mjs`'s
spelling, at this file's own bad-usage exit code 2. The valid-invocation body is
byte-identical: the whole diff outside the new block is `if (isMain) {` becoming
`} else if (isMain) {`.

Pinned by 35 new self-test cases — the pure refusals and roster/usage parity,
plus four spawned entry runs under a `globalThis.fetch` trap that exits 97, with
the default sweep as the control that proves the trap is armed.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
…entry guard

Two gates judged the first shape and both were right.

`check:entry-guard` refused `if (CLI_HELP) {} else if (…) {} else if (isMain) {}`:
a scripts/** file that exports bindings must put its top-level dispatch behind a
literal `isEntrypoint(import.meta.url)` guard, and mine was reachable only
through a variable. Restored to the untouched `if (isMain) {`, with the two new
branches as early exits inside it — so the valid-invocation body is byte-
identical and the whole diff outside the new block is one import line.

`check-closing-keyword-parity` went red naming the REGISTRY ("remove it, or fix
the signature"). The registry is correct; the cause is size. Its sweep skips any
tracked file over 2 MiB (`statSync(...).size > 2 * 1024 * 1024`), this file was
2,082,119 bytes — 15,033 under — and the first draft added 15,213. Measured, not
inferred: appending 260 lines of pure comment to the unmodified file reproduces
the identical failure, and restoring it byte-for-byte clears it. The block is
now 13,380 bytes, leaving 1,653 bytes of headroom, and the cliff is reported as
a finding: the failure text prescribes deleting a live registry row.

`--help` writes through `writeSync(1, …)` rather than `console.log`, because the
next statement is `process.exit` and stdout to a pipe is asynchronous on some
platforms — the self-test reads it through one.

Self-test 4,270 -> 4,303 cases, all green, 1.5s.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude
@os-elon-musk
os-elon-musk marked this pull request as ready for review September 16, 2026 10:50
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit b6fe311 Sep 16, 2026
39 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18369-check-half-states-help-argv branch September 16, 2026 11:12
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…objectstack-ai#18449)

Fixes objectstack-ai#18435

`scripts/check-closing-keyword-parity.mjs` sweeps the whole tracked tree
hunting for a
fourth, unregistered spelling of GitHub's closing-keyword grammar. Its
sweep skipped any
file over 2 MiB **before reading it** — and the registry check
downstream had no way to
tell "this file stopped carrying the grammar" from "the sweep never
opened this file", so
it reported the second as the first.

## Before / after, on the real blocker

`scripts/pm/check-half-states.mjs` is a `PARSERS`-registered file and
the repo's largest
script. PR objectstack-ai#18430 landed while this branch was in flight, which puts it
at **2,095,499 B —
1,653 bytes under the 2,097,152 B cutoff**. Appending 40 lines of plain
comment (2,000 B,
the size of a paragraph of prose, no grammar touched) takes it to
2,097,499 B. Both readings
below are on this branch's base `b6fe311a`, same padded file, only the
gate swapped.

**Before** — the gate exactly as it ships on `origin/main` (exit 1):

```
check-closing-keyword-parity: 1 failure(s)

  • [sweep] scripts/pm/check-half-states.mjs is registered as carrying the closing-keyword
    grammar but no longer matches the sweep. Stale registry entries verify nothing --
    remove it, or fix the signature.
```

The prescribed remedy — *remove it* — deletes a correct registry row and
silently retires a
live parity check on H7. The actual cause, the file's size, is named
nowhere in the text.

**After** — this PR's gate, same 2,097,499 B file (exit 0):

```
check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured
separators; sweep found 5 file(s) carrying the grammar across 8733 tracked file(s), all
registered).

1 UNREGISTERED file(s) were not read, so the hunt for a fourth parser did not cover them
(every registered file WAS read -- the cutoff does not apply to those):
  • packages/spec/CHANGELOG.md -- 6077484 bytes exceeds the sweep's 2097152-byte cutoff
    for UNREGISTERED files
```

The padding was a transient measurement: restored byte-for-byte
afterwards, proven by
`git hash-object` back to the HEAD blob and an empty `git diff HEAD`.
`scripts/pm/check-half-states.mjs` carries **no change in this diff** —
three other PRs own
its regions.

## The rule

Registration is the statement *this file carries the grammar*. A sweep
that skips a
registered file does not refute that statement; it simply fails to look.
So:

- **A registered file is read unconditionally.** `REGISTERED` is hoisted
out of `judge()`
to above `sweep()`, and the cutoff now reads `size > SIZE_CUTOFF &&
!REGISTERED.has(f)`.
A few megabytes of `readFileSync` is by a wide margin the cheaper half
of that trade.
- **An unregistered file over the cutoff is still skipped** — the
sweep's job on those is a
hunt, and a multi-megabyte blob is overwhelmingly a fixture or a
generated corpus — **but
the skip is now RECORDED.** `sweep()` returns `skipped` (file to cause,
in words) beside
  `hits`.
- **The registry check reads that cause.** A registered file missing
from `hits` *and*
present in `skipped` now fails with the cause named and the opposite
prescription: the
entry is NOT stale, do not remove it. A registered file missing from
`hits` and NOT
  skipped still gets the original stale-registry text, unchanged.
- **The green line says what it did not cover.** A sweep reporting
"across 8733 tracked
file(s)" while having silently read fewer overstates itself. That hole
was already there
and nobody could see it: `packages/spec/CHANGELOG.md` at 6,077,484 B has
been outside this
sweep the whole time. It is a changelog, not a fourth parser — but the
gate now says so
  out loud instead of implying full coverage.

`skipped` is part of the sweep result rather than an optional extra, and
the one hand-built
sweep object in the self-test was updated to carry it. A default in
`judge()` would have
silently restored exactly the misdirecting message this PR removes.

## New self-test cases, each shown red under a control

`--self-test` goes from 30 assertions to 40.

**X14 / X15 — the cutoff, on a real tree.** `extraFiles` hands the sweep
text directly and
therefore never reaches the `statSync` branch at all, so a case built
that way would pin
nothing about the cutoff. These build a real git-tracked temp tree with
two real oversized
files (3,690,082 B each), one registered and one not, and grade both
arms: the registered
one is read, the unregistered one is skipped *and* the skip is recorded
with its cause.

Control — restore the unconditional skip (`size > SIZE_CUTOFF` alone),
self-test exit 1:

```
  • X14: an oversized REGISTERED parser is still SCANNED and found (3690082 bytes), got hits: nothing
  • X14: and is not recorded as skipped, got: 3690082 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files
```

**X13 — unread is not stale.** Control: delete the new diagnostic
branch, self-test exit 1,
and the red prints the exact defect text this PR exists to remove:

```
  • X13: a registered file the sweep never read is reported with its CAUSE, got:
    scripts/pm/check-half-states.mjs is registered as carrying the closing-keyword grammar
    but no longer matches the sweep. Stale registry entries verify nothing -- remove it,
    or fix the signature.
  • X13: and never with the stale-registry remedy, which would delete a correct registry row
```

**X7 keeps reading as stale.** The genuinely-stale case is unchanged and
now also asserts
the *negative*: it must NOT be reported as unread. An arm that swallowed
the other would
retire the check X7 exists to be.

Each control arm was applied to the committed file, proven on disk by
grep counts before the
run, restored with `git checkout HEAD -- PATH`, and the restore proven
by blob hash and an
empty `git diff HEAD`. The restored tree is green again (exit 0, 40
assertions).

## Gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from this
worktree derived 30 commands for this diff; every one was run in the
foreground. Results are
in the report comment on the card.

## Changeset

None — `skip-changeset`. AGENTS.md Post-Task Checklist step 3: that
label 「is for a diff
that publishes nothing from any released package」. This diff is one
repo-root script. The
root package is `private: true`, no package's `files[]` ships repo-root
`scripts/`, and no
package imports this file — `git grep` finds it referenced only from
`.github/workflows/`,
`docs/audits/`, and comments in three sibling scripts.

## Acceptance notes

- **Scope.** `scripts/check-closing-keyword-parity.mjs` only. The fix is
in the gate, not in
the file that trips it — `scripts/pm/check-half-states.mjs` carries no
change here.
- **The cutoff is kept, not raised.** The alternative of raising or
deleting it would have
made the whole-tree read unconditionally expensive and would not have
fixed the diagnostic,
  which was the part that prescribed the wrong remedy.
- **Noted, not filed:** `packages/spec/CHANGELOG.md` (6,077,484 B) has
been outside this
sweep since the cutoff was introduced. It is release-compiled prose, not
a parser, so
there is nothing to register — the new green-line disclosure is the
whole remedy. No other
gate was audited for a similar silent size skip; the card records that
census as not run.
- **Not measured:** whether any other whole-tree gate carries the same
silent skip.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01HPfcjvF23QBoBj7P47DDxs)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants