Skip to content

fix(client): auth.me / auth.refreshToken deliver the SessionResponse envelope they declare, and refreshToken reads session.token - #17237

Merged
os-project-manager merged 4 commits into
mainfrom
claude/issue-16760-get-session-shape-and-refresh-read
Sep 9, 2026
Merged

os-project-manager merged 4 commits into
mainfrom
claude/issue-16760-get-session-shape-and-refresh-read

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #16760

Clause-②: no

auth.me and auth.refreshToken annotate their return as SessionResponse — ObjectStack's REST { success, data } envelope — for GET /api/v1/auth/get-session, a route better-auth owns and answers bare. This lifts the bare answer into the envelope both methods declare, with SessionResponse and both published return annotations unchanged, and corrects refreshToken to read the token the route actually serves.

This is triage's route 3: the lift auth.login has always carried, applied to the two methods that never got it. Not route 1 (rebinding to the wire shape) and not route 2 (enveloping /auth/* at the producer) — neither was needed, and neither is in this diff.

The measurement this card turned on

The card claimed refreshToken's broken data.data?.token read was a consequence of the envelope misdeclaration. Triage judged that causal claim wrong but could not finish the reading, because the card's pasted body elides "session":{...}. That measurement is here, driven signed-in against a real AuthManager (better-auth 1.7.2, organization plugin on by its own default) over a real ObjectQL on a real SqliteWasmDriver:

GET /api/v1/auth/get-session  (signed in) -> 200
  top-level keys : ["user","session"]
  session keys   : ["expiresAt","token","createdAt","updatedAt","ipAddress",
                    "userAgent","userId","activeOrganizationId","activeTeamId","id"]
  body.token         === undefined
  body.session.token === "RMjPkloC0BqTdI4gQxZUNWm4arQYsliY"

GET /api/v1/auth/get-session  (anonymous) -> 200 null

session.token exists, so the dispatch's branch 2 applies: refreshToken changes its read and genuinely captures a credential. Triage's correction stands unchanged — there is no top-level token, so enveloping the body would not have put one at data.token either. The old read named a field this route does not produce at any nesting, which is why fixing the shape alone would have left the method exactly as inert as it was.

Two further facts the measurement turned up, neither of which triage had:

  • session.token is the UNSIGNED spelling; bearer() hands clients the signed token.signature form. Both authenticate — plugin-auth's resolveActor strips the signature on the bearer branch, and the measurement confirms both resolve to the same principal — so storing it keeps the caller signed in. Case ④ asserts exactly that, so the spelling swap is proven safe rather than merely observed.
  • /get-session neither rotates the token nor moves expiresAt across two calls seconds apart. ⚠️ This is not evidence that it never refreshes: AuthManager configures better-auth's session.updateAge at a 1-day default, so a brand-new session is exactly the case where nothing is expected to move. The reading is inconclusive by construction and no claim is built on it.

What changed

  • normalizeSessionResponse (module-private, no new export) lifts a bare { user, session } into { success: true, ...body, data: { user, session } }.
  • success is filled, not only data. SessionResponseSchema is BaseResponseSchema.extend(...) and that base declares success as a required boolean, so a body carrying data alone still does not parse as the declared type. This is what makes the fix closeable: without it the card's own class — declared contract not delivered — stays open on me.
  • The raw .user / .session keys are kept. They are the read the field was pushed onto while the declared shape was unreachable; dropping them would trade one silent breakage for another.
  • data.token is deliberately NOT synthesized from session.token. The declared key is optional, and the two spellings are not one string — login puts the signed form there. Populating it would file two different credentials under one key depending on which method produced the body.
  • refreshToken reads data.session.token.

Tests

packages/client/src/auth-get-session-envelope.test.ts — a real AuthManager over a real driver, transport stood in only so the client's fetch hands the Request to manager.handleRequest. Five blocks: the envelope parses against the declared schema; the raw keys survive; the anonymous null passes through; refreshToken captures a credential that resolves to the right principal; and a negative control that data.token is absent before and after the lift, so a regression to the old read cannot pass by accident.

The unit fixture in client.test.ts was replaced, not adjusted: it served { data: { token } }, a body /get-session has never produced, so it pinned the very read that made the method a no-op. A fixture modelling the misdeclaration cannot witness the fix.

Ablation — direction predicted in writing before any leg ran

leg mutation predicted observed
A1 drop success: true from the lift envelope case reddens 2 failed / 4 passed — both ① cases
A2 restore me to return res.json() every me() case reddens 4 failed / 2 passed — ①a ①b ② ⑤
A3 restore the data.data?.token read credential case reddens 1 failed / 5 passed — ④ only
A3u same mutation, unit suite replaced fixture reddens 1 failed / 215 passed

Each leg proved the mutation reached disk by occurrence count and a blob hash differing from HEAD's, and each restore by a blob equal to HEAD's plus an empty git diff HEAD; the whole script ran under trap ... EXIT INT TERM with absolute paths, and the final git status --porcelain was empty.

⚠️ Two honest deviations from the predictions, reported as observed:

  1. A1 reddened two cases, not one — the residue tripwire in ①b also fires when the envelope regresses, which is what it is for.
  2. Case ③ (anonymous) is green under A2 and cannot redden on it: the anonymous answer is null with or without the lift. It pins the residue, not the fix, and is named that way in the file.

