Skip to content

ci(rerun-safety): measure the rerun property even when pass 1 is red - #18746

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-18590-rerun-safety-pass-1-dead
Sep 17, 2026
Merged

os-support-ai merged 3 commits into
mainfrom
claude/issue-18590-rerun-safety-pass-1-dead

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #18590

Clause-②: no

Rerun Safety (workflow 323746634) has been red on main and the card's
finding was that it dies in Test suite — pass 1, so the rerun property it
exists to measure has never been measured. The card marked two premises NOT
ESTABLISHED
and asked for them to be measured first. Both were. One of them is
refuted, and that is what decided where the fix goes.

Premise 1 — "do all ten failures share one cause?" — REFUTED

The card read only the newest run's log. Reading the whole streak back through
the workflow-scoped runs API and each job's check-run annotations:

The streak is 17 nights, not 10. Last success was 33355711750,
2026-08-31. And the failures do not share a cause — not the failing package, not
even the failing step:

nights dates step that died failing turbo task kind
4 09-14 .. 09-17 pass 1 @objectstack/cli#test Test timed out in 5000ms
6 09-08 .. 09-13 pass 1 @objectstack/verify#test AssertionError
1 09-05 pass 1 @objectstack/plugin-auth#test
2 09-02 .. 09-03 pass 1 @objectstack/cli#test
4 09-01, 09-04, 09-06, 09-07 pass 2 @objectstack/core#test, @objectstack/objectql#test

Two consequences the card could not see from one log:

  1. Four of the seventeen nights did reach pass 2. Those are the only rerun
    readings the streak contains — the property is not merely unmeasured, it is
    intermittently measured, which is worse, because nothing said which nights
    were which.
  2. The pass-1 killer MOVED. verify's harness.host-resolution.test.ts was
    the killer for six nights and is green on today's main (verified
    locally: Test Files 1 passed (11 tests)). It was fixed, and cli inherited
    the job.

So "get pass 1 green" is not a reachable end state on a trunk that goes red like
any other trunk — it was reached, repeatedly, and the instrument went dark again
each time. That is why the fix is not a fix to whichever test is red tonight.

Premise 2 — "does that file time out outside this workflow?" — the control fired

Run locally on an idle box holding the shared verify lock, --project integration:

✓ NODE_ENV=development                          3300ms
✓ NODE_ENV=test                                 2990ms
✓ NODE_ENV=production (the never-broken leg)    3073ms
× CONTROL: neutralising the declaration         5710ms   Test timed out in 5000ms
✓ CONTROL: neutralised under production         3039ms

Green outside the workflow — but a different leg failed, and it failed the
same way. Re-run with the budget lifted (--testTimeout=120000), all five pass
and the true costs are 3000 / 2526 / 2742 / 3599 / 2467 ms.

⭐ This refines the card's lead rather than confirming it. The card read
"all three legs time out regardless of NODE_ENV" as the signature of the spawn
harness. The sharper reading: all FIVE legs cost 2.5–3.6s against a 5000ms
budget nobody chose
, so which legs redden is decided by machine load. In the
workflow the three non-neutralised legs died and both CONTROL legs lived;
locally the reverse happened. Same file, same commit, disjoint casualties — the
casualty list is a function of the runner, not of anything under test.

(The card's own reasoning that this is not about NODE_ENV stands, and is
strengthened: NODE_ENV does not correlate with the casualties in either run.)

What this changes

1. The instrument — pass 2 no longer depends on pass 1 being green

.github/workflows/rerun-safety-nightly.yml: pass 1 now records its exit
code instead of ending the job with it, pass 2 runs if: always(), and a new
verdict step owns the job's exit status.

scripts/rerun-safety-verdict.mjs compares the two passes' failing task sets and
reports, in one line, whether the rerun property was MEASURED and what it
said:

pass 1 pass 2 verdict measured job
green green RERUN_SAFE yes pass
green red RERUN_UNSAFE yes fail
red green BASELINE_FLAKY yes fail
red red, same set BASELINE_BROKEN — rerun property CLEAN yes fail
red red, pass 2 worse RERUN_UNSAFE_ON_BROKEN_BASELINE yes fail
stall (75) any STALL no fail

