Skip to content

fix(rest): compile the data doors' protocol requests against the declared contract - #16071

Merged
os-litant merged 5 commits into
mainfrom
claude/issue-15866-protocol-dispatch-casts
Sep 6, 2026
Merged

fix(rest): compile the data doors' protocol requests against the declared contract#16071
os-litant merged 5 commits into
mainfrom
claude/issue-15866-protocol-dispatch-casts

Conversation

@os-litant

@os-litant os-litant commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15866

The DELETE and PATCH data handlers dispatched through p.deleteData({ … } as any) / p.updateData({ … } as any), which erased TypeScript's check of the assembled request against DeleteDataRequest / UpdateDataRequest. This restores that check — at 22 dispatch sites, not two.

⚠️ Generics are spelled in words throughout this body (ServerScopedDataRequest of DeleteDataRequest, never the angle-bracket form). The body sanitizer strips short tag-shaped fragments, including inside code fences, so a diagnostic quoted verbatim would arrive with its type name eaten.

1. Whose are environmentId and context? — the question the card said to answer first

Both the card and triage measured the casts as load-bearing: these call sites pass environmentId and context, and neither is a member of any data request schema. Confirmed. The answer splits by field, and neither answer is "put it in the schema".

environmentId is transport-level, and this was already ruled. resolveProtocol(environmentId) selects the target kernel before the protocol call; @objectstack/metadata-protocol's data methods never read it off the request; protocol.zod.ts records the same exclusion schema-side. That is the 2026-08-18 ruling on #9741, which this file already implements for the meta doors through TransportScopedMetaRequest.

context is server-derived, and putting it in the schema would re-open a closed hole. Unlike environmentId it is consumed — findData / getData / createData / updateData / deleteData all declare context?: any and forward it so the RBAC/RLS middleware can enforce. But the request schema is the catalog's published requestSchema, so declaring context there would make it caller-supplied, and the ingress deletes any inbound context unconditionally for exactly that reason:

The execution context is SERVER-DERIVED and never caller input. … What rides on it is total: plugin-security's middleware opens with if (opCtx.context?.isSystem) return next() — the entire RLS / FLS / CRUD chain skipped. … Drop any inbound context unconditionally: the protocol must not depend on a gate above it staying switched on.

⇒ Both members are server-side, so both are declared on a typed envelope, not in the spec: a new ServerScopedDataRequest alias sitting beside the ruled TransportScopedMetaRequest, declaring environmentId and context and nothing else. Every other member of every literal is now compiled against the spec contract. No file under packages/spec/src is touched.

2. Clause ② — one sentence per limb, judged from the delivered diff

Content limb — Clause-②: no. The delivered diff touches no file under packages/spec/src and adds, removes or moves no key on any published payload: every door assembles and forwards byte-identically the object it forwarded before, the two (Schema as any) removals leave the same schema running the same safeParse over the same input, and the type: req.params.type as any simplification is a no-op because req is any.

Conformance limb — Clause-②: no. No input class is re-selected between two published verdicts on any shipped face: no validation was added, tightened or loosened, no request that was accepted is now refused and none that was refused is now accepted, because the instrument this card restores is a build-time check that no request ever passes through.

3. The full residue — every as any on a protocol dispatch in this file

Triage counted 15 occurrences of the } as any) form and explicitly did not judge whether all 15 were protocol dispatch. Classified: 14 of the 15 were, 1 was not. The stronger form-B set is larger than triage's 5.

