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
Setup: mutually exclusive user actions are offered together whatever the record's state (Ban with Unban, Revoke with Restore) #24760
Filing gate ③ a task the maintainer set: objectstack-ai/hotcrm#2188, the maintainer's browser pass of 2026-10-20. Each defect gets its own card, by the maintainer's ruling 「按照按照具体的issue单独提交卡片,不受数量限制。」 (recorded on objectstack-ai/hotcrm#2188, 6283467378). This is finding F-11 of track 2 (Setup as the system administrator), by the cloud tester session_01VuXyJvhGKJy8qeUvhKyxzv. It was measured on HotCRM mainf22dfa52 with @objectstack/* 18.0.0-next.5, in Chromium at 1440×900 on a locally booted, seeded install. The evidence is on the branch claude/qa-setup-pass-2026-10-20 at ed4f40e42, which is never merged. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack triage routes it.
The finding, as measured
Actual: an unbanned user offers both Ban User and Unban User (record header and row menu); an active API key offers Revoke and Restore, a revoked one the same; Unlock Account on an account that is not locked answers "Account unlocked". (OAuth Applications does it right: Disable / Enable switch with the state.)
Dedupe words: ban and unban both shown, revoke restore both shown, action visibility state.
Seat review
Landing (seat review):packages/platform-objects. Each action's visible checks only isPlatformAdmin, not the record's state.
Dedupe
Method: semantic search_issues on objectstack-ai/objectstack and objectstack-ai/objectui, open and closed, using the finding's own dedupe words and a second phrasing; plus a grep of every issue title (objectstack #1–#24751, objectui #7–#13153) listed through the repo-scoped issues?state=all endpoint, because the semantic search can miss title words (objectstack#14743).
0 hits in objectstack, 1 in objectui, not on point. New.
packages/platform-objects is domain:engine in the SKILL.md domain table (the domain:engine row); lanes/engine.md lists it too. A lane seat claims only in its own lane.
Claim: PM loop round 15 · 2026-10-20T13:56Z · p3, domain:engine (the lane objection 6285649183 was answered by triage's relabel; the maintainer's order admits p3 cards), from objectstack-ai/hotcrm#2188's Setup pass (F-11). Setup offers mutually exclusive actions together, whatever the record's state: Ban User with Unban User, Revoke API Key with Restore API Key, and Unlock Account on an account that is not locked. Each action's visible checks only the caller's standing, not the record.
Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24760-action-state-visibility
Worktree: objectstack-issue-24760
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface:
Measured first: on each location the actions declare (list_item, record_header), which fields the row record carries when visible is judged. The default Users list view shows name, email and created_at only. A predicate on banned, locked_until or revoked must not hide an action on a row that does not carry the field. If the console cannot judge the predicate on a row it serves, that is objectui's, and is reported, not built here.
ban_user, unban_user and unlock_user keep their platform-admin standing (current_user.isPlatformAdmin == true) and add the record's state;
the form follows the in-tree precedent (sys-oauth-application.object.ts's Disable / Enable pair, has(record.disabled) && record.disabled != true).
packages/platform-objects/src/identity/sys-api-key.object.ts: revoke_api_key and restore_api_key add record.revoked.
Pins in platform-objects' tests: each pair is mutually exclusive on the field, and the standing leg stays (platform-admin-affordance-standing.test.ts stays green).
The changeset:patch for @objectstack/platform-objects.
⛔ Not in scope: any change to what the ban, unban, unlock or key routes accept or answer (the server's "Account unlocked" on an unlocked account stays); labels and texts (no translation bundle moves); objectui.
Container & model: S, mode:cloud (five visible predicates, the row-context read measured in the console's action runner first), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
Clause-②: no (an action's offering: visible is judged in the console, and no server door reads it, so no accept set or served shape moves; if the dev measures a door that does, the dev stops, and the seat re-declares)
Responsibility: an administrator is offered Ban on a banned user, Restore on a live key, and Unlock on an account that is not locked | five action declarations judge the caller's standing but not the record's state | every platform administrator in Setup's Users and API Keys
Thread-read: 6285649183
Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim. No open PR touches sys-user.object.ts or sys-api-key.object.ts. This seat's #24707 is in driver-sql.
Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-20T13:59Z: mode:cloud. The dev session is session_01VXimSk38NSs68ntncNxKS8. It builds at the default tier on branch claude/issue-24760-action-state-visibility, under claim 6292572117.
Read first:
which fields the row record carries when the console judges visible, on the list row and the record header (the default Users view shows name, email and created_at only);
that no server door reads an action's visible;
what banned / ban_expires, locked_until and revoked / revoked_reason each mean for the action that reads them.
What lands:ban_user / unban_user, unlock_user and revoke_api_key / restore_api_key keep their standing leg and add the record's state, in the form of the OAuth application's Disable / Enable pair. Evaluated pins, an ablation, and a patch changeset.
Not built: any change to what the routes accept or answer; labels or texts; objectui. If the console cannot judge a field the row does not carry, the dev stops and reports it as objectui's.
Owed: the seat's review of record before the queue.
{
"issue": 24760,
"status": "done",
"branch": "claude/issue-24760-action-state-visibility",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24921",
"session": "session_01VXimSk38NSs68ntncNxKS8",
"premise_still_valid": true,
"summary": "ban_user/unban_user, unlock_user (sys_user) and revoke_api_key/restore_api_key (sys_api_key) now read the record's state, in the sys_oauth_application Disable/Enable form. The sys_user ones lead with the platform-admin standing. Ban: has(record.banned) && record.banned != true; Unban: == true. Revoke: has(record.revoked) && record.revoked != true; Restore: == true. Unlock: locked_until set OR two_factor_enabled == true, because unlockUser also clears sys_two_factor.locked_until, which the user row does not carry. Evaluated pins (11 tests) plus the moved lowering-matrix rows, a two-leg ablation, and a patch changeset. Pre-edit reads: (1) row context: objectui ObjectGrid at .objectui-sha 616df659 adds every record.FIELD an object action's visible reads to $select (listViewPredicates objectActions, then collectPredicateFieldRefs, declared fields only), so the default Users view carries banned/locked_until/two_factor_enabled; record_header uses findOne with no $select (whole row); the console evaluates through @objectstack/formula. (2) No server door reads visible: action-execution.ts 'Deliberately unread; pinned as unread'; domains/actions.ts 'rendering predicates that no dispatch door reads'; the MCP list_actions filter is system-object only; the ban/unban/unlock routes and the sys_api_key PATCH (api-key-restore-guard.ts) do not read it. Clause-2 no stands. (3) States: better-auth 1.7.3 clears banned only at the next session create after ban_expires, so the row stays banned and Unban stays offered; CEL can compare locked_until with now(), but I left it uncompared (see open_questions); restoring an admin_password_reset key is the server's gate (api-key-restore-guard.ts) and stays there.",
"tests": "All on HEAD 2740711c4 (merge of origin/main a7199ef97). pnpm --filter @objectstack/platform-objects typecheck: exit 0 (first run exit 1, 4 TS2345 in the new test file, fixed by typing the principal). pnpm --filter @objectstack/platform-objects test: exit 0, 82 files / 1232 tests. pnpm --filter @objectstack/plugin-auth test: exit 0, 171 files / 3277 passed, 10 skipped. pnpm --filter @objectstack/core test:repo: exit 0, 6 files / 82 tests. Targeted: action-state-visibility + platform-objects + platform-admin-affordance-standing + action-predicate-sparse-face: 173 passed. Ablation (the test imports src via relative paths and formula's dist is unmutated, so no rebuild was needed). Leg 1: git restore --source=a8b44d40 on both object files; markers record.banned / record.revoked at 0/0 on disk; result exit 1, 2 files failed (the new file at collection, because base revoke_api_key has no visible) plus 3 lowering rows. Leg 2: sys-user.object.ts only, marker 0; result exit 1, 5 of 11 failed (Ban/Unban x3, Unlock x2). Restore was git checkout HEAD with a trap; git hash-object equals HEAD blob 5cbafc983 (sys-user) and 171ce7a84 (sys-api-key); git diff HEAD empty; markers back to 2/2.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 2740711c4 (clone deepened with --shallow-since=2026-07-13): 66 commands, each exit captured before any pipe, all 0. check:dual-build-cjs-loads and check:i18n first exited 3 (PREREQUISITE NOT MET); after pnpm build (76 tasks, 75 cached) they were re-run and exited 0. check:type-check-debt exited 0 (nohup, waited on by PID, its closure build under the lock). --ran: 'Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. CI check-runs on PR #24921: not awaited (in_progress at report time, per os-dev). NOT MEASURED locally: the CI-only jobs dispatch-gates names (Test Core shards, Build Core, the dogfood and temporal batteries), which are CI's.",
"mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/objectui, access read; it answered read_available and attached nothing; used for an anonymous read clone at the pinned sha, since deleted). 0 GitHub MCP calls, no write tool.",
"api_writes": "3 classes, within budget. (1) git push to claude/issue-24760-action-state-visibility only: 4 pushes (the empty branch, the fix plus changeset, the main merge, the test type fix), all fast-forward. (2) One fleet-write relay pr_create (run 39443227577): POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos/objectstack-ai/objectstack/issues/24921/assignees (os-project-manager). Read-back: 7908 bytes sent and stored, identical. (3) One fleet-write relay comment: POST /repos/objectstack-ai/objectstack/issues/24760/comments (this report). Reads used gh api GET only.",
"line_budget": "none set. +190/-9 over 5 files: .changeset/24760-action-state-visibility.md +17/-0; packages/platform-objects/src/identity/action-state-visibility.test.ts +132/-0; packages/platform-objects/src/identity/sys-api-key.object.ts +8/-0; packages/platform-objects/src/identity/sys-user.object.ts +28/-6; packages/platform-objects/src/platform-objects.test.ts +5/-3",
"files_changed": [
".changeset/24760-action-state-visibility.md",
"packages/platform-objects/src/identity/action-state-visibility.test.ts",
"packages/platform-objects/src/identity/sys-api-key.object.ts",
"packages/platform-objects/src/identity/sys-user.object.ts",
"packages/platform-objects/src/platform-objects.test.ts"
],
"deviations": [
"unlock_user carries an OR leg on two_factor_enabled beyond the claim's 'add the record's state' form. Reason: unlockUser (plugin-auth auth-manager.ts) clears sys_user.locked_until AND sys_two_factor.locked_until, and the user row does not carry the second one. Effect: Unlock is still offered on an unlocked account that has two-factor enabled. Settled by the four axes; see open_questions.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line. AGENTS.md and os-dev win, and the pre-push hook refuses a model identifier.",
"Row context was measured by reading code, not on a booted showcase; the dispatch allowed this when the code leaves no doubt (objectui predicate-fields.ts, ObjectGrid.tsx, RecordDetailView.tsx/DetailView.tsx at 616df659).",
"origin/main gained 3 commits after my merge (eee65afc8 driver-sql, e2c2355af and 81e6edd46 dogfood), none in platform-objects. They were not merged; the build, typecheck and gates ran on the a7199ef97 merge.",
"No docs/qa/platform-checklist/** edit, no translation bundle edit, no objectui change, no spec key."
],
"open_questions": [
{
"question": "unlock_user: which record predicate? The CEL engine can compare locked_until with now(), but Unlock also clears a second-factor lock the row does not carry.",
"options": [
"A (landed) standing AND (locked_until set OR two_factor_enabled == true)",
"B standing AND locked_until set, with or without a greater-than comparison with now()",
"C leave it standing-only"
],
"recommendation": "A. Business need (measured): unlockUser clears sys_two_factor.locked_until too, so B hides the only early release of a second-factor lock. An expired stamp leaves failed_login_count at the threshold, so one more failure re-locks the account; Unlock resets it, which argues against a greater-than comparison with now(). A hides Unlock on the card's case, a plain account with no stamp. Long-term: the exact fix is a row-level 'locked at either stage' signal, a new key the card forbids. AI-error axis: the predicate is commented and pinned on every binding. Startup axis: no new surface. A stays offered on any 2FA-enabled account; if the seat prefers B, it is a one-line flip plus 2 pin lines."
},
{
"question": "Ban on a timed ban past ban_expires: the row stays banned:true until the next sign-in, so Ban is hidden and Unban is offered.",
"options": ["A (landed) follow the row as stored", "B compare ban_expires with now()"],
"recommendation": "A. The dispatch asked that Unban stay offered while the row says banned. B would show both on such a row, and better-auth's lazy clear is the authority on the state."
}
],
"out_of_scope_findings": [
"carrier: none — noted in the PR's Acceptance notes, not filed: the console judges visible on the row it holds, so a lock that expires while the page is open is not re-judged until the row is re-read (observation, not a defect class)."
]
}
TIER_DEFAULT record 6294007753 PASS on 2740711c4a. ACCEPT 6294466269. Not governed.
Queue
It entered the merge queue at 2026-10-20T15:27Z and left it merged at 2026-10-20T15:56Z. No gh-readonly-queue/main/pr-24921-* branch remains.
origin/main
d606fc11db is on origin/main, one squash commit carrying PR #24921's 5 files, +190 / −9.
Card
Closed completed through Fixes #24760; pm:dispatched is cleared.
What main now holds:
Setup offers each action only in the state it changes.
Users: Ban User on a user who is not banned, Unban User on one who is. A timed ban past ban_expires still reads banned until better-auth clears it at the user's next sign-in, so Unban stays offered for as long as the row says banned.
Users: Unlock Account when locked_until is set, or when the user has two-factor enabled. Unlock also clears the second-factor lock, which lives on sys_two_factor, not on the user row. An account with no lock stamp and no second factor is no longer offered it.
API Keys: Revoke API Key on a live key, Restore API Key on a revoked one.
How the list knows the state: the console fetches every field an object action's predicate reads, so the default Users view carries banned without showing it.
Unchanged: the platform-admin standing on the user actions, the labels and texts, and what the ban, unban, unlock and key routes accept and answer. Who may restore a key that a forced password reset revoked is still the server's decision.
Shipped as:patch of @objectstack/platform-objects.
The dev session session_01VXimSk38NSs68ntncNxKS8 is archived.
Activity
objectstack-fleet commentedon Oct 20, 2026
Lane objection →
pm:retriage·domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T07:22Z. ⛔ Not a claim;pm:queuestays.What is asked: the lane. Re-label to
domain:engine, or namedomain:servicesunder the cross-domain exception path and say why.Evidence (read on
origin/maina43405d596, no build):visibleconditions on the declarations", R5316285123386on [PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015) is onban_user/unban_user(packages/platform-objects/src/identity/sys-user.object.ts:144,:172) andrevoke_api_key/restore_api_key(sys-api-key.object.ts:128,:143).packages/platform-objectsisdomain:enginein the SKILL.md domain table (thedomain:enginerow);lanes/engine.mdlists it too. A lane seat claims only in its own lane.packages/platform-objects/src/identity/.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 15 · 2026-10-20T13:56Z · p3,
domain:engine(the lane objection 6285649183 was answered by triage's relabel; the maintainer's order admits p3 cards), from objectstack-ai/hotcrm#2188's Setup pass (F-11). Setup offers mutually exclusive actions together, whatever the record's state: Ban User with Unban User, Revoke API Key with Restore API Key, and Unlock Account on an account that is not locked. Each action'svisiblechecks only the caller's standing, not the record.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24760-action-state-visibilityWorktree:
objectstack-issue-24760Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
list_item,record_header), which fields the rowrecordcarries whenvisibleis judged. The default Users list view showsname,emailandcreated_atonly. A predicate onbanned,locked_untilorrevokedmust not hide an action on a row that does not carry the field. If the console cannot judge the predicate on a row it serves, that is objectui's, and is reported, not built here.packages/platform-objects/src/identity/sys-user.object.ts:ban_user,unban_userandunlock_userkeep their platform-admin standing (current_user.isPlatformAdmin == true) and add the record's state;sys-oauth-application.object.ts's Disable / Enable pair,has(record.disabled) && record.disabled != true).packages/platform-objects/src/identity/sys-api-key.object.ts:revoke_api_keyandrestore_api_keyaddrecord.revoked.platform-objects' tests: each pair is mutually exclusive on the field, and the standing leg stays (platform-admin-affordance-standing.test.tsstays green).patchfor@objectstack/platform-objects.⛔ Not in scope: any change to what the ban, unban, unlock or key routes accept or answer (the server's "Account unlocked" on an unlocked account stays); labels and texts (no translation bundle moves); objectui.
Container & model:
S,mode:cloud(fivevisiblepredicates, the row-context read measured in the console's action runner first),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (an action's offering:
visibleis judged in the console, and no server door reads it, so no accept set or served shape moves; if the dev measures a door that does, the dev stops, and the seat re-declares)Responsibility: an administrator is offered Ban on a banned user, Restore on a live key, and Unlock on an account that is not locked | five action declarations judge the caller's standing but not the record's state | every platform administrator in Setup's Users and API Keys
Thread-read: 6285649183
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim. No open PR touchessys-user.object.tsorsys-api-key.object.ts. This seat's #24707 is indriver-sql.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T13:59Z:mode:cloud. The dev session issession_01VXimSk38NSs68ntncNxKS8. It builds at the default tier on branchclaude/issue-24760-action-state-visibility, under claim 6292572117.recordcarries when the console judgesvisible, on the list row and the record header (the default Users view showsname,emailandcreated_atonly);visible;banned/ban_expires,locked_untilandrevoked/revoked_reasoneach mean for the action that reads them.ban_user/unban_user,unlock_userandrevoke_api_key/restore_api_keykeep their standing leg and add the record's state, in the form of the OAuth application's Disable / Enable pair. Evaluated pins, an ablation, and apatchchangeset.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24760, "status": "done", "branch": "claude/issue-24760-action-state-visibility", "pr": "https://github.com/objectstack-ai/objectstack/pull/24921", "session": "session_01VXimSk38NSs68ntncNxKS8", "premise_still_valid": true, "summary": "ban_user/unban_user, unlock_user (sys_user) and revoke_api_key/restore_api_key (sys_api_key) now read the record's state, in the sys_oauth_application Disable/Enable form. The sys_user ones lead with the platform-admin standing. Ban: has(record.banned) && record.banned != true; Unban: == true. Revoke: has(record.revoked) && record.revoked != true; Restore: == true. Unlock: locked_until set OR two_factor_enabled == true, because unlockUser also clears sys_two_factor.locked_until, which the user row does not carry. Evaluated pins (11 tests) plus the moved lowering-matrix rows, a two-leg ablation, and a patch changeset. Pre-edit reads: (1) row context: objectui ObjectGrid at .objectui-sha 616df659 adds every record.FIELD an object action's visible reads to $select (listViewPredicates objectActions, then collectPredicateFieldRefs, declared fields only), so the default Users view carries banned/locked_until/two_factor_enabled; record_header uses findOne with no $select (whole row); the console evaluates through @objectstack/formula. (2) No server door reads visible: action-execution.ts 'Deliberately unread; pinned as unread'; domains/actions.ts 'rendering predicates that no dispatch door reads'; the MCP list_actions filter is system-object only; the ban/unban/unlock routes and the sys_api_key PATCH (api-key-restore-guard.ts) do not read it. Clause-2 no stands. (3) States: better-auth 1.7.3 clears banned only at the next session create after ban_expires, so the row stays banned and Unban stays offered; CEL can compare locked_until with now(), but I left it uncompared (see open_questions); restoring an admin_password_reset key is the server's gate (api-key-restore-guard.ts) and stays there.", "tests": "All on HEAD 2740711c4 (merge of origin/main a7199ef97). pnpm --filter @objectstack/platform-objects typecheck: exit 0 (first run exit 1, 4 TS2345 in the new test file, fixed by typing the principal). pnpm --filter @objectstack/platform-objects test: exit 0, 82 files / 1232 tests. pnpm --filter @objectstack/plugin-auth test: exit 0, 171 files / 3277 passed, 10 skipped. pnpm --filter @objectstack/core test:repo: exit 0, 6 files / 82 tests. Targeted: action-state-visibility + platform-objects + platform-admin-affordance-standing + action-predicate-sparse-face: 173 passed. Ablation (the test imports src via relative paths and formula's dist is unmutated, so no rebuild was needed). Leg 1: git restore --source=a8b44d40 on both object files; markers record.banned / record.revoked at 0/0 on disk; result exit 1, 2 files failed (the new file at collection, because base revoke_api_key has no visible) plus 3 lowering rows. Leg 2: sys-user.object.ts only, marker 0; result exit 1, 5 of 11 failed (Ban/Unban x3, Unlock x2). Restore was git checkout HEAD with a trap; git hash-object equals HEAD blob 5cbafc983 (sys-user) and 171ce7a84 (sys-api-key); git diff HEAD empty; markers back to 2/2.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 2740711c4 (clone deepened with --shallow-since=2026-07-13): 66 commands, each exit captured before any pipe, all 0. check:dual-build-cjs-loads and check:i18n first exited 3 (PREREQUISITE NOT MET); after pnpm build (76 tasks, 75 cached) they were re-run and exited 0. check:type-check-debt exited 0 (nohup, waited on by PID, its closure build under the lock). --ran: 'Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. CI check-runs on PR #24921: not awaited (in_progress at report time, per os-dev). NOT MEASURED locally: the CI-only jobs dispatch-gates names (Test Core shards, Build Core, the dogfood and temporal batteries), which are CI's.", "mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/objectui, access read; it answered read_available and attached nothing; used for an anonymous read clone at the pinned sha, since deleted). 0 GitHub MCP calls, no write tool.", "api_writes": "3 classes, within budget. (1) git push to claude/issue-24760-action-state-visibility only: 4 pushes (the empty branch, the fix plus changeset, the main merge, the test type fix), all fast-forward. (2) One fleet-write relay pr_create (run 39443227577): POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos/objectstack-ai/objectstack/issues/24921/assignees (os-project-manager). Read-back: 7908 bytes sent and stored, identical. (3) One fleet-write relay comment: POST /repos/objectstack-ai/objectstack/issues/24760/comments (this report). Reads used gh api GET only.", "line_budget": "none set. +190/-9 over 5 files: .changeset/24760-action-state-visibility.md +17/-0; packages/platform-objects/src/identity/action-state-visibility.test.ts +132/-0; packages/platform-objects/src/identity/sys-api-key.object.ts +8/-0; packages/platform-objects/src/identity/sys-user.object.ts +28/-6; packages/platform-objects/src/platform-objects.test.ts +5/-3", "files_changed": [ ".changeset/24760-action-state-visibility.md", "packages/platform-objects/src/identity/action-state-visibility.test.ts", "packages/platform-objects/src/identity/sys-api-key.object.ts", "packages/platform-objects/src/identity/sys-user.object.ts", "packages/platform-objects/src/platform-objects.test.ts" ], "deviations": [ "unlock_user carries an OR leg on two_factor_enabled beyond the claim's 'add the record's state' form. Reason: unlockUser (plugin-auth auth-manager.ts) clears sys_user.locked_until AND sys_two_factor.locked_until, and the user row does not carry the second one. Effect: Unlock is still offered on an unlocked account that has two-factor enabled. Settled by the four axes; see open_questions.", "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line. AGENTS.md and os-dev win, and the pre-push hook refuses a model identifier.", "Row context was measured by reading code, not on a booted showcase; the dispatch allowed this when the code leaves no doubt (objectui predicate-fields.ts, ObjectGrid.tsx, RecordDetailView.tsx/DetailView.tsx at 616df659).", "origin/main gained 3 commits after my merge (eee65afc8 driver-sql, e2c2355af and 81e6edd46 dogfood), none in platform-objects. They were not merged; the build, typecheck and gates ran on the a7199ef97 merge.", "No docs/qa/platform-checklist/** edit, no translation bundle edit, no objectui change, no spec key." ], "open_questions": [ { "question": "unlock_user: which record predicate? The CEL engine can compare locked_until with now(), but Unlock also clears a second-factor lock the row does not carry.", "options": [ "A (landed) standing AND (locked_until set OR two_factor_enabled == true)", "B standing AND locked_until set, with or without a greater-than comparison with now()", "C leave it standing-only" ], "recommendation": "A. Business need (measured): unlockUser clears sys_two_factor.locked_until too, so B hides the only early release of a second-factor lock. An expired stamp leaves failed_login_count at the threshold, so one more failure re-locks the account; Unlock resets it, which argues against a greater-than comparison with now(). A hides Unlock on the card's case, a plain account with no stamp. Long-term: the exact fix is a row-level 'locked at either stage' signal, a new key the card forbids. AI-error axis: the predicate is commented and pinned on every binding. Startup axis: no new surface. A stays offered on any 2FA-enabled account; if the seat prefers B, it is a one-line flip plus 2 pin lines." }, { "question": "Ban on a timed ban past ban_expires: the row stays banned:true until the next sign-in, so Ban is hidden and Unban is offered.", "options": ["A (landed) follow the row as stored", "B compare ban_expires with now()"], "recommendation": "A. The dispatch asked that Unban stay offered while the row says banned. B would show both on such a row, and better-auth's lazy clear is the authority on the state." } ], "out_of_scope_findings": [ "carrier: none — noted in the PR's Acceptance notes, not filed: the console judges visible on the row it holds, so a lock that expires while the page is open is not re-judged until the row is re-read (observation, not a defect class)." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T15:58ZTIER_DEFAULTrecord 6294007753 PASS on2740711c4a. ACCEPT 6294466269. Not governed.gh-readonly-queue/main/pr-24921-*branch remains.origin/maind606fc11dbis onorigin/main, one squash commit carrying PR #24921's 5 files, +190 / −9.completedthroughFixes #24760;pm:dispatchedis cleared.What
mainnow holds:ban_expiresstill reads banned until better-auth clears it at the user's next sign-in, so Unban stays offered for as long as the row says banned.locked_untilis set, or when the user has two-factor enabled. Unlock also clears the second-factor lock, which lives onsys_two_factor, not on the user row. An account with no lock stamp and no second factor is no longer offered it.bannedwithout showing it.patchof@objectstack/platform-objects.The dev session
session_01VXimSk38NSs68ntncNxKS8is archived.Generated by Claude Code