fix(pm): H64 keys on the session id in the text, not on the author account - #18257
Conversation
…count
H64's premise — MCP writes are user-authored, REST-proxy writes are
`claude[bot]` — was superseded by the rule that write identity follows the
channel and `user.login` records the token, not the seat. The row fired on
compliant REST writes from user-token sessions, told their authors they had
used the MCP tool, and prescribed a re-post through the REST proxy that lands
under the same login — an unsatisfiable row re-filing every sweep against a
trimmed anchor budget.
Measured before re-keying: `performed_via_github_app` reads
`{ id: 1236702, slug: 'claude' }` for a REST-proxy comment authored
`claude[bot]`, for a REST-proxy comment authored by a user token, and for all
four 2026-09-13 specimens. The field names the App whose credential signed the
write, never the tool, so the channel cannot be read from a row at all.
So the finding is now a seat/dev-signed artefact carrying no `session_` id
anywhere — the attribution the landed rule requires — and its remedy is an edit
that gives the artefact its id. The suspension exposure of a user-authored
artefact is not relaxed: it is reported as an informational count plus a login
roster in the summary clause, on every run, with no remedy named and no row
filed, because no act available to a user-token session can clear it.
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing provenance — skills seat, session Generated by Claude Code |
Fixes #18237
H64 keyed on the author account. The rule it was built on had already been superseded: write identity follows the CHANNEL, the REST actor is
claude[bot]or a user per session, anduser.loginrecords the token rather than the seat — attribution is thesession_id in the text (.claude/skills/pm-dispatch/references/rest-channel.md:54,.claude/skills/pm-dispatch/SKILL.md:98). So the row fired on compliant REST writes from user-token sessions, told their authors the write had gone through the MCP GitHub tool, and prescribed a re-post through the REST proxy that lands under the same login and fires again.This re-keys the row onto the attribution the landed rule actually requires. One file,
scripts/pm/check-half-states.mjs(non-governed PM tooling;check-governed-merges.mjs --teston this file list reads0 of 1 path(s) hit the register).The measurement first — the discriminator the card doubted does not exist
The PM's mechanism assumption P1 was that
performed_via_github_app.slugtells the MCP tool from the REST proxy. Measured with GET-only reads on live rows, 2026-09-15:user.loginuser.typeperformed_via_github_appclaude[bot]Bot{ id: 1236702, slug: 'claude', name: 'Claude' }os-warrenUser{ id: 1236702, slug: 'claude', name: 'Claude' }os-warrenUser{ id: 1236702, slug: 'claude', name: 'Claude' }And the four specimens H64 was built from, all read the same slug: #18045 (
os-project-manager/User), objectui#9404 (os-project-manager/User), PR #18051 (os-project-manager/User), comment 5652138683 on objectui#9370 (os-tesla/User); with the clean control comment 5654046782 (claude[bot]/Bot).P1 is falsified. The field names the App whose credential signed the write, never the tool. The first two rows above are both REST-proxy writes differing only in the token class the session was handed, and they are byte-identical in this field. So no channel is inferred anywhere in this family any more, and
artefactChannel's docblock now says what it does read: an App credential versus none. Following P1's own instruction for this outcome, the MCP half of the seat direction (candidate (i)) collapses — it has no readable population — and candidate (ii) is the whole firing row.What fires now
A seat/dev-signed artefact — a claim, a report, a contract review, a filing header — carrying no
session_id anywhere in its text. The text asserts a seat or a dev wrote it; nothing says which session, and the author field records only the token. Remedy: the owner gives the artefact its id (an edit in place is enough). The account is explicitly stated not to be the repair.A structural consequence worth naming: two of the six signature forms are session ids (the bare id in the head window, and the attribution footer's
claude.ai/code/session_URL), so a text matching either carries its attribution by construction and can never fire. The firing population is the other four forms with no id anywhere. The id test reads the whole body while the signature's head window stays narrow — pinned as a case.The exposure is not relaxed — it moved to where it costs nothing
The addendum stands: a suspended user account 404s everything it authored, measured on this board. Nothing here relaxes it. It is now reported as an informational count plus a login roster inside the H64 summary clause, on every run, with no remedy named and no row filed.
That is P5's first option, chosen for the reason P5 gives: no act available to a user-token session moves its content to the App, because the token class is handed to a session at start rather than chosen at write time. A row naming no remedy re-files every sweep at full weight against an anchor that trimmed 423 of 441 rows. The clause states the same fact for free, unconditionally, and is strictly more visible than the old rows were — those competed for the cap and were trimmed; a clause is not.
Board measurement, before and after (live sweep, GET only)
ecf91cb4)c509364b)The two sweeps ran ~40 min apart, so the corpus moved slightly on its own (1697 to 1703 texts).
Reverse verification on the live board. Every row the card named as a false positive is gone: comments 5673315903 (#17396), 5673859871 (#17502), 5673413139 (#18237), 5670954004 (#13801) and 5671601791 (#16695) each appear in 0 rows after, having appeared in 1 before. #18237's own two rows are gone.
And the retired test was blind in the other direction too. Of the 10 rows filed now, 7 are authored
claude[bot]— for example comment 5674562475 on #16166 and comment 5674534458 on #18174, bothos-dev-reportpayloads whose only footer is the platform's bare one, and card #18167'sFiled by the ... seatbody. Every one of those is exactly as unattributable as the ones the old row shouted about, and the old row could not see any of them. Spot-checked by GET: none carries a session id anywhere.Fixtures — one kept its role, three were re-classified
Each with its reason, as P4 asks:
os-tesla)os-project-manager)Filed and claimed byis not the filing header, so the id was its whole signature. Pinned.os-project-manager)BODY9404_NOIDfires).os-project-manager)/pullsfire control is a filer-signed body with only the platform bare footer.claude[bot])Session:line. The byte-identical comment under a USER login is also clean, which is the inversion of the old case.Two new fixtures were added,
restBot64andrestUser64, carrying the 2026-09-15 fields above, so the indistinguishability is pinned offline and the channel inference cannot be re-derived from the reader's name.Ablation (one-off proof, not a landed test)
Predicted direction: removing the session-id gate from
h64SpeaksAboutshould turn the attributed-side silences RED, and should not abort the suite.ac93c610dc39c93444e72605ab45fafb827c6b32; mutated blob35505df98ecef337f80d1ebccc514257a9ff4897(distinct, so the mutation reached disk).8 of 4113 case(s) failed, exit 1 — and the 8 are exactly the attributed-side silences (board-snapshot: once the open set completes, every run spends its whole budget on the closed history and never re-reads the live board — cards created or updated since 2026-09-10 are in no snapshot #18045, objectui#9404, PR docs(pm,agents): three rules-layer lines catch up with the charter rulings #18051, the cleared claim, comment 5654046782 under both logins, and the two structural cases). No abort.git checkout HEAD --, under atrap ... EXIT INT TERMwith an absolute repo root:git status --porcelainempty,git diff HEADempty, restored blobac93c610dc39c93444e72605ab45fafb827c6b32equal to HEAD's, injected marker count 0.Deviations from the suggested route, stated rather than buried
claude[bot]fixtures stay clean" does not survive the re-keying, by design. Aclaude[bot]-authored claim with no session id now fires, and a case pins it. The account was the retired test; keeping the App side categorically clean would keep half the retired premise alive, and the live board shows 7 of 10 rows are that shape.USER_AUTHORED_WRITE_SINCEwas renamedUNATTRIBUTED_WRITE_SINCEandh64UserAuthoredSeatContentbecameh64UnattributedSeatContent. Both names encoded the superseded premise, and both are file-local (grepped: no reference anywhere else in either repo tree). The pin's instant is unchanged.User-authored seat content (H64):toUnattributed seat content (H64):; the window keyh64SeatSignedis unchanged, andsummaryClause's uniqueness cases still pass.artefactChannelwas kept rather than deleted — with corrected semantics, and used by the exposure clause's PAT/unread split. Deleting it would have removed the only place the measurement is enforceable.Self-test and gates
pnpm check:pm-half-states: 4075 cases before, 4113 after, all passing. (The 4075 baseline was read by running theecf91cb4copy of the file; 7 of its cases fail when it is run outside the tree because they read sibling scripts by path, which is a harness artefact, not a baseline failure — in the tree it is green.)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths (change set: 1 path, three-dot vs merge baseecf91cb44): 41 commands. 40 ran in the foreground with exits captured by redirect — all exit 0. The first pass of 14 read exit 3PREREQUISITE NOT METin a fresh worktree with nonode_modules; afterpnpm installall 14 re-ran green, and no exit-3 reading is reported here as a measurement.pnpm check:pm-dispatch-gateswas run detached per its own header (it exceeds the foreground cap), and waited on rather than polled.skip-changeset:scripts/pm/**publishes nothing — no package'sfiles[]ships it.Acceptance notes
Out-of-scope observations, noted and not filed:
os-dev-reportcomment a candidate, because the report JSON carriesbranchbut no session id and the platform appends only a bare footer. That is the row working as the landed rule specifies, not a defect — but if the maintainer would rather the report template carry the id, that is a change to.claude/agents/os-dev.md, a different (governed-adjacent) surface and a different card. Successor: the nextdomain:skillscard touching the dev report template. Noted, not filed.H64_ROW_CAPstill binds at 10 with 26 judged findings, so 16 clearable rows are unfiled per sweep. The cap is correct as designed (the trim eats the tail) and the clause states the full count. Successor: whoever next revisitsrenderMarkdown's ordering, which is the real constraint. Noted, not filed.Generated by Claude Code