tooling(pm): H22's closed pass pages to its horizon under a 40-page cap, and states the horizon it reached - #16391
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…ap, and states the horizon it reached Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
|
ACCEPT — in-seat review of PR #16391, head Implemented-by: Verified in a detached compare worktree at the PR head, not from the report:
The one Landing regime: pure code ⇒ the seat flips ready and arms SQUASH auto-merge in this same act; the merge is read on the PR object, never inferred. On the merge #16217 closes by Generated by Claude Code |
Fixes #16217
H22's closed-card pass was TRUNCATED at its 12-page quota ceiling on this board, on every run, and said so in its own words while saying nothing about how short it was. This raises the backstop to a measured 40 and makes the clause self-describing: it now prints the horizon it actually REACHED, as a date, on every run.
改前 → 改后
Same board, same instant (2026-09-06, live read-only sweep on
objectstack-ai/objectstack), the pre-change file swapped in and restored byte-identically around the reading:改前
改后
The old ceiling was one page short. Page 12's oldest
updated_atthat day was2026-09-03T17:41:14Z, about an hour newer than the 3-day horizon — which is exactly the reading nobody could take from the old clause, because a pass an hour shy of its horizon and one that read a third of it printed the same sentence.objectstack-ai/objectui, same session, same policy:Neither board truncates at 40. The card asked for that number if it still did; it does not.
What changed
One file,
scripts/pm/check-half-states.mjs, H22's leg only.closed_atand pages onupdated_at, stopping once a page's oldest update predates the horizon (pageExhaustsWindow, shared with H8). The defect was never the pager — it was the backstop in front of it.CLOSED_ISSUE_WINDOW_PAGE_CEILING12 → 40, and sized in the unit it is spent in. The old 12 was derived as "twice the ~6 pages the pinned 188.3 updates/day needs"; that divisor was measured at depth, over a span whose older half is quiet, while the pages a 3-day horizon costs all come out of the fastest part of the stream. Re-measured at depth on 2026-09-06 — page 12026-09-06T08:25:42Z, page 122026-09-03T17:41:14Z, page 202026-09-01T06:54:23Z, page 402026-08-24T21:44:57Z— 4,000 rows reach 12.8 days, so 40 is ~4.3x headroom on the horizon and 40 REST calls is the pass's budget. The docblock carries the history (2 → 4 → 12 → 40) and says in terms why a bind is a signal to narrow the stream rather than to raise the number again. No second constant.oldestPageStampis a new pure reading — the oldest READABLE ordering stamp on a page, a minimum rather than the last row. It is deliberately not the stop predicate's reading: the stop predicate must stay conservative on an unreadable last row so one bad stamp cannot end the pass early, while the reach must not let one bad last row erase a reach the page already proved.reachedDaterenders it, andnullstaysnull— a pass that read nothing dateable reached nowhere and must never render today.closedReachedAtridesSWEEP_COUNT_KEYS, so the reading gathered in the pager cannot be dropped on the way to the summary.describeWindowBoundtakes an optionalreached, printed beside the day count in both directions. Passing it also retires this clause'sthe ceiling needs raisingremedy — by coupling, not by a second flag: that sentence was the only thing a truncated pass said about what to do, and it was wrong as often as not. Callers that cannot state a reach keep it, so H23's clause and the open-listings clause are byte-identical; two cases pin that.MEASURED_CLOSED_ISSUE_UPDATES_PER_DAYis not overwritten (H8's rule: constants here are checked against a sweep, never updated by one). The stale~6 pages for 3 dayscost note now states the disagreement — ~13 pages measured, leading rows at ~414/day against the pinned 188.3 — instead of quietly editing it.Self-test
2553 → 2588 cases (+35), all green.
selfTest()becameasync(one call site) so the three shapes could be driven through the REAL pager rather than a re-typed copy of it —listRecentlyClosedIssuestakes its page reader as a parameter, which is the seam.The three shapes the ruling named, plus the one with teeth:
boundary reached⛔ TRUNCATED, the new ceiling named, the reach stated anyway, and no remedy prescribedclosedReachedAtnullso the clause invents no dateAblation, each leg restored to a byte-identical tree (
git checkout HEAD --, verified by blob hash and an emptygit diff HEAD):stats.closedReachedAtnever assignedGates
Run at the final commit
680df51ecc.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 36 commands; all 36 run, all exit 0.--ranreconciles: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN.Named beyond the derivation, all exit 0:
check:pm-clause2-carriers(it imports this module),check:pm-skill-id-lint,check:pm-label-desc-cap,check:pm-governed-prose,check:pm-skill-ratchet,check:engine-double-contract,check:console-injection,check:i18n-stale-fill,check-published-list-mirrors.check:nul-bytesis inside the 36; the diff was also scanned by hand for control bytes, and carries none.node scripts/pm/check-governed-merges.mjs --test scripts/pm/check-half-states.mjs, verbatim:check:pm-skill-id-lintcannot reach this file, checked rather than recalled: its corpus isSCAN_ROOT = '.claude/skills/pm-dispatch'plusEXTRA_FILES = ['.claude/agents/os-dev.md', 'AGENTS.md'],.mdonly. It ran green regardless.Whole-repo
pnpm lint(eslint . --no-inline-config) throughscripts/pm/os-verify-lock.sh, slotissue-16217:VERDICT command-exit 0 · held the lock 110s · waited 0s. No narrowing — the whole farm ran.Two artefact-roster gates report NOT MEASURED, not red:
check:published-readme-exportsandcheck:dts-closureboth exit 3PREREQUISITE NOT METbecause no package has adist/in this worktree. Both need a whole-repo build that a one-filescripts/pmdiff cannot affect; CI runs them after its build.One staleness reading, stated rather than papered over:
origin/mainmoved to0374bcba9fafter the battery, and two files the derivation reads changed in that range (scripts/check-partof-closing-keyword.mjsand its workflow). The derived command list is byte-identical either way, and CI evaluates the merge result, so it runs the current checker.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code