Skip to content

fix(cli)!: objectstack build refuses to lower a hook body reaching ctx.dispatch.scope or ctx.submitted (#22810) - #22834

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22810-lowered-body-scope-refusal
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22810-lowered-body-scope-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22810

Clause-②: no (narrowing)

What changes

extractHookBody (packages/cli/src/utils/extract-hook-body.ts) now refuses a hook handler that reaches ctx.dispatch.scope or reads ctx.submitted. Both are new FORBIDDEN_PATTERNS entries beside .sudo( and .create(, of kind forbidden-token. The refusal names the member and the remedy. The hook-body-lowering lint rule calls the same function (judge() in packages/cli/src/lint/hook-body-lowering.ts, read at the call site), so one edit covers objectstack build and os lint.

The premise was verified on origin/main b7cd1af9df before any code was written:

  • buildSandboxContext (packages/runtime/src/sandbox/body-runner.ts) copies ctx.dispatch as { mode, index } ("scope is deliberately not copied") and assembles no submitted.
  • HookContextSchema (packages/spec/src/data/hook.zod.ts) says the body face carries { mode, index } "and not scope", and that submitted is "NOT marshalled into the sandboxed body face".

So direction (A) holds. There is no runtime change and no spec change.

What "refused" means on each door (measured)

I built a scratch app with 7 hooks through the real CLI (packages/cli/bin/run-dev.js build, from source). I then booted it with bootStack(config, { artifact }) in two ways: on the build's own dist/, with its runtime module beside it, and on the dogfood stand-in artifact, which has no runtime module.

door base b7cd1af9df this PR
objectstack build scope and submitted handlers lowered to bodies, exit 0, no warning bundled instead, exit 0, with a warning that names each hook and the member
objectstack build --strict-body exit 1, for the fetch( control only exit 1, also naming hook 'zz_scope', hook 'zz_scope_alias' and hook 'zz_sub'
os lint silent a hook-body/bundled-fallback warning at hooks[i].handler, the rule .sudo( lands on; exit 0
artifact door, runtime module served scope: SandboxError: hook 'zz_scope' threw: TypeError: cannot set property 'stashed' of undefined, REST 500 INTERNAL_ERROR, nothing stored. submitted: typeof ctx.submitted is 'undefined' scope: 201, the row is stored and stashed. submitted: 'object'. Both match the source boot
artifact door, artifact alone (no runtime module) as above the hook is not bound (the boot logs hook refused: its handler names no function of its own package); the insert answers 201 without the hook
source boot (in-process control) 201, stashed; 'object' unchanged

Mechanism assumption 4 was measured before its refusal was pinned: a lowered body reads ctx.submitted as undefined on an update, both on the stand-in artifact and on the real build's artifact. In-process it reads 'object'.

The last two rows follow the precedent's documented fallback: .sudo(, .create( and fetch( behave the same way. A deployment that must be body-only builds with --strict-body. content/docs/automation/hook-bodies.mdx now says so.

Reach (mechanism assumption 3)

The precedents are receiver-loose, because sudo names nothing except the host method. scope and submitted are also ordinary field names: platform objects declare a scope field, and reading it through ctx.input.scope is working, lowerable code. The reach is therefore receiver-loose on ROOT identifiers (ctx or any local alias of it), and never on a member reached through a record image:

  • dispatch.scope is refused when the body names dispatch and reaches scope. The spellings covered are ctx.dispatch.scope (with or without ?.), ctx.dispatch['scope'], const d = ctx.dispatch; d.scope, const { dispatch } = ctx; dispatch.scope, const { scope } = ctx.dispatch and const { dispatch: { scope } } = ctx.
  • submitted is refused for ctx.submitted, ctx?.submitted, ctx['submitted'], const c = ctx; c.submitted, const { input, submitted } = ctx and const { submitted: s } = ctx.
  • Not refused (pinned controls): ctx.dispatch.mode and ctx.dispatch.index, and ctx.input.scope / ctx.previous.submitted, also beside a dispatch read.
  • Over-refused, knowingly, in the safe direction (the handler is bundled and keeps working): .submitted off a root-bound row (const row = ctx.input; row.submitted), a field of that name destructured from a record, and x.submitted / x.scope inside a string literal.
  • Knowingly not caught:
    • computed keys (ctx.dispatch[k]);
    • Reflect.get(ctx.dispatch, 'scope');
    • a parenthesised receiver ((ctx).submitted);
    • destructuring in the handler's parameter list. The lowering drops the parameter list entirely, so such a handler fails once lowered whatever it touches. That is the out-of-scope finding below.

A probe of 29 spellings matched this list exactly. The regex stays linear: a 680 KB body extracts in 480 ms, including the TypeScript parse.

The dogfood pin: moved, not deleted

packages/qa/dogfood/test/lowered-body-door.dogfood.test.ts pinned lbd_stash, which writes ctx.dispatch.scope, as its divergence. Against this change the base version of the file goes red on exactly the three cases the filing seat predicted: the anti-vacuity case and both divergence cases. I ran it once and then deleted the copy.

  • The divergence moves to lbd_param, a handler whose parameter is named hookCtx rather than ctx. The lowering ships the body and drops the parameter list, so the body's hookCtx is unbound. Lowered, it throws ReferenceError and REST answers 500 INTERNAL_ERROR. In-process it answers 201. This is the next real lowered-only divergence measured, and it is a defect in its own right (see the findings below).
  • lbd_stash is re-pointed at the refusal. The lowering bundles it and ships no body for it, while the other three hooks still lower. In-process it still stashes, through the handle and over REST.
  • lbd_stash stays out of the booted artifact. The stand-in writes no runtime module, so a bundled handler would have nothing to resolve against there. A real build ships it in the .mjs.

Tests

All runs below are at 405c2c3c2 unless noted otherwise.

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 281 files and 4185 tests passed (VERDICT command-exit 0). The integration tier is declared to CI: this diff touches no spawn entry and no integration-tier file.
  • pnpm --filter @objectstack/cli typecheck (tsc --noEmit plus check:test-typecheck): exit 0. pnpm --filter @objectstack/dogfood typecheck: exit 0. --listFiles confirms that both changed test files are in the type-check programs.
  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/lowered-body-door.dogfood.test.ts: 11 passed, at 9c47337a2. The file is unchanged since.
  • New pins, which assert the kind, the origin and the named member rather than the prose:
    • packages/cli/test/extract-hook-body.test.ts: the scope write, direct and aliased; the submitted read, direct and destructured; the controls for mode / index and for fields named scope / submitted; and lowerCallables keeping the very function bundled, which still stashes on a host-shaped context.
    • packages/cli/src/lint/hook-body-lowering.test.ts: both refusals land on hook-body/bundled-fallback at their paths, the mode/index control stays silent, and lint agrees with what the build records.
  • Ablation, run through node scripts/ablation-replace.mjs in wrap mode with the anchor counted, the blob changed and then restored to the HEAD blob with an empty git diff HEAD, on committed 9c47337a2. The subject is reached by relative source import, so no dist/ leg was needed.
    • rx: DISPATCH_SCOPE_RX replaced with a never-matching regex: 5 cli tests and 1 dogfood test went red.
    • rx: SUBMITTED_RX replaced the same way: 4 cli tests went red.
    • Both runs went red in the expected direction.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands derived 94 commands from the actual diff. That set is a superset of the 58 in the dispatch order apart from pnpm lint. All 94 were run and reconciled with --ran: 94 run, 0 NOT MEASURED, every one exit 0. Two of them first exited 3 (PREREQUISITE NOT MET, because unrelated packages had no dist/): check:skill-examples and check:dual-build-cjs-loads. Both were re-run green after building those packages. The gates that read dist/ were re-run after @objectstack/cli was rebuilt at HEAD.
  • Lint, as a proven narrowing rather than a full pnpm lint:
    1. The population comes from eslint's own configuration. eslint --no-inline-config --format json over the 7 changed paths reports 4 files linted and 3 ignored as "no matching configuration": the changeset and the two .mdx files.
    2. The JSON output reports 7 results, with 0 errors and 0 warnings on the 4 linted files.
    3. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project). Its only cross-file reads are two baseline JSON files this diff does not touch. So the diff cannot move any untouched file's verdict.

Acceptance notes

  • origin/main moved 3 commits past the base (efcbac73c), and none of them touch these surfaces. I did not merge; the queue rebuilds on merge.
  • File surface beyond the claim: two documentation edits, both describing this refusal. They are the "What the sandbox forbids" list in content/docs/automation/hook-bodies.mdx and the ctx.submitted line in content/docs/protocol/objectql/security.mdx.
  • The changeset states each door's behaviour before and now in prose. A FROM → TO label line reads to check:adr-0087-registration as a code-rewrite prescription, which contradicts the only disposition that fits (not-required (no-migration-prescription): no metadata, export or stored shape moves). This was measured: the gate refused the labelled draft and passed the prose form.
  • An artifact served without its runtime module leaves any bundled hook unbound. The boot logs it as refused, and writes proceed without the hook. This is the existing fallback for every forbidden token and is not new here. It is documented in hook-bodies.mdx.

Out-of-scope findings (handed to the seat, not filed here)

  1. Class a. The lowering drops the handler's parameter list. A handler whose parameter is not named ctx (async (hookCtx) => …), or that destructures it (async ({ input }) => …), lowers at exit 0. Its body then throws ReferenceError on every run, and REST answers 500 INTERNAL_ERROR at the artifact door, both with the runtime module served and without it. In-process it answers 201. extractHookBody peels the body, and detectFreeIdentifiers counts the parameters as bound. It is the same family as this card, and it is now the dogfood divergence pin.
  2. Class a, misleading text. objectstack build --strict-body counts one more callable than it found. With 4 refused handlers it prints 5 callable(s) lack a metadata body; on base, 1 refused handler printed 2. In packages/cli/src/commands/compile.ts, issues.length counts the extraction warnings plus an aggregate row. The --json issues list carries that aggregate row as if it were a callable.

Generated by Claude Code

…x.dispatch.scope or ctx.submitted

The sandboxed body face carries ctx.dispatch as { mode, index } and no
ctx.submitted, by declared contract, so a lowered handler writing scope
TypeErrors on its first run and one reading submitted sees undefined,
while the in-process handler works. extractHookBody now refuses both
members as forbidden tokens, the way it refuses .sudo( and .create(, so
the build bundles the handler instead and --strict-body fails on it.

Claude-Session: https://claude.ai/code/session_01B5CHJNXuuqzChM4w6hkTN4
Co-authored-by: Claude <noreply@anthropic.com>
…scope, which the build now refuses

lbd_stash no longer lowers, so its divergence cannot reach the artifact
door; it is re-pointed at the refusal (bundled, no body; in-process it
still stashes). The divergence pin moves to lbd_param, a handler whose
parameter is not named ctx: the lowering drops the parameter list, so the
body throws a ReferenceError the in-process handler never meets.

Claude-Session: https://claude.ai/code/session_01B5CHJNXuuqzChM4w6hkTN4
Co-authored-by: Claude <noreply@anthropic.com>
…R-0087 disposition reads

Claude-Session: https://claude.ai/code/session_01B5CHJNXuuqzChM4w6hkTN4
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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

Coarse fallback — 28 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 efcbac73cc58510278b970949ac3b65cb9712815 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json efcbac73cc58510278b970949ac3b65cb9712815

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 405c2c3c21092a6a631e139a9dea7544f96ba061
Local-runs: none

Inputs read: card #22810 (body and all four comments: triage ruling 6107382836, filing-seat note 6107432274, claim 6108435063, os-dev-report 6109013472), PR #22834 (body, 7-file list, net diff against base efcbac73cc58), and the check-runs on the head (read three times, last at 2026-10-11T12:43Z). Context was read on origin/main only: packages/spec/src/data/hook.zod.ts, packages/runtime/src/sandbox/body-runner.ts, packages/cli/src/utils/extract-hook-body.ts, packages/cli/src/utils/lower-callables.ts, packages/cli/src/lint/hook-body-lowering.ts, packages/cli/src/commands/compile.ts, packages/objectql/src/hook-binder.ts and engine.ts (bindHooks), packages/qa/dogfood/test/build-shaped-artifact.ts, the base pin file, AGENTS.md section 10 step 3, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, the two docs pages, and issue #22837.

① Derived judgments

1. The accept-set change — right. extractHookBody gains two FORBIDDEN_PATTERNS entries of kind forbidden-token, DISPATCH_SCOPE_RX and SUBMITTED_RX, placed beside .sudo( (#14010) and .create( (#16249). The match surface is unchanged: the loop at extractHookBody still runs rx.test(block.source) over the peeled block body, with no comment or string stripping, exactly as the precedents are matched. Because lowerCallables keeps its catch (records the refusal, registers the function, emits a handler ref) and judge() in the lint rule calls the same function, one edit moves all three doors. Read against origin/main:

  • objectstack build: compile.ts step 2c prints one warning line per refusal naming w.origin (hook 'NAME'), and step 4b emits objectstack-runtime.{hash}.mjs whenever bodyExtractionWarnings.length is above zero; exit stays 0. Right.
  • objectstack build --strict-body: the block at compile.ts step 2 lists every warning as an issue and exits 1. Right.
  • os lint: judge() maps forbidden-token to severity: 'warning', rule hook-body/bundled-fallback, path hooks[i].handler. Right, and the same rule .sudo( lands on, as the triage ruling asked.

2. Regex reach — right, with one unstated gap of the Reflect.get kind. I traced both patterns by hand against the spellings the PR claims.

  • ROOT_RECEIVER (an identifier preceded by a negative lookbehind that refuses a word character, a dollar sign, or a dot followed by optional whitespace) admits only a bare identifier that is not itself a member: in ctx.input.scope, input and scope are both preceded by a dot and ctx is followed by .input, so no alternative fires. The variable-length lookbehind also covers ctx . input . scope with spaces.
  • DISPATCH_SCOPE_RX is gated by a start-anchored lookahead for the whole word dispatch anywhere in the body, so a body that never names dispatch is never refused for scope. Alternative 1 catches ctx.dispatch.scope, ctx?.dispatch?.scope, dispatch.scope after const { dispatch } = ctx, and d.scope after const d = ctx.dispatch; alternative 2 the bracket spellings with the quote backreference (group 1 is the only capture in the pattern, so \1 is right); alternative 3 const { scope } = ctx.dispatch and ({ scope } = ctx.dispatch), where the equals sign must not be followed by a second equals sign or by the arrow token's closing character, so == and the arrow are excluded; alternative 4 const { dispatch: { scope } } = ctx.
  • SUBMITTED_RX is ungated (there is no anchor for a top-level member): it catches ctx.submitted, ctx?.submitted, ctx['submitted'], c.submitted after const c = ctx, const { input, submitted } = ctx and const { submitted: s } = ctx. ctx.previous.submitted and ctx.previous?.submitted are not caught (previous is a member, not a root).
  • Controls (ctx.dispatch.mode, ctx.dispatch.index, ctx.input.scope, ctx.previous?.scope, ctx.previous?.submitted beside a dispatch read) do not match any alternative; the pin in extract-hook-body.test.ts asserts the same. Nothing lowerable and working is refused.
  • Declared over-refusals, verified as real matches: row.scope or row.submitted off a root-bound row, const { submitted } = ctx.input (and const { scope } = ctx.input when the body names dispatch), and x.scope or x.submitted inside a string literal. All bundle the handler and keep it working. The one qualification: under --strict-body these are hard failures of working code, and the refusal text then asserts a reach of ctx.dispatch.scope or ctx.submitted that the body does not make. The .sudo( precedent has the same property (any receiver), and the PR, the code comment and the changeset all declare it, so this is a stated trade-off and not a defect at this tier.
  • Declared not caught, verified: computed keys, Reflect.get, a parenthesised or indexed receiver ((ctx.dispatch).scope, ctx['dispatch'].scope), and destructuring in the parameter list (which the lowering drops, so that handler fails lowered whatever it touches; that is finding: objectstack build lowers a hook handler whose parameter is not named ctx (hookCtx, or a destructured ({ input })) at exit 0, and the body ReferenceErrors in the sandbox — REST answers 500 where the source boot answers 201 #22837).
  • Unstated: an existence probe ('scope' in ctx.dispatch, 'submitted' in ctx, Object.keys(ctx.dispatch).includes('scope')) is not caught and reads a different answer in a body. It is not a reach of the member, the spec's own back-compat rule already tells an author to read ctx.submitted?. tolerating absence, and it is of the same low-reach kind as Reflect.get. Noted for the dev's "knowingly not caught" list; it does not move the verdict.

3. The declared contract — the omission is declared, so this is a narrowing to it, not a new rule. hook.zod.ts says at the submitted key (about :852–:856) "NOT marshalled into the sandboxed body face, for the same reason dispatch.scope is not", and at the dispatch key (about :929–:942) "The sandboxed body face carries { mode, index } — and not scope", with the remedy the PR repeats (work once at index === 0, or keep state in the record through ctx.api). buildSandboxContext (body-runner.ts about :1094–:1106) copies { mode, index } only and the object it returns (about :1124–:1167) assembles no submitted; the word does not occur in the file. The diff touches nothing under packages/runtime or packages/spec (the file list is seven paths: one changeset, two docs pages, four under packages/cli and packages/qa), so direction (A) of the triage ruling is what landed and direction (B) was not folded in.

4. Public surface. No export moves. HookBodyExtractionError was already exported; the two regexes and ROOT_RECEIVER are module-private. What narrows is @objectstack/cli's behaviour at the three doors above. In-repo population, counted here on origin/main because the Responsibility line left it to the dev and the report gives no number: zero handlers under examples/** reach dispatch.scope or ctx.submitted, so no in-repo build output changes shape (the dogfood gates on the head agree). The four in-repo readers of ctx.dispatch.scope (plugin-security grant-holder-membership-refusal and grant-permission-set-name, plugin-sharing bulk-recompute, service-storage file-reference-lifecycle) are plugin-side in-process registrations that lowerCallables never walks, so they are unaffected.

5. The dogfood pin — moved, not deleted, and still load-bearing. packages/qa/dogfood/test/lowered-body-door.dogfood.test.ts:

6. The two docs pages — every changed sentence checked, all true against the diff and origin/main.

  • content/docs/automation/hook-bodies.mdx, new bullet: "a body's ctx.dispatch is { mode, index } only, and it has no ctx.submitted" (true, item 3); "a write through scope TypeErrors on its first run and a read of submitted sees undefined" (true by mechanism and by the card's measurement); the remedy sentence matches the spec's own text; "ctx.dispatch.mode / ctx.dispatch.index, and a field that merely happens to be named scope or submitted (ctx.input.scope), are not affected" (true, item 2).
  • Same page, new paragraph: "objectstack build bundles the handler into its runtime module (objectstack-runtime.*.mjs) instead and still exits 0 (with a warning naming the hook)" (true, compile.ts step 2c and step 4b); "os lint reports it as hook-body/bundled-fallback" (true); "objectstack build --strict-body fails on it" (true); "a deployment that serves the artifact alone has no function to bind the hook to, so it logs the hook as refused at boot and the hook never fires" (true on the default binding: hook-binder.ts about :232–:250 logs "hook refused: its handler names no function of its own package" and continues; engine.bindHooks turns strict on only under _strictHookBinding, where the boot throws instead, which is the stricter outcome and not a contradiction); "build with --strict-body when the artifact must be body-only" (true).
  • content/docs/protocol/objectql/security.mdx: "objectstack build refuses to lower a handler that reads it (the handler is bundled instead)" is the new clause; true. The rest of the bullet is unchanged.

7. New pins in packages/cli. extract-hook-body.test.ts asserts kind forbidden-token, originLabel, and the member name in the message, never the remedy prose, which respects the byte-identical-message discipline the error class documents (the two messages are new, so no quoted sentence moves). hook-body-lowering.test.ts asserts rule, severity and path for both refusals, the mode/index control, and lint-versus-build parity through lowerCallables (bodyExtracted 1 of 3). Right.

② Semver level

  • The changeset .changeset/22810-lowered-body-scope-submitted-refused.md grades @objectstack/cli minor, titled fix(cli)!:, and carries Clause-②: no (narrowing); the PR body carries the same line. AGENTS.md section 10 step 3: (narrowing) is BREAKING, and scripts/check-changeset-no-major.mjs is a launch-window guard that refuses a major bump, so minor is the level this repository gives a BREAKING narrowing. This is the exact shape of the precedent .changeset/22019-object-save-door-formula-verdict.md (minor, fix(lint)!:, no (narrowing), not-required (no-migration-prescription)). patch would be wrong for a declared BREAKING; major is refused by the gate. Consistent with what the diff publishes: only @objectstack/cli publishes from this diff (@objectstack/dogfood is "private": true; docs and the changeset publish nothing).
  • The ADR-0087 marker not-required (no-migration-prescription) (an HTML-comment line in the changeset body) fits the diff: no authorable key, spelling, export, type or stored shape moves; no stored row is read or converted; the refused handler keeps running bundled; the edit that makes it a body again is handler code no ledger entry can derive. The marker closes the other categories on facts (publishes, no covering ADR-0087 id, a build verdict not a declaration). Consistent.
  • The body gives an upgrading author the before and now per door (objectstack build: lowered silently, now bundled with a warning at exit 0; --strict-body: exit 0, now exit 1 naming hook and member; os lint: silent, now a hook-body/bundled-fallback warning), the refused and not-refused spellings, the over-refusal side, the one-line fix (index === 0 or ctx.api instead of scope; ctx.previous instead of submitted; a bundled handler needs no change), and an Unchanged section. That satisfies "state the FROM to TO mapping and the one-line fix" in prose. The dev's reason for prose rather than a labelled line is consistent with the gate: check-adr-0087-registration.mjs documents a from-to-label branch that reads such a label as a prescription and contradicts no-migration-prescription.
  • Gate verdicts on the head: Check Changeset is success. The ADR-0087 and no-major gates run inside lint.yml, whose job Lint & Repo Gates was in_progress when read; the dev reports pnpm check:adr-0087-registration exit 0 locally ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"). The CI reading of that gate is still owed by the queue.

③ Boundary flags

Dev flags (the report's deviations), each answered:

  1. FROM-to-TO label refused by the ADR-0087 gate, prose form used instead: consistent with the gate's documented branch and with the 22019 precedent. Accepted.
  2. File surface beyond the claim, two docs pages: the claim comment 6108435063 was widened by edit before the ACCEPT to name both pages. Within claim. Accepted.
  3. Dogfood pin took both options (moved to lbd_param, lbd_stash re-pointed): judged in ① item 5. Accepted.
  4. pnpm lint run as a proven narrowing (eslint's own population over the seven paths, 0 errors and 0 warnings, no type-aware or cross-file reads): the argument holds for this config; the full scan is what Lint & Repo Gates answers and it was in_progress. Accepted, CI owed.
  5. origin/main not pulled: the PR's recorded base is efcbac73cc58, the three later commits are already under it, and the API reports mergeable: true. Accepted.
  6. Scratch tests deleted, never committed: the seven-path file list carries no zz-* file. Accepted.
  7. Attribution reminder yielded to the role file: not a contract matter. Noted.
  8. Dist-reading gates re-run after the cli rebuild at HEAD: accepted.

open_questions: the report lists none, and the card's rulings leave none open (direction (A) decided in 6107382836; the sibling submitted included as that ruling asked; the pin moved as 6107432274 asked).

Out-of-scope findings, each placed:

Flags raised by this review:

  • The Responsibility line's "current authors are for the dev to count" was not answered with a number in the report; answered in ① item 4 (zero in examples/**, four in-process plugin readers unaffected).
  • The existence-probe spellings in ① item 2 are an unstated not-caught case; a one-line addition to the code comment's list would close it. Not blocking.
  • The over-refusal reason text under --strict-body names a reach the body may not make (① item 2); declared, same as the .sudo( precedent. Not blocking.

Check-runs on the head at the last read (2026-10-11T12:43Z), collapsed to the latest run per name, 35 names: 32 completed with conclusion success (all six Test Core shards, all three Dogfood Regression Gate shards and their aggregate, Dogfood Verify CLI, Build Core, Build Docs, Check Changeset, every Type Check job, Temporal Conformance, Spec property liveness, and the branch, card and single-writer guards among them), 2 completed with conclusion skipped (Console Pin Gate, Packed-tarball smoke (opt-in), both opt-in or path-gated), 0 failure, and 1 in_progress with no conclusion: Lint & Repo Gates. That one is not green; it is unfinished, and it is the job that carries the full pnpm lint scan and the ADR-0087 and no-major changeset gates, so the merge gate still waits on it. The verdict below is on the contract; the queue owns convergence of that run.

Implemented-by: claude/issue-22810-lowered-body-scope-refusal
Reviewed-by: session_01B5CHJNXuuqzChM4w6hkTN4

VERDICT: PASS


Generated by Claude Code

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