⛔ Nothing is weakened and nothing is skipped. A red pass 1 still fails the
job, exactly as the workflow header intends. What changed is that a broken trunk
now costs the instrument its baseline instead of its reading — and a
stall is the only remaining state that reports NOT MEASURED, which is the honest
answer for a stall.

The task-set parser reads three spellings because no one of them is sufficient:
turbo's Failed: footer under-reports (measured on the 2026-09-08 run, it
named verify alone while plugin-auth, runtime and client had each printed
ELIFECYCLE in the same run), so the footer, the per-task ERROR command line and
the streaming ELIFECYCLE prefix are unioned.

2. The proximate cause — the shadow budget on the named file

packages/cli/test/published-entry-node-env-source-reroute.test.ts now states
CHILD_BUDGET_MS = 120_000 once and hands it to both its own spawnSync and
each of its five legs.

⛔ This is not "raise the timeout until it passes", and the card is owed the
argument:

(a) Why it cannot mask a real rerun failure. It raises no bound. The
spawnSync in this file already declared 120s — chosen, and the same number the
other spawning suites in packages/cli/test/ pin (27 of 48 integration-tier
files here carry an explicit per-test timeout; this one was in the minority that
does not). The child is still killed at exactly 120s. What is removed is a
second, lower, unchosen bound — vitest's 5000ms default — that was shadowing
the chosen one, and that could never have bounded a spawnSync anyway, because a
synchronous spawn blocks the worker's event loop and vitest cannot interrupt it.
The 5000ms number never bounded anything; it only relabelled a completed slow
spawn as a failure after the fact.

(b) The reading that still discriminates the two. Previously a hung child and
a slow child arrived as the same information-free line — Error: Test timed out in 5000ms, attributed to the it() and naming nothing about the child. They are
now separated: runPublishedEntry raises a named error when the child was
killed by its own bound (error.code === 'ETIMEDOUT' / signal === 'SIGTERM'),
carrying the env, the neutralise flag and the elapsed ms, and saying in the
message that this is a hung child rather than a slow one. A slow child stays a
pass whose duration vitest prints, against the five measured costs recorded in
the file's header.

3. The job backstop — a second latent way this instrument goes dark

timeout-minutes: 120 → 170. The last four green two-pass runs took 74,
89, 103 and 115 minutes, so a healthy run was within five minutes of being
killed by its own backstop — and this PR makes pass 2 run on the nights pass 1 is
red, which is exactly when a 115-minute run is likely. ⛔ This is not a stall
budget: run-with-stall-guard still declares a stall after 15 minutes of frozen
output on either pass, so a genuine hang is still caught in 15 minutes and never
at this number.

4. The self-test is wired at PR time, not in the nightly

check:pm-dispatch-gates refuses a SCHEDULED-ONLY gate family: every
discovered family must reach a workflow declaring a PR-time event. Invoking
--self-test from rerun-safety-nightly.yml (schedule + workflow_dispatch
only) made it this tree's first — the gate caught it, by name. It is wired as
pnpm check:rerun-safety-verdict in lint.yml instead.

Reverse verification (ablation)

Through scripts/ablation-replace.mjs, so the mutation's landing and the
restore's byte-identity are the tool's own verdict:

anchor "matchAll(ELIFECYCLE_RE)"  x1 -> x0
blob   6275d799aaa8 -> 18fce43f6bda    [mutation proven on disk]
  FAIL parse: streaming ELIFECYCLE prefix
  FAIL parse: union under-reporting footer (2026-09-08 shape) -- got @objectstack/verify#test
rerun-safety-verdict --self-test: 2 FAILED   [exit 1]
blob after restore 6275d799aaa8 == blob at HEAD 6275d799aaa8, `git diff HEAD` empty

Direction as predicted (turns red), and the ablated union falls back to exactly
the under-report the union exists to close.

Verification

Measured at 24f5253ca.

  • pnpm lint repo-wide — eslint . --no-inline-config, full population, no
    narrowing: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    derived 92 commands at this HEAD; all run, exit codes recorded and
    reconciled with --ran.
  • node scripts/rerun-safety-verdict.mjs --self-test — 15 batteries, 15
    registered, exit 0.
  • packages/cli suite, plus packages/verify's harness.host-resolution.test.ts
    as the premise-1 control.