Form A — the argument object is cast (the card's form). 14 of 15; all repaired.

# Door Method Verdict
1 GET /ui/view/:object/:type getUiView repaired — meta-family, so it reuses the existing TransportScopedMetaRequest
2 GET /data/:object findData repaired
3 GET /data/:object/:id getData repaired
4 POST /data/:object createData repaired
5 POST /data/:object/query findData repaired
6 PATCH /data/:object/:id updateData repaired — the card's site
7 DELETE /data/:object/:id deleteData repaired — the card's site
8 public form submit createData repaired
9 public reference picker findData repaired — carried both forms on one call
10 cross-object batch createData repaired — triage's third site
11 POST /data/:object/batch batchData repaired
12 createMany createManyData repaired
13 updateMany updateManyData repaired — see §4
14 deleteMany deleteManyData repaired — see §4

The 15th is not protocol dispatch: guardedRouteManager.register({ … } as any) is a RouteManager.register call against RouteEntry. Out of this card's class; left alone.

Form B — the protocol object itself is cast, erasing every method. 9 repaired, the rest load-bearing.

Triage was right that this form is stronger and that the card's argument holds more firmly on it. The axis that decides each one is whether the member is declared:

Repaired (9) — all on REQUIRED DataProtocol members, where the cast was pure erasure and no guard depended on it: import-job persist (createData), import-job progress patch (updateData), import-job cancel (updateData), import-undo delete (deleteData), import-undo restore (updateData), import-undo stamp (updateData), import-job listing (findData), export chunk loop (findData — its request variable was additionally typed any, which had to go too), public picker (findData, also form A).

Not repaired, and correctly so — the cast carries member existence, not request shape. cloneData, searchAll, getObjectSchema, omitInternalWriteFields, getMetaDiagnostics, listDrafts, migrateStoredMetadata, findReferencesToMeta, getMetaItemLayered, rollbackMetaItem, diffMetaItem are server-only extensions that RestProtocol deliberately does not declare ("Server-only extensions … are feature-detected via runtime casts and so don't widen this contract"); each is reached behind a typeof … === 'function' guard that answers 501. Removing the cast is TS2339, not a typing improvement.

Not repaired, deliberately — getMetaItems / getMetaTypes / getMetaItem. These are declared members, but the #9805 comment already rules that this exact optional-call spelling survives: a host may occupy the protocol slot with an object that does not implement the whole surface, so retiring the guard turns a tolerated absence into a TypeError. That is a behaviour change, not a typing fix.

Residue after this PR: } as any) is 15 → 1 (the non-dispatch register call). Every remaining (p as any) reaches a member that is either undeclared or guard-paired for a documented reason.

4. Proof the check is actually restored — predicted first, then observed

A cast removed into a type that accepts anything is indistinguishable from a cast removed. Two mutations, each written down before it was run.

M1 — the card's own scenario: a newly-REQUIRED field

Predicted: adding a required member to DeleteDataRequestSchema turns tsc --noEmit on @objectstack/rest red, naming the file and the line.

Applied to the spec source, proven on disk (injected marker count 1, anchor intact), then pnpm --filter @objectstack/spec build, then proven to have reached dist/ with scripts/ablation-dist-preflight.mjs — that build step is load-bearing, because packages/rest resolves @objectstack/spec/api through the package exports map to dist, so an unbuilt mutation would have left this ablation silently green.

Observed — RED, at two sites:

src/rest-server.ts(8475,31): error TS2322: Type '{ context?: any; environmentId?: any;
  expectedVersion?: string | undefined; object: any; id: any; }' is not assignable to type
  'ServerScopedDataRequest of { object: string; id: string; os15866MutationProbe: string;
  expectedVersion?: string | undefined; }'.
src/rest-server.ts(8921,35): error TS2322: … (the import-undo deleteData site)

⚠️ Correction to my own prediction: I predicted TS2739/TS2741 (missing-property). The actual code is TS2322 — the literals carry conditional spreads, so tsc reports whole-object assignability rather than a missing property. The direction predicted was right and the diagnostic still names the absent member and the exact line; the error code I guessed was not. Recorded rather than quietly re-fitted.

M1 control — is it the cast that erased it, or the field that is loud?

With the same mutated spec still in place, the DELETE door alone was reverted to its original p.deleteData({ … } as any) form (proven on disk: repaired form 0, cast form 1) and tsc re-run.

Observed: the error at 8475 disappeared; the still-repaired sibling at 8921 kept reporting. ⇒ the cast is what erases the check, per site. (I had loosely predicted "green"; the run is sharper than that prediction — the control is site-local, and the surviving sibling error is the positive control proving the instrument was still measuring.)

M1 restore — the leg that is usually skipped

The preflight caught that the spec build had also rewritten a checked-in artifact (packages/spec/authorable-surface/api.json), which the first restore attempt missed, and that restoring source without rebuilding leaves the mutated marker in dist/ where every later run reads it. Both were repaired: artifact restored from HEAD, spec rebuilt, then verified — git status --porcelain empty, and preflight --absent reporting "marker absent from all 217 built files".

M2 — an undeclared member

Predicted: TS2353. Observed, exactly:

src/rest-server.ts(8478,29): error TS2353: Object literal may only specify known properties,
  and 'os15866UndeclaredKey' does not exist in type 'ServerScopedDataRequest of
  { object: string; id: string; expectedVersion?: string | undefined; }'.

Restore proven by whole-tree git diff HEAD empty.

One thing measured that changed the repair

{ ...someAnyValue, bogusKey: 1 } assigned to a declared type produces no excess-property error — spreading an any makes the whole literal any. Measured directly, with a passing control on the same instrument. So at the updateMany / deleteMany doors, typing the const would have restored nothing while parsed.data was any; dropping the (Schema as any) on those two safeParse calls is what makes the repair real rather than cosmetic. Same defect class, same gate family, no behaviour change.

