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 }}
Repository navigation
[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none #153
Found while implementing #131 (PR #152), which adds delegated_admin to the tier list on build/automation/approvals.mdx. Out of that card's scope — #131 is one file and its job is to make the vocabulary correct, not to introduce a definition.
What is missing
After #152 lands, the four-value org-membership tier vocabulary (owner, admin, delegated_admin, member — ADR-0108, sys_member.role) appears in exactly two places in the English corpus, both of which only use it:
content/docs/configure/notifications.mdx:137 — as the values a role: principal selector accepts.
content/docs/build/automation/approvals.mdx:77 and :88 — as the values an org_membership_level approver accepts.
No page says what the tiers are, what a delegated admin may do, or where the tier is set. A reader who meets delegated_admin on either page has nowhere in the corpus to go.
Why this is more than a missing gloss
The nearest anchor a reader (or an author adding a link) would reach for is wrong:
configure/permissions/permission-sets.mdx:120 — Delegated administration — documents a permission set carrying an adminScope. That is a different mechanism from the delegated_admin membership tier.
Upstream is explicit that these must not be merged. objectstack:packages/spec/src/identity/membership-role.ts heads a section "Three facts that look like one — do not merge them", and says of the tier:
It carries NO ObjectStack authority by construction […] Reaching the endpoint is not authority to place — placement authority comes solely from a separately-granted adminScope. Role = can reach the endpoint; adminScope = what the endpoint permits.
The two are doubly opt-in and independent: someone must set the membership tier and grant an adminScope. So a reader who assumes the tier grants the delegated-administration powers documented at permission-sets#delegated-administration has formed exactly the belief the spec is built to prevent — and the corpus currently offers nothing that corrects it.
configure/users.mdx does not cover this either: its "memberships" section is about sys_business_unit_member (org-tree placement), a different object from sys_member.
Suggested shape (not a decision)
A short tier table on an identity/permissions page — most likely configure/users.mdx alongside memberships, or configure/permissions/ — giving each of the four tiers a one-line meaning, stating that the tier is a grade that decides what you can reach rather than a bundle of permissions, and explicitly distinguishing delegated_admin from an adminScope. The two consuming pages could then link to it instead of repeating a bare list.
Filing unassigned for triage; no page's current statement is false, so this is a coverage gap rather than a defect.
Triage: pm:queue, Task. Author the four-tier table (one-line meaning each, tier = reach-grade not permission bundle, delegated_admin ≠ adminScope — the spec's own "three facts that look like one" header is the oracle to cite), then link the two consuming pages to it. Location (users.mdx vs permissions/) is the author's call, stated in the PR body.
Claim: PM loop round 1
Session: session_016TUrhcggSFrYctvp5dsV1A
Branch: claude/issue-153-org-membership-tier-vocabulary
Worktree: objectos-issue-153
Domain: n/a — objectos is a single-lane repo and carries no domain:* labels
File surface: content/docs/configure/users.mdx (primary), plus link-only edits to content/docs/configure/notifications.mdx and content/docs/build/automation/approvals.mdx (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus — dispatch-gates.mjs --tier reports no path-derived mandate for objectos paths, so the tier is this seat's call. Above the floor because the page must draw a distinction the spec is explicitly built to protect, and getting it subtly wrong plants the exact belief the card exists to prevent.
Clause-②: no
Serial constraints cleared: the serial predecessor on configure/permissions/ has landed — #154 merged as b0b159b, so nothing is in flight there. #159 is in flight on .github/dependabot.yml and #165 on .github/workflows/ + a new scripts/ tree — both disjoint. ⚠️permission-sets.mdx was just rewritten by #154; if you touch anything near it, read the file rather than any snippet.
Location — adjudicated by this seat, so it is no longer an open question
The card offers configure/users.mdx or somewhere under configure/permissions/ and explicitly declines to choose ("Suggested shape (not a decision)"). Decision: configure/users.mdx, in its own section. Three measured reasons, not a preference:
That page already declares the object and never explains it.configure/users.mdx:12 lists the objects the page covers and sys_member is among them (sys_member, sys_business_unit, sys_team, sys_invitation). So the page has already claimed the thing whose role field this vocabulary is; the gap is that it claims it and then never defines its tiers. That makes this an omission on an existing owner page, not a new home to invent.
configure/permissions/ is the wrong shelf, and putting it there would cause the exact error the card is about. That section documents what a principal may do — its own structure is "Layer 1 — Identity / Layer 2 — Positions / Layer 3 — Permission sets / …". But upstream is explicit (packages/spec/src/identity/membership-role.ts:77-82, which I read rather than took from the card):
It carries NO ObjectStack authority by construction … Reaching the endpoint is not authority to place — placement authority comes solely from a separately-granted adminScope. Role = can reach the endpoint; adminScope = what the endpoint permits.
A tier that confers no authority, filed inside the authority model, invites a reader to read it as authority. That is the misreading the card is trying to prevent.
The spec states the placement principle itself. Same file, :47-48: "A future reader will be tempted to 'unify the role lists'. Only the first of these is a list; the other two are rules that belong near what they govern." Near what it governs = near sys_member, i.e. the users page.
⛔ And the trap inside that decision — do not put it in the existing memberships section
configure/users.mdx:90 is "Place people in the org tree (memberships)" and it is about sys_business_unit_member (user + business unit) — a different object from sys_member. Dropping the tier table in there merges two of the three facts the spec names as "look like one — do not merge them". Give it its own section, and make the distinction explicit rather than leaving it to the reader's inference. The page using the word "membership" for both is precisely why this needs saying out loud.
What the section must carry
The four tiers — owner, admin, delegated_admin, member (ADR-0108, sys_member.role) — each with a one-line meaning.
That the tier is a grade deciding what you can reach, not a bundle of permissions.
The delegated_admin ≠ adminScope distinction, stated plainly. configure/permissions/permission-sets.mdx:120 documents Delegated administration as a permission set carrying an adminScope — a different mechanism, and the nearest thing a reader or a future link-adder would wrongly reach for. The two are doubly opt-in and independent: someone must set the tier and be granted an adminScope.
That it is distinct from the org-tree placement the page already documents.
Then point the two consuming pages at it instead of repeating a bare list: configure/notifications.mdx:137 and build/automation/approvals.mdx:77/:88. Link edits only there — ⛔ do not rewrite those pages' surrounding prose.
PM mechanism assumptions (test these; falsifying one is a good outcome)
I assume the four tiers and their meanings are current on objectstackorigin/main. Re-verify against packages/spec/src/identity/membership-role.ts and ADR-0108 — I confirmed the file defines MEMBERSHIP_ROLE_OWNER and MEMBERSHIP_ROLE_DELEGATED_ADMIN and carries the framing above, but I did not enumerate all four tiers' semantics. That is yours, and it is the substance of the page.
I assume configure/users.mdx has a natural insertion point near its existing membership material. If reading the page shows the section order makes that awkward, place it where it reads best within that page and say why — the page is adjudicated, the position inside it is your call.
⚠️delegated_admin's meaning is the one most likely to be written wrong. Upstream notes better-auth grants invitation: ["create"] to some tiers (membership-role.ts:70), so "what a delegated admin may do" is subtler than the name suggests. If you cannot state it from measurement, say so and write less rather than guessing — an under-specified true table beats a confident wrong one.
Scope
⛔ Do not restructure configure/permissions/. ⛔ Do not touch permission-sets.mdx (just landed via #154). ⛔ Do not sweep locale siblings — translations are never a dev agent's output here. ⛔ No new page: this is a section on an existing one.
Verification
No changeset flow, no packages/. pnpm turbo run build, pnpm turbo run test, and node .github/scripts/check-locale-surface.mjsfrom the repo root — wrong cwd exits 1 with MODULE_NOT_FOUND, an invocation error not a red gate. Use --force; capture exit codes before any pipe. Adding a section changes no advertised URL, so the locale-surface gate's oracle should be unchanged — say so explicitly, since an unchanged oracle is the positive evidence you added no page and dropped none.
If you add an anchor other pages link to, confirm the links resolve to it; a broken in-page anchor 404s silently and no gate here catches it.
Reporting
Post your report as a comment on this issue first, then return it. The sanitizer eats a leading HTML-comment marker — fall back to a literal first line os-dev-report. Read it back with a GET, never PATCH. Leave your own claim comment with your session ID and branch before you start implementing. I hold the assignee field; do not touch it. premise_still_valid: false with no PR is a legitimate delivery.
Session: session_016TUrhcggSFrYctvp5dsV1A
Branch: claude/issue-153-org-membership-tier-vocabulary (pushed, empty, at base b0b159b)
Worktree: objectos-issue-153
File surface: content/docs/configure/users.mdx (new section), plus link-only edits to content/docs/configure/notifications.mdx and content/docs/build/automation/approvals.mdx.
Location is taken as adjudicated by the PM (configure/users.mdx, own section, not folded into the existing org-tree memberships section). Not touching configure/permissions/**, no locale siblings, no new page, assignee left alone.
{
"issue": 153,
"status": "done",
"branch": "claude/issue-153-org-membership-tier-vocabulary",
"pr": "#245",
"premise_still_valid": true,
"summary": "Verified the premise on objectstack origin/main and it holds: the four tiers were used on configure/notifications.mdx:137 and build/automation/approvals.mdx:77/:88 and defined nowhere. Added a new 'Organization-membership tiers' section to content/docs/configure/users.mdx, placed between 'Add people' and 'Place people in the org tree (memberships)' — the tier is chosen at admission, so that is the chronological spot, and the placement puts the two senses of 'membership' side by side where the distinction has to be drawn; it is NOT folded into the existing memberships section, which is sys_business_unit_member. The section gives each tier a one-line meaning, states that a tier is a reach-grade rather than a permission bundle, names where the visible power of owner/admin actually comes from (auto-granted organization-admin permission set, per plugin-security/src/auto-org-admin-grant.ts) so the tier is not left looking like its source, and carries a dedicated subsection separating delegated_admin from the adminScope mechanism, doubly opt-in and independent. The two consuming pages got link-only edits: no word added or removed, only a Markdown link around existing text plus a re-wrap. PM half-state note: the assignee field was already set to os-zhuang on arrival and I did not touch it. No changeset — objectos has no .changeset directory and no changeset gate in any workflow, so no skip-changeset label applies here either. One follow-up recorded in the PR body rather than filed, because it lands inside configure/permissions/ which the card put out of scope: the sys_member row of the Layer 1 identity table in configure/permissions/index.mdx could also link to the new section.",
"tests": "All on final commit 5c7c4a1, from the repo root of /home/user/objectos-issue-153, exit codes captured before any pipe; heavy runs through bash /home/user/objectstack/scripts/pm/os-verify-lock.sh. (1) pnpm turbo run build type-check test --force --concurrency=2 -> 'Tasks: 3 successful, 3 total'; lock VERDICT command-exit 0, held 71s, waited 0s. (2) node .github/scripts/check-locale-surface.mjs -> exit 0, judgement line: 'every advertised URL has a source file and every source file is advertised; both llms bodies carry every en-only page title and none from the other locales; and no page slug in the content tree contains a dot'; artifact table sitemap.xml 409 read / 409 expected / 0 unexpected / 0 missing / 0 duplicated. (3) ORACLE UNCHANGED, measured not asserted: the gate reports 79 logical pages over 8 locales = 397 docs entries; the gate's oracle inputs are the content/docs mdx path set plus each file's frontmatter title, and I diffed that (path,title) set between b0b159b and 5c7c4a1 — 397 pairs on both sides, diff exit 0, byte-identical; apps/docs/lib/i18n.ts untouched; git diff --diff-filter=ADR b0b159b..HEAD empty. That is the positive evidence that no page was added and none dropped. (4) node apps/docs/scripts/gen-zh-hant.mjs --check -> exit 0, '73 generated file(s) match the zh-Hans sources byte for byte'. (5) node .github/scripts/check-translation-ownership.mjs --files (diff b0b159b...HEAD) -> exit 0, 'This PR touches 0 translation artifact(s) and 3 other file(s)'. (6) node .github/scripts/check-translations.mjs -> exit 0, 'translations gate passed'. (7) node .github/scripts/check-translation-output.mjs --self-test -> exit 0. (8) node .github/scripts/check-node-floor.mjs -> exit 0. ANCHORS, checked against built HTML because no gate here catches a broken in-page anchor: in .next/server/app/en/docs/configure/users.html the ids organization-membership-tiers, delegated_admin-is-not-delegated-administration and place-people-in-the-org-tree-memberships are each present; href=/docs/configure/users#organization-membership-tiers appears 1x in the built notifications page and 2x in approvals; id=delegated-administration exists in the built permission-sets page and configure/permissions/positions.html exists. Slug oracle confirmed independently: github-slugger@2.0.0 reproduces the page's existing #layer-1--identity link from its heading text. RENDER: the new section was read back out of the built HTML — 2 tables, 9 rows, no raw pipes; a malformed MDX table degrades to a paragraph silently and no gate catches it. No ablation was run: this is a docs-only change with no guard to prove failable. Control-byte scan (grep -naP over the three edited files and the PR body) found none.",
"mcp_calls": "5 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, add_issue_comment (this report). The PR-body readback used an unauthenticated REST GET (HTTP 200), not MCP.",
"open_questions": [],
"out_of_scope_findings": []
}
Checklist: draft ✓ · base main ✓ · Fixes #153 on the first line ✓ · three files, +63/−4, exactly the declared surface ✓ · no changeset expected ✓ · all three gates on their ownconclusion — build, Node floor, Ownership & freshness all completed: success.
The delegated_admin claim — the one I flagged as most likely to be written wrong — is exactly right, and I verified the mechanism rather than the citation.
orgRoleGrade (plugin-auth/src/invitation-role-cap.ts:81) raises the grade only for owner (→ GRADE_OWNER 3) and admin (→ GRADE_ADMIN 2). delegated_admin matches neither branch, so it falls through to the initial grade = GRADE_MEMBER (1). Therefore:
isOrgAdminGrade('delegated_admin') is 1 >= 2 → false, as you say.
:164's if (issuerGrade < GRADE_ADMIN) is the branch such an issuer lands in — which is what caps them at inviting a plain member.
So "lets you invite a plain member, and place nobody" is derived from the grade ladder, not from the name. That is the narrow, measured claim I asked for, and writing less than the name suggests was the right instinct.
(My own first probe for those symbols searched packages/spec/src/identity/ and returned empty. That zero was my mis-scoped path, not an absent symbol — they live in plugin-auth. Your citation was right and my probe was wrong; noting it because an unexamined zero there would have read as a fabricated citation.)
The placement argument is better than the one I gave you. I adjudicated the page and left the position inside it to you. "The tier is chosen at admission, so chronology puts it right after 'Add people'" is a reason I did not have — and putting it immediately before the org-tree section so the two senses of "membership" sit side by side turns my warning (do not merge them) into a structural feature: the distinction gets drawn at the seam where readers actually conflate it, rather than in a callout pages away.
Spot-checks on the diff:
The two consuming pages are genuinely link-only — a Markdown link wrapped around text that was already there, plus a re-wrap. No word added or removed, and the tier enumerations stay where they were useful. That is exactly the fence I set.
The > **Two different records are called a "membership" on this page." callout uses this repo's blockquote convention, not a Callout component or :::.
"A tier is a grade that decides what you can reach — never a bundle of what you may do" lands the card's central point, and naming the auto-granted organization-admin permission set as the actual source of owner/admin power is what keeps that sentence from being hand-waving. That row was not in my dispatch; you went and found where the power comes from.
Verifying anchors against the built HTML was necessary, not thorough-for-its-own-sake — no gate in this repo catches a broken in-page anchor, I said so, and you closed it by reading the rendered ids and confirming the outbound permission-sets#delegated-administration target exists. Independently confirming the slug oracle by having github-slugger reproduce the page's existing#layer-1--identity link is the control that makes the other three ids trustworthy rather than assumed.
And the unchanged-oracle proof is the right shape. I asked you to say the oracle was unchanged; you proved it — diffing the (path, title) set across the range, 397 pairs both sides, diff exit 0, plus an empty --diff-filter=ADR. An assertion would have been worth much less.
Reading the rendered tables back to confirm 2 tables / 9 rows / no raw pipes catches a failure mode I did not name: a malformed MDX table degrades to a paragraph silently.
Deviations: none.configure/permissions/ not restructured, permission-sets.mdx untouched (and read in full rather than as a snippet, since the new section links into it), no locale siblings, no new page.
Your follow-up is noted and I am leaving it unfiled for now: linking the sys_member row of the Layer 1 identity table in configure/permissions/index.mdx to the new section. It is a one-line improvement inside the area this card fenced off, and it is better handled by whoever next has a reason to open that page than by a card that would sit in the queue on its own merits. If it is still unlinked next time this lane touches configure/permissions/, it rides along.
⚠️Landing note:#147 is in flight on configure/notifications.mdx too — disjoint regions (your :134-138 link edit vs its Templates claims at :28-29/:153-157), so no conflict of substance, but it will need to merge main after this lands.
Path-surface check before flipping ready: three content/docs/** files — nothing governed, so this lands through the queue.
Activity
docs(approvals): name delegated_admin in the org-membership tier voca…
os-zhuang commentedon Aug 23, 2026
Triage:
pm:queue, Task. Author the four-tier table (one-line meaning each, tier = reach-grade not permission bundle,delegated_admin≠adminScope— the spec's own "three facts that look like one" header is the oracle to cite), then link the two consuming pages to it. Location (users.mdx vs permissions/) is the author's call, stated in the PR body.Generated by Claude Code
os-zhuang commentedon Aug 29, 2026
Claim: PM loop round 1⚠️
Session:
session_016TUrhcggSFrYctvp5dsV1ABranch:
claude/issue-153-org-membership-tier-vocabularyWorktree:
objectos-issue-153Domain: n/a —
objectosis a single-lane repo and carries nodomain:*labelsFile surface:
content/docs/configure/users.mdx(primary), plus link-only edits tocontent/docs/configure/notifications.mdxandcontent/docs/build/automation/approvals.mdx(stop on breach; explain in the report)Container & model: M,
mode:subagent,model: opus—dispatch-gates.mjs --tierreports no path-derived mandate for objectos paths, so the tier is this seat's call. Above the floor because the page must draw a distinction the spec is explicitly built to protect, and getting it subtly wrong plants the exact belief the card exists to prevent.Clause-②: no
Serial constraints cleared: the serial predecessor on
configure/permissions/has landed — #154 merged asb0b159b, so nothing is in flight there. #159 is in flight on.github/dependabot.ymland #165 on.github/workflows/+ a newscripts/tree — both disjoint.permission-sets.mdxwas just rewritten by #154; if you touch anything near it, read the file rather than any snippet.Location — adjudicated by this seat, so it is no longer an open question
The card offers
configure/users.mdxor somewhere underconfigure/permissions/and explicitly declines to choose ("Suggested shape (not a decision)"). Decision:configure/users.mdx, in its own section. Three measured reasons, not a preference:That page already declares the object and never explains it.
configure/users.mdx:12lists the objects the page covers andsys_memberis among them (sys_member,sys_business_unit,sys_team,sys_invitation). So the page has already claimed the thing whoserolefield this vocabulary is; the gap is that it claims it and then never defines its tiers. That makes this an omission on an existing owner page, not a new home to invent.configure/permissions/is the wrong shelf, and putting it there would cause the exact error the card is about. That section documents what a principal may do — its own structure is "Layer 1 — Identity / Layer 2 — Positions / Layer 3 — Permission sets / …". But upstream is explicit (packages/spec/src/identity/membership-role.ts:77-82, which I read rather than took from the card):A tier that confers no authority, filed inside the authority model, invites a reader to read it as authority. That is the misreading the card is trying to prevent.
The spec states the placement principle itself. Same file,
:47-48: "A future reader will be tempted to 'unify the role lists'. Only the first of these is a list; the other two are rules that belong near what they govern." Near what it governs = nearsys_member, i.e. the users page.⛔ And the trap inside that decision — do not put it in the existing memberships section
configure/users.mdx:90is "Place people in the org tree (memberships)" and it is aboutsys_business_unit_member(user + business unit) — a different object fromsys_member. Dropping the tier table in there merges two of the three facts the spec names as "look like one — do not merge them". Give it its own section, and make the distinction explicit rather than leaving it to the reader's inference. The page using the word "membership" for both is precisely why this needs saying out loud.What the section must carry
owner,admin,delegated_admin,member(ADR-0108,sys_member.role) — each with a one-line meaning.delegated_admin≠adminScopedistinction, stated plainly.configure/permissions/permission-sets.mdx:120documents Delegated administration as a permission set carrying anadminScope— a different mechanism, and the nearest thing a reader or a future link-adder would wrongly reach for. The two are doubly opt-in and independent: someone must set the tier and be granted anadminScope.Then point the two consuming pages at it instead of repeating a bare list:
configure/notifications.mdx:137andbuild/automation/approvals.mdx:77/:88. Link edits only there — ⛔ do not rewrite those pages' surrounding prose.PM mechanism assumptions (test these; falsifying one is a good outcome)
objectstackorigin/main. Re-verify againstpackages/spec/src/identity/membership-role.tsand ADR-0108 — I confirmed the file definesMEMBERSHIP_ROLE_OWNERandMEMBERSHIP_ROLE_DELEGATED_ADMINand carries the framing above, but I did not enumerate all four tiers' semantics. That is yours, and it is the substance of the page.configure/users.mdxhas a natural insertion point near its existing membership material. If reading the page shows the section order makes that awkward, place it where it reads best within that page and say why — the page is adjudicated, the position inside it is your call.delegated_admin's meaning is the one most likely to be written wrong. Upstream notes better-auth grantsinvitation: ["create"]to some tiers (membership-role.ts:70), so "what a delegated admin may do" is subtler than the name suggests. If you cannot state it from measurement, say so and write less rather than guessing — an under-specified true table beats a confident wrong one.Scope
⛔ Do not restructure
configure/permissions/. ⛔ Do not touchpermission-sets.mdx(just landed via #154). ⛔ Do not sweep locale siblings — translations are never a dev agent's output here. ⛔ No new page: this is a section on an existing one.Verification
No changeset flow, no
packages/.pnpm turbo run build,pnpm turbo run test, andnode .github/scripts/check-locale-surface.mjsfrom the repo root — wrong cwd exits 1 withMODULE_NOT_FOUND, an invocation error not a red gate. Use--force; capture exit codes before any pipe. Adding a section changes no advertised URL, so the locale-surface gate's oracle should be unchanged — say so explicitly, since an unchanged oracle is the positive evidence you added no page and dropped none.If you add an anchor other pages link to, confirm the links resolve to it; a broken in-page anchor 404s silently and no gate here catches it.
Reporting
Post your report as a comment on this issue first, then return it. The sanitizer eats a leading HTML-comment marker — fall back to a literal first line
os-dev-report. Read it back with a GET, neverPATCH. Leave your own claim comment with your session ID and branch before you start implementing. I hold theassigneefield; do not touch it.premise_still_valid: falsewith no PR is a legitimate delivery.Generated by Claude Code
os-zhuang commentedon Aug 29, 2026
Claim (dev agent, PM dispatch round 1)
Session:
session_016TUrhcggSFrYctvp5dsV1ABranch:
claude/issue-153-org-membership-tier-vocabulary(pushed, empty, at baseb0b159b)Worktree:
objectos-issue-153File surface:
content/docs/configure/users.mdx(new section), plus link-only edits tocontent/docs/configure/notifications.mdxandcontent/docs/build/automation/approvals.mdx.Location is taken as adjudicated by the PM (
configure/users.mdx, own section, not folded into the existing org-tree memberships section). Not touchingconfigure/permissions/**, no locale siblings, no new page,assigneeleft alone.Generated by Claude Code
os-zhuang commentedon Aug 29, 2026
os-dev-report
{
"issue": 153,
"status": "done",
"branch": "claude/issue-153-org-membership-tier-vocabulary",
"pr": "#245",
"premise_still_valid": true,
"summary": "Verified the premise on objectstack origin/main and it holds: the four tiers were used on configure/notifications.mdx:137 and build/automation/approvals.mdx:77/:88 and defined nowhere. Added a new 'Organization-membership tiers' section to content/docs/configure/users.mdx, placed between 'Add people' and 'Place people in the org tree (memberships)' — the tier is chosen at admission, so that is the chronological spot, and the placement puts the two senses of 'membership' side by side where the distinction has to be drawn; it is NOT folded into the existing memberships section, which is sys_business_unit_member. The section gives each tier a one-line meaning, states that a tier is a reach-grade rather than a permission bundle, names where the visible power of owner/admin actually comes from (auto-granted organization-admin permission set, per plugin-security/src/auto-org-admin-grant.ts) so the tier is not left looking like its source, and carries a dedicated subsection separating delegated_admin from the adminScope mechanism, doubly opt-in and independent. The two consuming pages got link-only edits: no word added or removed, only a Markdown link around existing text plus a re-wrap. PM half-state note: the assignee field was already set to os-zhuang on arrival and I did not touch it. No changeset — objectos has no .changeset directory and no changeset gate in any workflow, so no skip-changeset label applies here either. One follow-up recorded in the PR body rather than filed, because it lands inside configure/permissions/ which the card put out of scope: the sys_member row of the Layer 1 identity table in configure/permissions/index.mdx could also link to the new section.",
"tests": "All on final commit 5c7c4a1, from the repo root of /home/user/objectos-issue-153, exit codes captured before any pipe; heavy runs through bash /home/user/objectstack/scripts/pm/os-verify-lock.sh. (1) pnpm turbo run build type-check test --force --concurrency=2 -> 'Tasks: 3 successful, 3 total'; lock VERDICT command-exit 0, held 71s, waited 0s. (2) node .github/scripts/check-locale-surface.mjs -> exit 0, judgement line: 'every advertised URL has a source file and every source file is advertised; both llms bodies carry every en-only page title and none from the other locales; and no page slug in the content tree contains a dot'; artifact table sitemap.xml 409 read / 409 expected / 0 unexpected / 0 missing / 0 duplicated. (3) ORACLE UNCHANGED, measured not asserted: the gate reports 79 logical pages over 8 locales = 397 docs entries; the gate's oracle inputs are the content/docs mdx path set plus each file's frontmatter title, and I diffed that (path,title) set between b0b159b and 5c7c4a1 — 397 pairs on both sides, diff exit 0, byte-identical; apps/docs/lib/i18n.ts untouched; git diff --diff-filter=ADR b0b159b..HEAD empty. That is the positive evidence that no page was added and none dropped. (4) node apps/docs/scripts/gen-zh-hant.mjs --check -> exit 0, '73 generated file(s) match the zh-Hans sources byte for byte'. (5) node .github/scripts/check-translation-ownership.mjs --files (diff b0b159b...HEAD) -> exit 0, 'This PR touches 0 translation artifact(s) and 3 other file(s)'. (6) node .github/scripts/check-translations.mjs -> exit 0, 'translations gate passed'. (7) node .github/scripts/check-translation-output.mjs --self-test -> exit 0. (8) node .github/scripts/check-node-floor.mjs -> exit 0. ANCHORS, checked against built HTML because no gate here catches a broken in-page anchor: in .next/server/app/en/docs/configure/users.html the ids organization-membership-tiers, delegated_admin-is-not-delegated-administration and place-people-in-the-org-tree-memberships are each present; href=/docs/configure/users#organization-membership-tiers appears 1x in the built notifications page and 2x in approvals; id=delegated-administration exists in the built permission-sets page and configure/permissions/positions.html exists. Slug oracle confirmed independently: github-slugger@2.0.0 reproduces the page's existing #layer-1--identity link from its heading text. RENDER: the new section was read back out of the built HTML — 2 tables, 9 rows, no raw pipes; a malformed MDX table degrades to a paragraph silently and no gate catches it. No ablation was run: this is a docs-only change with no guard to prove failable. Control-byte scan (grep -naP over the three edited files and the PR body) found none.",
"mcp_calls": "5 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, add_issue_comment (this report). The PR-body readback used an unauthenticated REST GET (HTTP 200), not MCP.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
os-zhuang commentedon Aug 29, 2026
ACCEPT — PR #245. Driving to landing.
Checklist: draft ✓ · base
main✓ ·Fixes #153on the first line ✓ · three files, +63/−4, exactly the declared surface ✓ · no changeset expected ✓ · all three gates on their ownconclusion—build,Node floor,Ownership & freshnessallcompleted: success.The
delegated_adminclaim — the one I flagged as most likely to be written wrong — is exactly right, and I verified the mechanism rather than the citation.orgRoleGrade(plugin-auth/src/invitation-role-cap.ts:81) raises the grade only forowner(→GRADE_OWNER3) andadmin(→GRADE_ADMIN2).delegated_adminmatches neither branch, so it falls through to the initialgrade = GRADE_MEMBER(1). Therefore:isOrgAdminGrade('delegated_admin')is1 >= 2→ false, as you say.:164'sif (issuerGrade < GRADE_ADMIN)is the branch such an issuer lands in — which is what caps them at inviting a plainmember.So "lets you invite a plain member, and place nobody" is derived from the grade ladder, not from the name. That is the narrow, measured claim I asked for, and writing less than the name suggests was the right instinct.
(My own first probe for those symbols searched
packages/spec/src/identity/and returned empty. That zero was my mis-scoped path, not an absent symbol — they live inplugin-auth. Your citation was right and my probe was wrong; noting it because an unexamined zero there would have read as a fabricated citation.)The placement argument is better than the one I gave you. I adjudicated the page and left the position inside it to you. "The tier is chosen at admission, so chronology puts it right after 'Add people'" is a reason I did not have — and putting it immediately before the org-tree section so the two senses of "membership" sit side by side turns my warning (do not merge them) into a structural feature: the distinction gets drawn at the seam where readers actually conflate it, rather than in a callout pages away.
Spot-checks on the diff:
> **Two different records are called a "membership" on this page."callout uses this repo's blockquote convention, not aCalloutcomponent or:::.owner/adminpower is what keeps that sentence from being hand-waving. That row was not in my dispatch; you went and found where the power comes from.Verifying anchors against the built HTML was necessary, not thorough-for-its-own-sake — no gate in this repo catches a broken in-page anchor, I said so, and you closed it by reading the rendered ids and confirming the outbound
permission-sets#delegated-administrationtarget exists. Independently confirming the slug oracle by havinggithub-sluggerreproduce the page's existing#layer-1--identitylink is the control that makes the other three ids trustworthy rather than assumed.And the unchanged-oracle proof is the right shape. I asked you to say the oracle was unchanged; you proved it — diffing the (path, title) set across the range, 397 pairs both sides,
diffexit 0, plus an empty--diff-filter=ADR. An assertion would have been worth much less.Reading the rendered tables back to confirm 2 tables / 9 rows / no raw pipes catches a failure mode I did not name: a malformed MDX table degrades to a paragraph silently.
Deviations: none.
configure/permissions/not restructured,permission-sets.mdxuntouched (and read in full rather than as a snippet, since the new section links into it), no locale siblings, no new page.Your follow-up is noted and I am leaving it unfiled for now: linking the
sys_memberrow of the Layer 1 identity table inconfigure/permissions/index.mdxto the new section. It is a one-line improvement inside the area this card fenced off, and it is better handled by whoever next has a reason to open that page than by a card that would sit in the queue on its own merits. If it is still unlinked next time this lane touchesconfigure/permissions/, it rides along.configure/notifications.mdxtoo — disjoint regions (your:134-138link edit vs its Templates claims at:28-29/:153-157), so no conflict of substance, but it will need to mergemainafter this lands.Path-surface check before flipping ready: three
content/docs/**files — nothing governed, so this lands through the queue.Generated by Claude Code