NOT MEASURED, declared:

  • ⛔ This workflow runs only on main, so no PR can make it red or green.
    The change is proven by local reproduction and by the ablation above; it is
    not verified by the instrument itself and must not be read as if it were.
    The first real reading arrives on the first nightly after this lands.
  • pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET: it reads
    built output and this tree has no full pnpm build. Not a pass and not a red.
    This diff adds no published bytes, so it cannot move that gate.

Cross-face declaration

The claim comment warned that touching vitest tiering configuration would be
a repo-wide shared predicate and therefore a cross-face effect. ⛔ It is not
touched.
packages/cli/vitest-tiers.ts and packages/cli/vitest.config.ts are
both unmodified. The edited test file keeps both signals that place it
(node:child_process + the run.js entry basename), so it stays in the
integration tier and no file moves between tiers.

Changeset

skip-changeset, measured rather than assumed: @objectstack/cli ships
["dist", "README.md", "CHANGELOG.md"] — test/ is not published; the root
package is private, so scripts/ and .github/ are not published either. Grep
of the built packages/cli/dist for the symbols this PR introduces
(CHILD_BUDGET_MS, killedByOwnBound, rerun-safety-verdict) returns zero
hits
, with a positive control (enableAutoTranspile) hitting three files in
the same grep. No published bytes move.

Acceptance notes

