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
platform-objects: the Setup user screens show the raw stored platform role (user, admin) because sys_user.role is a text field #28105
Ruled: 6557746678 · letter D1 · 2026-11-01T12:11Z
Ruling-ref: 6557746678
Filing gate: ① out-of-scope finding of the sys_user fold dev (PR #28065, os-dev report on #27858), carried to its own card by seat ruling A in the seat-1 thread. Filed by domain:server seat 1. ⛔ Not a claim. No grade: triage sets the lane and grade.
Reach: every Setup admin, today — the user record highlight and the new all_users Role column (PR #28065) show user / admin instead of a label.
Reader who acts: domain:server (packages/platform-objects, with driver-sql / schema-drift in the question).
What the code does (read, not measured)
sys_user.role (packages/platform-objects/src/identity/sys-user.object.ts) is Field.text, so every renderer shows the stored value. #27859 and #27863 asked for the role to read through its label. A label needs Field.select with options, but driver-sql's createColumn emits TEXT for text and the default varchar(255) for select, so schema-drift would read every existing deployment's column as narrow_varchar (a destructive drift reading).
Wanted (route is triage's)
Decide how the platform role reads through a label without a storage change: for example a select whose storage stays TEXT (a declared storage width), or a display-only label map. Measure the drift reading on an existing deployment first.
Dedupe
Read the open titles for sys_user + role, platform role, role label, select storage width: none. New.
Read fresh. A raw stored value shows where a label belongs. Nothing is wrong in the data, so the grade is p3.
The route is the card's second option, a display-only label map, or a select with a declared storage width. Either way, measure the drift reading on an existing deployment first. A storage change is out of scope.
Claim: PM loop round 1 · card thread · 2026-11-01T04:57Z
Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01FEkQ5pdrHj6ad5C2N4Rcrx
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-8e8iio (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#1
Ruling-ref: the maintainer's 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」 (2026-11-01T03:46Z); triage 6524799429 graded this card p3, and that line now lets a seat dispatch it. Seat ruling on triage's route: no storage change (the column stays TEXT, with the drift reading measured first on an existing deployment); a display-only label map is preferred, and a select with a declared storage width only if that key already exists in spec; no new spec key and no narrowing of accepted role values. If neither route exists, it goes to needs_decision.
File surface: packages/platform-objects/src/identity/sys-user.object.ts (sys_user.role), the display-label seam PR #28062 used if it fits, the platform translation bundles regenerated from them, pins, and a changeset. ⛔ No storage change, ⛔ no packages/spec/**, ⛔ no ui/**. Stop on breach and explain in the report.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no (expected: labels only; anything that would narrow accepted role values is out of scope and stops the work)
Metadata-protocol: none (expected)
Responsibility: the code is the platform role field on sys_user · the platform path is the Setup user record highlight and the all_users Role column · reached by every Setup admin, who reads user / admin instead of a label
Thread-read: 6524799429
Premise: holds (read 2026-11-01T04:57Z at origin/main35b178bd0a: role: Field.text( at sys-user.object.ts:917)
Serial constraints cleared: seat 1's #28105 (claude/project-thread-kdcgx5) also edits sys-user.object.ts (the env_native label); the dev runs git merge-tree against its PR and any other open PR touching the file or the bundles before pr_create, and merges origin/main once if it has landed
Resume-from: none — fresh from origin/main
State: pm:dispatched, assigned to os-project-manager, recorded by this claim.
os-dev-report
{
"issue": 28105,
"status": "needs_decision",
"branch": "claude/project-thread-8e8iio",
"pr": null,
"session": "session_01FEkQ5pdrHj6ad5C2N4Rcrx",
"premise_still_valid": false,
"summary": "Neither ordered route exists, so no edit and no PR (ruling: stop, needs_decision). Route (a): the platform has no display-only label map for a text field. PR #28062 used select options (severity was already a select) and the object-ref widget (a registered read face for object names); neither fits a role. options on a text field parses (FieldSchema safeParse true), but the text cell (ui TextCellRenderer) ignores it: only the highlight hover title and the compact grid card read it, so the drawn highlight value and the all_users cell would still print user. Route (b): spec has no storage width for a select: maxLength on select is refused at parse, and storage is strict with notNull only (storage.width answers unrecognized_keys). Premise correction, measured: the card's drift premise is false. On PostgreSQL 16, a sys_user table built by the platform's own driver from main's SysUser (role TEXT, rows user and admin,support), then re-synced as a select reads ZERO sys_user drift and the column stays text; the differ with varcharColumnChars bound reads zero for a select over text on postgres, mysql (text, longtext) and sqlite. Controls fire on the same rows: storage.notNull on role gives tighten_not_null destructive live, and a string maxLength 32 over varchar(64) gives narrow_varchar. The real storage effect of a plain select is on NEW deployments only: createColumn builds varchar(255) for a select (varcharColumnChars answers 255) where main builds TEXT, and the differ never reports the split. Write set, read: better-auth's admin plugin is OFF by default (plugins.admin defaults to SCIM-effective), so on a default deployment nothing writes role and it is NULL for every user. With the plugin on, its create hook stamps defaultRole user on every new user, and /admin/set-role, /admin/create-user and /admin/update-user store any string, arrays comma-joined (no roles table is configured, so no value check). Platform-admin standing does not come from this scalar (ADR-0068 D2: owner email or admin_full_access; role admin survives only as the isPlatformAdminUser back-compat fallback), so the seeded platform admin is created through signUpEmail and reads user. A readonly select does not narrow at the write door: the record validator never applies option membership to a readonly value (the sys_activity.type open-vocabulary precedent), and SelectCellRenderer draws an unknown value humanized.",
"tests": "No code change, so no red/green and no ablation. Readings (scripts and outputs in project files pm-orders/server/evidence/28105/): probe-options.mts on spec src: text+options safeParse true; select+maxLength refused (maxLength is only valid on field types that store a bounded string); select+storage.width refused unrecognized_keys. drift-probe.mjs on local PG 16 after a build of driver-sql and platform-objects closures (lock VERDICT command-exit 0, 10/10 tasks): base gives [] (column text before and after); text_options gives []; select gives [] (column text, rows unchanged: user, admin,support); ctl_notnull gives role nullability_mismatch destructive tighten_not_null; ctl_string64 gives [] (text column to string(64) is also silent). diff-probe.mjs (diffManagedTable with the driver's own varcharColumnChars): select over text on postgres, mysql text, mysql longtext, sqlite gives []; differ control string maxLength 32 over postgres varchar(64) gives narrow_varchar destructive; createColumn width for select 255, for main's role text null (TEXT).",
"gates": "n/a: no diff. dispatch-gates --commands not run (it derives from a change set; there is none).",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "5: issue_read x4 (#28105 get and get_comments, #27859, #27863), pull_request_read x1 (#28065 get). No write tools.",
"api_writes": "1: POST /repos//issues/28105/comments (this report, via post-stamped relay). One REST read: GET issues/28105/comments for the claim.",
"open_questions": [
{
"question": "How should the Setup user screens treat sys_user.role, given that no label route exists without a storage change on new deployments, a spec key, or a ui change, and given that the scalar does not answer whether a user is a platform admin?",
"status": "needs_decision",
"options": [
"A. Readonly select now: sys_user.role becomes Field.select with readonly and options User / Admin (labels through i18n:extract into the four platform bundles), platform-objects only, Clause-2 no, Metadata-protocol none, patch. Existing deployments keep their TEXT column with no drift reading (measured, all three dialects); unknown and comma values still store (readonly open vocabulary) and draw humanized. Cost: new deployments get varchar(255) instead of TEXT, a silent physical split between old and new installs the differ never reports, and a value over 255 chars refused only on new installs (no writer produces one today). This waives the ruling that the column stay TEXT. Size S.",
"B. Spec lane first: add a declared storage width for select (for example a storage member that keeps TEXT), then do A with TEXT everywhere. p1 protocol addition owned by the spec lane, a new authorable key with one consumer (this legacy field). Size M across two lanes.",
"C. ui lane: the text cell and highlight draw a label from a text field's declared options (a display-only label map), plus a spec describe change sanctioning options on text. Changes what options means on every non-option type; spec + ui. Size M.",
"D. Stop presenting the legacy scalar as Platform Role: drop role from the all_users columns and from the record highlights (keep the field readable in its Admin group), platform-objects only, patch. Reverses the role half of #27863 and #27859. Spotting admins from the list would need a column derived from real platform-admin standing, which is new capability and not proposed here."
],
"four_axes": {
"business_need": "Measured readers and writers: no ObjectStack code writes role; on a default deployment (admin plugin off) the column is empty for every user, and with the plugin on every new user is stamped user, including the seeded platform admin. A Setup admin scanning the Role column to spot admins (#27863's stated need) gets a wrong answer under any label. A and C label that wrong answer; D removes it; B serves no measured second consumer.",
"long_term": "ADR-0068 D2 retired this scalar as an authority, and isPlatformAdminUser keeps it only as a back-compat fallback. D follows that direction. A leaves a permanent, silent old/new storage split. B and C add contract surface (a storage key, a wider meaning for options) for one retired field.",
"ai_error_prevention": "A label (A, C) makes a legacy value look authoritative to people and to AI agents reading the record (Platform Role: User for a platform admin). C makes options on a non-option type half-meaningful, the trap of a key the runtime honours on some faces only. D leaves nothing misleading on the screen.",
"startup_focus": "D and A are platform-objects-only S changes. B is a protocol addition with no puller beyond this card, and C spans two lanes; the startup rule defaults both to no."
},
"recommendation": "D, because the four axes agree that the problem is the value, not its spelling: the scalar does not answer the question the column was added to answer, and a label would make a wrong answer look right. If the maintainer still wants the label, A is the cheapest route, but it needs an explicit waiver of the keep-TEXT ruling for new deployments."
}
],
"out_of_scope_findings": [
"class: b · reach: read, not driven (Setup user record and all_users list on a deployment with the better-auth admin plugin on show Platform Role user for a platform admin promoted through admin_full_access) · evidence: better-auth admin.mjs create hook stamps defaultRole user; plugin-auth platform-admin-gate.ts isPlatformAdminUser reads isPlatformAdmin first and role only as fallback; sys-user.object.ts role description says ObjectStack no longer writes it · carrier: this card's decision (option D covers it) · dedupe words: sys_user role platform admin standing, Platform Role column misleading, legacy role scalar",
"class: none · noted, not filed: the card body's premise that schema-drift reads a select over an existing TEXT column as narrow_varchar is false as measured (zero findings on postgres, mysql, sqlite); the true effect is varchar(255) on fresh tables only · carrier: none"
]
}
Release: to the decision box · 2026-11-01T05:11Z · the dev's needs_decision report 6553744903
Produce: none (no edit, no PR; branch claude/project-thread-8e8iio unchanged at origin/main)
Resume-from: the report's options A–D and the evidence under pm-orders/server/evidence/28105/
Premise corrected (measured): a select over an existing TEXT column reads zero drift on PostgreSQL 16, MySQL and SQLite. The real cost is that new deployments get varchar(255) instead of TEXT.
Business finding: no ObjectStack code writes role. better-auth's admin plugin is off by default, so the column is empty. With the plugin on, every new user is stamped user, including the seeded platform admin, and platform-admin standing does not come from this scalar (ADR-0068 D2). So the column does not tell an admin apart.
Seat's recommendation to the maintainer: D. Take role out of the all_users columns and the record highlights, and keep the field readable in its Admin group. The other options build a label for a value that is empty by default and wrong when filled.
Labels: needs-user-decision only; pm:dispatched and the assignee are cleared.
Director seat (summon #55). Presented to the maintainer in batch #438 (three items: #28590, #28105, #27855). The dev's report 6553744903 (four axes, options A–D) and the seat's release 6553778805 are the reading; the seat's increment:
Premise re-read on origin/main35b178bd0a: sys-user.object.ts:917role: Field.text({ label: 'Platform Role', readonly: true, maxLength: 64, … }), its description "Legacy better-auth role scalar … ObjectStack no longer writes it"; ADR-0068 D2 (docs/adr/0068-unified-user-context-and-built-in-identity-roles.md:60–67) confers platform_admin through the unscoped admin_full_access set, not this scalar. The card's own drift premise is corrected by the dev's measurement: zero drift on PostgreSQL 16, MySQL and SQLite; the real effect of a select is varchar(255) on new installs only.
Facets, read by this seat: ① D follows ADR-0068's direction; A leaves a permanent, silent old/new storage split; B and C add contract surface (a storage key, a wider meaning for options) for one retired field. ② No measured reader is served by a label on a value that is wrong when filled. ③ A label (A, C) makes a legacy value look authoritative to people and to agents reading the record: "Platform Role: User" on a platform admin. ④ D is platform-objects-only, size S.
Recommendation: D. ① alone reaches it (the problem is the value, not its spelling); ②③④ agree. One sub-decision ruled in the same stroke so Setup → Users list has no Status (banned) or Role column, so a banned user or an admin cannot be spotted from the list (#27521 row 15) #27863's need is not dropped silently: whether a follow-up card is filed for a derived "Platform admin" indicator on the Users list, computed from real standing (owner e-mail or admin_full_access), the way a mainstream admin console shows the profile or role that actually governs. The seat recommends D1 = D + file that card at p3; D0 = D alone. Not taken: A (a label on a wrong value), B and C (contract surface for a retired field).
Director seat (summon #55; brief-claimed in 6556630620, re-analysed at the maintainer's request in 6556798793, presented in the summon chat). The maintainer answered verbatim: 「同意」 = the seat's recommendation, so this card's letter is D1.
The Setup user screens stop presenting the scalar as the platform role: role leaves the all_users list columns and the record highlights; the field stays readable in its Admin group (packages/platform-objects/src/identity/sys-user.object.ts:917, Field.text, readonly). No type, storage or spec change (Clause-②: no, Metadata-protocol: none).
Not taken: D0 (no follow-up card), S1 (one-way projection into the scalar; would need an ADR-0068 revisit first), A (a readonly select labelling the legacy value).
Not ruled here, carried as a finding for the ui seat: the expression-context example ${user.role === 'admin'} (ui/packages/app-shell/src/providers/ExpressionProvider.tsx:550) teaches a gate that is always false for a platform admin; the documented gate is isPlatformAdmin.
Workload of the letter:domain:server · one PR · S (precedent PR #28065, +194/−66, the fold that added the column) · plus one new p3 card (the derived platform-admin column; lane by triage, likely domain:server for the object's list view).
Execution: the domain:server seat picks the card up with Ruling-ref: naming this comment; the needs-user-decision → pm:queue stroke and the filing of the follow-up card (pointer back here) are that seat's. The Ruled: body line is written by this act.
Ruling-ref: 6557746678 (letter D1). Executing the ruling's strokes for the domain:server seat: needs-user-decision → pm:queue in this stroke, and the follow-up card (a Users-list column derived from real platform-admin standing) is filed as #28793, blocked on this card. Server seat 5 picks this card up next; its claim follows when the dev branch exists. The ExpressionProvider.tsx example the ruling carries is a ui finding and is named in the order, not edited here.
Claim: PM loop round 1 · card thread · 2026-11-01T20:05Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5msFrPy8g6WzAxVUjG
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-8n9g96 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 6557746678 (director ruling, batch #438 item 2, letter D1; maintainer 「同意」 2026-11-01T12:10Z); pick announced to seats 1-3, all three confirmed no clash.
File surface: packages/platform-objects/src/identity/sys-user.object.ts (the all_users columns and any highlight / compact surface showing role), its tests in that folder (the #27863 pin), a @objectstack/platform-objects changeset, any content/docs/** page pinning the Users list columns. ⛔ No packages/spec/**, ⛔ no ui/**.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no
Metadata-protocol: none
Responsibility: the code is the sys_user object's Setup list and highlight declarations · the Users list presents better-auth's legacy role scalar as if it were the platform role · reached by every Setup admin opening Users
Thread-read: 6563160193
Premise: holds (read at origin/maind9c4cdc4e: all_users columns list role; sys_user.role is Field.text, readonly)
Serial constraints cleared: none needed; follow-up #28793 is blocked on this card.
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28105.md in the project.
Claim amendment to 6563185751 · 2026-11-01T20:06Z · branch claude/project-thread-8n9g96 unchanged
File surface, added: packages/platform-objects/src/pages/sys-user.page.ts (its record:highlights node lists role; the object-level highlightFields does not) and its tests in packages/platform-objects/src/pages/. Measured by the dev; ruling 1 of the order (role leaves the record highlights) lands there. Everything else in the claim stands.
domain:server#5 · os-project-manager · session_01FEkQ5msFrPy8g6WzAxVUjG
os-dev-report
{
"issue": 28105,
"status": "done",
"branch": "claude/project-thread-8n9g96",
"pr": "#28810",
"session": "session_01FEkQ5msFrPy8g6WzAxVUjG",
"premise_still_valid": true,
"summary": "Ruling D1 (Ruling-ref 6557746678) executed: role leaves the all_users list columns (now name, email, phone_number, email_verified, banned, source, created_at) and the sys_user_detail record:highlights chip strip (now email, phone_number, email_verified). PM assumption 1 confirmed: the highlight lived on the record page, not object highlightFields (already name/email/email_verified); claim amended 6563206575 to add pages/sys-user.page.ts. sys_user.role is untouched: Field.text, readonly, group Admin, same label, storage and description, still served by the data API. The #27863 pin is renamed sys-user-list-columns.test.ts and pins the new columns, role absent from every list view and highlightFields, and the field guard; a new page pin covers every highlights node and details section.",
"tests": "Red first at base d9c4cdc: the two pin files 4 failed / 4 passed (red: all_users columns pin, no list view shows role, header chip-strip pin, no highlights node carries role; green on both: highlightFields, columns declared, role field guard, details sections). Green at head 280a84d: 8/8; pnpm --filter @objectstack/platform-objects test 108 files / 1503 tests passed; typecheck exit 0. Live probe on pnpm dev --fresh (showcase), GET /api/v1/meta/object/sys_user and /api/v1/meta/page/sys_user_detail as admin@objectos.ai: base all_users.columns [name,email,phone_number,email_verified,banned,role,source,created_at], header highlights [email,phone_number,email_verified,role]; head [name,email,phone_number,email_verified,banned,source,created_at] and [email,phone_number,email_verified]; role field text/readonly/Admin on both; head data read serves role user for the seeded dev admin and null for the auditor demo. Base leg restored with git checkout HEAD, git diff HEAD empty, both blob hashes equal HEAD, dist rebuilt. No ablation: declarative metadata, red-on-base is the negative leg.",
"gates": "Local subset at 280a84d: 59 derived families (dispatch-gates --commands --repo objectstack-ai/objectstack), all exit 0, captured before any pipe; --ran reconciliation: 59 derived, 59 run, 0 NOT-MEASURED. check-plugin-teardown-shape --self-test first exit 3 (shallow clone), exit 0 after fetching its positive-control commit. pnpm build 117/117; eslint --no-inline-config packages/platform-objects exit 0. CI check-runs on PR 28810: in_progress at report time (not awaited, per os-dev.md). Full list in the PR body Verification section.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub calls; reads went through gh api REST",
"api_writes": "2 — relay pr_create (#28810, draft, no labels, no assignees; read-back identical 12119 bytes) and this os-dev-report comment via post-stamped; plus 1 git push",
"line_budget": "6 files, +151/-44 against origin/main (two source files +17/-12, one renamed pin, one new pin, one changeset)",
"deviations": [
"File surface widened by claim amendment 6563206575 to packages/platform-objects/src/pages/sys-user.page.ts and its folder tests: the measured highlight surface lives there.",
"pnpm lint scoped to the touched package run as pnpm exec eslint --no-inline-config packages/platform-objects: platform-objects has no lint script of its own.",
"No labels on pr_create: the order names none, and a changeset ships, so skip-changeset does not apply."
],
"files_changed": [
".changeset/28105-sys-user-role-off-setup-screens.md",
"packages/platform-objects/src/identity/sys-user.object.ts",
"packages/platform-objects/src/identity/sys-user-list-columns.test.ts (renamed from sys-user-list-status-role-columns.test.ts)",
"packages/platform-objects/src/pages/sys-user.page.ts",
"packages/platform-objects/src/pages/sys-user-record-highlights.test.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat (named in the order) · ui/packages/app-shell/src/providers/ExpressionProvider.tsx expression-context example ${user.role === admin} teaches a gate that is always false for a platform admin (documented gate isPlatformAdmin); not edited",
"carrier: none · noted, not filed · sys_user.role keeps the field label Platform Role (ruling 2: no field change), so the standard edit form and the data API still call it that; observation, not a filing class",
"carrier: #28793 · the derived platform-admin column on the Users list, the need #27863 stated"
]
}
ACCEPT → PR #28810 comment 6564064548 · head 280a84dd · 2026-11-01T21:22Z. No contract review owed (no schema, rules or narrowing face). Landing: pr_ready + automerge_enable through the relay; the merge queue closes this card, and #28793 is unblocked on merge.
Landed · 2026-11-01T22:00Z · PR #28810 merged through the merge queue as 1e019240f9; this card closed as completed by its Fixes line.
reading
value
PR state
merged, 1e019240f9
card
closed (completed); pm:dispatched removed and the seat unassigned in this stroke, domain:* / area:* kept
Ruling D1 (6557746678) is executed: the follow-up column from real platform-admin standing, #28793, is unblocked in the same round, and the ui example the ruling carries is #28812.
Activity
objectstack-fleet commentedon Oct 30, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p3 ·
domain:server·area:identity· 2026-10-30T14:07Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim: PM loop round 1 · card thread · 2026-11-01T04:57Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5pdrHj6ad5C2N4RcrxAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-8e8iio(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: the maintainer's 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」 (2026-11-01T03:46Z); triage
6524799429graded this card p3, and that line now lets a seat dispatch it. Seat ruling on triage's route: no storage change (the column staysTEXT, with the drift reading measured first on an existing deployment); a display-only label map is preferred, and aselectwith a declared storage width only if that key already exists in spec; no new spec key and no narrowing of accepted role values. If neither route exists, it goes toneeds_decision.File surface:
packages/platform-objects/src/identity/sys-user.object.ts(sys_user.role), the display-label seam PR #28062 used if it fits, the platform translation bundles regenerated from them, pins, and a changeset. ⛔ No storage change, ⛔ nopackages/spec/**, ⛔ noui/**. Stop on breach and explain in the report.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no (expected: labels only; anything that would narrow accepted role values is out of scope and stops the work)
Metadata-protocol: none (expected)
Responsibility: the code is the platform role field on
sys_user· the platform path is the Setup user record highlight and theall_usersRole column · reached by every Setup admin, who readsuser/admininstead of a labelThread-read: 6524799429
Premise: holds (read 2026-11-01T04:57Z at
origin/main35b178bd0a:role: Field.text(atsys-user.object.ts:917)Serial constraints cleared: seat 1's #28105 (
claude/project-thread-kdcgx5) also editssys-user.object.ts(theenv_nativelabel); the dev runsgit merge-treeagainst its PR and any other open PR touching the file or the bundles beforepr_create, and mergesorigin/mainonce if it has landedResume-from: none — fresh from
origin/mainState:
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim amendment to
6553646989· 2026-11-01T04:58Z · correction onlySerial constraints cleared:line should read "seat 1's Setup: System Overview and Notifications show raw values (config_change, approval.requested, lowercase severity) (#27521 row 4) #27855" (claude/project-thread-kdcgx5), not platform-objects: the Setup user screens show the raw stored platform role (user,admin) becausesys_user.roleis a text field #28105. Setup: System Overview and Notifications show raw values (config_change, approval.requested, lowercase severity) (#27521 row 4) #27855 is the card that also editssys-user.object.ts(theenv_nativelabel). Nothing else changes.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
os-dev-report
{
"issue": 28105,
"status": "needs_decision",
"branch": "claude/project-thread-8e8iio",
"pr": null,
"session": "session_01FEkQ5pdrHj6ad5C2N4Rcrx",
"premise_still_valid": false,
"summary": "Neither ordered route exists, so no edit and no PR (ruling: stop, needs_decision). Route (a): the platform has no display-only label map for a text field. PR #28062 used select options (severity was already a select) and the object-ref widget (a registered read face for object names); neither fits a role. options on a text field parses (FieldSchema safeParse true), but the text cell (ui TextCellRenderer) ignores it: only the highlight hover title and the compact grid card read it, so the drawn highlight value and the all_users cell would still print user. Route (b): spec has no storage width for a select: maxLength on select is refused at parse, and storage is strict with notNull only (storage.width answers unrecognized_keys). Premise correction, measured: the card's drift premise is false. On PostgreSQL 16, a sys_user table built by the platform's own driver from main's SysUser (role TEXT, rows user and admin,support), then re-synced as a select reads ZERO sys_user drift and the column stays text; the differ with varcharColumnChars bound reads zero for a select over text on postgres, mysql (text, longtext) and sqlite. Controls fire on the same rows: storage.notNull on role gives tighten_not_null destructive live, and a string maxLength 32 over varchar(64) gives narrow_varchar. The real storage effect of a plain select is on NEW deployments only: createColumn builds varchar(255) for a select (varcharColumnChars answers 255) where main builds TEXT, and the differ never reports the split. Write set, read: better-auth's admin plugin is OFF by default (plugins.admin defaults to SCIM-effective), so on a default deployment nothing writes role and it is NULL for every user. With the plugin on, its create hook stamps defaultRole user on every new user, and /admin/set-role, /admin/create-user and /admin/update-user store any string, arrays comma-joined (no roles table is configured, so no value check). Platform-admin standing does not come from this scalar (ADR-0068 D2: owner email or admin_full_access; role admin survives only as the isPlatformAdminUser back-compat fallback), so the seeded platform admin is created through signUpEmail and reads user. A readonly select does not narrow at the write door: the record validator never applies option membership to a readonly value (the sys_activity.type open-vocabulary precedent), and SelectCellRenderer draws an unknown value humanized.",
"tests": "No code change, so no red/green and no ablation. Readings (scripts and outputs in project files pm-orders/server/evidence/28105/): probe-options.mts on spec src: text+options safeParse true; select+maxLength refused (maxLength is only valid on field types that store a bounded string); select+storage.width refused unrecognized_keys. drift-probe.mjs on local PG 16 after a build of driver-sql and platform-objects closures (lock VERDICT command-exit 0, 10/10 tasks): base gives [] (column text before and after); text_options gives []; select gives [] (column text, rows unchanged: user, admin,support); ctl_notnull gives role nullability_mismatch destructive tighten_not_null; ctl_string64 gives [] (text column to string(64) is also silent). diff-probe.mjs (diffManagedTable with the driver's own varcharColumnChars): select over text on postgres, mysql text, mysql longtext, sqlite gives []; differ control string maxLength 32 over postgres varchar(64) gives narrow_varchar destructive; createColumn width for select 255, for main's role text null (TEXT).",
"gates": "n/a: no diff. dispatch-gates --commands not run (it derives from a change set; there is none).",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "5: issue_read x4 (#28105 get and get_comments, #27859, #27863), pull_request_read x1 (#28065 get). No write tools.",
"api_writes": "1: POST /repos//issues/28105/comments (this report, via post-stamped relay). One REST read: GET issues/28105/comments for the claim.",
"open_questions": [
{
"question": "How should the Setup user screens treat sys_user.role, given that no label route exists without a storage change on new deployments, a spec key, or a ui change, and given that the scalar does not answer whether a user is a platform admin?",
"status": "needs_decision",
"options": [
"A. Readonly select now: sys_user.role becomes Field.select with readonly and options User / Admin (labels through i18n:extract into the four platform bundles), platform-objects only, Clause-2 no, Metadata-protocol none, patch. Existing deployments keep their TEXT column with no drift reading (measured, all three dialects); unknown and comma values still store (readonly open vocabulary) and draw humanized. Cost: new deployments get varchar(255) instead of TEXT, a silent physical split between old and new installs the differ never reports, and a value over 255 chars refused only on new installs (no writer produces one today). This waives the ruling that the column stay TEXT. Size S.",
"B. Spec lane first: add a declared storage width for select (for example a storage member that keeps TEXT), then do A with TEXT everywhere. p1 protocol addition owned by the spec lane, a new authorable key with one consumer (this legacy field). Size M across two lanes.",
"C. ui lane: the text cell and highlight draw a label from a text field's declared options (a display-only label map), plus a spec describe change sanctioning options on text. Changes what options means on every non-option type; spec + ui. Size M.",
"D. Stop presenting the legacy scalar as Platform Role: drop role from the all_users columns and from the record highlights (keep the field readable in its Admin group), platform-objects only, patch. Reverses the role half of #27863 and #27859. Spotting admins from the list would need a column derived from real platform-admin standing, which is new capability and not proposed here."
],
"four_axes": {
"business_need": "Measured readers and writers: no ObjectStack code writes role; on a default deployment (admin plugin off) the column is empty for every user, and with the plugin on every new user is stamped user, including the seeded platform admin. A Setup admin scanning the Role column to spot admins (#27863's stated need) gets a wrong answer under any label. A and C label that wrong answer; D removes it; B serves no measured second consumer.",
"long_term": "ADR-0068 D2 retired this scalar as an authority, and isPlatformAdminUser keeps it only as a back-compat fallback. D follows that direction. A leaves a permanent, silent old/new storage split. B and C add contract surface (a storage key, a wider meaning for options) for one retired field.",
"ai_error_prevention": "A label (A, C) makes a legacy value look authoritative to people and to AI agents reading the record (Platform Role: User for a platform admin). C makes options on a non-option type half-meaningful, the trap of a key the runtime honours on some faces only. D leaves nothing misleading on the screen.",
"startup_focus": "D and A are platform-objects-only S changes. B is a protocol addition with no puller beyond this card, and C spans two lanes; the startup rule defaults both to no."
},
"recommendation": "D, because the four axes agree that the problem is the value, not its spelling: the scalar does not answer the question the column was added to answer, and a label would make a wrong answer look right. If the maintainer still wants the label, A is the cheapest route, but it needs an explicit waiver of the keep-TEXT ruling for new deployments."
}
],
"out_of_scope_findings": [
"class: b · reach: read, not driven (Setup user record and all_users list on a deployment with the better-auth admin plugin on show Platform Role user for a platform admin promoted through admin_full_access) · evidence: better-auth admin.mjs create hook stamps defaultRole user; plugin-auth platform-admin-gate.ts isPlatformAdminUser reads isPlatformAdmin first and role only as fallback; sys-user.object.ts role description says ObjectStack no longer writes it · carrier: this card's decision (option D covers it) · dedupe words: sys_user role platform admin standing, Platform Role column misleading, legacy role scalar",
"class: none · noted, not filed: the card body's premise that schema-drift reads a select over an existing TEXT column as narrow_varchar is false as measured (zero findings on postgres, mysql, sqlite); the true effect is varchar(255) on fresh tables only · carrier: none"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Release: to the decision box · 2026-11-01T05:11Z · the dev's
needs_decisionreport6553744903Produce: none (no edit, no PR; branch
claude/project-thread-8e8iiounchanged atorigin/main)Resume-from: the report's options A–D and the evidence under
pm-orders/server/evidence/28105/varchar(255)instead ofTEXT.role. better-auth's admin plugin is off by default, so the column is empty. With the plugin on, every new user is stampeduser, including the seeded platform admin, and platform-admin standing does not come from this scalar (ADR-0068 D2). So the column does not tell an admin apart.roleout of theall_userscolumns and the record highlights, and keep the field readable in its Admin group. The other options build a label for a value that is empty by default and wrong when filled.Labels:
needs-user-decisiononly;pm:dispatchedand the assignee are cleared.objectstack-fleet commentedon Nov 1, 2026
director#1 · os-steve · session_01ADHkmPVvKkogo96sJpUuyB
Brief-claimed-by: session_01ADHkmPVvKkogo96sJpUuyB · batch #438 · 2026-11-01T10:17Z
Director seat (summon #55). Presented to the maintainer in batch #438 (three items: #28590, #28105, #27855). The dev's report 6553744903 (four axes, options A–D) and the seat's release 6553778805 are the reading; the seat's increment:
origin/main35b178bd0a:sys-user.object.ts:917role: Field.text({ label: 'Platform Role', readonly: true, maxLength: 64, … }), its description "Legacy better-auth role scalar … ObjectStack no longer writes it"; ADR-0068 D2 (docs/adr/0068-unified-user-context-and-built-in-identity-roles.md:60–67) confersplatform_adminthrough the unscopedadmin_full_accessset, not this scalar. The card's own drift premise is corrected by the dev's measurement: zero drift on PostgreSQL 16, MySQL and SQLite; the real effect of a select isvarchar(255)on new installs only.userfor every user, the seeded platform admin included, when the better-auth admin plugin is on).options) for one retired field. ② No measured reader is served by a label on a value that is wrong when filled. ③ A label (A, C) makes a legacy value look authoritative to people and to agents reading the record: "Platform Role: User" on a platform admin. ④ D is platform-objects-only, size S.domain:server· 1 PR · S (precedent PR fix(platform-objects): Setup's user screens show a user's real state, and the copy-password line only when one was generated #28065, +194/−66, the fold that added the column); A = server · 1 PR · S, with a waiver of the keep-TEXT condition on new installs; B = spec p1 + server, M; C = spec + ui, M.admin_full_access), the way a mainstream admin console shows the profile or role that actually governs. The seat recommends D1 = D + file that card at p3; D0 = D alone. Not taken: A (a label on a wrong value), B and C (contract surface for a retired field).1 remaining item
objectstack-fleet commentedon Nov 1, 2026
director#1 · os-steve · session_01ADHkmPVvKkogo96sJpUuyB
Ruling: batch #438 item 2 · letter D1 · maintainer 「同意」 2026-11-01T12:10Z
Director seat (summon #55; brief-claimed in 6556630620, re-analysed at the maintainer's request in 6556798793, presented in the summon chat). The maintainer answered verbatim: 「同意」 = the seat's recommendation, so this card's letter is D1.
What the ruling binds (letter D1):
sys_user.rolestays better-auth's own column: no synchronisation, no projection of platform-admin standing into it. The 2026-08-18 veto on re-synthesisinguser.role = 'admin'(Every better-auth-native /admin/ route refuses ObjectStack platform admins — the vendor gates on the legacyuser.role === 'admin'scalar ADR-0068 D2 stopped synthesizing #9652, comment 5334874978) stands; the vendor plugin'sdefaultRolestamp and the ADR-0092 import / create-user pass-through are untouched.roleleaves theall_userslist columns and the record highlights; the field stays readable in its Admin group (packages/platform-objects/src/identity/sys-user.object.ts:917,Field.text, readonly). No type, storage or spec change (Clause-②: no,Metadata-protocol: none).hasPlatformAdminStanding,packages/core/src/security/resolve-authz-context.ts:2145: theOS_PLATFORM_OWNER_EMAILlist, or the unscopedadmin_full_accessgrant under thesingleposture), which is the need Setup → Users list has no Status (banned) or Role column, so a banned user or an admin cannot be spotted from the list (#27521 row 15) #27863 stated.${user.role === 'admin'}(ui/packages/app-shell/src/providers/ExpressionProvider.tsx:550) teaches a gate that is always false for a platform admin; the documented gate isisPlatformAdmin.Workload of the letter:
domain:server· one PR · S (precedent PR #28065, +194/−66, the fold that added the column) · plus one new p3 card (the derived platform-admin column; lane by triage, likelydomain:serverfor the object's list view).Execution: the
domain:serverseat picks the card up withRuling-ref:naming this comment; theneeds-user-decision→pm:queuestroke and the filing of the follow-up card (pointer back here) are that seat's. TheRuled:body line is written by this act.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Ruled → queued · 2026-11-01T20:02Z
Ruling-ref: 6557746678 (letter D1). Executing the ruling's strokes for the
domain:serverseat:needs-user-decision→pm:queuein this stroke, and the follow-up card (a Users-list column derived from real platform-admin standing) is filed as #28793, blocked on this card. Server seat 5 picks this card up next; its claim follows when the dev branch exists. TheExpressionProvider.tsxexample the ruling carries is a ui finding and is named in the order, not edited here.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-01T20:05Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5msFrPy8g6WzAxVUjGAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-8n9g96(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6557746678 (director ruling, batch #438 item 2, letter D1; maintainer 「同意」 2026-11-01T12:10Z); pick announced to seats 1-3, all three confirmed no clash.
File surface:
packages/platform-objects/src/identity/sys-user.object.ts(theall_userscolumns and any highlight / compact surface showingrole), its tests in that folder (the #27863 pin), a@objectstack/platform-objectschangeset, anycontent/docs/**page pinning the Users list columns. ⛔ Nopackages/spec/**, ⛔ noui/**.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is the
sys_userobject's Setup list and highlight declarations · the Users list presents better-auth's legacyrolescalar as if it were the platform role · reached by every Setup admin opening UsersThread-read: 6563160193
Premise: holds (read at
origin/maind9c4cdc4e:all_userscolumns listrole;sys_user.roleisField.text, readonly)Serial constraints cleared: none needed; follow-up #28793 is blocked on this card.
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28105.mdin the project.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim amendment to 6563185751 · 2026-11-01T20:06Z · branch
claude/project-thread-8n9g96unchangedFile surface, added:
packages/platform-objects/src/pages/sys-user.page.ts(itsrecord:highlightsnode listsrole; the object-levelhighlightFieldsdoes not) and its tests inpackages/platform-objects/src/pages/. Measured by the dev; ruling 1 of the order (role leaves the record highlights) lands there. Everything else in the claim stands.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5msFrPy8g6WzAxVUjG
os-dev-report
{
"issue": 28105,
"status": "done",
"branch": "claude/project-thread-8n9g96",
"pr": "#28810",
"session": "session_01FEkQ5msFrPy8g6WzAxVUjG",
"premise_still_valid": true,
"summary": "Ruling D1 (Ruling-ref 6557746678) executed: role leaves the all_users list columns (now name, email, phone_number, email_verified, banned, source, created_at) and the sys_user_detail record:highlights chip strip (now email, phone_number, email_verified). PM assumption 1 confirmed: the highlight lived on the record page, not object highlightFields (already name/email/email_verified); claim amended 6563206575 to add pages/sys-user.page.ts. sys_user.role is untouched: Field.text, readonly, group Admin, same label, storage and description, still served by the data API. The #27863 pin is renamed sys-user-list-columns.test.ts and pins the new columns, role absent from every list view and highlightFields, and the field guard; a new page pin covers every highlights node and details section.",
"tests": "Red first at base d9c4cdc: the two pin files 4 failed / 4 passed (red: all_users columns pin, no list view shows role, header chip-strip pin, no highlights node carries role; green on both: highlightFields, columns declared, role field guard, details sections). Green at head 280a84d: 8/8; pnpm --filter @objectstack/platform-objects test 108 files / 1503 tests passed; typecheck exit 0. Live probe on pnpm dev --fresh (showcase), GET /api/v1/meta/object/sys_user and /api/v1/meta/page/sys_user_detail as admin@objectos.ai: base all_users.columns [name,email,phone_number,email_verified,banned,role,source,created_at], header highlights [email,phone_number,email_verified,role]; head [name,email,phone_number,email_verified,banned,source,created_at] and [email,phone_number,email_verified]; role field text/readonly/Admin on both; head data read serves role user for the seeded dev admin and null for the auditor demo. Base leg restored with git checkout HEAD, git diff HEAD empty, both blob hashes equal HEAD, dist rebuilt. No ablation: declarative metadata, red-on-base is the negative leg.",
"gates": "Local subset at 280a84d: 59 derived families (dispatch-gates --commands --repo objectstack-ai/objectstack), all exit 0, captured before any pipe; --ran reconciliation: 59 derived, 59 run, 0 NOT-MEASURED. check-plugin-teardown-shape --self-test first exit 3 (shallow clone), exit 0 after fetching its positive-control commit. pnpm build 117/117; eslint --no-inline-config packages/platform-objects exit 0. CI check-runs on PR 28810: in_progress at report time (not awaited, per os-dev.md). Full list in the PR body Verification section.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub calls; reads went through gh api REST",
"api_writes": "2 — relay pr_create (#28810, draft, no labels, no assignees; read-back identical 12119 bytes) and this os-dev-report comment via post-stamped; plus 1 git push",
"line_budget": "6 files, +151/-44 against origin/main (two source files +17/-12, one renamed pin, one new pin, one changeset)",
"deviations": [
"File surface widened by claim amendment 6563206575 to packages/platform-objects/src/pages/sys-user.page.ts and its folder tests: the measured highlight surface lives there.",
"pnpm lint scoped to the touched package run as pnpm exec eslint --no-inline-config packages/platform-objects: platform-objects has no lint script of its own.",
"No labels on pr_create: the order names none, and a changeset ships, so skip-changeset does not apply."
],
"files_changed": [
".changeset/28105-sys-user-role-off-setup-screens.md",
"packages/platform-objects/src/identity/sys-user.object.ts",
"packages/platform-objects/src/identity/sys-user-list-columns.test.ts (renamed from sys-user-list-status-role-columns.test.ts)",
"packages/platform-objects/src/pages/sys-user.page.ts",
"packages/platform-objects/src/pages/sys-user-record-highlights.test.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat (named in the order) · ui/packages/app-shell/src/providers/ExpressionProvider.tsx expression-context example ${user.role === admin} teaches a gate that is always false for a platform admin (documented gate isPlatformAdmin); not edited",
"carrier: none · noted, not filed · sys_user.role keeps the field label Platform Role (ruling 2: no field change), so the standard edit form and the data API still call it that; observation, not a filing class",
"carrier: #28793 · the derived platform-admin column on the Users list, the need #27863 stated"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #28810 comment 6564064548 · head
280a84dd· 2026-11-01T21:22Z. No contract review owed (no schema, rules or narrowing face). Landing:pr_ready+automerge_enablethrough the relay; the merge queue closes this card, and #28793 is unblocked on merge.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Landed · 2026-11-01T22:00Z · PR #28810 merged through the merge queue as
1e019240f9; this card closed as completed by itsFixesline.1e019240f9pm:dispatchedremoved and the seat unassigned in this stroke,domain:*/area:*keptRuling D1 (6557746678) is executed: the follow-up column from real platform-admin standing, #28793, is unblocked in the same round, and the ui example the ruling carries is #28812.