Skip to content

fix(pm): declare ISSUE_BODY_LIMIT from a measurement, in the unit the platform refuses in - #18796

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18664-issue-body-limit-measured
Sep 17, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18664-issue-body-limit-measured

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18664

Clause-②: no

The defect

scripts/pm/check-half-states.mjs declared export const ISSUE_BODY_LIMIT = 65536; with a docblock asserting "GitHub's hard cap on an issue body ... A body that exceeds the cap is REJECTED by the API", and no provenance at all: no unit, no reading, no date. It was false in both directions at once. Bodies four times larger store fine, so the number is too strict where it is used to bound a report; and a real refusal boundary sits above it that nothing was watching, and crossing it is SILENT — the platform keeps the OLD body, answers 200 and reports nothing (which is precisely why post-stamped.mjs grew EXIT_NOT_STORED). One constant, over-strict where it cuts and blind where the refusal lives.

Premise re-derived on main before any edit

  • The filing card's first artefact has MOVED since it was measured. objectstack#6015 was read by the triage seat at 150,507 chars / 257,945 bytes; fetched again for this PR at 2026-09-17T21:03Z it had been compacted to 72,754 chars / 125,561 bytes (updated_at 2026-09-17T20:04:42Z). The card's own numbers are therefore quoted here as the card's, not re-taken. The falsification survives the compaction anyway and is re-taken here: 72,754 chars / 125,561 bytes is past 65,536 in EITHER unit, so no reading of the old constant survives.
  • The constant and its docblock were confirmed on the tip this branch was cut from (6de7a2d6e6), at :17148 / :17149, with the renderer's cut at :19420 and the pin at :28367. SEAT_BODY_SOFT_LIMIT is at :2037 (the card's :1802 is stale); it and H6 were READ and are untouched, as the card directs.
  • One nuance the card's causal sentence does not carry, stated so the next reader is not surprised: what trims the report today is MARKDOWN_BODY_BUDGET at :19420, not ISSUE_BODY_LIMIT — the cap is what the budget was chosen to sit under, and it appears in the self-test pins. The defect is the same; the mechanism has one step in it.

The measurement

A throwaway issue was opened for it — objectstack#18793, never a seat post and never another card — and its body rewritten 17 times with filler of chosen byte sizes through scripts/pm/post-stamped.mjs --body=18793, each write read back byte-exact. Budget set for this: 18 writes (a dozen was the guide; the exact bracket cost 15, the unit question 2, the record 1). Exit 0 with class identical = every byte sent is stored; exit 4 (EXIT_NOT_STORED) = the platform kept the old body.

# size sent (bytes) chars exit class verdict
1 257,945 257,945 0 identical landed
2 263,533 263,533 4 not stored refused
3 260,739 260,739 0 identical landed
4 262,136 262,136 0 identical landed
5 262,834 262,834 4 not stored refused
6 262,485 262,485 4 not stored refused
7 262,310 262,310 4 not stored refused
8 262,223 262,223 4 not stored refused
9 262,179 262,179 4 not stored refused
10 262,157 262,157 4 not stored refused
11 262,146 262,146 4 not stored refused
12 262,141 262,141 0 identical landed
13 262,143 262,143 0 identical landed
14 262,144 262,144 0 identical landed
15 262,145 262,145 4 not stored refused
16 262,145 222,145 4 not stored refused (multi-byte)
17 262,144 222,144 0 identical landed (multi-byte)

Rows 1 and 2 re-take the card's two endpoints as this run's OWN bracket rather than inheriting them. Rows 3 to 15 bisect it.

Interval reached: a single value. 262,144 bytes stored; 262,145 bytes refused. The card forbade writing 262,144 as "known" unless exactly that size landed and one byte more was refused — row 14 and row 15 are that pair. 262,144 is 256 KiB, which is why the guess was tempting; it is reported here because the bisection landed on it, not because it is round.

The unit answer

Rows 16 and 17 fill the body with a 20,000-character run of U+4E2D (three UTF-8 bytes each) plus ASCII padding, so byte length and character length differ by 40,000.

  • Row 16 sends 262,145 bytes but only 222,145 characters. A cap counted in characters — or in UTF-16 code units, the same number here — would have taken it with 40,000 to spare. It was REFUSED.
  • Row 17 is the positive control: the same multi-byte shape at 262,144 bytes / 222,144 characters LANDED. So what refused row 16 is the one extra byte, not the multi-byte content.

The platform counts UTF-8 BYTES.

