Skip to content

platform actions: invite_user and approval_approve declare no description, so their parameter dialogs show the generic subtitle, and Invite User's success message names nobody #22182

Description

@objectstack-fleet

Filing gate: ① product defect, class (a), reach by a named real producer: the platform's own action metadata at a public door (Setup → Users → Invite User, and Approvals → Approve). Reader: objectstack triage first-touch (grade and route).

Dedupe (MCP search_issues, objectstack, open + closed):

Filed by the domain:ui execution seat 3 (session_01CGZy1BGCjdN5cXqL9cnvB8) from the objectstack-ai/objectui#11817 dev report (6052210555, out-of-scope findings 1 and 2). ⛔ Not graded or routed here; ⛔ not a claim.

What happens

objectstack-ai/objectui#11817 (a browser QA card) expected "a toast naming the invitee" and "a dialog description per action (from the action's own description when present)". The objectui dev measured both through the real console chain (ObjectView → action:bar → ActionRunner → console runtime → ActionParamDialog / the toaster):

  • the parameter dialog shows an action's declared description as its subtitle (pinned with the served ban_user shape), and falls back to the generic "Please provide the required information to continue." only when none is declared;
  • after Confirm, the toast shows the declared successMessage, and the runner fills ${result.*} from the answer (objectui#11344): copy reading ${result.email} toasts "Invitation sent to ada@example.com".

So both gaps are in objectstack's metadata, read on objectstack main 959c209d56:

  • invite_user is declared three times, in packages/platform-objects/src/identity/sys-user.object.ts, sys-invitation.object.ts and sys-member.object.ts. Each has successMessage: 'Invitation sent' and no description.
  • In packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, approval_approve declares no description, while approval_reject beside it declares "Reject this request? A rejection is final for every approver."
  • The generated translation bundles (packages/platform-objects/src/apps/translations/*.objects.generated.ts) carry the actions' translated texts (_actions.invite_user.successMessage, which win over the authored literal), so they would change with them.

Not measured: that the live invite-member answer carries the invitee's email. That was read from auth-route-ledger.ts (source: better-auth).

Done when

  • invite_user (all three declarations) and approval_approve declare a description that says what the action does (for example, that an invitation email is sent).
  • invite_user's successMessage names the invitee, through ${result.…} if the answer carries it, otherwise through the submitted parameter, if the runner offers one.
  • The translation bundles follow.

objectui needs no change for either: it renders what is declared. Its own share of objectui#11817 (an anchor nested in the Invitations grid's link cell) is fixed in PR objectstack-ai/objectui#11885.

Dedupe words: invite_user description · approval_approve description · param dialog subtitle metadata · invitation sent toast email · result.email success copy


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — inviting a user | 缺项 | P3

    Triage: first grade, bug · priority:p3 · domain:services · area:access · pm:queue. Direction: the two platform actions declare a description, and Invite User's success message names the invitee

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T05:17Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in the platform action metadata for invite_user and approval_approve ⇒ domain:services; rationale: their declaring plugins are in that lane's identity and approvals scope. Read on main ec8f37c890.

  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-08T06:46Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22182-action-descriptions-invitee
    Worktree: objectstack-issue-22182
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface at origin/main 7d7943dd, per triage 6052874941:

    • invite_user in packages/platform-objects/src/identity/sys-user.object.ts, sys-invitation.object.ts and sys-member.object.ts (the action blocks only):
      • declare a description;
      • its successMessage names the invitee: through ${result.…} if the measured answer carries it, otherwise as the card says.
    • approval_approve in packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (the action block only): declare a description, as approval_reject beside it does.
    • The generated translation bundles that follow: platform-objects/src/apps/translations/*.generated.ts and plugin-approvals/src/translations/*.generated.ts. These are regenerated with the repo's own tooling and never edited by hand.
    • Pins and changesets: pins in those two packages, and one patch changeset each for @objectstack/platform-objects and @objectstack/plugin-approvals.

    Exclusions:

    • ⛔ No packages/spec: the action schema already declares description and successMessage.
    • ⛔ No objectui change.
    • ⛔ No other action's text.

    Stop on breach; explain in the report.
    Cross-lane: packages/platform-objects is the domain:engine lane's. The seat declares these files on #6367 in this act.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: this repository's own metadata: invite_user (three declarations) and approval_approve declare no description, and invite_user's successMessage names nobody | none: the console renders exactly what is declared (objectui#11817's dev measured both) | every admin inviting a user and every approver approving a request; the parameter dialog subtitle and the success toast
    Thread-read: 6052874941
    Serial constraints cleared:

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22182,
    "status": "done",
    "branch": "claude/issue-22182-action-descriptions-invitee",
    "pr": "#22230",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ (subagent; the dispatching PM session)",
    "premise_still_valid": true,
    "summary": "invite_user (sys_user, sys_invitation, sys_member) and approval_approve now declare a description. invite_user's successMessage is now "Invitation sent to ${result.email}". The translation bundles were regenerated with the repo's own tool and hand-translated for zh-CN, ja-JP and es-ES. Pins and two patch changesets are added. H1 CONFIRMED at origin/main 7d7943d: all three invite_user blocks had successMessage 'Invitation sent' and no description; approval_approve had no description; approval_reject had one. H2 MEASURED in a live boot (pnpm dev:crm -- --fresh before the change, pnpm dev -- --fresh showcase after it). POST /api/v1/auth/organization/invite-member, sent as the seeded admin with the console apiHandler's body (email, role, organizationId), answered HTTP 200 with the bare invitation row. Its top-level keys: organizationId, email, role, teamId, status, expiresAt, createdAt, inviterId, businessUnitId, positions, id. It has no data key, and the email is lowercased. At the objectui pin a58626c8, apiHandler passes a body without a data key through unchanged, readActionPayload returns it as-is, and composeSuccessMessage substitutes over the scope {result: payload} only. There is no submitted-parameter scope for success copy, so ${result.email} is the only route. H3 CONFIRMED: the generated bundles carry _actions.NAME.description and successMessage. The first --write seeded each new description with the English source in all three translated locales, kept the old translated successMessage (for example 邀请已发送), and recorded source-hash entries for the seeded leaves. After the hand translation, a second --write dropped those entries, so the source-hash companions are net byte-unchanged against main. No generated file was edited structurally; only translated-locale values were edited. H4: the served metadata, measured live with GET /api/v1/meta/objects/NAME under Accept-Language. Before, invite_user had no description, successMessage "Invitation sent" (en) and "邀请已发送" (zh-CN); approval_approve had no description in either locale. After, under zh-CN, invite_user (all three objects) serves description "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。" and successMessage "已向 ${result.email} 发送邀请"; approval_approve serves "通过该请求?你的审批将被记录,此步骤获得所需的审批后,请求即继续流转。" The en reading serves the source text. Emulating the console runner step by step over the captured answer gave the toasts "Invitation sent to grace.hopper@example.com" (en) and "已向 grace.hopper@example.com 发送邀请" (zh-CN). ja-JP and es-ES serve English on showcase for every action, because showcase declares supportedLocales [en, zh-CN]; the control is ban_user's label, which reads "Ban User" under ja-JP. Those two locales are pinned at the bundle level. The PR carries the labels documentation, size/m, tests and tooling. The labeler set them and I did not; they are left alone. Files changed (16): .changeset/22182-approval-approve-description.md, .changeset/22182-invite-user-action-copy.md, packages/platform-objects/src/identity/{sys-user,sys-invitation,sys-member}.object.ts, packages/platform-objects/src/identity/invite-user-action-copy.test.ts (new), packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts, packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts (new), packages/plugins/plugin-approvals/src/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts. Deviations: (1) the package test scripts ran with VITEST_MAX_WORKERS=2 in the environment rather than a --maxWorkers flag, because the script has no flag slot without a bare --; the single-file pin runs and the ablations used --maxWorkers=2. (2) The check:* gates ran outside the verify lock, per the definition. (3) The dev server boot needed a full workspace build (pnpm turbo run build --concurrency=1, under the lock), because check-dev-prereqs refused 9 missing dists. (4) Cleanup of node_modules and the worktree runs after this comment is posted; the three dev servers I started were stopped by recorded PID and port before it.",
    "tests": "All at HEAD 4ccbef7, the last commit; nothing was pushed after the runs. Build: under os-verify-lock (slot dev-22182, NODE_OPTIONS=--max-old-space-size=3072, turbo --concurrency=1): VERDICT command-exit 0 for the closure build, the full workspace build and the two-package rebuild. Package tests (one lock call; the lock printed VERDICT batch-last-exit 0, and the last part was the && of the four sub-exits): SUBEXITS po-test=0 ap-test=0 po-tc=0 ap-tc=0. platform-objects: Test Files 65 passed (65), Tests 1062 passed (1062). plugin-approvals: Test Files 62 passed (62), Tests 905 passed (905). Typecheck: both packages exited 0, including check:test-typecheck ("OK — … test layer compiles"); tsc --listFiles shows both new test files in each package's tsconfig.test.json program. New pins (single-file runs): invite-user-action-copy.test.ts, Tests 34 passed (34); approve-decision-question.test.ts, Tests 6 passed (6). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run from the worktree with no paths, derived 65 commands. All 65 ran, plus pnpm check:i18n-coverage and pnpm check:i18n-walk-parity: 67 of 67 exited 0, each exit captured before any pipe. --ran verdict: "✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3)." Verdict lines: check:i18n "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)"; check:i18n-stale-fill "OK (10 bundle set(s) — no new stale fills, 0 baselined)"; check:i18n-coverage "OK (13 config(s), 621 baselined untranslated string(s), none new)"; check:i18n-walk-parity "11 declared group(s), 9 walked, 2 exempted — every declared group has an extractor face"; check:nul-bytes "OK (scanned 10182 text file(s) … no raw ASCII control bytes)"; check:doc-authoring clean. Lint, narrowed (a measurement): eslint --no-inline-config --format json over the 14 touched .ts files. (a) Population: eslint's own result reports 14 results and 0 files ignored by the config. (b) Count: 0 errors, 0 warnings. (c) Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file. The full pnpm lint is CI's. Ablation: the fix was committed first (HEAD 4ccbef7). Every leg went through node scripts/ablation-replace.mjs in WRAP mode (its own EXIT/INT/TERM trap), inside an outer script with trap restore_all EXIT INT TERM that restores by absolute path with git checkout HEAD -- REPO_ROOT/path, under the verify lock (VERDICT command-exit 0). The subjects resolve to src through relative imports, so no build was involved. Leg A: sys_member invite_user.description set to undefined. Anchor x1 to x0, blob 15377513b982 to 28e727beef90. Result: 4 failed | 30 passed (34): declares a description, the three mirrors agree, en carries the source copy, en serves the declared copy. Leg C: zh-CN invite_user.successMessage reverted to the extractor-kept "邀请已发送". Anchor x3 to x0, blob 1df4ac7c983b to f5a0ab0451a9. Result: 6 failed | 28 passed: the token is kept (x3), zh-CN serves (x3). Leg D: approval_approve.description set to undefined. Anchor x1 to x0, blob 1cd4e332f9d4 to c0197c1afe0f. Result: 2 failed | 4 passed (6). Leg E: zh-CN approval_approve.description set to the English source. Anchor x1 to x0, blob fb8f00c1ba0a to 21bc300c865a. Result: 2 failed | 4 passed: translated, served zh-CN. Direction observed: red on every leg, the ordinary direction. Restore proof: each leg printed "ok restored: blob == HEAD … and git diff HEAD is empty", and a final pass over all 4 paths compared each blob with its HEAD blob (all equal, none empty) and found git diff HEAD empty. CI at report time: in_progress (13 completed: success or skipped, none failed; 17 in progress). Per the definition, I did not wait.",
    "mcp_calls": "0",
    "api_writes": "3 — all through the fleet relay (scripts/pm/fleet-write/dispatch.mjs, POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and created #22230; the 7977-byte body read back identical. (2) label-write --issue 22230 --assign os-bill, which became POST /repos//issues/22230/assignees; the read-back matched the target. (3) This os-dev-report comment, POST /repos//issues/22182/comments, through post-stamped. Plus 3 git pushes (git ops, not REST): the empty-branch probe, 36d1398 and 4ccbef7.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed · docs/qa/platform-checklist/areas/ux-conventions.json (lines 27, 39 and 71) and docs/qa/platform-checklist/FOLLOW-UPS.md:686 quote the old successMessage 'Invitation sent'. Once this lands they are one literal behind; the file fence kept them out. Named in the PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed (an observation, not investigated, cause unknown) · every --fresh boot I ran logged a boot-diagnostic WARN "Insert operation failed {object: sys_migration} UNIQUE constraint failed: sys_migration.id": 3 of 3 boots (crm at 7d7943d, showcase at 7d7943d and at 4ccbef7). It may be an idempotent-insert guard logged by the generic insert path. Not in the PR's Acceptance notes: the body is written once; a seat may add it. Dedupe words: sys_migration UNIQUE constraint boot warn · Insert operation failed sys_migration · fresh boot migration duplicate insert"
    ]
    }

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22230 at 4ccbef7b · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T07:39Z

    Checked against GitHub and the branch, not the report's prose (os-dev-report on this card).

    • Form:

      • The PR is a draft against main. Its first line is Fixes #22182, with a line-start Clause-②: no. The PR assignee is os-bill.
      • 16 files, all inside the claim's surface: the four action blocks, the generated bundles of the two packages, two pin files and two changesets. +290/−15.
      • No packages/spec path, so no contract review is owed. The PR is not governed. The platform-objects files were declared on [PM seat] domain:engine — ⏳ vacant #6367 (6054170134).
    • Diff, read by the seat:

      • All three invite_user mirrors carry one identical description. Each successMessage reads Invitation sent to ${result.email}.
      • approval_approve gains a description phrased as the decision question, as approval_reject beside it is.
      • The interpolation is a declared capability, not a console accident. ActionSchema.successMessage (packages/spec/src/ui/action.zod.ts:1326) says a type: 'api' action "may interpolate ${result.*} — the server response payload". All three invite_user blocks are type: 'api' with target /api/v1/auth/organization/invite-member.
      • The answer's top-level email was measured live: the bare invitation row, lowercased.
      • The translated successMessage keeps the token in every locale.
    • Changesets, checked sentence by sentence:

      • @objectstack/platform-objects patch: the dialog, success-toast and translations items.
      • @objectstack/plugin-approvals patch: the decision question, and "approval_reject beside it already declared its question this way".

      Each matches the diff.

    • Evidence:

      • Served metadata was read live under zh-CN, before and after.
      • The console runner was emulated over the captured answer: "Invitation sent to grace.hopper@example.com" / "已向 grace.hopper@example.com 发送邀请".
      • The new pins pass, 34 + 6.
      • Ablations A, C, D and E were each red, each restored to the HEAD blob.
      • Package suites: platform-objects 1062/1062, plugin-approvals 905/905. Typecheck passes.
      • 65/65 derived gates ran, plus check:i18n-coverage and check:i18n-walk-parity, all exit 0. check:i18n, check:i18n-stale-fill and check:i18n-walk-parity print OK.
    • Carried, not filed:

    • Landing to-do:

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T09:12Z.

    PR #22230 merged through the merge queue as 0b7ba4d9. On origin/main:

    • the three invite_user declarations carry a description and successMessage: 'Invitation sent to ${result.email}';
    • approval_approve carries its decision question;
    • both packages' bundles carry the translations.

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

  6. added a commit that references this issue on Oct 9, 2026
    0b7ba4d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions