Repository navigation
console: org record page's default Members tab hides the email-invite entry — invite_user lives only on the Invitations tab #11544
Description
Activity
Claim: PM loop round 1 (maintainer direct-dispatch channel)
Session:session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Branch:claude/issue-11544-invite-entry-members-tab
Worktree:objectstack-issue-11544
Domain:domain:engine
File surface:packages/platform-objects/src/identity/sys-member.object.ts,packages/platform-objects/src/pages/sys-organization.page.ts; read-mostly:packages/platform-objects/src/identity/sys-invitation.object.ts(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus—node scripts/pm/dispatch-gates.mjs --tier@ a2ec377: "no path-derived mandate … tier stays the PM's per-card judgment call"; judgment card ⇒ default judgment tier
Clause-②: no
Serial constraints cleared: PR #11538/#11537/#11536/#11533 et al. checked — only #11530 (claude/issue-9968, retiresset_user_rolefromsys_user) touches the same packageplatform-objects/src/identity/; different file, no region overlap, no barrel edits on either side. Dev must mergeorigin/mainbefore opening the PR if #11530 lands first. No in-flight claim on this card's files found.Audit (direct-dispatch): maintainer instruction, 2026-08-24 chat, quoted verbatim: 「使用 ../objectstack 项目经理技能派发这个任务」. Routing and dispatch of this single card are performed under that instruction; triage may re-check
domain:*/type on next sweep.- added a commit that references this issue
on Aug 24, 2026 { "issue": 11544, "status": "done", "branch": "claude/issue-11544-invite-entry-members-tab", "pr": "https://github.com/objectstack-ai/objectstack/pull/11562", "premise_still_valid": true, "summary": "sys_member now declares its own invite_user on list_toolbar, ahead of add_member, so the email-invite flow is reachable from the org record page's default Members tab; add_member is differentiated in chrome only (variant secondary + icon link-2), behaviour and label untouched. PM assumptions 1 and 3 CONFIRMED against objectui at bc21c704b (deriveActions assigns the whole filtered list with no cap; the bridge dispatches runRowAction with an undefined row). Assumption 2 REFUTED in its literal form: sys_member has no `email` field, so the sys_invitation copy cannot be mirrored unchanged -- resolveActionParams answers an unresolvable field-backed param with a type:'text' fallback labelled by the raw field name, an ADR-0078 valid-but-inert degradation with nothing red. The fix stayed inside platform-objects via the existing authorable objectOverride key (same device sys_user already uses for its role half), so NO spec schema change was needed and the card's premise held. DECLARED DEVIATION: one line of registry bookkeeping in packages/spec/src/kernel/public-auth-features.ts. feature-gate-guard.test.ts enforces bidirectionally that every features.* reference is booked in gatedInputs, so declaring requiresFeature:'organization' mechanically obliges the entry -- the twelve siblings beside it are the same obligation discharged; the alternatives were an ungated button that 404s (Prime Directive #10) or an undelivered card. No schema, export, authorable key or accept/reject behaviour moved, and check:generated reports 14/14 artifacts unchanged. Flagged for PM adjudication rather than decided quietly, since the card was claimed Clause-2: no.", "tests": "All on merge commit 22ecd75cb (the PR head); every exit code captured before any pipe. pnpm --filter @objectstack/platform-objects test -> EXIT=0, 'Test Files 29 passed (29) / Tests 476 passed (476)'. typecheck (platform-objects + spec) EXIT=0. check:i18n EXIT=0 ('all bundles in sync, no undeclared authoring keys'). spec check:generated EXIT=0 ('All 14 generated artifacts are up to date' -- no regeneration, the mechanical proof the public surface did not move). Also green: check:nul-bytes, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:type-check-coverage, check-adr-0087-registration, check-empty-changeset, check-changeset-no-major. Pre-merge, the rest of the dispatch-gates family also ran green: check:changeset-gate-self-tests, check:query-options-erasure, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:spec-parsed-alias, check:merge-driver, check-ci-filter-parity, check-plugin-teardown-shape, check:doc-formula-expressions, spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs. NEW TEST: identity/invite-entry-toolbar.test.ts, 21 cases; its load-bearing case is stated over all THREE mirrors, not just the broken one -- every field-backed param of every invite_user copy must name a field that really exists on the object it resolves against, which closes the class. ABLATION (predicted direction: red; observed: red): sys-member.object.ts reverted to origin/main, everything else intact. Mutation PROVEN ON DISK before any result was read -- grep counts for name:'invite_user', objectOverride:'sys_invitation' and icon:'link-2' each went to 0 and the pre-fix icon:'user-plus' on add_member returned. Mutated: 10 failed | 11 passed (21). Restored: 21 passed (21), git status --porcelain empty on the path (byte-exact restore), trap fired. NO REBUILD was needed for either leg and this is why: these suites import the object modules by RELATIVE SOURCE path, so no dist/ sits between the mutation and the assertion -- the exports-resolution precondition for a stale-dist false green does not hold here. The 11 green-under-mutation cases are the sys_user / sys_invitation mirror rows the revert did not touch. DECLARED NARROWINGS (host capability, not diff findings): this seat ran on macOS bash 3.2.57 with no `mapfile` and no `flock`. scripts/pm/os-verify-lock.sh refused with its own line 'VERDICT lock-unusable (exit 99) - never acquired - refused before waiting - nothing was built or tested', so every build and test ran UNLOCKED. check:objectui-changeset EXIT=1 with 7 self-test failures, all ten diagnostics naming 'scripts/bump-objectui.sh: line 324: mapfile: command not found' -- that script is not in this diff (0 hits in git status) and the self-test runs on temp fixtures, so it is a host gap, not a diff finding. scripts/pm/os-regen-merge.sh could not run for the same reason; its documented order was followed by hand, no merge=os-regen path is on either side of the merge, and no deferral marker is outstanding. check-dev-prereqs EXIT=1 reporting the workspace unbuilt (12 of 67 packages lack a dist/ entry point -- client-react, studio, embedder-openai and others, none touched here); that is a consequence of building only the affected closure per resource discipline, NOT a verdict about this diff. check:type-check-debt --re-measure was NOT run: it requires the full workspace closure built, which the targeted-build discipline deliberately does not produce; CI runs it.", "open_questions": [ { "question": "The card was claimed Clause-2: no with an instruction to stop if packages/spec had to be touched. No spec SCHEMA change was needed, but the feature-gate completeness guard mechanically obliges one line in PUBLIC_AUTH_FEATURES.organization.gatedInputs. Does that ledger append count as clause-2 territory for enqueue purposes?", "options": [ "A: treat it as bookkeeping and let the PR proceed -- no schema, export, authorable key or accept/reject behaviour moved, check:generated reports 14/14 artifacts unchanged, and the twelve sibling entries plus the just-landed #11530 (which edits this same list for the same reason) establish the shape", "B: re-dispatch or re-tier the card as clause-2 because the diff touches packages/spec/src/** at all", "C: split the ledger line into a separate PR -- rejected on inspection: the guard is bidirectional, so a platform-objects-only PR would be red on its own and a spec-only PR would be red on its own; neither half can land alone" ], "recommendation": "A. Real business need: the maintainer personally ordered this outcome and the button cannot ship gated without the entry. Long-term soundness: the entry is contract-first -- it is what makes 'declared = enforced' hold for the new gate, and omitting it would mean shipping either an ungated 404-ing button or an unbooked gate the guard exists to catch. Hard to get wrong for AI authors: the ledger is the structural record that stops the next mirror shipping unbooked, which is the opposite of a consumer-side tolerance. Startup scope discipline: it is one line appended to an existing list with no new surface, no new capability and no new authorable key. C is not available on the mechanism, and B costs a re-dispatch for a line whose correct form has zero latitude. The enqueue gate judges the actual diff regardless, so A is also the option that keeps the mechanical check in the loop rather than substituting my judgement for it." } ], "out_of_scope_findings": [] }ACCEPT — PR #11562
Review of record (verified against GitHub, not the report's self-description):
- PR shape: draft ✓, base
main✓, body first lineFixes #11544✓. Changed files = claimed surface + declared deviations only; no governed-face paths. - Spot readings: the sys_member
invite_usermirror carries the same target/params/requiresFeature: 'organization'as its two siblings; theemailparam'sobjectOverride: 'sys_invitation'uses the pre-existing spec key (action.zod.ts:177on origin/main; same device as sys_user'srolehalf) — zero spec schema change, as claimed.add_memberdifferentiation is chrome-only (secondary+link-2), label untouched with a defensible i18n rationale. Ablation evidence (mutation proven on disk, 10 fail → restore 21 pass, relative-source imports rule out stale-dist false greens) is the strong form. - Declared narrowings acknowledged: verify-lock unusable on this host (bash 3.2, no flock) — gates ran unlocked;
check:objectui-changesetred is a hostmapfilegap, not a diff finding;check:type-check-debt --re-measuredeferred to CI. All accepted as environmental.
Clause-② adjudication (the open question): Option A's outcome, via the gate — not around it. The enqueue gate judges the actual diff, and the diff touches
packages/spec/src/**, so the path limb fired regardless of the card'sClause-②: no(which remains a correct content-limb claim: no accept/reject behaviour moved, no surface widened —check:generated14/14 unchanged).needs:contract-reviewwas hung on both carriers and the review chain ran; it is now cleared by review, below. Option C is confirmed impossible (the guard is bidirectional — verifiedforward:/reverse:describe blocks infeature-gate-guard.test.ts), and B would re-dispatch for a line with zero latitude.Contract review (
needs:contract-review→ PASS): reviewed the contract increment — onegatedInputsentry'sys_member.actions.invite_user'appended toPUBLIC_AUTH_FEATURES.organizationin exact sibling shape; it is the booking that keeps declared = enforced for the new gate; no schema, export, or authorable key moved. Reviewer tier: this dispatch seat, machine-verified atCONTRACT_REVIEW_TIER(claude-fable-5) — host lacks theclaude-code-remote get_sessionreading; substitute machine reading declared: the session transcript's harness-recorded served-model field (claude-fable-5, last 3 records). Self-review of an own-dispatched card per the 2026-08-21 relaxation (the seat wrote no code on this card).Next: gate jobs → green ⇒ ready + auto-merge (queue). Consumer follow-up: cloud-side
.objectstack-shabump card to be filed on acceptance per multi-repo rule 3.- PR shape: draft ✓, base
Contract review — PASS (fable seat, session
5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse readingget_session→external_metadata.last_served_model=claude-fable-5, matchingCONTRACT_REVIEW_TIERread from origin/main; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: not this seat's dispatch — independent review.Reviewed PR #11562 @
22ecd75cbdagainst the actual diff.- The affordance lands as pure metadata:
sys_membermirrorsinvite_uservia the EXISTING authorableobjectOverridekey (the same mechanismsys_user's copy already uses) — assumption 2's refutation is the substantive finding: an unchanged mirror would have shipped an ADR-0078 valid-but-inert dialog (unresolvableemailparam silently degrading to an untyped text box). No schema, no export, no authorable key moved —check:generatedreports all 14 artifacts up-to-date, the mechanical confirmation the public surface did not move. - The one
packages/specline is bookkeeping the guard mechanically obliges:feature-gate-guard.test.tsenforces bidirectionally that arequiresFeature-gated action is booked inPUBLIC_AUTH_FEATURES— the alternative was an ungated button that 404s where the capability is off. The dev flagged it for this gate rather than deciding quietly — correct posture, and this gate judges it: registry booking, not clause-② accept/reject movement. - The class gets closed, not just the instance: the new test's load-bearing case quantifies over ALL THREE
invite_usermirrors — every field-backed param must name a real field on its resolution object — so the next inert-dialog copy lands red instead of silent. - Chrome differentiation verified as real (variant/icon both render through measured paths); UI-side bridge behavior read-only measured against objectui, nothing edited there; ablation predicted red, observed red.
Verdict: PASS. Clearing
needs:contract-reviewon both carriers. Enqueue/flip belongs to the dispatching seat's landing window.
Generated by Claude Code
- The affordance lands as pure metadata:
- added a commit that references this issue
on Oct 7, 2026
Symptom
The in-shell Team surface (
sys_organizationrecord page, ADR-0081) defaults to tab-0 Members. Its related-list toolbar carries exactly one action, Add Member (add_member— attach an EXISTING user by userId). The email-invite entry (invite_user) lives only on tab-1 Invitations (sys_invitationrelated list). Both buttons render asvariant: primarywith the sameuser-plusicon.Measured effect: the maintainer, looking to "invite a teammate by email", landed on Members and concluded the product has no invite entry. If the author cannot find it, users will not.
Mechanism / file surface
packages/platform-objects/src/pages/sys-organization.page.ts— tab order: Members (sys_member) / Invitations (sys_invitation) / Teams.packages/platform-objects/src/identity/sys-member.object.ts—list_toolbar:add_memberonly (primary,user-plus).packages/platform-objects/src/identity/sys-invitation.object.ts—list_toolbar:invite_user(primary,user-plus) →POST /api/v1/auth/organization/invite-member. The delivery half works once found:sendInvitationEmailis wired (templateauth.invitation, accept URL/_console/accept-invitation/<id>).Premise verified against
origin/main@ 4c9780c (2026-08-24):invite_userappears only onsys_invitationandsys_user;sys_member's toolbar has onlyadd_member.Requested outcome
Make the email-invite flow discoverable from the default Members tab. Suggested route (PM suggestion, not a ruling — implementer verifies):
invite_userontosys_member'slist_toolbar(same target / params /requiresFeature: 'organization'gate as thesys_invitationcopy), andadd_member(icon and/or label) so the two toolbar actions do not render as identical primaryuser-plusbuttons.Mechanism to verify before writing metadata: the objectui related-list toolbar bridge must render BOTH declared toolbar actions on the record page's related list.
Notes
sys_member"Add Member" action targetsPOST /organization/add-member, which better-auth 1.7.0-rc.2 never mounts (server-only) — on multi-org there is NO remaining UI path to attach an existing user to an org #9941 / docs: the newly mountedPOST /organization/add-memberis undocumented — and it is the ONLY multi-org path to attach an existing user #10050 (add_memberendpoint mounting — resolved).