What the restored check does NOT cover, stated so it is not overread

These handlers declare req: any, so keys sourced from the request bag arrive as any. What is regained is the key set — an undeclared member and a missing required member — not the value types of keys read off req.

5. Handed back, not acted on — filed as #16066

Restoring the check reddened three sites on one slot, and the honest answer there was neither repair this card allows. FindDataRequest.query declares the QueryAST, but the shipped findData ingress also folds an undeclared wire dialect ($top, $orderby, filter / filters / $filter, $expand) that its own normalizer documents as "the wire-only spellings no schema declares". Three server-built literals speak it (import-job listing, export chunk loop, public picker).

Widening QuerySchema is forbidden by Prime Directive 12 and by this card; a runtime safeParse is closed by the card's own reasoning. So the erasure went from one call wide to one slot wide, behind a named, greppable wireDialectQuery helper carrying the reasoning, and the contract question is filed as #16066 rather than settled here. At those three sites the method name, the arity and every other request member are now compiled.

6. Verification

Union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, never from a hand-built diff. Exit codes captured before any pipe.

⚠️ The union was re-derived twice, because a new path entered the diff and then the merge base moved: 55 → 84 families (the second derivation pulled in 29 docs gates the first could not have named), then 84 again on the delivered head. Each derivation was asserted against its own Reconciliation — N famil(ies) line (55 = 55, then 84 = 84, then 84 = 84).