A third correction, on the first attempt at case ④: the firing control was initially a deliberately-wrong seeded token. That unauthenticates the client, so /get-session answers null for the anonymous reason and the case fails against a correct implementation. It was rebuilt on the signed/unsigned spelling difference, which is a real measured difference that moves only when the capture works.

Type-level face

The card's first consequence is a type-level one, so the pin is a parse against the declared schema, not a key spot-check. me's annotation does not move in this diff, so the runtime assertions genuinely redden on the defect (A1/A2 above measure that they do). What could not have caught this: the session.data.user example in content/docs/permissions/authentication.mdx is not marked os:check, and even marked it would type-check both before and after — the annotation was right all along, the body was wrong.

Evidence

  • Gates — derived on the final tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 58 commands, reconciled with --ran: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN. All 58 exit 0. Two needed a build first and exited PREREQUISITE-NOT-MET (check:dual-build-cjs-loads exit 3, check:skill-examples exit 1, both naming an unbuilt dist); after pnpm --filter @objectstack/client build both measured green — 104 require entry points across 67 packages, and 258 prose examples type-checking. ⛔ Neither was read as a pass while unmeasured.
  • Lint — the repo-level run, not a narrowed one: eslint . --no-inline-config at c4cbdb0db6, exit 0, zero output lines.
  • Tests — @objectstack/client: 40 files / 486 tests pass; typecheck clean, and tsc -p tsconfig.test.json --listFiles confirms index.ts, client.test.ts and the new suite are all in the compiled program (not merely assumed covered).
  • Consumer — packages/cli's whoami reads auth.me() through a response.data || response fallback written for this very defect; it now takes the data branch. @objectstack/cli unit tier: 190 files / 2635 tests pass. The integration tier is declared to CI — this diff touches no spawn entry point.
  • Every exit code above was captured by redirect-then-$?, never through a pipe.
  • Single-writer — measured from the open PR list (23 PRs), each against its own merge base via /pulls/{n}/files: no open PR touches packages/client/src/index.ts. Positive controls fire on the same predicate — packages/client/package.json names chore: version packages #17076, packages/spec/src/api/contract.zod.ts names feat(spec): ADR-0112 error envelope gains a producer-side refusal declaration so a deliberate 5xx refusal keeps its caller-authored message (#16335) #17090.
  • Docs-drift — five pages named; none goes stale and one is made true: content/docs/permissions/authentication.mdx already documented session.data.user, which was undefined before this change. That page is independent evidence that the declared envelope was the intended contract, i.e. that route 3 fixes the body rather than the promise. Checked by hand for the two blind spots the tool has: the prose class (sso.mdx, error-catalog.mdx — general envelope prose, untouched; references/** is auto-generated from a packages/spec this diff does not touch) and the docs/ tree the tool never walks (only HTTP-level QA checklist rows, which already record better-auth's 200-with-null convention). ⛔ No edit under content/docs/releases/**.

验收备注

Out of scope, filed rather than fixed here:

Noted, not filed:


Generated by Claude Code

… envelope

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
…se envelope both methods declare

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/client, touching 5 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via expiresAt (literal, a string literal in auth), auth.me (sdk, the route ledger binds it to GET /api/v1/auth/get-session))
  • content/docs/deployment/cli.mdx (via /api/v1/auth/* (route, a path literal on a changed line))
  • content/docs/deployment/self-hosting.mdx (via /api/v1/auth/* (route, a path literal on a changed line))
  • content/docs/kernel/contracts/auth-service.mdx (via expiresAt (literal, a string literal in auth))
  • content/docs/permissions/authentication.mdx (via expiresAt (literal, a string literal in auth), auth.me (sdk, the route ledger binds it to GET /api/v1/auth/get-session), /api/v1/auth/* (route, a path literal on a changed line), /api/v1/auth/get-session (route, a path literal on a changed line))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 14 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 559e531a94dab4938d1ab54fd6631acbc5303d61 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e2fe2f76e4d8b0b02118d9ebf7c183eef4d6198d — the merge of head c4cbdb0db6c01602bc1bd0e7df924a8ae27f443f into base 559e531a94dab4938d1ab54fd6631acbc5303d61, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e2fe2f76e4d8b0b02118d9ebf7c183eef4d6198d && git checkout e2fe2f76e4d8b0b02118d9ebf7c183eef4d6198d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 559e531a94dab4938d1ab54fd6631acbc5303d61 c4cbdb0db6c01602bc1bd0e7df924a8ae27f443f && git checkout -B drift-repro 559e531a94dab4938d1ab54fd6631acbc5303d61 && git merge --no-ff c4cbdb0db6c01602bc1bd0e7df924a8ae27f443f

node scripts/docs-audit/affected-docs.mjs --json 559e531a94dab4938d1ab54fd6631acbc5303d61

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 559e531a94dab4938d1ab54fd6631acbc5303d61 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-project-manager
os-project-manager marked this pull request as ready for review September 9, 2026 21:17
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 5de9372 Sep 9, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-16760-get-session-shape-and-refresh-read branch September 9, 2026 21:43
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

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants