Repository navigation
fix(pm): a size refusal is a refusal — post-stamped stops reporting a real 422 as a missing route - #18934
Conversation
…and the measured cap post-stamped reported a real HTTP 422 「Body is too long」 as exit 3 PREREQUISITE NOT MET — 「no route, no act at all」 — and prescribed a transport remedy: run this somewhere node's fetch reaches api.github.com with a writing token. The write had already reached the platform, been read and been refused, so that remedy sends the caller to another route to be refused identically. The file's own header names the shape: a refusal text prescribing a refused remedy is a tool arguing with itself. The fifth code, EXIT_TOO_LARGE, is that class. It triggers on HTTP 422 plus a refusal text naming the body's LENGTH, case-insensitively — the two measured spellings differ by one letter — and never on the platform's own number, which is false in unit and value. Its text carries the bytes SENT, the measured cap for the surface they were sent to, the overage, and the remedy that can work: shorten the body or split it. The route remedy and the size remedy are each one constant, pinned held apart in both directions. rest() no longer discards what the platform said, so a refusal this class does not recognise still prints the sentence it was answered with instead of a bare status code. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
An ablation dropping the case-insensitive flag from the declaration left every case-insensitivity pin green: the classifier rebuilt the regex from `.source` and retyped the flags at the call site, so the constant documented a rule it did not carry. Tested in place instead, which the non-global pin already makes safe. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
… classifying Under the case-flag ablation the battery died on a TypeError before the verdict machinery ran: a crash is red, but it prints no named case and never reaches the floor. The assertions read through the null the classifier is allowed to return. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
…st-stamped-size-refusal
…s own bisection The comment surface was bisected on objectstack#18826 to the same 262,144 bytes the issue-body surface was bisected to on #18793 — two surfaces, two brackets, one number — and post-stamped needs the comment one to name the cap in a size refusal. It is declared here rather than in post-stamped because this is already where this fleet's measured body caps live: five workflow comments send a reader to `ISSUE_BODY_LIMIT` in this file for the number, and the 65,536 folklore this pair replaces grows back wherever one of two caps is kept somewhere else. ⛔ Two constants and not one shared number: two measurements that agree is not one measurement, and the day the platform moves one surface a shared number would lie about the other. The docblock-anchored provenance pins of the `ISSUE_BODY_LIMIT measured cap` battery are extended to the sibling, including the control that each provenance cites only the probe that measured it. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing record — blocked at the channel, not at a pre-check (skills seat, Every check on Generated by Claude Code |
…xy plan
`post-stamped`, `label-write` and `board-snapshot` each declare their own guard
variable and set it on the child they spawn, but they handed `process.env`
straight to `proxyRearmPlan`, which read the patrol's shared name. So an
inherited `OS_HALF_STATES_PROXY_REARMED=1` answered "already re-armed" for all
three — silently — and the run then bypassed the proxy and 401'd.
Each now passes `guard: PROXY_REARM_GUARD`, and the own-guard `if` below the
plan call goes: it sat after `if (!plan.rearm) return null;` and was never
reached for that case, so it was dead for the defect and redundant otherwise.
The child env is unchanged — each still sets its own name.
Measured A/B (`--dry-run`, no write), same box, same minute:
OS_HALF_STATES_PROXY_REARMED=1 label-write … before: 401, exit 3, no banner
after: banner, exit 0
OS_LABEL_WRITE_PROXY_REARMED=1 label-write … after: exit 3, and the FIRST
line names the variable, the proxy and the 401 it would be mistaken for
Each file gains a self-test battery pinning both directions, the loud line, the
unchanged runner leg, and two structural properties. Floors rise with the
rosters (label-write 8→9, board-snapshot 10→12); post-stamped's floor line sits
inside PR #18934's open range and is left alone — its roster rises 13→14 while
the pinned floor holds at 13.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
…when it suppresses (objectstack-ai#18980) Fixes objectstack-ai#18939 Clause-②: no `skip-changeset`: the whole diff is `scripts/pm/**` (PM loop tooling). Nothing in any package's `files[]` moves, so nothing publishes. ## What was wrong `proxyRearmPlan` (`scripts/pm/check-half-states.mjs`) read **one hard-coded** guard variable — `OS_HALF_STATES_PROXY_REARMED`, this file's own — while every consumer sets its **own** name on the child it re-execs. So any sibling instrument's guard, once inherited, answered "already re-armed" for a tool that had **never** re-armed. The suppressed run then took the bypassed route and answered **401 Bad credentials** on every endpoint, `/rate_limit` included — a self-consistent story about a dead credential, and the recorded cost of reading it as one is about an hour spent treating a live channel as dead. Two separate halves, and the second one is the expensive one: 1. the guard **name** was shared where it should have been per-caller; 2. the suppression was **silent** — `rearm: false, hint: false` prints nothing at any consumer, so the run log carried no line pointing at the cause. ## The fix ```js proxyRearmPlan({ env, execArgv, flagSupported, guard = PROXY_REARM_GUARD }) // -> { rearm, hint, flag?, guarded?, reason } ``` - `guard` is a parameter defaulting to this file's own name. A caller passes the variable it actually sets, so only its own guard can stop its own re-exec. - The guard branch returns `hint: true` and `guarded: guard`, and its `reason` names the variable, the proxy and the 401 it would otherwise be mistaken for. `hint` is the one branch **every** consumer already prints, so the silence is gone repo-wide — including for the nine consumers this PR does not edit. - That branch can only fire on the anomaly: a real re-exec's child carries `--use-env-proxy` in its `execArgv` and is answered by the `routed` branch above it, never by this one. - `post-stamped.mjs`, `label-write.mjs` and `board-snapshot.mjs` each pass `guard: PROXY_REARM_GUARD` and drop the own-guard `if` that sat **after** `if (!plan.rearm) return null;` — it was never reached for this defect and is now the plan's own branch. Their child env is unchanged: each still sets its own name. ⛔ No behaviour change where no proxy is configured. On a GitHub Actions runner the `no HTTPS_PROXY` branch returns first, so no child is spawned and no extra word is printed; re-pinned in both directions. ## A/B — measured, same box, same minute, `--dry-run`, no write Exit codes captured redirect-then-`$?`, never across a pipe. No token is printed by any of these. **Before** (`26c73fb4e`), the card's own repro, reproduced here: ``` $ node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 18932 --add finding --dry-run ℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:37101) and node's fetch does not read it. […] ① 取现集 — objectstack-ai#18932 (open) carries 3 label(s) … CONTROL_EXIT=0 $ OS_HALF_STATES_PROXY_REARMED=1 node scripts/pm/label-write.mjs (same args) […] ① read — COULD NOT READ: GET /repos/objectstack-ai/issues/18932 -> HTTP 401 (Bad credentials) PREREQUISITE NOT MET … ⛔ NOT MEASURED PROBE_EXIT=3 ← and not one line said why ``` **After** (`e424b1cf9`), the same two commands plus the own-guard leg: ``` $ node scripts/pm/label-write.mjs (same args) ℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:37101) … AFTER_CONTROL_EXIT=0 $ OS_HALF_STATES_PROXY_REARMED=1 node scripts/pm/label-write.mjs (same args) ℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:37101) … […] ① 取现集 — objectstack-ai#18932 (open) carries 3 label(s) … AFTER_SHARED_EXIT=0 ← the sibling's guard no longer suppresses this tool $ OS_LABEL_WRITE_PROXY_REARMED=1 node scripts/pm/label-write.mjs (same args) ℹ️ OS_LABEL_WRITE_PROXY_REARMED=1 is set and this process is NOT routed through http://127.0.0.1:37101, so the one re-exec this run allows was already spent and every request below bypasses the proxy — if they answer 401 Bad credentials, that inherited guard is why, and unsetting OS_LABEL_WRITE_PROXY_REARMED is the fix rather than a new token. A refusal below may be about the route, not this container. […] ① read — COULD NOT READ: … -> HTTP 401 (Bad credentials) AFTER_OWN_EXIT=3 ← still exit 3, but the cause is now the FIRST line on stderr ``` And the half that needed no edit — an **unedited** consumer, `sweep-stale-finding.mjs`: ``` $ OS_HALF_STATES_PROXY_REARMED=1 node scripts/pm/sweep-stale-finding.mjs --dry-run ℹ️ OS_HALF_STATES_PROXY_REARMED=1 is set and this process is NOT routed through … 401 … sweep-stale-finding: PREREQUISITE NOT MET — GET /repos/…/issues?… -> HTTP 401 EXIT=3 ``` Its re-exec is still suppressed (it passes no `guard` — see the table), but it is no longer suppressed **in silence**, which is what the triage asked the deliverable to contain. ⛔ Not measured, and not claimed: whether a real seat's process actually inherits one of these variables from a sibling. The card flagged that as unmeasured and it stays unmeasured — every leg above sets the variable by hand. ## The 13 consumers, read at `26c73fb4e` `git grep -l proxyRearmPlan scripts` lists 14 files; one of them defines the plan, so 13 consume it. Only the three in the first row are edited. | consumer | shape | after this PR | |---|---|---| | `pm/post-stamped.mjs`, `pm/label-write.mjs`, `pm/board-snapshot.mjs` | own guard name, `process.env` handed to the plan raw | **edited** — each passes its own `guard`; cross-suppression gone, and a suppressing guard speaks | | `check-single-claim-paths.mjs` (`proxyPlanEnv()`), `pm/check-expected-skips.mjs`, `pm/check-prior-rulings.mjs` | map their own name **onto** the shared default, then call | unchanged and still correct — the default is untouched, pinned in this PR from the plan's side; they gain the loud line for free | | `pm/changeset-deadline-census.mjs`, `pm/check-clause2-carriers.mjs`, `pm/sweep-closed-cards.mjs`, `pm/sweep-stale-finding.mjs` | own guard name, `process.env` raw — **the same live defect** | **not edited** (out of this card's ruled surface): still cross-suppressible, but no longer silently. Filed as a finding | | `check-issue-citations.mjs` | calls the plan for refusal **text** only; never re-execs | unaffected | | `pm/check-governed-merges.mjs` (declares a second plan of the same name), `pm/ci-failure.mjs` (imports it) | a separate implementation reading `OS_GOVERNED_MERGES_PROXY_REARMED`, shared across those two files | untouched by this PR; the same one-name-two-scripts shape one layer over. Filed as a finding | ## Self-test counts, before / after | file | cases | batteries | floor | |---|---|---|---| | `pm/check-half-states.mjs` | 4963 → **4975** | 6 → 6 (new cases are unbatteried `t()`) | 6 → 6, every battery pin holds | | `pm/post-stamped.mjs` | 349 → **360** | 13 → **14** | 13 → 13 (held, see below) | | `pm/label-write.mjs` | 64 → **75** | 8 → **9** | 8 → **9** | | `pm/board-snapshot.mjs` | 145 → **156** | 11 → **12** | 10 → **12** (it had drifted below its own roster) | `post-stamped`'s `SELF_TEST_BATTERY_FLOOR` line sits inside PR objectstack-ai#18934's open hunk range, so it is deliberately **not** touched: the roster rises to 14 while the pinned floor holds at 13. Both sides stay green whichever lands first, and objectstack-ai#18934's own bump to 14 still applies. Every new battery is declared, registered and floored. Each of the three callers gains the same battery — *the re-exec guard: the name this tool sets, and the patrol name that must not silence it* — pinning: this tool's guard is not the patrol's; the patrol literal pinned there **is** the plan's default (so renaming the shared constant reds the pin instead of leaving a phantom); a proxied run with no guard re-execs; its own guard suppresses; the patrol's does not; the suppression prints a line naming the variable and the 401; the runner leg is unchanged; and two structural properties — the dispatch really hands the plan this file's guard, and the plan is imported rather than restated. ## Reader test A seat whose parent process leaked `OS_HALF_STATES_PROXY_REARMED=1` runs `label-write` and reads its **banner**, not a 401 story. If the leaked variable is instead that tool's *own* guard, the seat still gets exit 3 — correctly, the re-exec really was spent — but the first line on stderr names the variable and says unsetting it is the fix, so nobody goes looking for a new token. ## Acceptance notes Noted while reading the surface, ⛔ not fixed here (outside this card's ruled surface), reported to the seat for filing: - Four more consumers carry the **identical** defect and were not in the card's count: `pm/changeset-deadline-census.mjs`, `pm/check-clause2-carriers.mjs`, `pm/sweep-closed-cards.mjs`, `pm/sweep-stale-finding.mjs`. Each declares its own guard and hands `process.env` to the plan raw. One line each would fix them. The card explicitly read **0** own-guard names in `check-clause2-carriers.mjs`; the name is there at `:816`. - `pm/check-governed-merges.mjs` declares a **second** `proxyRearmPlan` rather than importing the patrol's, and shares its single guard name with `pm/ci-failure.mjs` — the same cross-suppression shape between those two files, in a plan this PR does not touch. - `noted, not filed:` the card's 「装饰性」 reading of the three own-guard constants is half right — each **is** read, by its own file and by nothing else. No carrier: this is a correction to a card body, not a defect in the tree. --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18843
Clause-②: no
post-stamped.mjsreported a real HTTP 422 from the platform as exit 3EXIT_PREREQUISITE_NOT_MET— 「no route, no act at all」 — and prescribed atransport remedy. This gives the size refusal an exit class of its own, whose
text carries the bytes SENT, the measured cap for THAT surface, and the remedy
that works; and it homes the comment cap beside
ISSUE_BODY_LIMIT, which thecard says is the same card.
The fixture, verbatim, and where it came from
Built from the record on probe objectstack#18826 — comment id 5722505167,
written 2026-09-17T23:19:18Z. ⛔ No new oversized write was made anywhere to
reproduce this. Captured there from
POST /issues/{n}/commentsat 262,145 bytes:{"resource":"IssueComment","code":"unprocessable","field":"data", "message":"Body is too long (maximum is 65536 characters)"}The two readings the dispatch carried as unmeasured, now measured
is explicit: the create side answers
Body is too long (maximum is 65536 characters), andPATCH /issues/comments/{id}answers the same sentence witha lower-case
body;POST /issuessays it twice. So the trigger iscase-insensitive rather than an equality — an equality pinned to the
create-side capital would have left the update side at exit 3. Pinned in both
spellings, and the ablation below proves the flag is load-bearing.
PATCH /issues/comments/{id}isnot a surface this tool ever writes. Its two write surfaces are
POST /issues/{n}/comments(--comment) andPATCH /issues/{n}(--body).Both caps are declared; the update-side spelling still matters, because it is
the second measured spelling of the sentence the trigger keys on.
check-half-states.mjs.COMMENT_BODY_LIMITis declared afterMARKDOWN_BODY_BUDGET, with its own bisection provenance. It is homed thererather than in
post-stamped.mjson a measured ground: five workflow comments(
cross-repo-issue-closer.yml,merge-queue-triage.yml,scheduled-full-run-card.yml,test-nightly-tiers.yml) already send a readerto
ISSUE_BODY_LIMITin that file for the number and warn againstre-deriving 65,536 — the folklore grows back wherever one of two caps is kept
somewhere else. ⛔ Two constants and not one shared number: two surfaces
measured independently that agree is not one measurement, and the day the
platform moves one a shared number would lie about the other.
The exit register, before and after
EXIT_TOO_LARGE = 5is declared inpost-stamped.mjsbesideEXIT_NOT_STORED.The usage text, the not-stored text and the prerequisite text each name it, so a
reader of a 3 or a 4 learns the fifth code exists.
The reader test
A seat that sends a 300 KB comment body now reads: exit 5, 307,200 byte(s)
sent, cap 262,144 bytes for a comment, 45,056 byte(s) over it, shorten or split
the body — and ⛔ not a word about routes or tokens. Verbatim from the
--self-testfixture:What the class deliberately does NOT take
page 100 answers 422 (measured 2026-08-31, recorded in
check-half-states.mjs) — and that refusal is about a cursor. It keeps its 3,pinned as a control, as do the same sentence on a 403, a 500 and a 200.
maximum is, sokeying on a false clause cannot un-classify the refusal the day GitHub
corrects its own text; a text carrying only that clause is pinned NOT to
classify.
--bodypre-read keeps itsPREREQUISITE reading unchanged.
--bodystory. Over the cap,PATCH /issues/{n}refusesSILENTLY (200, old body kept, [probe] #18664 — bisecting GitHub's real issue-body refusal boundary, in BYTES #18793), which is exit 4's population; only a
422 the platform actually answers reaches 5.
A refusal the cap does not explain
If the platform ever refuses a body at or under the measured cap, the text says
the refusal CONTRADICTS the reading and asks for a re-measurement rather than
printing an overage of zero —
overisnull, never 0. The ordinary over-captext is pinned NOT to carry that sentence.
Self-test counts
post-stamped --self-testcasespost-stampedbatteries (floor)the size refusal: …(registered/floor)check-half-states --self-testcasesISSUE_BODY_LIMIT measured cap(registered/floor)The check-half-states "before" is the merge base
d5ccfb4e3: 4,978 minus the 15cases this PR registers. Two independent readings agree on it — the dispatch
recorded 4,963 at PR #18911's head, and this branch measures 4,921 at its own
branch point
625db0e85, which is before #18911 landed. Both floors RISE byexactly the number of cases added; neither is held flat.
Ablations — each mutation proven on disk, each restore proven against HEAD
All four through
scripts/ablation-replace.mjs, which asserts the anchor countfell, the blob hash changed, and after restore that the blob equals HEAD's and
git diff HEADis empty.iflag fromBODY_TOO_LONG_REROUTE_REMEDYin the size text'sFix:lineCOMMENT_BODY_LIMIT262144 → 65536UTF-8 BYTESfrom the comment cap's docblock⭐ The first ablation found a defect in this PR's own first draft. It came
back GREEN: the classifier was rebuilding the regex from
.sourcewith theflags retyped at the call site, so the exported constant documented a rule it
did not carry and every case-insensitivity pin was vacuous. Fixed in
36ba41a64, after which the same ablation turns 19 cases red. A second pass(
290acfad0) made those pins fail by NAME rather than crash on a TypeErrorbefore the verdict machinery ran.
Gates
Derived in the worktree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat HEAD
0e5e054d3: 39 families, all 39 run, all exit 0, exit codes capturedby redirect-then-
$?.--ranreconciliation: 39 derived, 39 run, 0 UNRUN. Thefirst derivation (before the merge, one path) gave 29; adding
check-half-states.mjsgrew it by 10 and those were run too. Each editedscript's own
--self-testis in that set(
check:pm-post-stamped,check:pm-half-states).git grepof both filenamesunder tests finds no separate suite — the
--self-testIS the suite.ESLint was not narrowed at all. The repo-wide run is the one that was taken:
pnpm lint(=eslint . --no-inline-config, this repo's only style authority)exit 0 at HEAD
0e5e054d3. The targeted run over the two changed files isthere too — 2 files, 0 errors, 0 warnings — but it is not what this PR rests on.
Acceptance notes
pagination 422 is the measured one, and its remedy text is a route remedy just
as wrongly. ⛔ Not filed: it is unreachable through this tool (a comment list
would need 10,000 comments to reach page 100), so it is an observation, not a
reproducible defect.
rest()now surfaces the platform's sentence on every non-2xx, which isthe same defect family (the tool discarding the answer it was given) and is
mechanically part of this change; it is not a separate fix.
Generated by Claude Code