Repository navigation
feat(pr-pipeline): lane-arm the merge gate's own code on a verified pr-merge attestation - #10966
kyle-sexton wants to merge 24 commits into
Conversation
…main-run reviews Owner decision Q7 (#10743): merge.lane-arm-gate (on) lets a pull request held only by `gate` floor paths merge with no person once both review lanes are bound to the main-run review of its head with no finding and no review thread is open. merge.person-held is the merge-enforcement set that stays with a person: the pr-merge scripts that apply the holds, pr-merge.yml, ci-status, the review workflows, the main-run review's scripts and pr-verify.yml. Every `trust` path stays with a person too. - sync-hold-floor.py copies both keys into babysit_risk.py and merge-holds.mjs and refuses a person-held entry that is not a trust or gate floor row; only a literal `true` turns the switch on. - babysit_risk drops the `high-path:` hold on a lane-armed glob only when both lanes are main-run bound (LaneEvidence.main_run) with no open finding and open_threads reads 0; the gate reads the thread count only when it could lane-arm. - merge-holds.mjs holds a lane-armed path neither in ci-status nor in enable-auto-merge, and an approval never carries over a push touching one. - clear-escalations removes the human-gated label at the same head when the refusal names lane-armed paths alone and the gate's reader refuses nothing there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate docs - sync-hold-floor's lane_arm: only a literal true turns the switch on; a person-held entry off the floor, an enforcement row or a repeat stops it. - enable-auto-merge: a hold-path refusal the switch lane-armed does not relabel once the gate passes the head. - The README's gate bullet, babysit_risk's floor comment, merge-holds.mjs's header and the diff-check comment name the switch where they said a gate path always waits for a person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unk offsets sameNetChange and babysit_merge_policy.same_net_change compared patch text with each hunk header's line numbers, so a base merge editing the same file above the pull request's hunk refused to carry an approval and cost the owner a re-approval. Both now drop the header numbers before comparing; the hunk bodies, context lines, section heading, hunk count, status and previous_filename still compare exactly. The #10224 exact-carry fixtures gain a context change so they still exercise exactBaseCarry. Also: ci-lanes-pr-hold.test.sh drops a one-item loop shellcheck flags (SC2043), which blocked the pre-push check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
claude-security-review reviewed Incremental security review of critical-findings: 0 What I checked
Finding (1, IMPORTANT): the Spend: none. No manifest fields were touched, so there are no |
|
claude-security-review holds this pull request for a person (critical-security) at The PR turns on merge.lane-arm-gate in docs/conventions/pr-pipeline.yaml, which weakens a merge hold. Credentialed workflows (release-publish-plugins.yml, work-resolve-issue.yml) and agent settings and instruction files (.claude/, CLAUDE.md, AGENTS.md, .mcp.json, docs/standards/) then merge on a model-only verdict, with no person. The remaining control is a model review of attacker-influenced diff text, and the repository rulesets are outside the diff. Whether that suffices is unsettled, so it is rated CRITICAL under the fail-closed rule. claude-lane: claude-security-review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6696af5bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
claude-review reviewed Incremental review of What I checked
Finding (1, doc drift)
Observation, not counted as a finding
No cost or critical-security hold applies. No closing issues were checked against criteria in this pass. |
…unk-offset carry An independent security review of #10966 found the lane-armed set too wide. Owner decision Q11 stays intact: - merge.person-held now lists the pr-merge scripts with every module and script they import or run, the rate guard and its limits, .github/actions/report-check-run/** (widened from posting-run.mjs), the main-run review's scripts, and the review criteria and agent settings and instruction files. sync-hold-floor.py adds every gated workflow (gated_workflow), so a workflow that gains a credential is person-held at once. - plugins/review/skills/** and plugins/review/reference/**, which pr-review-main.yml copies from main as the model's criteria, are new `gate` floor rows outside the trusted-author lift, and are person-held. - With the switch on, sync-hold-floor.py refuses to leave any `gate` row lane-armed. No ci-status attestation exists yet, and the closure test only proves no person-held file loads a lane-armed path. The lane-armed set is empty. - The rule, the same-head clear and the approval-carry refusal take an injectable floor, so their tests run against a sample set. - sameNetChange and same_net_change compare patches exactly again. An evil merge could relocate an approved hunk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rkflows # Conflicts: # scripts/sync-hold-floor.py
…on the floor Remove the merge.lane-arm-gate switch, the merge.person-held list, the lane-arm scoring, the clear-escalations same-head clear and their tests: the lane-armed set was empty, so all of it was dead code. Drop the new floor rows the revision added, so the pull request adds no person hold. test_merge_holds_sync.py now checks that what every trust row and the merge-enforcement gate scripts import or run sits on a trust or gate row. The paths it reaches today that do not are pinned in OFF_FLOOR_ALLOWED for the owner's decision. Refs: #10743 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-merge attestation Owner decision Q5 option 1: babysit_risk.py and the pr-merge scripts merge with no person once both review lanes are bound to a clean main-run review of the head, no review thread is open, the hold floor does not shrink against the base, and pr-merge has attested the head. - sync-hold-floor.py generates LANE_ARMED (the merge gate's own code, trust rows included) and PERSON_HELD (every other trust and gate row); it refuses a gate row outside the merge gate's code left unlisted. - pr-merge's lane-armed job posts pr-merge/lane-armed at the head, uploads the run-scoped artifact pr-merge-lane-armed-<pr>-<head>, and re-runs the head's red ci-status job. Each attestation and refusal is a countable record line. - ci-status (merge-holds.mjs laneArmedHold) passes a lane-armed path only on that check run naming a pr-merge.yml run on the default branch that uploaded the artifact; a trusted approval still lifts it. - pr-review-main.yml reviews a lane-armed pull request after a red ci-status too, which breaks the label and status cycle the same-head clear needed. - Only the lanes App's own pull requests lane-arm; the 8413b31 floor rows are dropped. Refs: #10743 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
claude-review holds this pull request for a person (critical-security) at The head ships claude-lane: claude-review |
…ed or hand-started run The lane-armed job runs only on schedule and workflow_dispatch, so a workflow_run run of pr-merge.yml can never have attested a head. laneArmedHold now refuses one. The README states that a lane-armed merge also needs the attestation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the floor markers - laneArmedHold requires the attesting run's commit to be the default branch tip or an ancestor of it (compare API against the tip from the branches API), since head_branch is only a ref name a tag can share. - lane-armed.mjs reads floor rows only between each file's markers and refuses missing, repeated or out-of-order markers, and block comments or multi-line strings inside a region. - plugins/review/skills/** and plugins/review/reference/**, the criteria pr-review-main copies from main, become enforcement floor rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e head's text A decoy marker pair in a docstring or block comment, or a row inside an expression that drops it, fools any text parse of the head. lane-armed.mjs now reads the patch to each floor-definition file against the base's marked regions, read in the exact generated shape: a line naming a floor marker, a hold identifier outside the regions, or anything but an added row or person-held entry inside them refuses, as does a removed row, a missing patch, a renamed floor file or a cut-short file list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ebinding lines GitHub measures a pull request's patch from the merge base, so a base copy read at the moved base branch tip could place a removed row outside its region. lane-armed.mjs now takes the base copy and the patches from one compare of the base branch with the head, reading the base at its merge_base_commit. An added line in a floor file that holds a non-ASCII character (Python folds a fullwidth identifier to ASCII) or a reflective call (globals, setattr, vars, exec, eval, __dict__) refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t lines A line denylist misses locals(), an alias of globals, a call split over lines, object.__setattr__, sys.modules and the rest. floor-ast.py, run from the base checkout with python3 -I, parses the base and head copies of babysit_risk.py and runs neither. It refuses a head that references a reflective name more often than the base, or that binds a floor table or a derived set other than once at module level (a table to a literal, a derived set to the base's expression). The line rule keeps only its non-ASCII refusal; floor-ast.py joins the floor as a lane-armed row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… AST check A function's __globals__ or a frame's f_globals is the module namespace, so (lambda: 0).__globals__[...] and sys._getframe().f_globals[...] rebind a floor table past the reflective-name count. floor-ast.py now also refuses a head with more dunder names, more imports, or more frame and introspection names than the base copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the floor files floor-ast.py holds no floor table, so reading it as one refused every pull request that touched it; it is now judged for markers and hold names only. merge-holds.mjs and lane-armed.mjs regain a reflective check: the head copy may hold no more eval, Function(, globalThis, Reflect., defineProperty, __proto__, import(, require(, vm. or process.binding than the base copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lane-armed.mjs holds the tokens it counts as literals, so a head could drop one literal and add one real call. The list now sits in a js-dynamic marker block that is not counted and that no patch line may touch. eval counts only when called, so a floor row naming an eval-* workflow is no longer refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| `Object.freeze`, which no later line can rebind by name. Its head copy, and `lane-armed.mjs`'s, | ||
| also refuses when it holds more `eval(`, `Function(`, `globalThis`, `Reflect.`, | ||
| `Object.defineProperty`, `__proto__`, `import(`, `require(`, `vm.` or `process.binding` than the | ||
| base copy. Any other logic edit there stays the residual below. `floor-ast.py` holds no table: |
There was a problem hiding this comment.
This paragraph covers the new eval( wording, but not the rest of this change. lane-armed.mjs now holds its token list in a js-dynamic JS_DYNAMIC marker block. The block's literals are excluded from the dynamic-token count. Any patch line inside the block refuses, and so does a changed line that names the js-dynamic marker. The step-4 text above lists only the floor-rows and hold-floor markers. A reader of this page will not learn that editing the token list in lane-armed.mjs always refuses attestation, or why the count skips that block. Add a sentence here and name the new marker alongside the others at lines 2494-2496.
claude-lane: claude-review
| // room for a real call. | ||
| const JS_DYNAMIC = Object.freeze([ | ||
| // BEGIN js-dynamic JS_DYNAMIC | ||
| ["eval(", /\beval\s*\(/g], |
There was a problem hiding this comment.
The eval token is narrowed from /\beval\b/g to /\beval\s*\(/g, so jsRefusal no longer counts forms where eval is not directly followed by (. Examples: eval?.(src), (0, eval)(src), and const e = eval; e(src). All of these were counted before and now count as zero.
The path is a pull request that edits merge-holds.mjs or lane-armed.mjs, the files this check guards. It can add one of these forms and jsRefusal returns null. The text of an eval call can be assembled from split strings, which is the case the comment above this list describes. These forms are indirect eval, which runs in global scope. It cannot rebind the module's frozen const tables, and plain logic edits to these files are already the documented residual. That is why this is IMPORTANT and not CRITICAL.
The narrowing was made so a floor row such as .github/workflows/eval-browser-dispatch.yml counts as nothing. A pattern that keeps the alias forms and still skips eval- filenames is /\beval\b(?!-)/g.
severity: IMPORTANT
claude-lane: claude-security-review
|
Superseded by owner decision Q15 (no path-based merge holds; harden later). Path holds go off behind one config switch, merge.path-holds, on branch feat/merge-path-holds-switch. |
|
Replacement for owner decision Q15: #11058, which turns path-based merge holds off behind |
Refs: #10743
Lifted from a person by this PR
merge.lane-arm-gateistrue, so these 20 paths ship lane-armed (generatedLANE_ARMED). They are the merge gate's own code:babysit_risk.pyand every.github/scripts/pr-merge/script on the floor,floor-ast.pyincluded.Residual, accepted by owner decision Q5: a change that keeps the hold floor whole but weakens the logic of these files merges on the two bound AI reviews plus the
pr-merge/lane-armedattestation, with no person. No check reads what that logic does. The hub audits each such merge afterwards.Only the lanes App's own pull requests lane-arm. A pull request from any other author keeps these paths with a person.
Summary
Owner decisions, verbatim:
pr-merge/lane-armedcheck-run attestation that ci-status verifies, bound to a pr-merge run on main, and lane-arm the gate code when both bound review lanes are clean, with zero findings, zero threads and no hold-floor shrink. Workflows, credentials and the review criteria are not lane-armed. Owner arms continue for gate-code PRs until this PR merges.pr-review-main.ymlcopies from main): "Lets go with whatever aligns with: [standing direction]". Decision:enforcement.Still person-held
Every other
trustandgaterow (generatedPERSON_HELD):The trusted-actor list still holds through
trust-gate:trusted-actors.New floor rows
.github/scripts/pr-merge/lane-armed.mjsand.github/scripts/pr-merge/floor-ast.py: lane-armed.plugins/review/skills/**andplugins/review/reference/**:enforcement(Q-C). A change to them waits for a clean bound security review, with no person.Through the synced lists, these rows also hold the same pull requests every existing floor row holds:
hold_patternsinpr-require-checks.yml);.github/trust-path-hold.json).Fix
Lane-armed set.
scripts/sync-hold-floor.pygeneratesLANE_ARMEDandPERSON_HELDintobabysit_risk.pyandmerge-holds.mjs.LANE_ARMEDisbabysit_risk.pyplus every.github/scripts/pr-merge/floor row (lane_armable),trustrows included, lessmerge.person-held.PERSON_HELDis every othertrustandgaterow.gaterow outside the merge gate's code is missing frommerge.person-held.The gate.
babysit_risk.lane_armeddrops thehigh-path:hold on those paths only for a lanes App PR whose two lanes are bound to a main-run review with no open finding, and which has no open review thread.babysit_review_evidence.lane_source), so only a main-run review binds them.lane_arm_recordrecords the paths, the lane counts, the thread count and the first failed condition in the gate'slaneArmedrow.Attestation. pr-merge's
lane-armedjob runs only on schedule or workflow_dispatch on main. It attests at most one head per run, in three steps:pr-merge/lane-armedat the head, with the run id inexternal_idanddetails_url;pr-merge-lane-armed-<pr>-<head>;The floor check, judged on the change. The attester refuses a change that could weaken the hold floor (
lane-armed.mjsfloorChange). It never parses the head's text, because a decoy marker pair in a docstring or block comment, or a row inside an expression that drops it (.slice(0, 0),[:0]), fools any text parse.What it reads. The PR's patch to each floor-definition file (
babysit_risk.py,merge-holds.mjs,lane-armed.mjs), judged against the base's marked regions. The regions arefloor-rows FLOOR_CLASSES,hold-floor HOLD_PATH_CLASSESandhold-floor LANE_ARM. The base is main's copy, read in the exact generated shape: every line between the markers is a comment or a generated row.What refuses. A changed line that names a floor marker. Outside the regions, a line that names a floor table or a set derived from one (
FLOOR_CLASSES,HOLD_PATH_CLASSES,HOLD_PATH_FLOOR,HIGH_FLOOR,REVIEW_FLOOR,PERSON_HELD,LANE_ARM*and the rest). Inside them, anything but an added row of the generated shape or an added person-held entry. In any floor file, an added line with a non-ASCII character (Python folds a fullwidthFLOOR_CLASSESto ASCII). A removed row is a shrink.Reflection and rebinding in
babysit_risk.py, judged on the parsed trees. The base checkout'sfloor-ast.pyruns aspython3 -I <base>/.github/scripts/pr-merge/floor-ast.py <base-copy> <head-copy>. It parses both copies withastand never imports or runs either; a syntax error refuses. It compares counts between the base and the head, over everyName, attribute and imported name, so an alias (g = globals) or a split line still counts. It refuses a head with more of any of these:globals,locals,vars,setattr,delattr,getattr,exec,eval,compile(the builtin, notre.compile),__import__,importlib,__builtins__,builtins,__dict__,__setattr__,__delattr__,modules,__class__and__getattribute__.f_globals,f_locals,f_back,_getframe,currentframe,gi_frame,cr_frame,ctypes,gcandinspect. A frame's globals are the module's.__, such as a function's__globals__.importorfromstatement.A logic edit that adds none of these is accepted. These are counts: a dunder, import, frame or reflective reference refuses only when its count rises above the base's. Trading an existing reference for a new one passes. For example, a head that removes one existing
getattrand addsgetattr(lambda: 0, "__glob" "als__")[...]keeps every count. The JS count has the same limit:[].constructor.constructor(...)and a prototype patch such asArray.prototype.some = ...pass. All of these are the Q5 residual: a logic edit merges on the two bound reviews plus the attestation, and only a check that judges logic closes this class.It also refuses a head where a floor table (
FLOOR_CLASSES,LANE_ARM_GATE,LANE_ARMED,PERSON_HELD,COST_PATHS) or a set derived from one (FLOOR,HIGH_FLOOR,REVIEW_FLOORand the rest) is bound other than exactly once, at module level, by one plain or annotated assignment. A table must be bound to a literalast.literal_evalaccepts, and a derived set to the base's own expression. Any other binding refuses: augmented,del, afor,with,exceptormatchtarget, an import alias,globalornonlocal, a walrus, an argument, adefor aclass. The base's tables use annotated assignments, so those are allowed too.The JS floor.
merge-holds.mjsbinds its tables withconstandObject.freeze, which no later line can rebind by name. Its head copy, and the head copy oflane-armed.mjs, also refuses when it holds more of these than the base copy:eval(,Function(,new Function,globalThis,Reflect.,Object.defineProperty,__proto__,import(,require(,vm.andprocess.binding(jsRefusal). The tokens are counted over the whole text, so splitting a string around a table name does not help. Each token is call-shaped or not a path segment, so a floor row naming aneval-*workflow counts as nothing.lane-armed.mjs's own token list sits in ajs-dynamicmarker block: the block is not counted, and a patch line inside it or naming its markers refuses, so a literal cannot be traded for a real call. Any other JS logic edit stays the Q5 residual.Judge code.
floor-ast.pyholds no floor table. Its patch is judged for floor markers and hold names only, and its logic edits are the Q5 residual.Where the base is read. The base copy and the patches come from one compare of the base branch with the head (
GET /compare/<base>...<head>), and the base copy is read at itsmerge_base_commit, the commit GitHub measures the patch from. A merge base that cannot be read refuses.Unreadable input. Only a floor file with status
modifiedis judged; a renamed, added or deleted one refuses. A missing patch (GitHub omits one for a large or binary change) refuses. A compare file list shorter than the pull request'schanged_filesrefuses; the compare lists at most 300 files. A line inside a region with a CRLF ending is not the generated shape, so it refuses.What still passes. Pure row additions and logic edits that name no hold.
ci-status.
merge-holds.mjslaneArmedHold, loaded from the base commit and keyed on the PR head (so merge_group runs check the same thing), passes a lane-armed path only when all of these hold:github-actions, succeeded, and carries a record of this PR and head that covers every lane-armed path changed;.github/workflows/pr-merge.ymlon the default branch, in this repository, started by schedule or workflow_dispatch (never workflow_run), and running or succeeded;/branches/<default>, or an ancestor of it: the compare API returnsidenticalorahead.head_branchis only a ref name, and a tag can share the branch's name;Any workflow with
checks: writeposts asgithub-actionsand can copy a real run id, so the run and its artifact are the proof, not the check run. Anything missing or unreadable holds. A trusted approval of the head still lifts the hold.Cycle.
pr-review-main.ymlalso runs after a red pr-require-checks run, for a PR whose full file list matches main's lane-armed globs. A list of 3000 or more files reviews nothing. Neither the red status norneeds-humanblocks the review, so the same-head clear in clear-escalations stays.Measurement (§9). Each lane-armed merge is preceded by exactly one countable attestation record, the line
lane-armed {"v":1,"pr":N,"head":"<sha>","paths":[...],"lanes":{"claude-review":0,"claude-security-review":0},"threads":0,"run":<id>}. It is recorded in two places:pr-merge/lane-armedcheck run's summary at the head;::notice::in the pr-merge run.The same record is uploaded as the run's
pr-merge-lane-armed-<pr>-<head>artifact. Each head the job declines logs::notice::lane-armed refused {...,"reason":"..."}.Not done. Showing the person hold as neutral in ci-status is skipped: pr-review-main reads the workflow conclusion, so a neutral-looking annotation changes nothing it decides.
Verification
node --test .github/scripts/pr-merge/*.test.mjs .github/scripts/pr-review-main/*.test.mjs: 708 passed. These include:Attestation forgery. Another app; a copied genuine pr-merge run with no artifact for this head; an expired artifact, or an artifact of another run, name or PR; a run of another workflow, from a PR branch, from pull_request_target, pull_request or workflow_run, failed, queued, in a fork, or with another id; a run whose commit is not on main, which covers a tag named
main; a record for another head, run, version or PR, or one missing a path; each read failing. Mutating away the branch, app, event, artifact or on-branch check fails them.The floor judge (
floorChangeandfloorRefusal,.github/scripts/pr-merge/lane-armed.mjs). Patches come fromgit diff. Each of these refuses: a Python docstring decoy; a JS block-comment decoy; a LANE_ARM decoy;.slice(0, 0);[:0]; a removed or weakened row; a moved marker; aHOLD_PATH_CLASSES = []re-definition; a reboundFLOOR; a newly lane-armed glob; a dropped person-held entry; a fullwidth identifier; a removed last row when main has moved past the merge base; a missing patch; a renamed file; a cut-short list; an unreadable merge base. Each of these is accepted: a pure row addition, a person-held entry, a logic edit, and aRE.exec(method call.floor-ast.py, against the realbabysit_risk.py. Each of these refuses:locals()[...], an aliasg = globals,(globals\n)()split over lines,builtins.setattr,object.__setattr__,sys.modules[__name__].__setattr__,__import__(...).exec,importlib,getattr, thecompilebuiltin,__builtins__;(lambda: 0).__globals__[...],sys._getframe().f_globals[...],inspect.currentframe().f_globals[...], an addedimport helper;with, import-alias, walrus,def, argument,delorglobalbinding of a floor name;FLOOR, a non-literalLANE_ARM_GATE, and a syntax error.A logic edit that uses
re.compile, a pure logic edit inside an existing function, and a row addition are accepted.The JS count, and
floor-ast.py. An addedeval,globalThis,Function(,Reflect.,Object.defineProperty,__proto__,import(orrequire(refuses, and so does anevaladded tomerge-holds.mjsend to end. A logic edit is accepted, and so is a name likeevaluate. A logic edit tofloor-ast.pyis not refused for an unreadable base. The previous checker accepts the__globals__,_getframeandimport helpercases. End to end, alocals()rebinding that the patch rules accept is refused throughfloor-ast.py. The previous text-parsing floor check accepts the block-comment decoy.scripts/ci-lanes-pr-hold.test.sh: PASS=243 FAIL=0.scripts/ci-review-lanes.test.sh: PASS=140 FAIL=0.scripts/sync-hold-floor.py --checkexits 0 at this head, which shows only that the generated copies match. It is not a control on a lane-armed PR: it runs the head's owntest_merge_holds_sync.py(follow-up 1). actionlint is clean.Known follow-ups (MEDIUM, defense in depth over the Q5 residual)
These ship in a follow-up PR right after this one merges.
sync-hold-floor.py --checkruns through the head'stest_merge_holds_sync.pyintest-python, and the tests directory has no floor row, so a PR can skip or weaken it.constructor,prototype,getPrototypeOfandsetPrototypeOftoJS_DYNAMIC. Refuse agetattrorsetattrwith a non-literal name, and any string concatenation that joins into a dunder or a table name.FLOOR_FILES.clear-escalations.mjsandenable-auto-merge.mjsread the floor tables outsideFLOOR_FILES. A logic change there is the accepted Q5 residual.Related
lib/publish-gate.mjs: the main-run review's verdict writer imports it, and it sits on no floor row. Recorded on fix(source-control): close five pre-existing secret and hold-path detection gaps (hardening deferred) #10943; no row added here.🤖 Generated with Claude Code