Everything below was harvested on the delivered head 29356431077, after merging origin/main in.

  • Union, 84 families / 84 commands: 82 pass, 0 fail.
  • NOT MEASURED (never read as passes): check:dual-build-cjs-loads and check:type-check-debt — both exit 3, PREREQUISITE NOT MET; each needs every package's dist/, i.e. the whole-farm build CI runs. check:skill-examples and check:published-readme-exports first blocked the same way; their dependencies were built and both then passed (the former over 257 prose examples).
  • Artifact rosters block, run separately — 35 families: 31 pass; check-partof-closing-keyword.mjs and check-single-claim-paths.mjs exit 2, NOT WIRED (no PR context outside the workflow — the former was then re-run with this body supplied as PR_BODY and passed); check:published-readme-exports exit 3; check:react-declaration-parity exit 1 — identical at the merge base, a missing objectui manifest, so not this diff. ⚠️ The pnpm-spelled check:partof-closing-keyword and check:single-claim-paths resolve to --self-test only (Two artifact-roster gates report a green that is not PR clearance — the pnpm script names resolve to --self-test only #16030), so their green grades the checker's fixtures and not this diff.
  • packages/rest, on the delivered head 29356431077: pnpm typecheck OK (including check:test-typecheck, 0 files / 0 errors), vitest run 185 files / 3161 tests, all passing — re-run after the merge, on a rebuilt dependency closure, because the merge changed packages/spec.
  • pnpm lint narrowed, and the narrowing measured rather than asserted. ① The population is read from eslint's own config, which lints ts,tsx,mts,cts,js,jsx,mjs,cjs only — so of this diff's 4 paths, the .md and .mdx are outside it by configuration, not by my judgement. ② --format json reports 2 files linted, matching that lintable subset exactly. ③ eslint.config.mjs enables no type-aware linting (no projectService, no parserOptions.project, no tsconfigRootDir), so every verdict is a function of a file's own text plus the shared config, neither of which this diff moves for any file it does not contain — the narrowing therefore excludes nothing. Result: 0 errors, 0 warnings.

Not run locally and deliberately left to CI: the repo-level pnpm lint sweep and the two whole-build gates above.

The same gate reddened twice, for two different reasons — the second one is worth reading

First, line rot this branch caused. check-system-context-census went red because the envelope declaration and its imports shifted every isSystem citation below them by a constant +86. Verified green at the merge base first, so it was this branch's shift and not an inherited one, then repaired with the gate's own --fix: 4 anchors, no prose, no row, no verdict.

Then, a clean merge that was semantically wrong. Merging origin/main in reported no conflict, and git merge-tree had predicted none — but the census went red again with 4 findings in plugin-sharing/src/sharing-rule-service.ts, a file this branch never touched. origin/main (31403453dda) was verified green on its own, so the findings belonged to the merge. Cause: that page is a generated artifact merged through a merge driver rather than a text merge, so main's re-anchoring of row 39 to :278 / :503 was silently resolved back to this branch's older :202 / :427. Nothing about the merge said so; only the gate did. Re-derived from the merged sources with --fix, and the repo's own os-regen pre-commit hook independently confirmed the artifact was stale and then current. ⇒ On a line-anchor page, a clean merge is not evidence of a correct merge.

7. Merge state

origin/main was merged in, never rebased — delivered head 29356431077, merge base 31403453dda. Every number in §6 was harvested after that merge, on a rebuilt dependency closure.

8. Not claimed

Whether callers of these protocol methods outside packages/rest carry the same erasure was not swept — this was scoped to the file the card and triage named.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

…red protocol contract

The DELETE and PATCH data handlers dispatched through `p.deleteData({…} as any)` /
`p.updateData({…} as any)`, which erased TypeScript's check of the assembled request
against `DeleteDataRequest` / `UpdateDataRequest`. A sweep found the same erasure in
two forms across 22 protocol-dispatch sites in this file, not two.

The casts were load-bearing, as filed: `environmentId` and `context` are passed at
these call sites and are members of no data request schema. Neither belongs in one:
`environmentId` is the transport routing key already ruled out of the protocol request
shape (2026-08-18, #9741), and `context` is the SERVER-DERIVED execution context whose
caller-supplied form is a privilege escalation the ingress deletes unconditionally. So
both are declared on a typed envelope beside the ruled `TransportScopedMetaRequest`,
and every other member of every literal is now compiled against the spec contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…eset

The `ServerScopedDataRequest` doc-comment names `resolveExecCtx` to say where its
`context` member comes from, which moves the census's prose-mention control from 98
to 99. The invocation-site control is UNCHANGED at 77 — no consumer was added, moved
or removed — which is the split that census exists to keep visible, and the entry
records it in the block's house style.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…r the line shift

Pure line rot, applied with the gate's own `--fix`: the typed-envelope declaration and
its imports sit near the top of `rest-server.ts`, so every `isSystem` citation below
them moved by a constant +86. No prose, no row and no verdict changed — only the
`file:line` anchors. `check-system-context-census` was verified GREEN at the merge base
first, so this is a shift this branch caused rather than one it inherited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 13 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 31403453dda6de58d2bbed91a1448a65e2d0a1a3packageMentionDocs.

Which tree this was computed on

This run read content/docs from c40c69b05a2be6259dcfc16324c7f674b77cf0c5 — the merge of head 29356431077e74df8b6638c01951b260bd47ebe6 into base 31403453dda6de58d2bbed91a1448a65e2d0a1a3, 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 c40c69b05a2be6259dcfc16324c7f674b77cf0c5 && git checkout c40c69b05a2be6259dcfc16324c7f674b77cf0c5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 31403453dda6de58d2bbed91a1448a65e2d0a1a3 29356431077e74df8b6638c01951b260bd47ebe6 && git checkout -B drift-repro 31403453dda6de58d2bbed91a1448a65e2d0a1a3 && git merge --no-ff 29356431077e74df8b6638c01951b260bd47ebe6

node scripts/docs-audit/affected-docs.mjs --json 31403453dda6de58d2bbed91a1448a65e2d0a1a3

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
… reverted

⚠️ The merge with `origin/main` was textually CLEAN and semantically wrong, which on a
line-anchor page is the failure mode to expect rather than a surprise: `main` had
re-anchored row 39 to `sharing-rule-service.ts:278`/`:503`, the merge resolved that line
to this branch's older `:202`/`:427`, and nothing about the merge said so. Only
`check-system-context-census` did — it was verified GREEN at `origin/main` (3140345)
first, so the four findings were the merge's and not inherited.

Re-derived from the merged sources with the gate's own `--fix`; the two anchors now read
what `main` set them to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

Copy link
Copy Markdown
Collaborator Author

Seat ruling — clause ② is NO on both limbs, and the seat's own premise was wrong

Recording this so a later reader can tell a contract review that was not owed from one that was skipped — and because the reason it is not owed is a correction to this seat, not a formality.

The seat's premise, and why it failed

The seat's hypothesis was: if a safeParse that previously ran against a cast-away schema now runs against the real one, some input class may move from accepted to refused at runtime on a shipped door.

That premise does not hold. X as any is a TypeScript type assertion with no runtime representation. (UpdateManyDataRequestSchema as any).safeParse(input) and UpdateManyDataRequestSchema.safeParse(input) emit identical JavaScript. The cast never changed which schema ran — the real schema always ran, against the same input, producing the same verdict. What it changed was only that parsed.data came back typed any. There was no "cast-away schema" to swap for "the real one"; the compiler was blindfolded about the result, not about the executed path.

The evidence order matters and is the dev's, not this seat's: the strongest leg is a language guarantee rather than an inference about this codebase — type assertions, annotations and import type are all erased before emit, and packages/rest builds with tsup, which type-strips, so none of the 22 repairs emit code. The corroborating leg is also a control: parsed.data.records.length and parsed.data.ids.length are read immediately after those two safeParse calls and compiled clean against the now-real output type — had the code been shaped around a different schema than the one running, that is exactly where it would have reddened.

The declaration

  • Mechanical floor: NO. Nothing under packages/spec/src/** is touched, and no published payload gained a key. ⭐ The load-bearing nuance is the dev's: environmentId and context were already on the wire at every one of these doors — which is why the casts were load-bearing — so the ServerScopedDataRequest envelope describes keys that were already flowing rather than adding any.
  • Conformance limb: NO. No input class is re-selected between two published verdicts. Only what the compiler knows moved.

One exception the dev named rather than buried, and it is why "no runtime change" is stated precisely here: wireDialectQuery is an identity function, so three sites now carry one extra invocation returning its argument by reference. No verdict, no shape, no key moves through it. Everything else in the diff is type-level and erased.

The dev also scoped the 3161-test run honestly as corroboration, not targeted proof of the VALIDATION_FAILED arms of those two batch doors, because it did not enumerate which tests drive them.

needs:contract-review is therefore not applied to this PR or to card #15866, and none is being stripped.

Three things from this round that outlive it

  • "On a line-anchor page a clean merge is not evidence of a correct merge." Merging origin/main reported no conflict and git merge-tree predicted none, yet the system-context census reddened with four findings in a file this branch never touched — because that page is a generated artifact merged through a merge driver, and main's re-anchoring of row 39 was silently resolved back to this branch's older line numbers. origin/main was verified green on its own first, so the findings were the merge's. Only the gate said so. The seat has checked its own exposure: fix(rest): consume the parsed api sub-config so RestApiConfigSchema owns its defaults #15673 was updated via GitHub's server-side update-branch and its Lint & Repo Gates (census included) came back green; fix(cli): os migrate meta names the protocol, not a package version #16058 was never update-branched. Neither carries this defect — but the seat would not have known to check.
  • { ...anyValue, bogusKey: 1 } raises no excess-property error — spreading an any makes the whole literal any. So at the updateMany / deleteMany doors, typing the const alone would have restored nothing while parsed.data stayed any. Dropping the (Schema as any) on those two safeParse calls is what makes the repair real rather than cosmetic. This is the precise shape of "a cast removed into a type that accepts anything is indistinguishable from a cast removed", which the dispatch asked for a guard against.
  • A prediction corrected in the open. The ablation predicted TS2739/TS2741 and observed TS2322 — the literals carry conditional spreads, so tsc reports whole-object assignability. Direction right, code wrong, recorded rather than re-fitted to the result.

And the answer to "whose are environmentId and context?" is the part the seat could not have supplied: environmentId is the transport routing key already ruled out of the request shape (2026-08-18, #9741), and context is the server-derived execution context whose caller-supplied form is a privilege escalation the ingress deletes unconditionally — so declaring it on the published request schema would have re-opened that hole. The envelope is the third path, and it is a security reason rather than a stylistic one.

Scope, and what was handed back

Residue of the } as any) form went 15 → 1; the survivor is a RouteManager.register call, not protocol dispatch. #16066 was filed rather than folded in: FindDataRequest.query declares the QueryAST while the shipped findData ingress also folds an undeclared wire dialect ($top, $orderby, filter/filters/$filter, $expand) that its own normalizer documents as "the wire-only spellings no schema declares". Widening QuerySchema is forbidden by Prime Directive 12 and by the card; a runtime safeParse is closed by the card's own reasoning; so deciding it is a contract question. In the meantime the erasure there was narrowed from one call wide to one slot wide behind a named, greppable wireDialectQuery.

⚠️ One process note, and the fault is the seat's: the dispatch's ZONE 1 restated a bare-footer rule that this session's own attribution directive supersedes for PR bodies, so the brief contradicted itself and the dev had no clean option. It split the treatment and flagged it, which was right. The dispatch template is being fixed; nothing posted needs changing.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 6, 2026 00:27
@os-litant
os-litant enabled auto-merge September 6, 2026 00:27
@os-litant
os-litant added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 9b459b7 Sep 6, 2026
39 checks passed
@os-litant
os-litant deleted the claude/issue-15866-protocol-dispatch-casts branch September 6, 2026 01:33
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/m tests tooling

Projects

None yet

2 participants