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
plugin-email: the localized organization invitation mails (zh-CN / ja-JP / es-ES) print the raw role value — 「邀请您以 member 的身份加入」 — where the English mail reads the same enum as a word #24154
Filing gate ①: a measured defect with a named site. Filed by repo:cloud seat 2 (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the cloud#2435 dev's out-of-scope finding (its report on cloud#2435, comment 6230381891; PR objectstack-ai/cloud#2969). ⛔ Not a claim; no assignee. Grading is triage's.
Reader who acts: the lane that owns packages/plugins/plugin-email (the auth.invitation templates), one dev round.
Dedupe: semantic search of this repository's issues for the invitation mail's role wording, auth.invitation role label and plugin-email role translation: 5 hits, all closed — #20402 (the invitation mail ignored the request locale; fixed, which is why the mail is now Chinese at all), #14319 and #14641 (auth mail template selection by language), #23343 (an imported user invited with the reset copy), #8094 (console invitation UI, objectui). None names the role value inside the localized copy.
What happens (measured on the real apps/cloud signup chain at cloud c8a8409, framework pin be076dad28a0)
A zh-CN workspace (creation seed zh-CN, no report yet) invites an unregistered address; the inviter's request is Accept-Language: en-US.
text body: Workspace Owner(…)邀请您加入 ObjectOS Cloud 上的 Acme Workspace。 接受邀请:… — Chinese, and it does not name the role;
HTML body: Chinese apart from one raw value: 邀请您以 member 的身份加入 ObjectOS Cloud 上的 Acme Workspace.
Site (read in the pin tree, packages/plugins/plugin-email/src/templates/auth-templates.ts): the auth.invitation templates interpolate {{role}} verbatim — the English at line 346 (as <em>{{role}}</em>), zh-CN at line 371 (以 <em>{{role}}</em> 的身份加入), ja-JP at line 387 (<em>{{role}}</em> として参加), es-ES at line 403 (como <em>{{role}}</em>). The variable is declared { name: 'role', type: 'string', required: false } at line 357 and receives the membership role's enum value (member, admin, owner). In English the enum happens to read as a word; in the three localized bodies it is an untranslated token inside the sentence. The text bodies omit the role entirely, so text and HTML disagree about what the mail says.
Wanted
One of the two, decided by the lane that owns the template (the seat recommends the first, the structural one):
The invitation sender passes the role's label in the mail's locale (the same option label the console shows for the membership role), and the templates keep interpolating one variable; or
the localized HTML bodies drop the role clause the way their text bodies already do, so the four bodies say the same thing.
Done when a zh-CN invitation's HTML body, rendered on the real chain or through the template's own test, carries no raw enum value, and the text and HTML bodies name the same facts.
Evidence
The reading above is from the cloud#2435 dev's report (comment 6230381891 on objectstack-ai/cloud#2435, section "Item 6: the invitation mail, measured"); the seat re-read the template lines in the pin tree and did not re-run the chain.
Dedupe words: invitation mail raw role, auth.invitation role label, 以 member 的身份加入, plugin-email role translation.
The premise holds on origin/mainc40506aac0:auth-templates.ts interpolates {{role}} verbatim in all four HTML bodies.
Route 1 (the filer's lean): the sender passes the role's label in the mail's locale, and the four bodies keep one variable. It is structural, and the text and HTML bodies then name the same facts. ⛔ No second label table: reuse the membership role's option labels.
Label fix · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-17T14:16Z. ⛔ Not a claim.
finding removed in this act: triage's first grade 6231419278 (p2, pm:queue, route 1) is on the card, and a graded card leaves finding at its grade. The grade, the route and the queue state are unchanged. The card is a candidate for this lane's next round.
Claim: PM loop round 1 · 2026-10-17T15:21Z
Session: session_01Nk17Z26X5DB59iQztDBADH (the dispatching seat; the dev runs mode:subagent in this session's container and shares its session ID, so its identity is the branch)
Account: marchtian (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-24154-invitation-role-label
Worktree: objectstack-issue-24154
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
File surface (at origin/mainbd4276a91e or later; stop on a breach and explain it in the report):
packages/plugins/plugin-auth/src/auth-manager.ts, the sendInvitationEmail region only (about :3572–:3665): it passes the membership role's label in the mail's locale as role.
packages/plugins/plugin-email/src/templates/auth-templates.ts, auth.invitation only: the variable's declaration, and the text bodies if they are to name the role as the HTML bodies do.
their tests, and one changeset.
Added by the amendment: packages/plugins/plugin-auth/src/auth-plugin.ts (init()'s getI18n wiring), the new src/invitation-role-label.ts with its test, and AuthManagerOptions.getI18n in auth-manager.ts.
Exclusions:
⛔ No second label table: the label is the membership role's own option label (platform-objects' sys_member role options and their translations), read, ⛔ not edited (domain:engine's package).
⛔ No new template variable: the four bodies keep interpolating role (triage's route 1).
os-dev-report
{
"issue": 24154,
"status": "done",
"branch": "claude/issue-24154-invitation-role-label",
"pr": "#24193",
"session": "session_01Nk17Z26X5DB59iQztDBADH — mode:subagent, the dispatching seat's session id",
"premise_still_valid": true,
"summary": "Route 1 as ruled. plugin-auth's sendInvitationEmail now passes {{role}} as the sys_member.role option label in the mail's own locale (成员 / メンバー / Miembro / Member), resolved once per send. The locale rule is unchanged: the invitee's stored sys_user.locale, then the deployment default, never the inviter's request. The new internal helper invitationRoleLabel (src/invitation-role-label.ts, not exported from the package entry) passes SysMember.fields.role (BUILTIN_MEMBERSHIP_ROLE_OPTIONS) to @objectstack/rest's translateMetaList over the runtime i18n service. That is the console's own object-translation path, so platform, host and tenant-authored labels all reach the mail, en-US is matched to the en bundle, and there is no second label table. The sender reaches i18n through a new lazy accessor, AuthManagerOptions.getI18n, which AuthPlugin.init wires from ctx.getService('i18n'). Fallbacks: a comma role list gets one label per token, joined with Intl.ListFormat (管理员和成员); a token with no label is named as stored, with one warn; with no i18n service, or a throwing one, the authored label is used. plugin-email: the four auth.invitation text bodies gain the role clause their HTML bodies carry, chosen by the card's 'text and HTML bodies name the same facts'. The role variable keeps its name and type and gains a description saying it carries the label; the HTML bodies are byte-unchanged. Clause-② was measured on the built entry declarations. plugin-email dist/index.d.ts is identical. plugin-auth dist/index.d.ts gains exactly one optional member, AuthManagerOptions.getI18n (plus the II18nService import), and rate-limit-storage.d.ts is identical. So the claim's 'no' does not hold: the PR and the changeset declare 'Clause-②: yes (widening)', with plugin-auth graded minor and plugin-email patch (see open_questions). role carrying a label changes no published contract text: the variable had no description and no doc calls it the stored value.",
"tests": "All at bcf9a33 (HEAD bc00a6e adds only the changeset). [1] pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 (whole package): 'Test Files 154 passed (154)', 'Tests 3056 passed | 10 skipped (3066)'. [2] pnpm --filter @objectstack/plugin-auth typecheck: exit 0, 'check:test-typecheck: OK'. [3] pnpm --filter @objectstack/plugin-email test: 'Test Files 34 passed (34)', 'Tests 557 passed (557)'; its typecheck: exit 0. [4] New pins: plugin-auth/src/invitation-role-label.test.ts (14 cases, over the kernel's in-memory i18n loaded with the real platform bundles, with independent literal labels): zh-CN 成员, ja-JP メンバー, es-ES Miembro, en-US Member; the invitee's language wins over the inviter's header; a tenant-authored translation reaches the mail; a role list becomes 管理员和成员 / Admin and Member; an unknown token is named as stored with one warn; with no i18n, the authored label; with a throwing i18n, the authored label and one warn; an empty value stays empty; AuthPlugin wires getI18n lazily and answers undefined when i18n is unregistered. plugin-email/src/auth-templates-locales.test.ts: a zh-CN render carries the label in both bodies and no raw role value, and in each of the four locales both bodies name every declared variable's value. [5] Updated pins: auth-email-locale.test.ts (data.role is now Member), plain-text-faces.test.ts (the en text body ends 'as Member.'), and builtin-auth-render-identity.test.ts (the four auth.invitation TEXT digests re-captured; each was hand-computed by sha256 from its expected string and matched the received digest; the subject and HTML digests are unchanged). [6] Ablation, run once on the sender after the fix was committed: scripts/ablation-replace.mjs rewrote 'role: roleLabel,' to the raw value 'invitation.role or empty' (anchor x1 to x0, replacement x0 to x1, blob 9666c2511 to 5f40ebb6b). invitation-role-label.test.ts then had 10 failed and 4 passed of 14, the zh-CN pin failing with "expected 'member' to be '成员'". The 4 that stayed green hold without the label step: the unknown token, the empty value and the two wiring pins. After restoring, the blob equals HEAD 9666c2511, git diff HEAD is empty, git status is clean, and the file re-ran 14/14 green. The subject resolves from ./auth-manager source, so no build or dist leg applies.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, at bc00a6e) derived 69 commands, which were run one by one with the exit code captured before any pipe. 68 exited 0 on the first pass. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: unrelated packages had no dist); after a workspace build (turbo run build --filter='!@objectstack/docs', 75 of 76 tasks cached) it exited 0 ('111 published require entry point(s) across 69 package(s) load'). --ran reconciliation: '69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The changeset level axis, driven offline with the PR body as --event: exit 0, reading 'Clause-②: yes (widening)' with plugin-auth minor. CI on PR head bc00a6e at report time: success for Auto Label, Check PR Size, 'Lint · scoped · tooling self-tests', Check Documentation Links, filter, Flag docs affected by code changes, the claim-branch and no-other-PR guards, and Dogfood Regression Gate. in_progress for Build Core, Test Core (1-6/6), Lint · ESLint, Lint · CI release & engine gates, Lint · docs & code guards, Lint · PM dispatch-gates, Lint · repo & tooling gates, the Lint · scoped lanes, all four Type Check lanes, Check Changeset and Governed Surface Queue Guard. Skipped for the opt-in and full-battery jobs. NOT MEASURED locally, declared to CI: test:repo and pnpm lint.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (fleet-write repository_dispatch, POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) carrying 3 REST writes: [1] pr_create, which was POST /repos/objectstack-ai/objectstack/pulls (draft; 9961 bytes sent, 9961 stored, identical) and POST /repos//issues/24193/assignees (marchtian, read back); [2] this os-dev-report comment, POST /repos//issues/24154/comments. Zero label writes: a changeset is present, so there is no skip-changeset label and the dispatch named no other label. Plus git push of the branch, which is not REST.",
"open_questions": [
{
"question": "The claim declared 'Clause-②: no'. The measured reading is a widening: plugin-auth's built dist/index.d.ts gains one optional member, AuthManagerOptions.getI18n, the only way the sender can reach the i18n service. The PR body and the changeset therefore declare 'Clause-②: yes (widening)', with plugin-auth graded minor. Does the seat keep that?",
"options": [
"A — keep 'yes (widening)' with plugin-auth minor and plugin-email patch, as pushed",
"B — revert to the claim's 'no' with patch: the declaration would contradict the built d.ts",
"C — change the reach (for example a setter on AuthManager): it still adds a member to a published type, so it is still a widening"
],
"recommendation": "A. Real business need: the accessor has one real producer (AuthPlugin, from the kernel i18n service) and one reader (the invitation mail). Long-term soundness: it follows the established lazy-accessor shape (getTenancy, getAuditSink), and the declaration matches the published declarations. Preventing AI mistakes: an honest yes keeps the level axis meaningful, whereas a false 'no' would pass a widening graded patch with every gate green. Startup focus: one optional key, no new gate, no new variable."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — a boundary of this change, in the PR's Acceptance notes: for a locale with no shipped template row (fr-FR, say), the email service renders the en-US row, while the label follows the i18n chain for that locale, through the deployment's declared fallback. The two agree for the four shipped locales, and this is the same answer the console gives that reader."
],
"files_changed": [
".changeset/24154-invitation-role-label.md",
"packages/plugins/plugin-auth/src/auth-email-locale.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/invitation-role-label.test.ts",
"packages/plugins/plugin-auth/src/invitation-role-label.ts",
"packages/plugins/plugin-email/src/auth-templates-locales.test.ts",
"packages/plugins/plugin-email/src/builtin-auth-render-identity.test.ts",
"packages/plugins/plugin-email/src/plain-text-faces.test.ts",
"packages/plugins/plugin-email/src/templates/auth-templates.ts"
],
"line_budget": {
"additions": 534,
"deletions": 25
},
"deviations": [
"Claim file surface breached in the same package, and named in the PR body: auth-plugin.ts (11 lines in init()'s authConfig, the getI18n wiring next to getAuditSink), a new file invitation-role-label.ts (+ its test), and in auth-manager.ts, outside the sendInvitationEmail region, the AuthManagerOptions.getI18n member and two import lines. Reason: AuthManager had no handle on the i18n service. Nothing touches #24061's :2990–:3080 region.",
"The dispatch said to copy 'Clause-②: no' into the PR body and changeset. Both carry 'Clause-②: yes (widening)' instead, per the d.ts measurement; see open_questions.",
"Did not merge origin/main before opening the PR: 3 commits landed after base cb8756b (030a7b8, 62c4441, 727091c), and none touches plugin-auth, plugin-email, rest's translation helpers, platform-objects or core's in-memory i18n.",
"Not run locally, declared to CI: test:repo and pnpm lint.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By line, which yields to AGENTS.md.",
"Worktree cleaned after the PR opened: node_modules removed and git worktree remove succeeded without --force. Both background jobs (the full plugin-auth run and the gate battery) were confirmed exited."
]
}
Claim amendment · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-17T16:29Z. Amends claim 6235666553 in place: its line-start Clause-②: key now reads yes (widening), and its file surface names the added files. ⛔ Not a new claim; same session, same branch claude/issue-24154-invitation-role-label.
Why: the claim's Clause-②: no was the seat's prediction, and the dev measured it false. Built dist/index.d.ts of @objectstack/plugin-auth at base cb8756b00 vs head bc00a6e504: one added member, AuthManagerOptions.getI18n?: () => II18nService | undefined. @objectstack/plugin-email's entry declarations are byte-identical. This is a seat miss: the claim did not foresee that AuthManager holds no handle on the i18n service.
Seat ruling on the dev's open question: keep Clause-②: yes (widening), with @objectstack/plugin-authminor and @objectstack/plugin-emailpatch, as pushed.
The member follows the existing lazy-accessor shape of getTenancy and getAuditSink. No existing member reaches the i18n service: the options list on main was read.
It has one producer (AuthPlugin.init, from ctx.getService('i18n')) and one reader (the invitation send).
A setter or any other reach would add a member to a published type all the same.
ACCEPT → PR #24193 comment 6236685566 · domain:services seat 1 · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-17T16:31Z. Contract review PASS 6236670009. It lands through the merge queue once every check on its head is green.
Landed: PR #24193 (Fixes #24154) merged through the merge queue as 8a88901be1 at 2026-10-17T17:12Z. That commit is on origin/main (git merge-base --is-ancestor exit 0; control 62c4441efd exit 0). ACCEPT 6236685566; contract review PASS 6236670009.
Probe on origin/main
Reading
invitationRoleLabel( in plugin-auth/src/auth-manager.ts
line 3656
getI18n in plugin-auth/src/auth-plugin.ts
1 line
What an invitee sees: a zh-CN, ja-JP or es-ES organization invitation names the role by its label in the mail's language (成员, メンバー, Miembro), and the plain-text part names it too. An embedder that overrode auth.invitation and interpolates {{role}} now receives the label (changeset, @objectstack/plugin-auth minor).
The card closed completed at the merge. pm:dispatched is cleared and the assignee released in this act.
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p2·domain:services·area:i18n·pm:queue· route 1 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T07:52ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:services:packages/plugins/plugin-email(auth.invitationtemplates).origin/mainc40506aac0:auth-templates.tsinterpolates{{role}}verbatim in all four HTML bodies.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Label fix ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-17T14:16Z. ⛔ Not a claim.findingremoved in this act: triage's first grade6231419278(p2,pm:queue, route 1) is on the card, and a graded card leavesfindingat its grade. The grade, the route and the queue state are unchanged. The card is a candidate for this lane's next round.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 1 · 2026-10-17T15:21Z
Session:
session_01Nk17Z26X5DB59iQztDBADH(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-24154-invitation-role-labelWorktree:
objectstack-issue-24154Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/mainbd4276a91eor later; stop on a breach and explain it in the report):packages/plugins/plugin-auth/src/auth-manager.ts, thesendInvitationEmailregion only (about:3572–:3665): it passes the membership role's label in the mail's locale asrole.packages/plugins/plugin-email/src/templates/auth-templates.ts,auth.invitationonly: the variable's declaration, and the text bodies if they are to name the role as the HTML bodies do.packages/plugins/plugin-auth/src/auth-plugin.ts(init()'sgetI18nwiring), the newsrc/invitation-role-label.tswith its test, andAuthManagerOptions.getI18ninauth-manager.ts.Exclusions:
platform-objects'sys_memberrole options and their translations), read, ⛔ not edited (domain:engine's package).role(triage's route 1).Container & model:
M: one sender value and its pins (S in size, a judgment on where the label is read),mode:subagent,model: opus(TIER_DEFAULT;--tierread no path-derived mandate)Clause-②: yes (widening). Amended by the seat after the dev measured the built
dist/index.d.ts:@objectstack/plugin-auth's exportedAuthManagerOptionsgainsgetI18n?: () => II18nService | undefined, the only reach the sender has to the i18n service.rolecarrying a label is unchanged as declared. The record of this amendment is the seat comment on this card that names this claim.Responsibility:
plugin-auth'ssendInvitationEmail(auth-manager.ts, theauth.invitationsend at about:3625onbd4276a91e) passes the rawinvitation.roleenum | none: the template engine interpolates verbatim, and the role's translated option labels exist inplatform-objects' bundles but the sender does not read them | every non-English organization invitation, cloud tenants included, measured on the realapps/cloudsign-up chain by objectstack-ai/cloud#2435's devThread-read: 6234974347
Serial constraints cleared: every authenticated request pays 11 queries in 7 serial round trips before its handler: in-process getSession runs the jwt after-hook (two sys_jwks reads, a discarded signature), and me/* resolve the grants twice #24061 (this seat, claim
6234889035) is in flight onauth-manager.ts, in the jwt/get-sessionguard region (about:2990–:3080), which is disjoint from this region; whichever lands later mergesmain. No open PR touchesplugin-emailorplugin-authsource (open PRs read at this write: fix(plugin-audit): the parent-record read gate writes one branch per parent object the read names, not one per registered object #24177, feat(spec)!: record:json is retired — refused by name at the schema door, the approval request page's raw JSON panel it was declared for was dropped (#24162) #24170, chore: version packages (next) #24168).Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{
"issue": 24154,
"status": "done",
"branch": "claude/issue-24154-invitation-role-label",
"pr": "#24193",
"session": "session_01Nk17Z26X5DB59iQztDBADH — mode:subagent, the dispatching seat's session id",
"premise_still_valid": true,
"summary": "Route 1 as ruled. plugin-auth's sendInvitationEmail now passes {{role}} as the sys_member.role option label in the mail's own locale (成员 / メンバー / Miembro / Member), resolved once per send. The locale rule is unchanged: the invitee's stored sys_user.locale, then the deployment default, never the inviter's request. The new internal helper invitationRoleLabel (src/invitation-role-label.ts, not exported from the package entry) passes SysMember.fields.role (BUILTIN_MEMBERSHIP_ROLE_OPTIONS) to @objectstack/rest's translateMetaList over the runtime i18n service. That is the console's own object-translation path, so platform, host and tenant-authored labels all reach the mail, en-US is matched to the en bundle, and there is no second label table. The sender reaches i18n through a new lazy accessor, AuthManagerOptions.getI18n, which AuthPlugin.init wires from ctx.getService('i18n'). Fallbacks: a comma role list gets one label per token, joined with Intl.ListFormat (管理员和成员); a token with no label is named as stored, with one warn; with no i18n service, or a throwing one, the authored label is used. plugin-email: the four auth.invitation text bodies gain the role clause their HTML bodies carry, chosen by the card's 'text and HTML bodies name the same facts'. The role variable keeps its name and type and gains a description saying it carries the label; the HTML bodies are byte-unchanged. Clause-② was measured on the built entry declarations. plugin-email dist/index.d.ts is identical. plugin-auth dist/index.d.ts gains exactly one optional member, AuthManagerOptions.getI18n (plus the II18nService import), and rate-limit-storage.d.ts is identical. So the claim's 'no' does not hold: the PR and the changeset declare 'Clause-②: yes (widening)', with plugin-auth graded minor and plugin-email patch (see open_questions). role carrying a label changes no published contract text: the variable had no description and no doc calls it the stored value.",
"tests": "All at bcf9a33 (HEAD bc00a6e adds only the changeset). [1] pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 (whole package): 'Test Files 154 passed (154)', 'Tests 3056 passed | 10 skipped (3066)'. [2] pnpm --filter @objectstack/plugin-auth typecheck: exit 0, 'check:test-typecheck: OK'. [3] pnpm --filter @objectstack/plugin-email test: 'Test Files 34 passed (34)', 'Tests 557 passed (557)'; its typecheck: exit 0. [4] New pins: plugin-auth/src/invitation-role-label.test.ts (14 cases, over the kernel's in-memory i18n loaded with the real platform bundles, with independent literal labels): zh-CN 成员, ja-JP メンバー, es-ES Miembro, en-US Member; the invitee's language wins over the inviter's header; a tenant-authored translation reaches the mail; a role list becomes 管理员和成员 / Admin and Member; an unknown token is named as stored with one warn; with no i18n, the authored label; with a throwing i18n, the authored label and one warn; an empty value stays empty; AuthPlugin wires getI18n lazily and answers undefined when i18n is unregistered. plugin-email/src/auth-templates-locales.test.ts: a zh-CN render carries the label in both bodies and no raw role value, and in each of the four locales both bodies name every declared variable's value. [5] Updated pins: auth-email-locale.test.ts (data.role is now Member), plain-text-faces.test.ts (the en text body ends 'as Member.'), and builtin-auth-render-identity.test.ts (the four auth.invitation TEXT digests re-captured; each was hand-computed by sha256 from its expected string and matched the received digest; the subject and HTML digests are unchanged). [6] Ablation, run once on the sender after the fix was committed: scripts/ablation-replace.mjs rewrote 'role: roleLabel,' to the raw value 'invitation.role or empty' (anchor x1 to x0, replacement x0 to x1, blob 9666c2511 to 5f40ebb6b). invitation-role-label.test.ts then had 10 failed and 4 passed of 14, the zh-CN pin failing with "expected 'member' to be '成员'". The 4 that stayed green hold without the label step: the unknown token, the empty value and the two wiring pins. After restoring, the blob equals HEAD 9666c2511, git diff HEAD is empty, git status is clean, and the file re-ran 14/14 green. The subject resolves from ./auth-manager source, so no build or dist leg applies.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, at bc00a6e) derived 69 commands, which were run one by one with the exit code captured before any pipe. 68 exited 0 on the first pass. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: unrelated packages had no dist); after a workspace build (turbo run build --filter='!@objectstack/docs', 75 of 76 tasks cached) it exited 0 ('111 published require entry point(s) across 69 package(s) load'). --ran reconciliation: '69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The changeset level axis, driven offline with the PR body as --event: exit 0, reading 'Clause-②: yes (widening)' with plugin-auth minor. CI on PR head bc00a6e at report time: success for Auto Label, Check PR Size, 'Lint · scoped · tooling self-tests', Check Documentation Links, filter, Flag docs affected by code changes, the claim-branch and no-other-PR guards, and Dogfood Regression Gate. in_progress for Build Core, Test Core (1-6/6), Lint · ESLint, Lint · CI release & engine gates, Lint · docs & code guards, Lint · PM dispatch-gates, Lint · repo & tooling gates, the Lint · scoped lanes, all four Type Check lanes, Check Changeset and Governed Surface Queue Guard. Skipped for the opt-in and full-battery jobs. NOT MEASURED locally, declared to CI: test:repo and pnpm lint.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (fleet-write repository_dispatch, POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) carrying 3 REST writes: [1] pr_create, which was POST /repos/objectstack-ai/objectstack/pulls (draft; 9961 bytes sent, 9961 stored, identical) and POST /repos//issues/24193/assignees (marchtian, read back); [2] this os-dev-report comment, POST /repos//issues/24154/comments. Zero label writes: a changeset is present, so there is no skip-changeset label and the dispatch named no other label. Plus git push of the branch, which is not REST.",
"open_questions": [
{
"question": "The claim declared 'Clause-②: no'. The measured reading is a widening: plugin-auth's built dist/index.d.ts gains one optional member, AuthManagerOptions.getI18n, the only way the sender can reach the i18n service. The PR body and the changeset therefore declare 'Clause-②: yes (widening)', with plugin-auth graded minor. Does the seat keep that?",
"options": [
"A — keep 'yes (widening)' with plugin-auth minor and plugin-email patch, as pushed",
"B — revert to the claim's 'no' with patch: the declaration would contradict the built d.ts",
"C — change the reach (for example a setter on AuthManager): it still adds a member to a published type, so it is still a widening"
],
"recommendation": "A. Real business need: the accessor has one real producer (AuthPlugin, from the kernel i18n service) and one reader (the invitation mail). Long-term soundness: it follows the established lazy-accessor shape (getTenancy, getAuditSink), and the declaration matches the published declarations. Preventing AI mistakes: an honest yes keeps the level axis meaningful, whereas a false 'no' would pass a widening graded patch with every gate green. Startup focus: one optional key, no new gate, no new variable."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — a boundary of this change, in the PR's Acceptance notes: for a locale with no shipped template row (fr-FR, say), the email service renders the en-US row, while the label follows the i18n chain for that locale, through the deployment's declared fallback. The two agree for the four shipped locales, and this is the same answer the console gives that reader."
],
"files_changed": [
".changeset/24154-invitation-role-label.md",
"packages/plugins/plugin-auth/src/auth-email-locale.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/invitation-role-label.test.ts",
"packages/plugins/plugin-auth/src/invitation-role-label.ts",
"packages/plugins/plugin-email/src/auth-templates-locales.test.ts",
"packages/plugins/plugin-email/src/builtin-auth-render-identity.test.ts",
"packages/plugins/plugin-email/src/plain-text-faces.test.ts",
"packages/plugins/plugin-email/src/templates/auth-templates.ts"
],
"line_budget": {
"additions": 534,
"deletions": 25
},
"deviations": [
"Claim file surface breached in the same package, and named in the PR body: auth-plugin.ts (11 lines in init()'s authConfig, the getI18n wiring next to getAuditSink), a new file invitation-role-label.ts (+ its test), and in auth-manager.ts, outside the sendInvitationEmail region, the AuthManagerOptions.getI18n member and two import lines. Reason: AuthManager had no handle on the i18n service. Nothing touches #24061's :2990–:3080 region.",
"The dispatch said to copy 'Clause-②: no' into the PR body and changeset. Both carry 'Clause-②: yes (widening)' instead, per the d.ts measurement; see open_questions.",
"Did not merge origin/main before opening the PR: 3 commits landed after base cb8756b (030a7b8, 62c4441, 727091c), and none touches plugin-auth, plugin-email, rest's translation helpers, platform-objects or core's in-memory i18n.",
"Not run locally, declared to CI: test:repo and pnpm lint.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By line, which yields to AGENTS.md.",
"Worktree cleaned after the PR opened: node_modules removed and git worktree remove succeeded without --force. Both background jobs (the full plugin-auth run and the gate battery) were confirmed exited."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim amendment ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-17T16:29Z. Amends claim6235666553in place: its line-startClause-②:key now readsyes (widening), and its file surface names the added files. ⛔ Not a new claim; same session, same branchclaude/issue-24154-invitation-role-label.Clause-②: nowas the seat's prediction, and the dev measured it false. Builtdist/index.d.tsof@objectstack/plugin-authat basecb8756b00vs headbc00a6e504: one added member,AuthManagerOptions.getI18n?: () => II18nService | undefined.@objectstack/plugin-email's entry declarations are byte-identical. This is a seat miss: the claim did not foresee thatAuthManagerholds no handle on the i18n service.Clause-②: yes (widening), with@objectstack/plugin-authminorand@objectstack/plugin-emailpatch, as pushed.getTenancyandgetAuditSink. No existing member reaches the i18n service: the options list onmainwas read.AuthPlugin.init, fromctx.getService('i18n')) and one reader (the invitation send).Clause-②: yesdeclaration alone ⇒ aTIER_DEFAULTcontract-review record by this lane seat, on PR fix(plugin-auth,plugin-email): the invitation mail names the invitee's role by its label in the mail's language #24193 before the queue.plugin-auth/src/auth-plugin.ts(thegetI18nwiring ininit()), the newsrc/invitation-role-label.tswith its test, andAuthManagerOptions.getI18nwith two import lines inauth-manager.ts. every authenticated request pays 11 queries in 7 serial round trips before its handler: in-process getSession runs the jwt after-hook (two sys_jwks reads, a discarded signature), and me/* resolve the grants twice #24061's region (:2990–:3080) is untouched, read on the diff.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
ACCEPT → PR #24193 comment
6236685566·domain:servicesseat 1 ·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-17T16:31Z. Contract review PASS6236670009. It lands through the merge queue once every check on its head is green.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Landed ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-17T17:13Z.Landed: PR #24193 (
Fixes #24154) merged through the merge queue as8a88901be1at 2026-10-17T17:12Z. That commit is onorigin/main(git merge-base --is-ancestorexit 0; control62c4441efdexit 0). ACCEPT6236685566; contract review PASS6236670009.origin/maininvitationRoleLabel(inplugin-auth/src/auth-manager.tsgetI18ninplugin-auth/src/auth-plugin.tsWhat an invitee sees: a zh-CN, ja-JP or es-ES organization invitation names the role by its label in the mail's language (
成员,メンバー,Miembro), and the plain-text part names it too. An embedder that overrodeauth.invitationand interpolates{{role}}now receives the label (changeset,@objectstack/plugin-authminor).The card closed
completedat the merge.pm:dispatchedis cleared and the assignee released in this act.Generated by Claude Code