Repository navigation
platform-admin re-anchor L5 (migration): time-boxed legacy-grant dual read, then its removal one minor later #11975
Description
Activity
分诊解锁 ·
pm:blocked→pm:queue—— 阻塞已过期本轮「停放面专项」由
check-half-states.mjs的 H19 扫出:本卡的Blocked-by:目标已全部关闭,阻塞比阻塞方活得久。⛔ 本席没有采信 sweep 的那一行,逐个目标直读复核:
Blocked-by:目标直读结果 #11974(L4)closed / completed ⇒ 声明的前置条件已全部兑现 ⇒ 解除阻塞,回
pm:queue。⚠️ 本席只解除了「机器可读的那条阻塞线」。若本卡另有未写进Blocked-by:的实际前置(设计未定、上游 PR 在飞、等某人一句话),⇒ 请补一条Blocked-by:或Restart-when:再挂回pm:blocked,⛔ 不要只把标签改回去 —— 一条没有机器可读出口的阻塞,与被遗弃无法区分(H9 的判据)。⚠️ 链上串行提示(⛔ 不是阻塞,是顺序建议)本卡是
platform-admin re-anchor链的 L5。本轮同批解除的还有 #11976(L7 docs) 与 #11977(L8 cloud seam),三张的Blocked-by:都已清。⇒ 三张现在同时可派,但 L5(迁移)先落地、L7(文档)才写得准。顺序由 services / devx 两车道自行商定,⛔ 分诊不代排。
另:#13515(L5 的 removal 半边)本轮实测仍未分诊,同族,建议一并读。
Generated by Claude Code
Claim: PM loop round 9 — verification + disposition slice
Session:session_016ZC5rNQj3WEet5HAmmAkMs
Branch:claude/issue-11975-l5-migration-window
Worktree:objectstack-issue-11975
Domain:domain:services
File surface: expected small or empty — a pin and/or a doc line inpackages/core/src/security/, or nothing. ⛔ No behaviour change (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-opus-5— judgement tier. This is a verification-and-disposition slice, not a build.
Clause-②: no — nothing here changes a published shape.⚠️ Re-declare and stop if a gap-fill would.
Serial constraints cleared: ✅Blocked-by: #11974is genuinely discharged — L4 merged asb9972720f(verified by content onorigin/main, ⛔ not by the branch being gone). ⛔plugin-authis held by #11973 (L3), dispatched this same round — if this card's work reaches intoplugin-auth, stop and report; I will sequence rather than let the ladder's two legs contend.⚠️ Read this before planning: the card's ENTRY half may already be deliveredMeasured by this seat on
origin/mainbefore dispatching:DUAL READ IS LIVE: resolve-authz-context.ts:724 ps.name === ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id) → hasPlatformAdminGrant = true (legacy row) resolve-authz-context.ts:780 hasPlatformAdminGrant = true (config anchor) DEPRECATION REPORTER EXISTS AND IS EXPORTED: core/src/security/index.ts:204 reportLegacyPlatformAdminGrant core/src/security/platform-admin.test.ts:247 describe('[#11663 P5] reportLegacyPlatformAdminGrant', …) CONTROL: PLATFORM_OWNER_EMAIL present in admin-standing-surface.ts / index.ts — the greps read⇒ The window's ENTRY (dual read live + deprecation log firing) looks delivered by L2 and L4. And the window's EXIT is a different card — #13515, filed by the L4 dev at landing, carrying
Blocked-by: #11975.⚠️ So the honest question this slice answers is: what, if anything, does #11975 itself still owe? Its body says it "carries the window's discipline", and its acceptance criterion is that entry and exit are "both verifiable onorigin/mainhistory, each as its own reviewed change".Your job — in this order
- Verify the entry half against the tree, ⛔ not against my measurement above. I read four lines; you own the claim. In particular check the three disciplines the card names:
- derivation prefers env — is the config anchor actually preferred, or merely additive?
- a detected legacy grant logs its deprecation pointer every boot —
⚠️ the L4 dev described a once-per-process latch. "Once per process" and "every boot" are the same thing for a fresh process and different for a long-lived one. Measure which it is and say whether that satisfies the card. - ⛔ never a silent dual-track — is there any path where the legacy grant confers standing with no deprecation line?
- Establish the minor boundary platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515 must honour: which released minor carries L4, and therefore which minor the exit may land in. ⇒ Record it on platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515, which is blocked on this card and needs exactly that number.
- Then dispose of this card, honestly, on what you found.
⭐
premise_still_valid: falseis an EXPECTED and valuable outcome hereIf the entry is complete and correctly disciplined, then #11975 is satisfied by L2+L4 and its only live residue is #13515. Reporting that — with the measurements — is a full delivery, ⛔ not a failure, and it is probably the single most useful thing this slice can produce: it unblocks #13515 and closes a ladder rung honestly.
⛔ Do not manufacture work to justify a PR.
⚠️ And equally: ⛔ do not report "satisfied" because the pieces appear present — the three disciplines above are the test, and one of them (every-boot vs once-per-process) is a real distinction that could go either way.If you find a genuine gap
Fill it narrowly — a pin that would go red if the discipline regressed, or the one missing loud line. ⛔ No refactor, ⛔ no behaviour change, ⛔ nothing that belongs to L3 (
plugin-auth, dispatched in parallel) or to #13515 (the exit — ⛔ do not delete the legacy read here; the card says removal is never a rider, and it lands one minor later).Gates
Re-derive from your worktree at
origin/mainand your actual diff — including the empty case (an empty changeset owes no families).⚠️ Read BOTH sections ofdispatch-gates.mjsoutput, the path-derived block and the kind-gates section; two devs tonight each dropped one and were reddened in CI by a family the tool had named (#13642).Report
Terminal report as a comment on this issue first, then as your return message. Machine-checkable fields only.
Required regardless of disposition: the three discipline readings (env-preference · every-boot-vs-once-per-process · no-silent-dual-track), each with its evidence, and the minor boundary you established. Those are what #13515 and the next PM read; the code change, if any, is secondary.
Generated by Claude Code
- Verify the entry half against the tree, ⛔ not against my measurement above. I read four lines; you own the claim. In particular check the three disciplines the card names:
os-dev-report
{ "issue": 11975, "status": "done", "branch": "claude/issue-11975-l5-migration-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/13666", "premise_still_valid": false, "summary": "VERIFIED, not assumed: the migration window's ENTRY half is already delivered by L2 and L4, and all three disciplines this card names hold on origin/main. The card's own acceptance criterion additionally requires the EXIT on main, which is #13515 and cannot land before 17.4.0 — so this card's only live residue is that card, exactly as the dispatch anticipated. One genuine gap was found and filled narrowly: every existing pin asserts the pointer's SUPPRESSION and none asserts that the suppression ENDS, so a latch hardened onto globalThis or persisted would turn 'once per process' into 'once ever' with every pin still green. PR #13666 adds one pin (34 lines, one test file, no behaviour change) that goes red on exactly that regression. The legacy read was NOT deleted — that is #13515's, one minor later. plugin-auth was never touched (L3's fence held; its worktree is live in this container).", "discipline_readings": { "env_preference": "SATISFIED — precedence, not exclusion. resolve-authz-context.ts:778-792 is `if (configConfersPlatformAdmin) { hasPlatformAdminGrant = true; push ADMIN_FULL_ACCESS_CAPABILITIES } else if (hasPlatformAdminGrant) { report }`: the config arm is evaluated first, confers standing with the DECLARED envelope, and the legacy arm is reachable only when config does not confer. Pinned by resolve-authz-context.platform-admin-config.test.ts 'does NOT nag when the SAME user also resolves through the config anchor'. PRECISION THE PM SHOULD CARRY FORWARD: with administrators declared, a NON-matching caller's legacy row is still honoured — additive, which is design section 5 step 3's ruled shape ('Config-derived standing is added; the legacy grant read is still honoured. Nothing is revoked in this step'). Exclusion is the EXIT, not this card.", "every_boot_vs_once_per_process": "MEASURED — once per process, and a process IS a boot, so the card is satisfied. The latch is a module-scope `let legacyGrantPointerSaid` in packages/core/src/security/platform-admin.ts:335. Ran three fresh module instances via vi.resetModules() + dynamic import (closest in-process stand-in for a boot): lines per boot = [1,1,1], with two calls inside each instance still producing 1. Nothing persists the suppression past the process, so a restarted deployment IS told again. Two disclosed, RULED divergences from a literal per-detection line, both matching design P5 verbatim ('logged once per process (not per request) naming the row, its holder and the exact config line'): (a) a second boot inside ONE process is silent — deliberately pinned in plugin-security bootstrap-platform-admin-walled-owner.test.ts 'EXACTLY ONE line ... even across repeated bootstraps'; (b) a second distinct holder is never named — measured [1 line, usr_B not named], and the message text itself says 'Reported once per process; further holders are not listed'. Enumerating holders is the census's job (design section 5 step 7), not this pointer's.", "no_silent_dual_track": "NONE FOUND. Two detectors share one latch and cover both entry points: boot-side plugin-security/src/bootstrap-platform-admin.ts:399-407 (walled) and request-side resolve-authz-context.ts:785-792 (posture-independent, no early return can skip it). hasPlatformAdminGrant is written in exactly two places (:724 legacy, :780 config) and consumed once by derivePosture(:809); a repo-wide grep for 'PLATFORM_ADMIN' assignment sites finds no third route (explain-engine.ts:496 DERIVES from the resolved posture, it does not re-derive). The cross-request grants cache is in-process and default-OFF, so it cannot carry standing across a boot without a derivation having run. The one path where a legacy row confers with no line is the SAME caller also matching config — where standing no longer rests on the row, already pinned." }, "minor_boundary": "L4 ships in 17.3.0; the exit (#13515) may land no earlier than 17.4.0. Evidence: latest released tag is @objectstack/plugin-security@17.2.0 (and @objectstack/core@17.2.0) at commit e7d2cc67f, 2026-08-23. L4 = b9972720f, 2026-08-31 — `git merge-base --is-ancestor b9972720f '@objectstack/plugin-security@17.2.0^{commit}'` is FALSE, so L4 is not in 17.2.0. Its changeset .changeset/walled-bootstrap-stops-granting.md is still unconsumed on main and declares '@objectstack/plugin-security': minor. `grep -l '\": major' .changeset/*.md` matches 0 of 528 pending changesets, so the next release is 17.3.0, not 18.0.0. L2 landed 2026-08-30, also after the tag, so it rides the same train. RECORDED ON #13515 as issuecomment-5474330794, with the caveat that the boundary is anchored to whichever release consumes that changeset, not to the literal string 17.4.0.", "tests": "All run at d0c912847, the final commit; worktree clean (git status --porcelain = 0). (1) TARGETED: `pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/platform-admin.test.ts src/security/resolve-authz-context.platform-admin-config.test.ts src/security/admin-standing-surface.test.ts` -> 'Test Files 3 passed (3) / Tests 53 passed (53)', exit captured before any pipe. (2) ABLATION of the new pin, committed FIRST then mutated: latch rewritten to a globalThis-keyed one. Mutation confirmed ON DISK before measuring — HEAD blob c2abfe576bd5dea92d68615cbe031f3a99fae5a0 vs worktree blob 8e9478c36b3e478d39ec046307fc42724ebeffc9, injected marker '__osLegacyPtr' count 3, original 'let legacyGrantPointerSaid = false;' count 0. Result: the new pin FAILED ('AssertionError: expected [ +0, +0, +0 ] to deeply equal [ 1, 1, 1 ]') while the other 25 tests in the file stayed green — which is itself the proof that this was the only pin covering the every-boot half. REBUILD: none needed on either leg, and this is stated rather than skipped — the test imports './platform-admin.js', a package-relative specifier vitest resolves to source, not through exports/dist, so no dist could go stale; the red is the proof the mutation reached the code under test. RESTORE LEG verified by state observation, not by exit code: worktree blob back to c2abfe57..., marker count 0, `git diff HEAD` empty, `git status --short` empty. (3) GATES re-derived from the ACTUAL diff with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed; script derived its own change set), BOTH sections read: 22 path-derived + 6 convention-triggered under change kind 'adds or edits a test file' = 26 commands harvested with --commands. 23 green. TWO returned exit 3 = NOT MEASURED, each self-declared, neither a red and neither reported as one: check-test-completeness.mjs ('the local reading for this gate is NOT MEASURED ... It is not a red') and check:dual-build-cjs-loads ('PREREQUISITE NOT MET ... This is NOT a pass: nothing was measured'). (4) `pnpm lint` = `eslint . --no-inline-config`, WHOLE REPO, no narrowing claimed or needed — exit 0. (5) check:type-check-coverage green. Its --re-measure half (check:type-check-debt) needs a whole-workspace build and was NOT run locally — reported as not run, not as green. What WAS measured: packages/core has NO typecheck script (so `pnpm --filter @objectstack/core typecheck` would be a zero-match exit-0 phantom and was deliberately not used); instead `tsc --noEmit --listFiles` directly — the new test file IS in the program (525 files, 1 match; packages/core sets exclude: []) and contributes 0 of the package's 98 pre-existing errors, so this diff cannot ratchet the count up. CI runs the re-measure. (6) check:nul-bytes green (7551 files); `grep -naP '[控制字符类]'` self-scan over the edited file and over the PR body finds nothing. (7) PR body re-read in full after creation: byte-identical to what was written apart from the platform's own auto-appended session footer. (8) skip-changeset label written via the additive REST POST (probed first: repo-scoped REST is 200 for this session, not 403) and READ BACK: ['size/s', 'skip-changeset'] — union holds, nothing stripped by the size-labeler.", "mcp_calls": "8", "open_questions": [], "out_of_scope_findings": [ "filed as #13667: the P5 deprecation pointer fires on `single`-posture rigs (the DEFAULT posture), telling operators their first-user promotion row 'is removed in a later release' and to declare OS_PLATFORM_OWNER_EMAIL — both false under ruled Choice 4A, which pins the single promotion as permanent AND as never consulting that variable. The boot-side detector IS posture-gated (early-returns already_have_admin for !walled); the request-side call site is not. Wrong-advice class, not access-control: standing itself is correct in every arm. Not patched here because the fix is a behaviour change this card fenced off, and because its correct direction depends on the unruled #11979 / Choice 4B fork. Bears on #13515's `single` carve-out clause." ] }
Generated by Claude Code
✅ ACCEPT — PR #13666.
premise_still_valid: falseis the correct verdict, and this card is satisfieddomain:servicesPM seat, sessionsession_016ZC5rNQj3WEet5HAmmAkMs, R9. Reviewer of record. Verified against the tree, ⛔ not against the report.⭐ The gap it found is the kind a green suite hides
Every existing pin asserts the deprecation pointer's suppression — "exactly one line even across repeated bootstraps". None asserted that the suppression ENDS.
⇒ A latch hardened onto
globalThis, or persisted, would silently turn "once per process" into "once ever" — and every existing pin would stay green while a restarted deployment was never told again. That is precisely the discipline this card exists to hold, and nothing was watching it.⭐ The ablation proves the claim rather than asserting it: with the latch rewritten to
globalThis-keyed, the new pin failed (expected [ +0, +0, +0 ] to deeply equal [ 1, 1, 1 ]) while the other 25 tests in the same file stayed green. ⇒ It really was the only pin covering that half. 34 lines, one test file, ⛔ zero behaviour change.The three discipline readings — verified, and one of them is more precise than the card
discipline verdict env preference ✅ Precedence, not exclusion. :778-792evaluates the config arm first and reaches the legacy arm only when config does not confer.⚠️ Precision worth carrying: with administrators declared, a non-matching caller's legacy row is still honoured — additive, which is design §5 step 3's ruled shape ("Config-derived standing is added; the legacy grant read is still honoured. Nothing is revoked in this step"). Exclusion is the EXIT, ⛔ not this card.every boot vs once per process ✅ Measured, and the card is satisfied. Module-scope latch at platform-admin.ts:335. Three fresh module instances viavi.resetModules()+ dynamic import ⇒ lines per boot[1,1,1], two calls inside each still 1. Nothing persists past the process, so a restarted deployment IS told again. Two divergences from a literal per-detection line are disclosed and both match design P5 verbatim ("logged once per process (not per request)"): a second boot inside one process is silent (deliberately pinned), and a second holder is never named (the message says so, and enumerating holders is the census's job, §5 step 7)no silent dual-track ✅ None found, and answered structurally: two detectors share one latch covering both entry points; hasPlatformAdminGrantis written in exactly two places (:724legacy,:780config) and consumed once at:809; a repo-wide grep finds no third route (explain-engine.ts:496derives from the resolved posture rather than re-deriving). The one path where a legacy row confers with no line is the same caller also matching config — where standing no longer rests on the row, already pinnedThe minor boundary — verified by this seat
merge-base --is-ancestor b9972720f (L4) ⊂ @objectstack/plugin-security@17.2.0 → FALSE major changesets among the pending set → 0⇒ L4 rides the next minor (17.3.0); the exit may land no earlier than 17.4.0. ✅ And recorded on #13515 (
issuecomment-5474330794) with the right caveat: the boundary is anchored to whichever release consumes that changeset, ⛔ not to the literal string.⇒ Disposition: this card is satisfied
The entry half is delivered by L2 + L4, all three disciplines hold, and the one real gap is now pinned. The only live residue is #13515, which is
Blocked-by:this card and now has the boundary number it needed.⇒ On PR #13666's merge this card closes, and #13515 becomes dispatchable at the 17.4.0 boundary. ⛔ The legacy read was correctly not deleted here — that is #13515's, one minor later, never a rider.
⚠️ #13667 — verified by this seat, and it lands on the DEFAULT postureThe dev filed it; I checked the load-bearing line rather than forwarding it:
resolve-authz-context.ts:785 } else if (hasPlatformAdminGrant) { … reportLegacyPlatformAdminGrant({ userId, email: userRow?.email });
No posture gate. The boot-side detector is gated (it early-returns
already_have_adminfor non-walled), but this request-side call site is not. ⇒ On asingle-posture rig — the default — first-user promotion creates exactly such a grant row, and the pointer tells those operators their row "is removed in a later release" and to declareOS_PLATFORM_OWNER_EMAIL.⚠️ Under ruled Choice 4A both statements are false for them: 4A pins the single promotion as permanent and as never consulting that variable.⇒ Wrong-advice class, ⛔ not access control — standing itself is correct in every arm. Correctly not patched here: the fix is a behaviour change this card fenced off, and its direction depends on the unruled #11979 / Choice 4B fork. ⭐ It also bears directly on #13515's
singlecarve-out clause, which is the connection only someone holding both cards could have drawn.Checklist
item verdict Scope ✅ one test file, +34. ⛔ Zero plugin-auth(L3's fence held while L3 is live in the same container), zero behaviour changeLegacy read ✅ not deleted — correctly left to #13515 Clause ② ✅ no skip-changeset✅ applied and read back twice — union held, the size-labeler stripped nothing Gates ✅ 26 commands from both sections (22 path-derived + 6 convention-triggered), 23 green, 2 self-declared NOT MEASURED (⛔ neither reported as green), and check:type-check-debtreported as not run rather than greenCI ⏳ pending ⭐ One methodological note worth keeping: it declined
pnpm --filter @objectstack/core typecheckbecause that package has no typecheck script — which would have been a zero-match exit-0 phantom — and rantsc --noEmit --listFilesdirectly instead, confirming the new file is in the program (1 match of 525) and contributes 0 of the package's 98 pre-existing errors. ⇒ A green that would have meant nothing was refused in favour of one that means something.Card stays
pm:dispatcheduntil MERGED.
Generated by Claude Code
落地收口 — PR #13666 已合并(
1467b19f6)。pm:dispatched→pm:blockedBlocked-by: #13515
domain:servicesPM seat #6021,R10。与确认 MERGED 同一动作收口,⛔ 不留给下轮巡检。本卡三条纪律的交付状态(全部对
origin/main实测,非假设):纪律 状态 derivation prefers env ✅ 已满足 —— 但含义是推导内的优先级,不是排他:声明了管理员之后,一个不匹配的调用者仍然被其遗留行认可。这是裁定形状(设计 §5 step 3:配置来源的资格是追加,本步不撤销任何东西) 每次启动都说一次,不是每进程一次 ✅ 已测量,且本 PR 补上了唯一没被钉住的那一半 —— 闩随进程而死。三个全新模块实例各发一行,断言 [1,1,1]:0 抓「闩活过了进程」,2 抓「指针退化成每次调用」永不静默双轨 ✅ 未发现 本卡的验收准则还要求出口落在
origin/main—— 那是 #13515,而本 PR 顺带把它的发版边界钉死了:17.2.0切于e7d2cc67f(2026-08-23),L4 落地于b9972720f(08-31,在该 tag 之后),其 changeset 声明minor且未消费,.changeset/里没有任何major⇒ L4 走 17.3.0,出口最早 17.4.0。⇒ 本卡唯一残留即 #13515,且它是发版节奏约束而非技术约束。
Blocked-by:行已按正典家写进本评论(机器可读通道)。⛔ 不反射式放行:#13515 关闭时先重新推导。
Generated by Claude Code
State repair:
pm:on-hold→pm:blocked— same shape as #11978, repaired in the same rounddomain:servicesexecution seat, 2026-09-11T19:58Z. ⛔ Not a re-derivation, ⛔ not a release: the blocker, the work and the acceptance criterion are all unchanged. Only the label is corrected.Measured
reading value labels (read 18:25Z and 19:55Z) domain:services·pm:on-hold·pm:blockingbody Blocked-by:Blocked-by: #13515(one line; #13515 is OPEN)body Restart-when:/Restart-touch:NONE / absent assignees · PR refs none · none pm-label events (full page, filter applied before truncation, single page) 8, the last being pm:blockedadded 2026-08-31T15:29:31Z byos-steve, thenpm:blocking— ⛔ no event addspm:on-holdpm:on-holdis legal only with a machine-readableRestart-when:line; a wait on another issue ispm:blocked+Blocked-by:. This card carries theBlocked-by:line and no restart condition, and its own body says the same thing in prose — "The live blocker is #13515, this card's EXIT half" and "The only live residue is the exit, #13515."⚠️ The live label set disagreeing with a complete event log reads identically on #11978 and #15556. ⛔ The mechanism is not established here — see the fuller note on #11978 (issuecomment-5639937182), including the one operational consequence: on these cards a pm-state cannot be audited from the events API, so read the live label set as authority.Why this is the right half of the pair to move
This card's body already states the nature of its wait, verbatim: "#13515 waits on a release boundary, not on work: L4 rides 17.3.0, so the exit lands no earlier than 17.4.0." ⇒ The release-boundary hold belongs to #13515, and measured this round, #13515 already declares it properly — a real
Restart-when:naming a@objectstack/plugin-securityminor greater than the one carrying L4 (PR #13514), with its 2026-08-31 measurement, plus aRestart-touch:, and noBlocked-by:. ⇒ #13515 is correct as it stands and is deliberately left untouched. This card is not the hold; it is the waiter.The write
One label replace:
pm:on-holdremoved,pm:blockedadded;pm:blockinganddomain:servicesuntouched; assignees passed back empty. Target computed from the label set read immediately before the write and diffed againstunion(read, delta)on read-back.
Generated by Claude Code
Addendum —
Restart-when:reading widened to both channels (⛔ conclusion unchanged)issuecomment-5639940830above reported "bodyRestart-when:/Restart-touch:— NONE". True, but too narrow to carry the legality claim: a restart condition is legal in either channel — body or comment — and the patrol's H9 predicate reads both.Re-measured 2026-09-11T20:56Z across all 6 comments on this card: no
Restart-when:condition in any of them. The one near-hit,issuecomment-5473780552(2026-08-31), is prose asking for such a line rather than stating one — 「请补一条Blocked-by:或Restart-when:再挂回pm:blocked,⛔ 不要只把标签改回去 —— 一条没有机器可读出口的阻塞,与被遗弃无法区分(H9 的判据)」 — which points the same way as the repair: this card's machine-readable exit is itsBlocked-by: #13515, sopm:blockedis the legal state. Control: 4 comments mentionpm:, so the query answers.⇒ The repair stands as written. ⛔ No label, blocker or chain change. Full note on the instrument itself is on #11978 (
issuecomment-5640541722).
Generated by Claude Code
The one machine-readable exit on this card now names a number that does not resolve — raising
pm:retriagedomain:servicesexecution seat (#6021) · patrol read 2026-09-15T14:40Z · ⛔ no state label changed, ⛔ no body edit, ⛔ not a release and ⛔ not a re-derivation of the blockMeasured, three independent channels
probe #13515control #18275GET /repos/objectstack-ai/objectstack/issues/13515(session token)404 200 GET …/pulls/13515(same token, same loop)404 200 https://github.com/objectstack-ai/objectstack/issues/13515(unauthenticated)404, empty redirect_url302 → /pull/18275Further controls answering 200 on the same loop: #16099, #17512, #9857. ⇒ ⛔ Not a token-scope artefact (the unauthenticated channel agrees) and ⛔ not a transfer (a transferred issue keeps a redirect at its old URL; there is none).
This does not impeach the reading that put the line there
issuecomment-5639940830(2026-09-11T19:58Z) read #13515's body — a realRestart-when:naming a@objectstack/plugin-securityminor above the one carrying L4, aRestart-touch:, noBlocked-by:— and left it untouched as the correct carrier of the release-boundary hold. ⭐ That was accurate when taken. The number stopped resolving afterwards, and nothing in this repo changed to cause it.What it costs this card
Blocked-by: #13515is this card's only machine-readable exit, and the unlock scan cannot fire on a target it cannot read. Anchor #9857's sweep 2026-09-15T13:48Z carries this card as an H19 UNJUDGED row — 「an input this patrol could NOT read, not a state it read and found clean」. ⇒ ⛔ This card can no longer leavepm:blockedby any scan, in either direction, until its exit is restated by a person.The chain terminates here: #11973 ← #11978 ← this card ←
#13515(unreadable). #11978 and #11973 are both parked behind this card (H26 rows on the same sweep), so this is the root, which is why the annotation goes here and ⛔ not on all three.The question for triage — ⛔ this seat does not answer it
- Restate the exit here. This card's own body already carries the substance verbatim: "platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515 waits on a release boundary, not on work: L4 rides 17.3.0, so the exit lands no earlier than 17.4.0" ⇒ a
Restart-when:naming that minor would make the card machine-releasable without platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515.⚠️ Whether that boundary still reads 17.4.0 is NOT MEASURED here. - Re-file the exit platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515 carried and re-point this line at the new number.
- Withdraw the leg.
Each changes what the two cards above this one mean, so ⛔ none of the three is a patrol's call.
Class, ⛔ not a second filing: numbers that resolved and then stopped are #17512 (
domain:devx,pm:blocked, gatecheck-issue-citations.mjslanded at1bdbf82cbbut not yet called by anything). This instance is recorded there as evidence, together with three merged pull requests in the same condition.
Generated by Claude Code
- Restate the exit here. This card's own body already carries the substance verbatim: "platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515 waits on a release boundary, not on work: L4 rides 17.3.0, so the exit lands no earlier than 17.4.0" ⇒ a
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 15, 2026 huangyiirene commented
on Sep 20, 2026 CollaboratorMore actionsUnlock scan — the body's
Blocked-by:was STALE and has been rewritten to the live blockerdomain:servicesseat,session_01AhQASwqJr2Z7XfGWUdvnbF,Seat: domain:services#1, 2026-09-20T00:20Z. ⛔ Body line only: ⛔ no label written, ⛔ no state changed, ⛔ no assignee. This card keepspm:blocked.What the scan found
This card was a false unlock candidate. Its body named
#18336, which closed 2026-09-19T01:24:02Z — so an unlock scan reading the body alone would have judged every blocker discharged and returned this card topm:queue.It is ⛔ not unblocked. A comment on this thread carries
Blocked-by: #13515, and #13515 is open (priority:p2securitydomain:servicespm:on-holdpm:blocking). ⇒ the real prerequisite is live; only the body's record of it was stale.⚠️ This is the shapecheck-half-statesnames as H28 — the written half of a RE-PARK: 「a seat found the body's upstream closed, carded the real prerequisite, and wrote the new blocker into a comment (the cheaper, safer write) without spending the body line」 — and it records what that costs: 「one such card was released topm:queuewhile its real blocker was open and dispatched」.What was changed, and what was not
The body line now reads
Blocked-by: #13515. ⭐ That is H28's own named remedy: 「rewrite the body line to name the live blocker (the comment stays as history), so the next reader — human or sweep — finds the current wait in the canonical home」. The comment is ⛔ untouched and remains the history of how the block moved.⛔ Nothing else moved: the card keeps
pm:blocked, no label was written, no assignee was set, and ⛔ no release was made — the release is ⛔ not this act's to make, and on the readings above it would be wrong.Readings, taken 2026-09-20T00:19Z
- body before:
#18336· target state closed,closed_at2026-09-19T01:24:02Z - comment carrier:
Blocked-by: #13515· target state open - body after (read back from the API):
Blocked-by: #13515
Generated by Claude Code
- body before:
huangyiirene commented
on Sep 20, 2026 CollaboratorMore actions🔴 席位更正 —— 我在 00:20Z 改错了本卡正文的
Blocked-by:,已还原domain:services席位,session_01AhQASwqJr2Z7XfGWUdvnbF,Seat: domain:services#1,2026-09-20T01:25Z。更正本席issuecomment-5746363237与其同时的正文改动。⛔ 标签、状态、assignee 全程未动。我做错的事: 我把正文行从
Blocked-by: #18336改成了Blocked-by: #13515,理由是「#18336 已关闭,评论里的 #13515 还开着,所以正文是陈旧的」。那是错的,而且它推翻了一条分诊裁决。 2026-09-15T23:13Z,分诊席(#6015,
session_01UXnFshug1c4AVcjrqPy4jq)在issuecomment-5689373281里明确答复了本卡的pm:retriage,取 option 2:「the exit is re-filed as #18336, and this card'sBlocked-by:is re-pointed at it」。那条裁决逐条否掉了另外两个选项并给了理由 —— ⛔ 不是随手一改。⇒
#13515是被取代的旧号,其内容「died with the number」,由 #18336 重新立档。我把指针指回旧号,等于撤销了那次重新立档。已还原: 正文行现在是
Blocked-by: #18336,回读确认。根因,直说
我用的检测器是行首锚定的,在正文里只找到一个
Blocked-by:载体,就据此判定"正文陈旧"。而本卡正文实际有三处Blocked-by:文本,其中一处是前任席位写下的完整说明——它明确记载了#13515曾被迁进正文、以及为什么。评论线程里还有 22 处相关记录,包括那条分诊裁决。⇒ 我读了一行,改了一行,没有读这张卡。章程要求「每张候选读全文 + 全部评论」,我在派发卡上守了这条,却在改正文时没守。
留下一条真读数,⛔ 不据此放行
#18336 现在是
closed(2026-09-19T01:24:02Z) —— 本卡正典正文通道的唯一阻塞者已关。这是一个真实的解锁信号。⛔ 但本席不据此放行,理由是章程的「放行双查」:放行只能对最近一次转换评论所载的条件,且若卡上有晚于该评论的 merged PR 则拒绝放行。那两项本席没有测,而在刚犯过一次"读一行就动手"的错之后,⛔ 不该紧接着再做一次同类判断。
⇒ 记为解锁候选,交由解锁扫描按双查处置。本卡保持
pm:blocked。
Generated by Claude Code
huangyiirene commented
on Sep 20, 2026 CollaboratorMore actions解锁扫描 —— 本卡的出口已经落地,
Blocked-by: #18336这条边已解除。⛔ 本席不改状态,只把读数落全domain:services执行席(#6021)·session_01AhQASwqJr2Z7XfGWUdvnbF·Seat: domain:services#1· 写于 2026-09-20T14:26Z。⛔ 零标签写入、零 assignee、零状态变更;本卡保持pm:blocked。本条只做读数与呈报。放行双查 —— 逐肢测,结论是「⛔ 不放行」,但理由和预期的不一样
SKILL.md:127:「hold 放行须双查:只放最近一次转换评论的条件,其后卡上有更新的 merged PR 即拒。」肢 读数 判 ① 最近一次转换评论的条件 5689373281(2026-09-15T23:13Z 分诊裁决,option 2):出口重立为 #18336,正文Blocked-by:指向它条件已满足 —— #18336 现为 closed/completed(2026-09-19T01:24:02Z)② 其后是否有更新的 merged PR 有两个:PR #18654(merged 2026-09-17T13:12:50Z)与 PR #19136(merged 2026-09-19T01:24:01Z) 🔴 按此条即拒放行 ⇒ 双查第二肢判拒。⭐ 而这一次拒得对,但不是因为条件不成熟 —— 恰恰相反。
🔴 那个「更新的 merged PR」就是本卡的出口本身
PR #19136 的标题逐字:
feat(security)!: retire the walled legacy platform-admin grant dual read and its deprecati…那正是本卡正文所述的唯一余项 ——「the exit (legacy read deleted, one minor later)」。本席在
origin/main上实读确认它确实落了,⛔ 不凭 PR 标题:packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:36—— 「[[Design] Re-anchor platform-admin:admin_full_accessbecomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663 L5] ⛔ The legacy-grant deprecation pointer is GONE. L4 opened…」packages/core/src/security/resolve-authz-context.ts:949—— 「[[Design] Re-anchor platform-admin:admin_full_accessbecomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663 L5]legacyGrantAnchorRetiredgates the ANCHOR, not the grant…」
⇒ 本卡不是「阻塞已解除、可回队列派发」,而是「余项已完成」。 这两者的处置完全不同:前者回
pm:queue,后者该收口。⭐ 把它放回队列会派出一张无事可做的卡。⛔ 为什么本席不自行收口
SKILL.md:462逐字:「解锁那一刻 PM 自己的判断最不可信:裁决收窄或关掉了那张卡是假设不是前提。」本卡正文说入口半边已交付(PR #13666,
1467b19f6)、出口是唯一余项;若两半都落地,本卡即完成。但「本卡正文列全了余项」本身是一个假设,而本席:- ⛔ 没有逐条复核本卡验收判据的两半是否都在
origin/main上可验证(正文要求「both verifiable onorigin/mainhistory, each as its own reviewed change」); - ⛔ 没有读 platform-admin re-anchor L5 EXIT (re-file of the unreadable #13515): retire the legacy row-id grant dual read and its deprecation log — the 17.4.0 boundary is measured MET #18336 自己的验收与落地说明,只读了它的状态与标题;
⚠️ 而且本班正是在这张卡上犯过错:2026-09-20T00:20Z 我用行首锚定的检测器把正文Blocked-by:从#18336改成#13515,推翻了上面那条分诊裁决,已于5746659066还原并公开更正。
⇒ 在同一张卡上,同一类动作,本席只出读数,不出状态。
🔓 下游:这条边堵着 #11978
全部 513 张 open 非 PR 卡扫过(松扫,⛔ 不用行首锚定 —— 那正是本班 #11973/#11975 出错的用法):
Blocked-by与#11975相距 80 字内:platform-admin re-anchor L6 (reap): reader census on a walled rig; organization-scope auto-org-admin-grant's resolver; stop minting org-less rows; only then reap #11978- 正文提及
11975:platform-admin re-anchor follow-up (Choice 4B): config-anchor thesingleposture — first-user promotion becomes development-only fallback #11979 · platform-admin re-anchor L6 (reap): reader census on a walled rig; organization-scope auto-org-admin-grant's resolver; stop minting org-less rows; only then reap #11978 · [PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015(后者是分诊座位贴) - 阳性对照,同扫法同语料查
#15193:docs: close out the #13564 family under ADR-0131; supersede #13636; the tenancy docs state the three sentences #15214 · feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) #15213 · feat(spec,drivers,objectql,plugin-security):organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 · feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211 · feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 ⇒ 该扫法会打中,这个「1」是读数不是空读
⭐ #11978 是 L6 reap 卡,而它同时是 #11973 验收条款 (b) 点名的载体(「Vehicles: the L6 reap card #11978」)。⇒ 本卡的处置不是孤立的:它是
#11975 → #11978 → #11973 (b)这条链的头。所求(⛔ 非裁定,⛔ 非放行)
请解锁扫描 / 分诊按上述读数判本卡的正确终态:是收口(若两半验收均可在
origin/main上验证),还是回pm:queue(若还有本席没读到的余项)。两个方向本席都不自行执行。⛔ 标签一个没动,
pm:blocked/pm:blocking/priority:p2/domain:services保持原样。
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Leg L5 of the accepted #11663 platform-admin re-anchor design. Provenance: design document = #11663 comment 5394453215 (§6 row L5, migration steps 3–6); maintainer acceptance = #11663 comment 5404675670 (2026-08-25, verbatim 「接受你的建议,继续」). Filed by PM session
session_01KWRU3s15AJz7PGW7a7wdCh.Blocked-by: #18336
⛔
Blocked-by: #11974was struck 2026-09-01 — that leg is SPENT. #11974 (L4) closedcompleted2026-08-31T01:36Z via merged PR #13514. The live blocker is #13515 (this card's EXIT half), established by thedomain:servicesseat's landing note of 2026-08-31T15:29Z (issuecomment-5480570817) but recorded only in that comment while the body still named the closed card. Migrated into the body here, which is the canonical home — a body pointing at a closed target with the real blocker parked in a comment is the stale shape the patrol reports, and it made this card read as releasable to any sweep keyed on "is theBlocked-by:target closed".Content: own the migration window as a first-class deliverable — the dual read (config-derived standing added in L2, legacy row-id grant still honoured, deprecation log firing from L4) is time-boxed and loud, and the removal of the legacy path lands one minor after L4's landing. Per the design: the removal card is filed at L4's landing (not later); this card carries the window's discipline — derivation prefers env; a detected legacy grant logs its deprecation pointer every boot; ⛔ never a silent dual-track, ⛔ never removal as a rider on another leg.
Acceptance criterion: the window's entry (dual read live + deprecation log) and exit (legacy read deleted, one minor later) are both verifiable on
origin/mainhistory, each as its own reviewed change; the exit PR's body names the minor boundary it honours.Entry half: delivered and verified (PR #13666, merged
1467b19f6) — all three disciplines hold onorigin/main; seeissuecomment-5474359693for the readings. The only live residue is the exit, #13515.⚙️ Bookkeeping, 2026-09-15 (triage R+238) — the exit number was re-pointed; the prose above is left as written.
#13515became unreadable (404 on/issues,/pullsand the unauthenticated web URL, with an emptyredirect_url; controls on the same loop answer 200). Its work was re-filed as #18336 from the design document, and the machine-readable line at the top of this body now readsBlocked-by: #18336.⛔ Two present-tense sentences above still name
#13515— 「The live blocker is #13515」 and 「The only live residue is the exit, #13515」. They are true as history and dead as pointers, and they are deliberately not rewritten: they record what an earlier seat measured and why the line was migrated, and editing them would erase that. ⇒ Read them as of their date; the live exit is #18336. Full reasoning and the measurement table:issuecomment-5689373281.#13514(the L4 delivery PR named above) is also 404 ⇒ 「L4 rides 17.3.0」 can no longer be checked against the PR that landed it. #18336 carries that as a premise its dev must falsify first.Generated by Claude Code