Incidental, recorded because the tool's own header calls the cell unmeasured: every landed write above read back identical — no trailing-newline strip (these bodies carry none) and no synthesised footer on an ISSUE body. The cell for a body that already ends in the footer block stays unmeasured; none of these did.

The probe issue is closed completed, with the bisection table as its final landed body.

The constant, before and after

Before: export const ISSUE_BODY_LIMIT = 65536;, docblock "GitHub's hard cap on an issue body ... A body that exceeds the cap is REJECTED by the API".

After: export const ISSUE_BODY_LIMIT = 262144;, and the docblock now carries the provenance the old one lacked — the UNIT (UTF-8 bytes), BOTH sides of the bracket (262,144 bytes STORED / 262,145 bytes REFUSED), the DATE (2026-09-17) and the PROBE ISSUE (objectstack#18793) — plus the correction that the refusal is silent rather than an API rejection, and the reason the round number is not the reason.

The guard and the budget, in one unit

The renderer's cut at :19420 counted line.length against MARKDOWN_BODY_BUDGET, and seeded its accumulator with body.length + indexText.length. That unit is UTF-16 code units — JS .length — while the platform refuses in bytes, over rows that are largely CJK prose, where the two differ by 3x.

  • The accumulator and the guard now count bodyBytes() (a new tiny export, Buffer.byteLength(..., 'utf8')). The + 1 stays: the joining newline is one byte in UTF-8.
  • familyLedgerReservation is reserved out of the same budget, so it returns bytes too — which is what its own header has said since it was written ("in bytes"); it was measuring .length. Its two digit-slack terms are ASCII digits, already byte-correct.
  • H6 next door (h6SeatBodyOversized) has counted Buffer.byteLength since it was written. The renderer now agrees with it and with the platform.
  • MARKDOWN_BODY_BUDGET does NOT move — it stays 60,000, now BYTES. A cap that was mis-measured is a correction to a reading, not a licence to print more; how much the anchor issue prints is a fold decision nobody has taken, and this PR takes none. Because bytes are never fewer than characters, the new guard cuts at or before where the old one did: strictly narrowing, never widening, which is what the card asks for.
  • The margin is stated rather than left to arithmetic, in the docblock and in a pin: the budget sits 202,144 bytes below the measured cap, 22.9% of it. The FAMILY_LEDGER_WORST_CASE_BYTES docblock's stale sentence about "the 5,536-byte headroom ... (65,536)" is corrected in the same place, with a note that its two measured figures were taken with .length under a name that said BYTES.

The pins

The pin at :28367 is re-pointed to the measured cap and re-measured in bytes, and twelve other size pins across the suite move to bytes with it (they asserted .length against a budget the guard no longer counts that way). A new floored battery is added in the shape PR #18756 landed — SELF_TEST_BATTERIES gains 'ISSUE_BODY_LIMIT measured cap': 37 and SELF_TEST_BATTERY_FLOOR rises 5 to 6, with the two sibling floor pins that name the roster size updated. 40 cases register against it:

  • provenance (7): the docblock attached to the constant names the unit, both sides of the bracket, the date and the probe issue — anchored on the docblock slice, not on the file text, where these assertion strings would satisfy themselves; plus the two controls that the extractor found a real docblock and not the whole file.
  • the cap (4): the value, that it is 256 KiB exactly, that the retired 65,536 is gone, and that the measured cap is four times it.
  • the unit (4): bodyBytes('U+4E2D') is 3 where .length is 1; ASCII agrees; an absent body is 0 and not a throw.
  • the guard (7), with its firing controls inside: a 4,000-row CJK report is trimmed to the BYTE budget and announces the trim; the row text really is multi-byte (3x), so the bound is not vacuous; the block it lays out fills the budget in bytes while its character count is under half that; and the COUNTERFACTUAL, computed from that same run — how many rows a character-counting guard would have laid out from the same fixture, and what that body would have weighed.
  • the over-cap report (6), the check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 shape against the measured cap: a fixture whose rows alone exceed 262,144 bytes untrimmed (asserted, so it is not vacuous) renders under the cap AND under the budget, SAYS it trimmed, points at the run log, and still never reaches the loud row.
  • the margin (4) and the floor (6).

Read the counterfactual honestly, and the battery comment says so: it overruns the renderer's BUDGET, not the platform's measured cap, which no character count can reach at today's budget. The budget is the thing that stops meaning anything when it is kept in the wrong unit — and the thing that would become dangerous the day anybody raises it toward the cap. That is an argument for landing the unit fix before, not instead of, any budget decision.

Ablation

From the COMMITTED fix (5fc8e0ce57), two legs, each mutating on disk, proving the mutation by grep counts on both the removed and the injected text AND by git hash-object against the HEAD blob, running the suite, and restoring with git checkout HEAD -- path verified by hash equality and an empty git diff HEAD, under a trap ... EXIT INT TERM with absolute paths.

HEAD blob 153d10a0158ba27906a5400df8a9c486d77ae061.

Leg A — put 65536 back. Mutated blob 7d34ca5db0377d33d7e5062792dd54a51edbe763; removed-text count 1 to 0, injected-text count 0 to 1. Self-test exit 1, 5 cases red, every one of them in the new battery, nothing pre-existing red:

#18664 cap: the cap is the bisected value (got 65536, want 262144)
#18664 cap: ...which is 256 KiB exactly, checkable by hand (got false, want true)
#18664 cap: the retired 65,536 is gone (got true, want false)
#18664 cap: ...and the measured cap is four times it (got false, want true)
#18664 margin: ...by the stated margin, to the byte (got 5536, want 202144)

Leg B — put the character-counting accumulator back (let used = body.length + indexText.length + ledgerReservation;). Mutated blob d1ec008854fb628f8822232cbfa4344ace4d2342. Self-test exit 1, 9 cases red: two in the new battery (the CJK trim and the over-cap body's budget bound) and seven of the file's PRE-EXISTING budget pins — markdown: ...and under the renderer's own budget, markdown: ...and the body is still under budget, H17 budget, the ④ budget box, #13947 order, #13947 reserved and #13947's flood pin. That is the intended direction and worth reading twice: those seven pins are red under the old guard because this PR re-measured them in bytes. Under .length they could not fail — and the reason they fail now is that this file's own fixtures already carry enough multi-byte text (em dashes in the row messages) that a character-bounded body overruns the byte budget it was supposed to keep. The old guard was not bounding the thing that matters, on this suite's own inputs.

Both legs restored: hash matches the HEAD blob and git diff HEAD is empty. Working tree clean afterwards.

Self-test

check-half-states self-test: 4921 cases pass. Batteries: H66 released queue card 183/172,
H19 judged-set founding 37/34, H65 tier declaration spelling 46/42,
H67 queued merged-delivery reading 152/142, H2/H47/H66 decorated ownership marker 98/94,
ISSUE_BODY_LIMIT measured cap 40/37.

4,881 cases before, 4,921 after.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree, no hand-fed path list — 38 commands derived against the change set it took from the merge base itself (1 path: scripts/pm/check-half-states.mjs). Every one run, exit code captured by redirect-then-$?, never through a pipe. Reconciled with --ran: 38 derived, 38 run, 0 unrun.

0  node scripts/check-changeset-no-major.mjs --base origin/main
0  node scripts/check-changeset-no-major.mjs --self-test
0  node scripts/check-ci-filter-parity.mjs
0  node scripts/check-closing-keyword-parity.mjs
0  node scripts/check-closing-keyword-parity.mjs --self-test
0  node scripts/check-comment-mask-corpus.mjs
0  node scripts/check-declaration-mirrors.mjs
0  node scripts/check-declaration-mirrors.mjs --self-test
0  node scripts/check-scripts-symbol-anchors.mjs
0  node scripts/check-scripts-symbol-anchors.mjs --self-test
0  node scripts/check-self-test-wired.mjs
0  node scripts/check-self-test-wired.mjs --self-test
0  node scripts/check-self-test-workflow-commands.mjs
0  node scripts/check-self-test-workflow-commands.mjs --self-test
0  node scripts/check-whole-set-label-write.mjs
0  node scripts/check-whole-set-label-write.mjs --self-test
0  node scripts/pm/bare-root-worklist.mjs --self-test
0  node scripts/pm/board-snapshot.mjs --self-test
0  node scripts/pm/check-governed-queue-guard.mjs --self-test
0  node scripts/pm/sweep-closed-cards.mjs --self-test
0  pnpm check:agent-test-spelling
0  pnpm check:bash32-floor
0  pnpm check:changeset-gate-self-tests
0  pnpm check:cli-command-ids
0  pnpm check:closing-target-claim
0  pnpm check:commit-card-trailers
0  pnpm check:cross-package-test-inputs
0  pnpm check:driver-memory-census
0  pnpm check:entry-guard
0  pnpm check:nul-bytes
0  pnpm check:parse-guard
0  pnpm check:partof-closing-keyword
0  pnpm check:pm-half-states
0  pnpm check:pnpm-filter-targets
0  pnpm check:ratchet-remedy-authority
0  pnpm check:refd-timer-probe
0  pnpm check:watch-hint-literal
?  pnpm check:pm-dispatch-gates   -- see below

pnpm check:pm-dispatch-gates is the 38th. It runs past this container's foreground ceiling, so it was started DETACHED with its output redirected to a file, never under a foreground timeout. Its verdict is reported in the delivery comment on #18664, read from that log at report time; if it had not reached a verdict by then it is recorded there as NOT MEASURED by name, not as a green.

Repo-wide pnpm lint (eslint . --no-inline-config): exit 0, whole repo, no narrowing, run at 5fc8e0ce57. A control-character scan over the edited file (grep -naP over the C0 set plus DEL) matched nothing, exit 1.

Acceptance notes

Observations from this work, filed nowhere and changed nowhere, per the scope rule:

  • Five workflow files carry the same unmeasured 65,536, about COMMENTS rather than issue bodies — .github/workflows/cross-repo-issue-closer.yml:166, docs-drift-check.yml:557, merge-queue-triage.yml:178, scheduled-full-run-card.yml:125 and test-nightly-tiers.yml:436, each bounding a log tail or a report against "GitHub's 65536-character comment limit". This PR measured the ISSUE BODY cap only; the COMMENT cap is not measured, so nothing here shows those five are wrong — what it shows is that they carry a number of the same provenance as the one just falsified, in a unit ("character") that the body cap has now been measured NOT to use. Worth a card and its own bisection; out of this card's one-file surface. Dedupe words: 65536 comment limit workflow log tail truncation.
  • FAMILY_LEDGER_WORST_CASE_BYTES's two measured figures (2,285 B / 3,754 B) were taken with .length under a name that says BYTES. Their pins are now measured in bytes and still fit the 6,000 ceiling, so nothing is wrong; the numbers in the prose are simply readings in the other unit. Noted in the docblock rather than re-measured, because re-measuring them is a diff about that constant and not this one.
  • Raising MARKDOWN_BODY_BUDGET toward the measured cap is now possible and is deliberately NOT done here. It changes what the standing patrol's anchor issue prints, which is a fold decision for a seat or the maintainer, not a consequence of correcting a measurement. Successor: none queued — it wants a card of its own if anybody wants the longer report.

🤖 Generated with Claude Code


Generated by Claude Code

… platform refuses in

The constant read 65536 with no provenance and was false in both directions
at once: bodies four times larger store fine (objectstack#6015 read
150,507 chars / 257,945 bytes byte-complete, and 72,754 chars / 125,561
bytes after compaction), while the refusal that does exist sits above it
and is SILENT -- the platform keeps the old body and answers 200.

So the boundary was bisected on a throwaway issue opened for it
(objectstack#18793, closed completed with the table as its body), 17
measurement writes on 2026-09-17 through post-stamped's exit register,
each read back byte-exact: 262,144 bytes STORED, 262,145 REFUSED. One
value, bracketed on both sides, 256 KiB exactly.

The unit was measured too, not assumed: 262,145 bytes carrying only
222,145 characters was REFUSED where a character-counted cap would have
taken it with 40,000 to spare, and the same multi-byte shape at 262,144
bytes / 222,144 characters STORED. The platform counts UTF-8 bytes.

renderMarkdown's trim counted `.length` -- UTF-16 code units -- against a
budget whose cap is in bytes, over rows that are largely CJK prose. It now
counts bodyBytes, as familyLedgerReservation and the size pins do; H6 next
door has counted bytes since it was written. MARKDOWN_BODY_BUDGET does not
move: a corrected reading is not a licence to print more, and the byte
guard only ever cuts earlier than the character one did.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 21:52
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 8904880 Sep 17, 2026
39 checks passed
@os-justin
os-justin deleted the claude/issue-18664-issue-body-limit-measured branch September 17, 2026 22:15
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ps, each naming its record (objectstack-ai#18841)

Fixes objectstack-ai#18806

Clause-②: no

## The defect

Five workflow files asserted 「GitHub's 65536-character limit」 as a fact,
and none of them cited a measurement. PR objectstack-ai#18796 falsified that number on
the ISSUE-body surface by bisection (objectstack#18793: **262,144 UTF-8
bytes stored, 262,145 refused** — a 262,145-byte body carrying only
222,145 characters was refused, so the unit is bytes), and removed the
matching `ISSUE_BODY_LIMIT = 65536` from
`scripts/pm/check-half-states.mjs`; these five lines did not follow. Two
of them (`test-nightly-tiers.yml:436`,
`scheduled-full-run-card.yml:125`) post ISSUE bodies against the
falsified number, and one of those two SIZES a live bound from it
(`BUDGET = 40000`). The other three named a COMMENT limit that nobody in
this fleet had measured. This PR measures the comment surface, re-words
all five to the measured unit and value on each surface, and makes every
restatement name the record it was read from — because a `github-script`
step cannot import `ISSUE_BODY_LIMIT`, so these five are restatements by
construction and the only defence against the next drift is that `grep`
finds them. Comments only: no behaviour changes, and
`retry-exempt-status-codes: 400,401,404,422` is untouched on all four
files that carry it.

## ① The COMMENT surface, bisected — objectstack#18826

Probe issue **objectstack#18826**, opened for this measurement and
closed `completed` with the record as its last comment. Bodies of chosen
byte sizes written through `scripts/pm/post-stamped.mjs --comment=18826`
(writes 1–7, each read back byte-exact by the tool) and direct REST
`curl` (writes 8–12, read back and compared here).

**Result: 262,144 bytes stored, 262,145 refused — the same value as the
issue-body surface, and the same unit.**

| # | endpoint | sent bytes | sent chars | answer | class / exit |
verdict |
|---:|---|---:|---:|---|---|---|
| 1 | comment create | 200 | 200 | 201 | `footer-appended`, exit 0
(stored 258) | landed |
| 2 | comment create | 65,536 | 65,536 | 201 | `identical`, exit 0 |
landed |
| 3 | comment create | 65,537 | 65,537 | 201 | `identical`, exit 0 |
landed |
| 4 | comment create | **262,144** | 262,144 | 201 | `identical`, exit 0
| **landed** |
| 5 | comment create | **262,145** | 262,145 | **422** | exit 3, nothing
written | **refused** |
| 6 | comment create | 262,145 | 222,145 | 422 | exit 3 | refused
(multi-byte) |
| 7 | comment create | 262,144 | 222,144 | 201 | `identical`, exit 0 |
landed (multi-byte) |
| 8 | comment create | 262,145 | 262,145 | 422 | raw REST | refused —
captured the platform's text |
| 9 | comment UPDATE | 262,144 | 262,144 | 200 | stored byte-identical |
landed |
| 10 | comment UPDATE | 262,145 | 262,145 | 422 | previous body kept |
refused |
| 11 | comment UPDATE | 290 | 290 | 200 | stored 348 | landed — restores
write 1 |
| 12 | issue CREATE | 262,145 | 262,145 | 422 | nothing created |
refused |

**Twelve write attempts**, plus the probe issue and the record comment:
14 writes in total on the probe.

- **The bracket the card asked for, at the folklore number**: writes 2
and 3 (65,536 and 65,537 bytes) **both landed**, so 65,536 is not a
boundary on this surface in either unit. Writes 4 and 5 are the
boundary, one byte apart, taken on this run.
- **The unit is BYTES.** Write 6 sends 262,145 bytes carrying only
222,145 characters (a 20,000-character run of `U+4E2D`, 3 bytes each,
plus ASCII padding). A character cap — or a UTF-16 code-unit cap, the
same number here — would have taken it with 40,000 to spare. It was
refused. Write 7 is the positive control: the same multi-byte shape at
262,144 bytes / 222,144 characters landed, so what refused write 6 is
the one extra byte and not the content.
- **Both comment endpoints agree**: `issues.createComment` (writes 4/5)
and `issues.updateComment` (writes 9/10) are bracketed at the same
value.
- ⛔ **The refusal SHAPE differs from the issue-body surface, and that
half is new.** On `issues.update` an oversized body is SILENT — 200, the
old body kept, nothing reported (objectstack-ai#18793). On both comment endpoints and
on `issues.create` it is a real **HTTP 422** that writes nothing. Write
12 measures the issue-CREATE refusal; ⛔ its landing side was not taken,
because that write would leave a 256 KiB issue on the board — so the
create surface is bracketed from above only, and the refusal merely
agrees with objectstack-ai#18793's bisected boundary.

### ⛔ GitHub's own 422 text names 65,536 characters, and it is false

Captured verbatim from `POST /issues/{n}/comments` at 262,145 bytes:

```json
{"resource":"IssueComment","code":"unprocessable","field":"data",
 "message":"Body is too long (maximum is 65536 characters)"}
```

`PATCH /issues/comments/{id}` and `POST /issues` say the same thing.
**Write 4 stored a 262,144-CHARACTER comment** — four times the maximum
that string claims. This is the most likely provenance of the folklore,
it is still served today, and a reader who meets a 422 and re-derives
the cap from its text re-derives the false number. Every one of the five
restatements now says so by name; that sentence is the actual
inoculation, more than the number is.

## ② The five lines, before and after — each names the record it was
read from

Line numbers are on `origin/main` `ad1f94e8ec` (the branch point). After
this PR, `grep -rn 18793 .github/workflows` and `grep -rn 18826
.github/workflows` each find the files that restate the corresponding
measurement; `grep -rn 65536` still finds all five, now attached to the
sentence that says the number is false.

| file:line | surface | before | after — the number, the unit, and the
record |
|---|---|---|---|
| `test-nightly-tiers.yml:436` | ISSUE body (`issues.update` :541 /
`issues.create` :547) | 「a GitHub issue body caps at 65536 chars」 |
262,144 UTF-8 bytes stored / 262,145 refused — **objectstack#18793**,
restated as `ISSUE_BODY_LIMIT`; plus the silent-refusal shape (objectstack-ai#18793)
and the false 422 text (**objectstack-ai#18826**) |
| `scheduled-full-run-card.yml:125` | ISSUE body (`issues.update` :214 /
`issues.create` :219) | 「a body past the 65536-character limit is this
repo's own bug」 | same cap, **objectstack#18793**; and that on
`issues.update` this never arrives as a 422 at all, while
`issues.create` does answer 422 (**objectstack-ai#18826**) |
| `cross-repo-issue-closer.yml:166` | COMMENT (`createComment` :282 /
:477 / :488) **and** issue body (`issues.update` :492) | 「a body past
GitHub's 65536-character comment limit is this repo's own bug」 |
**both** caps, both 262,144 bytes: comment from **objectstack#18826**
(loud 422), issue body from **objectstack#18793** (silent) |
| `merge-queue-triage.yml:178` | COMMENT (`createComment` :879) **and**
issue body (`issues.update` :733, `issues.create` :771) | same sentence
as :166 | **both** caps with both records, and the per-endpoint refusal
shape: `create` 422 (**objectstack-ai#18826**), `update` silent (**objectstack-ai#18793**) |
| `docs-drift-check.yml:557` | COMMENT (`createComment` :665 /
`updateComment` :670) | 「past GitHub's 65536-character comment limit」 |
262,144 bytes stored / 262,145 refused with a real 422 on **both**
endpoints — **objectstack#18826**, bisected on each |

⛔ **No retry-exemption code changed.** `retry-exempt-status-codes:
400,401,404,422` is byte-identical in all four files that carry it; a
422 from a malformed request is still answered correctly on the first
try. What moved is the example the rationale gives for 「this repo's own
bug」 — and on the two issue-body files, the additional fact that an
oversized body is not the 422 the sentence implies.

## ③ `BUDGET = 40000` — the fold decision, taken and stated

**Kept at 40,000, and the comment above it now says what it is sized
against.** It is a READING budget, not a platform bound: 40,000 is about
as much log tail as a person reads off a card before clicking through,
and the run link in the same body carries every byte elided here. A cap
that was mis-measured is a correction to a reading, never a licence to
print more — the same posture `check-half-states.mjs` took when it left
`MARKDOWN_BODY_BUDGET` at 60,000 bytes while `ISSUE_BODY_LIMIT` moved
four times up. The comment also names the budget's unit (JS string
length, i.e. UTF-16 code units — what the `slice` below it actually
counts) and says why that is safe here: nothing judges it against the
byte cap, and even read as bytes of CJK prose 40,000 stays far under
262,144. ⛔ It is no longer inheriting the false 65,536 silently.

## ④ The run-history search — report only, zero state writes

The cheapest search the triage named. Windows are stated because they
are what was read, not what exists.

| workflow | runs seen | failed runs read | what the failures were | a
real 422 on a body/comment write? |
|---|---|---|---|---|
| `test-nightly-tiers.yml` | 12 (all, 2026-09-07 → 09-17) | 6 (all) |
every one at `Run this shard's nightly tiers`; the card-filing job `File
or refresh the red card` concluded **success in all six** | **no** |
| `scheduled-full-run-card.yml` | 5,923 total | **0 — the workflow has
never failed** | — | **no** |
| `cross-repo-issue-closer.yml` | 5,175 total | 4 (all) | 1 deliberate
(a closing keyword named a PR, not an issue); 2 `SyntaxError: Identifier
'octokit' has already been declared`; 1 `Set up job` | **no** |
| `merge-queue-triage.yml` | 18,415 total | 22 (all) | 17 startup
failures with no job; 4 `Set up job`; 1 `HttpError: We couldn't respond
to your request in time` (a platform timeout) | **no** |
| `docs-drift-check.yml` | 9,916 total | 100 most recent (2026-08-17 →
09-05) | exactly 1 job-level failure, at `Self-test the change → docs
mapper`; the rest are startup failures with no job | **no** in that
window |

Instrument note: **the raw job logs are not reachable from this
container** — the log endpoint 302s to `*.blob.core.windows.net`, which
the agent proxy denies at CONNECT (403; the proxy's own
`recentRelayFailures` shows another seat hitting the same host today).
So failure causes were read from **check-run annotations** instead,
which carry the `github-script` step's thrown error verbatim. That is
why the docs-drift window is the 100 most recent failures rather than
all 2,017.

### The escalation condition, answered

⛔ **Neither leg fires; this stays p2 on these readings.**

1. **A real 422 on a body or comment write**: none found in any of the
windows above. The only 422s this card produced are the ones this PR's
own probe manufactured on a throwaway issue.
2. **A comment cap BELOW a budget some workflow relies on**: the
measured comment cap is 262,144 bytes. The largest budget any of the
five relies on is `test-nightly-tiers.yml`'s `BUDGET = 40000` — **6.5×
under it**, and that one is on the issue-body surface anyway.
`check-half-states.mjs`'s `MARKDOWN_BODY_BUDGET = 60000` bytes is
likewise 4.4× under. Nothing in the fleet budgets anywhere near the cap
on either surface.

⭐ One reading that is worth the seat's eye even though it does not trip
the condition: the measured cap is four times what four of these files
assumed, so the fleet has been budgeting against a number that was **too
strict**, never too loose. That is the harmless direction, and it is why
「nothing breaks today」 in the card is still true after measurement.

## The YAML parse readings — the loud failure for a workflow edit

A YAML edit that breaks a workflow is the failure mode that costs the
most, so both layers were parsed after the edit, from the worktree at
the commit under review:

```
YAML_PARSE_EXIT=0
OK   .github/workflows/test-nightly-tiers.yml       name="Nightly Tiers"             jobs=2 [tiers,report]
OK   .github/workflows/scheduled-full-run-card.yml  name="Scheduled Full Run Card"   jobs=1 [file]
OK   .github/workflows/cross-repo-issue-closer.yml  name="Cross-repo Issue Closer"   jobs=1 [close-foreign-issues]
OK   .github/workflows/merge-queue-triage.yml       name="Merge Queue Triage"        jobs=1 [triage]
OK   .github/workflows/docs-drift-check.yml         name="Docs Drift Check"          jobs=1 [docs-drift]
```

`yaml@2.9.0` from the workspace, `YAML.parse` on each file, job names
read back off the parsed document so the parse is not merely "did not
throw".

And because four of the five edits sit **inside** a `github-script`
`script: |` block, the JS in every one of those blocks was compiled too
(`new AsyncFunction(...)` over the extracted source — github-script's
own wrapper shape):

```
JS_PARSE_EXIT=0
JS OK  test-nightly-tiers.yml       :: report               :: File or refresh the card
JS OK  scheduled-full-run-card.yml  :: file                 :: File or refresh the card
JS OK  cross-repo-issue-closer.yml  :: close-foreign-issues :: Close (or report) cross-repo closing keywords
JS OK  merge-queue-triage.yml       :: triage               :: Post the triage comment
JS OK  docs-drift-check.yml         :: docs-drift           :: Comment on PR
github-script blocks checked: 5
```

## `skip-changeset`, measured

Nothing published moves. Read mechanically rather than recalled: 70
`package.json` files in the tree declare a `files[]` array, and **zero**
of their entries name `.github` or `workflow` (positive control: the
reader really does see entries — `packages/spec` returns `dist`,
`json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`,
`src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`).
`.github/workflows/**` is CI configuration and is not shipped by any
package.

## Gates — derived, not hand-fed

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree (no path list passed; the
script took the change set from the merge base itself: 5 committed
paths, 0 working-tree, 0 untracked). **41 commands derived, 41 run,
every one exit 0**, each captured redirect-then-`$?`.

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

| exit | command |
|---:|---|
| 0 | `node scripts/check-aggregator-roster.mjs` (+ `--self-test`) |
| 0 | `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`)
|
| 0 | `node scripts/check-comment-mask-corpus.mjs` |
| 0 | `node scripts/check-cross-repo-closer-outcome.mjs` (+
`--self-test`) |
| 0 | `node scripts/check-merge-queue-triage-outcome.mjs` (+
`--self-test`) |
| 0 | `node scripts/check-position-name-fold-loaders.mjs` (+
`--self-test`) |
| 0 | `node scripts/check-self-test-wired.mjs` (+ `--self-test`) |
| 0 | `node scripts/check-self-test-workflow-commands.mjs` (+
`--self-test`) |
| 0 | `node scripts/check-step-collectors.mjs` (+ `--self-test`) |
| 0 | `node scripts/check-whole-set-label-write.mjs` (+ `--self-test`) |
| 0 | `node scripts/docs-audit/check-affected-docs.mjs` · `node
scripts/docs-audit/check-drift-comment.mjs` |
| 0 | `node scripts/nightly-tiers.mjs --self-test` · `node
scripts/pm/ci-failure.mjs --self-test` |
| 0 | `pnpm check:agent-test-spelling` ·
`check:declared-population-live` · `check:driver-memory-census` ·
`check:node-version` · `check:nul-bytes` |
| 0 | `pnpm check:pm-dispatch-gates` · `check:pm-expected-skips` ·
`check:pm-widening-tells` |
| 0 | `pnpm check:pnpm-acquisition` · `check:pnpm-filter-targets` ·
`check:refd-timer-probe` |
| 0 | `pnpm check:required-contexts` · `check:shard-attestation` ·
`check:stall-guard-budget` · `check:stall-guard-headroom` |
| 0 | `pnpm check:watch-hint-literal` ·
`check:workflow-status-functions` · `check:workflow-step-name-quoting` |

The derivation was re-taken after `git fetch origin main` (tree then 5
commits behind; the newer `origin/main` is `a84da6096a`) and the
41-command list came back **byte-identical**, so nothing that landed in
the meantime added a family to this diff. The run's own stale-tree note
names one file it derives from that moved upstream —
`scripts/check-sdui-manifest.mjs` — which sits in the artifact-roster
block and is silent for these paths in either direction.

**Repo-wide `pnpm lint`** (`eslint . --no-inline-config`): **exit 0**.

Beyond the derived 41, the derivation warns that an artifact roster's
silence is not a clearance when the roster lives in a directory one of
the changed paths is in. Those were run rather than assumed:

| exit | command | reading |
|---:|---|---|
| 0 | `node scripts/check-platform-checklist-watchdog.mjs` (+
`--self-test`) | green |
| 0 | `node scripts/check-ci-filter-parity.mjs` (+ `--self-test`) |
green |
| 0 | `node scripts/ci/scheduled-full-run.mjs --self-test` | green |
| 0 | `node scripts/ci/select-shard-timings-run.mjs --self-test` | green
|
| 0 | `node scripts/measure-test-shard-timings.mjs --self-test` | green
|
| 0 | `node scripts/pr-labels.mjs --self-test` | green |
| 0 | `node scripts/check-single-claim-paths.mjs --self-test` | green |
| 2 | `node scripts/check-single-claim-paths.mjs` | **NOT MEASURED, not
red** — it refuses with 「NOT WIRED — PR_NUMBER is not set … This is a
wiring or usage failure, NOT a verdict」. It needs the PR context
`single-claim-path-guard.yml` supplies; CI reads it. |

A control-character self-scan over the five edited files (`grep -naP`
for the C0 set minus tab/newline, and DEL) returns nothing, beside
`check:nul-bytes` green above.

## What this PR deliberately does NOT do

- ⛔ `scripts/pm/check-half-states.mjs` and its `ISSUE_BODY_LIMIT` are
**read, not touched**. The workflows restate that measurement; they do
not re-declare it.
- ⛔ No `retry-exempt-status-codes` value moved, and no other file is in
the diff.
- ⛔ `BUDGET` was not raised. The decision is stated in ③ above rather
than taken silently in either direction.

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

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

Development

Successfully merging this pull request may close these issues.

2 participants