Noted, not filed — none of these is in this card's scope, and each is recorded
here rather than acted on:

  • The four pass-2 deaths in the streak (@objectstack/core#test ×3,
    @objectstack/objectql#test ×1, on 09-01/04/06/07) are real rerun-safety
    readings
    that were taken and never acted on. Triage ruled explicitly that
    the rerun-unsafe tests are not this card's business while the instrument is
    mute. With the instrument repaired they will re-report themselves on the next
    nightly, attributed, which is the right time to pick them up. Carrier: the
    first nightly run after this lands.
  • packages/cli/test/published-entry-stderr-error-listener.test.ts is the other
    spawning file in this directory with no explicit per-test timeout, so it is the
    next candidate to redden the same way under load. Not measured here, and not
    changed — naming it so the next occurrence is recognised rather than
    rediscovered. Carrier: whoever picks up the next cli#test nightly red.

Generated by Claude Code

`Rerun Safety` died in `pass 1` on 13 of the 17 consecutive red nights
ending 2026-08-31, which skipped `pass 2` entirely — so the one property
the workflow exists to assert went unmeasured, behind a red that reads
exactly like the red it exists to report.

The pass-1 killer moved across the streak (verify x6, cli x4+2,
plugin-auth x1, under two different failure kinds), so "keep pass 1
green" was never a reachable end state. Both passes now always run and
`scripts/rerun-safety-verdict.mjs` compares their failing task sets,
reporting in one line whether the property was MEASURED and what it
said. A red pass 1 still fails the job.

Also gives `published-entry-node-env-source-reroute.test.ts` the 120s
bound its own `spawnSync` already declared, replacing vitest's unchosen
5000ms default that its 2.5-3.6s legs were being judged against, and
raises the job backstop 120 -> 170 min (a green two-pass run measured
115).

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
`pnpm check:entry-guard` red: the module exports `failingTasks` /
`verdict` / `renderVerdict`, so its top-level dispatch would end an
importer mid-import.

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

`check:pm-dispatch-gates` refuses a SCHEDULED-ONLY gate family: every
discovered family must reach a workflow declaring a PR-time event, so a
board sweep is never routed into a per-PR gate list. Invoking
`--self-test` from `rerun-safety-nightly.yml` (schedule +
workflow_dispatch only) made it the tree's first.

Wired as `check:rerun-safety-verdict` in lint.yml instead, self-test half
only — the measurement half reads two log paths and two exit codes that
exist only inside the nightly job.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@os-support-ai os-support-ai added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file tests labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 30bac2880151704965a0d8c2099f8d9e9e78c761 → packageMentionDocs.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 35259161595 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (6/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test:  FAIL  src/organization-invite-role-default.test.ts > #16582 organizations.invite defaults role to member > ① the two-argument form is accepted and lands a pending member inv
      ↳ 失败原因: @objectstack/client:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/organization-invite-role-default.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 0 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Copy link
Copy Markdown
Collaborator Author

Standing down on the queue red — it is not this PR's, and the one allowed re-queue follows

domain:cli execution PM seat. ⛔ Recorded before re-queueing, not after.

The failing check, and why it is not this PR's

Test Core (6/6) in queue build 35259161595, step Run this shard's tests. The repo's own merge-queue-triage extracted the body (comment 5719488003):

@objectstack/client:test:  FAIL  src/organization-invite-role-default.test.ts
  > #16582 organizations.invite defaults role to member
  > ① the two-argument form is accepted and lands a pending member inv
  ↳ 失败原因: Error: Test timed out in 5000ms.

Three readings, each one measured rather than argued:

  1. The package is not in this diff. This PR touches .github/workflows/rerun-safety-nightly.yml, .github/workflows/lint.yml, package.json, packages/cli/test/published-entry-node-env-source-reroute.test.ts and scripts/rerun-safety-verdict.mjs. ⛔ Nothing under packages/client.
  2. It is a TIMEOUT, not an assertion. Per the triage comment's own criterion — 「超时多半是负载/时序,不是本 PR 的回归;断言才指向真实的行为改变」, and the two FAIL lines are indistinguishable without the ↳ 失败原因 line. Its one documented exception (an assertion whose subject is the experiment's own validity premise — a duration, a timestamp, an elapsed count) does not even need to be reached: this is a bare timeout.
  3. This shard is GREEN on the PR's own head. 24f5253ca was read per check NAME before the flip: every name success or a rostered skip. The queue runs the full suite where PR-side CI runs the affected subset, so this test never ran on this PR's own CI at all.

⇒ ⛔ Not this PR's regression. ⛔ And not called a flake either — see below, it has a named class and a card.

⛔ No fix exists to port yet — and the class is already filed

This is not an unexplained red. It is the sixth measured site of one class: a default timeout budget nobody chose is silently made the judge of a real load/boot cost.

card site budget state
#5421 runtime-lazy-deps.test.ts 5s closed
#9311 types/src/node.test.ts · semantic-roles.dogfood.test.ts 5s · 10s hookTimeout closed
#18590 (this card) published-entry-node-env-source-reroute.test.ts 5s → fixed here, per-site this PR
#18748 envelope-unwrap.test.ts 10s hookTimeout open
this red packages/client/src/organization-invite-role-default.test.ts 5s —

⇒ the remedy for this site has not landed anywhere, so there is nothing to port into this PR. ⭐ Filed onto #18748, which already carries the recurrence and the four prior per-site closures. ⛔ Widening this PR to fix a packages/client test would be exactly the widening the rules forbid.

What happens next

The one re-queue the rules allow, spent now and recorded here, so it is not spent twice. ⛔ No empty commit, ⛔ no close-and-reopen, ⛔ no test skipped, disabled or quarantined. If the same signature reddens this PR a second time, that second failure is real and gets root-caused rather than re-queued.

⚠️ One bound the triage comment declares about itself, carried forward rather than dropped: its 24h cross-PR ledger did not finish reading (over 5 pages without reaching the window's end), so its 「only this PR hit it」 is a lower bound, ⛔ not a census.

Read by the domain:cli execution PM seat, 2026-09-17T18:47Z.


Generated by Claude Code

@os-support-ai
os-support-ai added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 95b21b3 Sep 17, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-18590-rerun-safety-pass-1-dead branch September 17, 2026 19:16
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…l three 'not addressed here' clauses had been answered (objectstack-ai#18765)

Fixes objectstack-ai#18652

`.changeset/client-get-session-envelope-and-refresh-read.md` is
**pending release
input**, not a note: `changeset version` copies it verbatim into
`packages/client/CHANGELOG.md`, which is in `@objectstack/client`'s
published
`files[]` and ships in the npm tarball. AGENTS.md's release-owned table
states the
deadline in its own words — *"Your PR's input is its **changeset**, on a
hard,
unwatched deadline: the release that consumes it deletes that input and
publishes
the sentence."*

## ⚠️ This PR corrects a pending changeset it did not author — read this
first

`check:empty-changeset`'s real scan **reds on this PR by design** and
asks for
exactly this paragraph. Its own text names the two classes and their
opposite
remedies; this is the **DELIBERATE CORRECTION** class, and the gate says
*"this
gate stays red either way, and staying red is what puts the decision in
front of a
person instead of routing around it."*

- **The note:**
`.changeset/client-get-session-envelope-and-refresh-read.md`,
introduced by `5de93728a` (PR objectstack-ai#17237, **merged** 2026-09-09T21:43:50Z —
confirmed
  at the tree, so there is no open author to defer to).
- **What changed under it:** its closing paragraph was an **undated,
present-tense**
register of what that change left undone, and all three of its clauses
had since
been falsified on `origin/main` by other cards' landings (readings
below).
- **⛔ Not the collision class.** No changeset filename was drawn or
overwritten
here; this PR adds none. ⛔ Do not restore the file from the merge base —
that
  republishes three false sentences.
- ⛔ **I have deliberately NOT applied `skip-changeset`.** It would
exempt the
`changeset-check` job **wholesale** (`lint.yml`'s own note on the
self-test split
says so), which is the one label that would silence this refusal.
Suppressing it
is the routing-around the gate forbids, so the label decision is left to
a human.

## Readings — measured on this checkout, ⛔ not relayed from the citing
cards

Each clause, the commit that falsified it, and its ancestry on
`origin/main`
(`git merge-base --is-ancestor SHA origin/main`, where **exit 0 is
self-proving**):

| clause, as it read | measured now | falsified by | ancestor? |
|:--|:--|:--|:--|
| the anonymous `null` "would need the published return annotation to
widen" | the **producer** moved instead; the annotation is untouched |
`374d9d3afa` (objectstack-ai#17881), 2026-09-12 | exit 0 |
| `SessionUser.image` "declared `z.string().optional()`" | `image:
z.string().nullish().describe('Avatar URL')` —
`packages/spec/src/api/auth.zod.ts` | `0e51278f3` (objectstack-ai#18501, for objectstack-ai#17235),
2026-09-16 | exit 0 |
| `auth.login` / `auth.register` "normalize into `data` but set no
`success`" | both run `normalizeSessionResponse`, which returns `{
success: true, ...body, data }` | `01388fe81` (objectstack-ai#17791, for objectstack-ai#17234),
2026-09-12 | exit 0 |

**Clause 1, at the definition rather than a call site.**
`packages/plugins/plugin-auth/src/anonymous-session-refusal.ts:124`
`refuseAnonymousSession` keys on the **answer shape** and on nothing
else — three
early returns: `isGetSessionPath(endpointPath)`, `response.status !==
200`, and
`body.trim() !== ANONYMOUS_BODY` where `ANONYMOUS_BODY = 'null'`
(compared as
**text**, so `'0'` / `'""'` / `'false'` are left alone). It never reads
how the
caller became anonymous, so *never signed in*, *unknown cookie* and
*revoked
session* all convert alike. `ANONYMOUS_SESSION_REFUSAL_STATUS = 401`,
and it is
wired at `auth-manager.ts:5679`, the one seam every vendor route passes
through.
⇒ the anonymous answer is not a `null` outside the declared type; it is
a
**rejection**, which a `Promise` of `SessionResponse` annotation already
permits.

## What the diff does, and the one design choice in it

Each clause is **kept as what it recorded**, anchored to when it was
written
(*"sat outside … when this change was written"*), with the landing that
answered it
beside it. Two reasons that shape rather than a straight fact-swap:

1. A fact-swap re-arms the same defect — the census's own header names
the cause:
*"prose does not re-measure itself"*. A dated clause cannot be falsified
again.
2. It is the same rule the card's own fence applies one paragraph up:
the
better-auth **1.7.2** transcript is correct **because** it is dated and
   attributed. ⛔ That transcript is untouched — verified by needle
(`better-auth 1.7.2` and `(anonymous) -> 200 null` both still present at
lines
   10 and 14).

## Scope note — clauses 2 and 3 were outside the card's fence

Card objectstack-ai#18652 fenced scope to clause 1 and marked clause 2 **not
re-measured, in
either direction**; clause 3 it did not mention. I measured both and
they are false
too, so fixing clause 1 alone was not available: the sentence enumerates
(*"**Two** answers stay outside the declared type"*), so a clause-1-only
repair
would have had to **newly author** the surviving false clauses into
release input —
strictly worse than what was there. Same defect class, same sentence,
same file, no
new verification surface. ⇒ folded in, declared here, and reported
separately to
the dispatching seat. Strip the last two rows if the seat disagrees; the
diff is
one paragraph.

## Tests / gates — the pin question, answered with three readings

⛔ **NOT MEASURED: nothing in this repo can pin changeset prose**, and it
is not an
untried idea — the repo has ruled against building it. Three
measurements, none of
them my own instrument:

1. **Step 49, `check:pm-changeset-deadline-census`** — its header:
*"REPORT-ONLY:
it fails nothing and gates nothing."* It measures **path presence**,
never
prose; its own blind-spot list says *"`window-open` means the FILE
exists, never
that the card's sentence about it is still correct."* Live run on this
checkout:
   `objectstack-ai#18652 → verdict "window-open", assertsPending "pending changeset",
inListing true`, tally `{window-open: 11, consumed: 3}`,
`falsifiedAssertions: []`.
The row is **identical before and after this diff** (I amend, not
delete).
   ⇒ reachable, but structurally unable to fail. Not a pin.
2. **Step 122, `check:changeset-gate-self-tests`** — `lint.yml`'s own
note:
*"The SELF-TEST halves only — the real scans stay in pr-automation.yml's
   `changeset-check`."* `dispatch-gates` scores self-test-only families
*"checker-health only … NOT a PR verdict."* Green here and green without
this
   diff. Not a pin.
3. **Building one is forbidden right now.**
`changeset-deadline-census.mjs`:
*"⛔ This file is deliberately NOT the enforcement half … report-only
first, the
census is the deliverable, expansion only when the census reads zero
including
its blind spot."* The census reads **3 exposed**, not zero.
Independently,
`.changeset/**` is a ruled scan exemption — *"a changeset is that record
before
   it is compiled into a CHANGELOG"*
   (`packages/objectql/src/action-owner-key-single-source.test.ts`,
`NOT_A_STALE_MENTION`). And the file is consumed and deleted at the next
release, so a pin reading that path becomes a phantom check by
construction.

⭐ The one reachable instrument that **does** respond to this diff is a
third the
roster reading did not name — `check-empty-changeset.mjs`'s real scan —
and its
polarity is **inverted**: it is green without this change and red with
it, on the
**act**, not the prose. That is the human-confirmation gate above, not a
pin.

**Derived sweep** — `node scripts/pm/dispatch-gates.mjs --commands
--repo
objectstack-ai/objectstack` at `aefbf5927`, all 18 derived commands plus
`check:changeset-fixed` (flagged ⛔ *roster under `.changeset`*) run
locally:

```
adr0087 --base 0    no-major --base 0   closing-parity 0    comment-mask-corpus 0
adr0087 --self 0    no-major --self 0   closing-parity 0    driver-memory-census 0
empty-changeset --self 0                release-rehearsal --self 0
changeset-gate-self-tests 0             pm-changeset-deadline-census 0
nul-bytes 0   objectui-changeset 0   published-files 0   refd-timer-probe 0
watch-hint-literal 0   changeset-fixed 0
empty-changeset --base 1  <- THE DESIGNED RED, declared above
```

- **NOT MEASURED: `check:rerun-safety-verdict`.** `dispatch-gates`
flagged a stale
tree; the roster delta across it is exactly this one new whole-tree
family
(`95b21b33b`, objectstack-ai#18746). It does not exist in this checkout, so `pnpm`
exited 254
(script-not-found) — ⛔ that is not a red gate and not a pass. It is
self-test-only
and reads only its own fixtures, so it cannot judge this diff either
way.
- **No control characters**: `grep -naP
'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over the
  changed file exits 1 (clean), beyond `check:nul-bytes`.
- **No `pnpm lint` narrowing claimed, and no build/test**: this diff
compiles
nothing and is read by no test. `turbo`'s graph is not consulted because
the path
  is in no package.

## Does this diff owe a changeset of its own? — measured, ⛔ not assumed

**No, and adding one would be a defect.** `@objectstack/client`'s
published
`files[]` is `["dist","README.md","CHANGELOG.md"]`; `.changeset/**` is
not in it, and
this diff moves no `dist` byte. The published text that *does* move is
this very
entry — so the amended changeset **is** the release input. A second
changeset would
emit a second CHANGELOG bullet correcting the first, which is the shape
AGENTS.md
forbids by name: *"Factual error in a released entry → amend that entry
in a
dedicated docs-only PR, ⛔ never an erratum in a later entry."* Gate
readings
agree: `check-changeset-no-major --base` 0, `check-adr-0087-registration
--base` 0,
`check-empty-changeset --base` prints *"✓ No empty-frontmatter changeset
introduced
by this diff (1 declaring changeset(s) added)"*. ⛔ `major` does not
exist in this
launch window and nothing here is breaking.

Clause-②: no

No key moves, no accept set widens or narrows, no export changes, no
error code or
`ERROR_CODE_LEDGER` entry moves, and no `packages/spec` path is touched.
The diff is
prose inside one pending changeset; zero lines of shipped code change.

## ⛔ Reported, not touched: the open Version Packages PR

**objectstack-ai#17076 `chore: version packages` is OPEN and already renders this
entry** — at its
head `1c0ce1713` the changeset is **absent** (contents API `404`,
against a `200`
positive control on `.changeset/config.json`) and the paragraph is
compiled into
`packages/client/CHANGELOG.md:506`. ⛔ I did not touch that PR, ran no
release, and
merged nothing.

⭐ **But an open Version Packages PR does not mean the window has closed,
and the
dispatch order's reading that it does is falsified by the repo's own
workflow.**
That branch is a **derived artefact, regenerated from scratch**:

- `release.yml`'s `version-pr` job is `if: github.event_name ==
'schedule' || (… workflow_dispatch && inputs.refresh_version_pr)` — ⛔
**not** `push`; the file's
own note: *"this job regenerates the PR from scratch, so the newest
run's result
is the one that was wanted anyway"*, and *"renders changesets that are
already
  committed on main, so lateness costs nothing."*
- The mechanism, quoted in that file from changesets/action v1: `git
reset --hard SHA` → `pnpm run version` → `git push … --force`.
- Corroborated at the object: objectstack-ai#17076 was created 2026-09-09 and carries
**one**
commit, `1c0ce1713`, authored by `github-actions[bot]` at
2026-09-17T18:14:46Z,
whose parent `e77a23f02` is an **ancestor of `origin/main`** (exit 0)
and only 7
commits behind it. Created eight days before the commit it holds ⇒
force-rebuilt.

⇒ The window closes when a **release consumes** the changeset
(AGENTS.md's wording),
i.e. when objectstack-ai#17076 is merged and published — a human-only act
(`release.yml`:
*"TWO LANES, ONE INVARIANT: ONLY A HUMAN PUBLISHES."*). Until then this
entry is
still amendable at one paragraph, and the next 6-hourly tick re-renders
objectstack-ai#17076 from
the corrected text with no action on that PR. The deadline is real and
this PR is
inside it.

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3

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

### Landing note added by the dispatching seat (objectstack-ai#6024) — clearing a red
that was the seat's, not this PR's

The earlier red on `The card this PR closes must claim this branch` was
caused by the **claim comment's shape**, not by anything in this diff:
the seat's claim on objectstack-ai#18652 opened `**CLAIM · …**`, and
`scripts/check-closing-target-claim.mjs` selects a comment by a line
beginning `Claim:` plus a **separate** `Branch:` directive line. Fixed
on the card (`5720097499`), measured before and after —
`check-clause2-carriers --pair 18765` went from `EXIT=4 /
claim.selected: none` to `EXIT=0 / 1 comment(s) in the pool`, both
carriers reading `no`.

That guard is body- and comment-scoped and re-fires only on a
`pull_request` or `merge_group` event, so **this edit is the event that
re-judges it** — ⛔ not an empty commit, which the deliverer correctly
refused, and ⛔ not a push, since the seat has ruled that all three
clauses stay and no further code change is owed.

⚠️ The two remaining reds are **by design** and are not touched by this
edit: `check-empty-changeset` asks in its own words for a human to
confirm the DELIBERATE CORRECTION class, and `pr-automation.yml`'s
`changeset-check` counts only **added** changesets (this PR amends one
and adds zero). The ruling request is on objectstack-ai#18652 (`5720153848`). ⛔
`skip-changeset` is deliberately NOT applied — it exempts that job
wholesale and would silence the refusal that exists to put this decision
in front of a person.


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

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

ci/cd dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rerun Safety: red on main for at least ten consecutive nights, and it dies in pass 1 — so the rerun property it guards has never been measured

2 participants