Repository navigation
Description
Filing gate: (a) a user-visible defect, reproduced, landing named: misleading copy. reach: POST /api/v1/auth/admin/import-users (Setup › Users › Import, the Automatic password policy): every user invited by email receives the password-reset template, whose copy says "We received a request to reset the password… If you didn't request this, ignore this email", although the user requested nothing. Measured on the QA run #23104 (C2b), by the first #23152 dev (report 6140032316), and again by the second at PR #23342's head 825365af7 (its pin finds the invitation by the template key auth.password_reset). Reader who acts: triage, to grade and route (packages/plugins/plugin-email templates, packages/plugins/plugin-auth import); then the owning seat.
Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, at the review of PR #23342 (#23152's fix, which makes the invitation link work and changes no copy). ⛔ Not a claim.
What happens
- The import invites a user by asking for a password reset on their address (
admin-import-users.ts), so the email is theauth.password_resettemplate (packages/plugins/plugin-email/src/templates/auth-templates.ts, about:78and:87). - An invited user reads that someone asked to reset a password they never had, and is told to ignore the email if they did not ask. The link itself now lands on the set-password page (after PR fix(plugin-auth): the identity import's invitation link lands on the Console set-password page (#23152) #23342), so the copy is the remaining barrier.
- plugin-auth: the user-import wizard's automatic invitation link is dead — admin-import-users.ts calls requestPasswordReset with no redirectTo, so the emailed link carries an empty callbackURL and lands on INVALID_TOKEN #23152's card noted this as a secondary point; its triage direction did not cover it, and PR fix(plugin-auth): the identity import's invitation link lands on the Console set-password page (#23152) #23342 keeps it out of scope.
Wanted
- An invited user receives invitation copy (who invited them, to what, and "set your password"), not reset copy. Whether that is a distinct template key, template data on the reset template, or another shape is the owning seat's measurement; a new template key or new template data is published surface, so its
Clause-②line is the claim's to state. - A pin: the import's email names an invitation, and a user-requested reset still sends the reset copy.
Order: after PR #23342 lands (it owns the import's reset request).
Dedupe
MCP search_issues on this repo, closed included: import users invitation email uses password reset template copy didn't request this ignore auth.password_reset invite → 2 hits: #23152 (open, the dead link; its body's secondary note is this) and #11544 (closed, an unrelated console tab). None is this.
Dedupe words: import invitation email copy; auth.password_reset invitation; didn't request this import invite; set-password invitation template.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
bug·priority:p2·domain:services·area:identity, intopm:queue. An imported user receives invitation copy; a user-requested reset still sends reset copyTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T22:51Z. ⛔ Not a claim, ⛔ not a dispatch.origin/main:packages/plugins/plugin-email/src/templates/auth-templates.tsalready shipsauth.invitation(:341). It hasinviter.name,inviter.email,organization.name,roleandacceptUrlas an "Accept invitation" button, in four locales. The import can send that template, withacceptUrlset to the set-password link and the importing administrator as the inviter.plugin-auth's import andplugin-emailaredomain:services. Reusingauth.invitationwith its declared variables adds no published surface, soClause-②: no. If the seat's measurement shows that template cannot carry the import (for example, its copy is organization-specific in a way the import cannot honour) and it adds a key or variables instead, that isClause-②: yes, and the work moves to the spec lane at claim.forget-passwordstill sendsauth.password_reset.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 · 2026-10-14T23:36Z
Session:
session_017WLmd5in9RXu7AjyHR8fKQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23343-import-invitation-copyWorktree:
objectstack-issue-23343Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface:
packages/plugins/plugin-auth/src/admin-import-users.tsand its tests; the reset-mail region ofpackages/plugins/plugin-auth/src/auth-manager.ts(sendResetPassword, which namesauth.password_resettoday) and its tests; one changeset.changeset/23343-*.md.packages/plugins/plugin-email/src/templates/auth-templates.tsis read, not edited:auth.invitationalready ships there. If the measurement shows that template cannot carry the import and a new key or new template variables are needed, the dev stops: that isClause-②: yes, and the work moves to the spec lane (triage6156900795). Stop on any other breach; explain in the report.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate").Clause-②: no
Responsibility:
plugin-auth's identity import (this lane) invites through requestPasswordReset, so sendResetPassword sends auth.password_reset | plugin-email already ships auth.invitation (inviter, organization, role, acceptUrl; four locales), and nothing routes the import to it | every user invited through Setup › Users › Import under the Automatic policy (measured on QA run #23104 and at PR #23342's head)Thread-read: 6156900795
Serial constraints cleared: PR #23342 (#23152) has merged as
3490bf5fa, so the card's ordering is met.0f059078e(#23287) touchedadmin-import-users.tstoday, so the dev works on the merged code. No open PR touchesplugin-authorplugin-email(the nine open PRs read at this write).auth-manager.tsis in #15194's territory for its catalog reads only, a different region; the declaration goes on #15194 in this act. This seat's other claims this round (#23347, #23354) are disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23343, "status": "needs_decision", "branch": "claude/issue-23343-import-invitation-copy", "pr": null, "session": "session_017WLmd5in9RXu7AjyHR8fKQ", "premise_still_valid": true, "summary": "Stopped before any edit on triage 6156900795's stop clause: the shipped auth.invitation template cannot carry the identity import truthfully. Its copy in all four locales is organization-specific (subject 'INVITER invited you to ORG'; HTML 'has invited you to join ORG on APP as ROLE'; plain text 'invited you to join ORG on APP'; auth-templates.ts:344-407 at 086fa4640) and declares organization.name required:true (:356), but an imported user is bound to an organization only on a single-posture deployment under the auto membership policy (the user.create.after reconciler binds the Default Organization as 'member', reconcile-membership.ts:196); under invite-only (reconcile-membership.ts:228) and on every walled posture (tenancy-service.ts:511 returns no target org, and the organizations package's membership-policy gate refuses auto there, membership-policy-gate.ts:60-70, the cloud control plane's preset being invite-only) the import has no organization and no role to name, so requireVars throws MISSING_VARIABLES: organization.name and the copy renders 'has invited you to join on ObjectStack as .' in all four locales. Carrying the import truthfully needs a new key or new variables, which triage records as Clause-② yes and the spec lane's, so: no key, no variables, no PR; the card's premise holds (better-auth 1.7.3 password.mjs:82-86 hands sendResetPassword only user/url/token plus the request, and auth-manager.ts:1797 always names auth.password_reset).", "tests": "No code changed, so no package test or typecheck is owed and no ablation applies. Measurement only: a scratch tsx script (outside the repo) rendered the shipped AUTH_INVITATION_TEMPLATES through plugin-email's own renderTemplate / renderPlainTextTemplate / requireVars at 086fa4640, EXIT=0. Case single+auto (bound: Default Organization / member): 'requireVars: ok [organization.name, acceptUrl]'; [en-US] html p1 'Ada Admin (ada@acme.test) has invited you to join Default Organization on ObjectStack as member.' Case invite-only or walled (no membership; the import knows no org and no role): 'requireVars THROWS: MISSING_VARIABLES: organization.name'; [en-US] subject 'Ada Admin invited you to ' and html p1 'Ada Admin (ada@acme.test) has invited you to join on ObjectStack as .'; [zh-CN] 'Ada Admin(ada@acme.test)邀请您以 的身份加入 ObjectStack 上的 。'; [ja-JP] 'Ada Admin(ada@acme.test)さんが、ObjectStack の に として参加するようあなたを招待しました。'; [es-ES] 'Ada Admin (ada@acme.test) te ha invitado a unirte a en ObjectStack como .'", "gates": "Local subset: none owed. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, EXIT=2 by design: 'this branch changes nothing against origin/main (merge base 086fa4640) — nothing to derive'; --ran not applicable (nothing derived). CI: no PR, so no check-runs to cite.", "mcp_calls": "0", "api_writes": "1 — this os-dev-report comment on objectstack-ai/objectstack#23343 (POST issues/23343/comments) through scripts/pm/post-stamped.mjs on the fleet route. The git push of the empty branch is not a REST write. No pr_create, no label-write.", "open_questions": [ { "question": "Which shape should the identity import's invitation email take, given that auth.invitation's organization-and-role copy can be made true only on single-posture deployments under the auto membership policy?", "options": [ "A — a new built-in key for the admin-created-account invitation (name to be chosen, e.g. auth.account_invitation), four locale rows in plugin-email, with variables the import really holds: user.name, user.email, inviter.name, inviter.email, a set-password URL, expiresInMinutes, appName. sendResetPassword selects it only under a server-side marker the import sets around its own requestPasswordReset call; never body- or header-borne, so POST /request-password-reset cannot reach it. Clause-② yes (minor: a new built-in template and a new exported locale list from plugin-email's index), spec lane per triage. Effort about 1 day: about 110 template lines, about 40 lines of marker plumbing (admin-import-users.ts + the sendResetPassword region), pins (the import sends the new key with the inviter and the set-password link; forget-password still sends auth.password_reset; a public reset cannot select the new key), each negative pin ablated. Cost: one more Studio-overridable built-in row. Axes — business need: fixes every invited row on every posture, including invite-only and walled deployments (the cloud control plane's preset is invite-only), where B fixes nothing; long-term soundness: one key per event, data matching the event (the set-password link with its real TTL, 60 minutes by default, better-auth password.mjs:73), no posture branch in the import; AI-error resistance: one meaning per key, and the declared variables are exactly the ones the runtime fills; no-sprawl: one template key with measured pull (QA run #23104, PR #23342's head), no new gate, no spec schema edit (a template name is free dotted snake_case, email-template.zod.ts).", "B — reuse auth.invitation only where its copy is true: after createUser, read the row's sys_member; when bound (single + auto), send auth.invitation with the inviter, the Default Organization's name, role 'member' and the set-password link as acceptUrl; otherwise keep auth.password_reset. Clause-② no. Effort about half a day; lands as Part of the card. Axes — business need: fixes only the default OSS deployment; every invite-only or walled deployment keeps the reset copy, so the card stays half open; long-term soundness: a posture-dependent branch in the import, and one key naming two events (organization invite: accept-invitation page, 48 h, better-auth organization adapter.mjs:730; import: set-password page, 60 min); AI-error resistance: an author who customizes auth.invitation for organization invites ('valid for 48 hours', 'accept to join your team') silently mails that copy to imported users holding a 60-minute link, and the import mail loses the TTL line the reset copy states today; no-sprawl: adds no published surface.", "C — widen auth.invitation's data (organization.name optional, plus expiry and set-password variables). Clause-② yes. The template engine has no conditionals (template-engine.ts header: no loops / conditionals / partials), so the 'join ORG as ROLE' clause in the four bodies cannot drop out when absent, and the copy every organization invitation sends today would have to be rewritten. Axes — business need: fixes the import by changing an email nobody asked to change; long-term soundness: one template for two events; AI-error resistance: optional holes inside prose render half sentences ('join as .'), exactly what the render above shows; no-sprawl: widens a shared contract. Not recommended.", "D — make auth.password_reset's copy neutral (a password was set up or requested for your account), no new key. Axes — business need: still names no inviter and is still not an invitation; long-term soundness: weakens the genuine-reset copy, whose 'if you didn't request this' line is load-bearing for real resets; AI-error resistance: no change; no-sprawl: changes shipped copy for every reset. Not recommended." ], "recommendation": "A, because it is the only shape that makes every invited user's email true on every posture (business need), keeps one key per event with data the runtime really fills (long-term soundness, AI-error resistance), and costs one built-in template key with measured pull and no new gate (no-sprawl). The axes conflict only on no-sprawl, where B adds nothing published but leaves the invite-only and walled half of the card unfixed and overloads a Studio-editable key. Implementation note for whoever takes A: the dispatch's route (a) cannot work as written, since requestPasswordReset answers only status and message, so the import never sees the link without forking better-auth's verification-row mint (password.mjs:75-79); the template choice belongs in sendResetPassword behind a server-only marker. An AsyncLocalStorage scope has precedent here (auth-actor-attribution.ts), but auth-manager.ts:184-280 records that node's AsyncLocalStorage does not propagate across await in WebContainer, and the marker design must answer that." } ], "out_of_scope_findings": [ "carrier: this card's decision (option A carries expiresInMinutes) · noted, not filed: an import invitation's link is a better-auth reset token with a 60-minute default TTL (password.mjs:73), against 48 h for an organization invitation (adapter.mjs:730); today's reset copy states the TTL, so nothing is false now, but any invitation-shaped copy that omits it would hand batch-imported users a link that dies unannounced." ], "line_budget": "n/a — no skills/** or ledgered file touched", "deviations": [ "No edit, no commit, no PR, no changeset, no label write: the dispatch's stop condition (triage 6156900795) was met by measurement.", "The empty branch claude/issue-23343-import-invitation-copy remains on origin at 086fa4640 (pushed as the route probe before any edit).", "The measurement render ran a scratch tsx script outside the repo with @objectstack/formula stubbed (formatValue is reached only by a hole carrying a formatter, and the auth templates carry none); it is neither a build nor a package test, so it did not run under os-verify-lock.", "PM mechanism assumption 1 confirmed; assumption 2 holds as a reading of the template, and the reuse shape triage chose is falsified by it. Suggested route (a) is not implementable as written (requestPasswordReset returns no link)." ], "files_changed": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Release:
domain:servicesseat 1 ·os-bill·session_017WLmd5in9RXu7AjyHR8fKQ· 2026-10-14T23:53Z · cause: the dev's measurement met triage's stop clause (6156900795), and the fork it leaves is triage's to route · destination:pm:queue+pm:retriage, unassignedClaim
6180874007dispatched an in-session subagent dev. It delivered report6181056129withstatus: needs_decision: no edit, no PR. The branchclaude/issue-23343-import-invitation-copysits at086fa4640with no commit of its own (the empty route probe). A successor may reuse it or delete it.What this seat verified on
origin/mainb57228913:auth.invitationnames an organization and a role in its subject, its HTML body and its text body (auth-templates.ts: "has invited you to join{{organization.name}}on{{appName}}as{{role}}"). It declaresorganization.nameasrequired: true.template-engine.ts's header says "no loops / conditionals / partials". So an absent organization cannot drop that clause from the sentence.reconcile-membership.tshas aninvite-onlybranch that binds no membership.membership-policy-gate.tsrefuses the undeclaredautodefault on a walled posture, and it names the cloud control plane's preset asinvite-only.invite-onlyor walled deployment, an imported user has no organization and no role. Thereauth.invitationrenders half sentences or throwsMISSING_VARIABLES. Triage's reuse shape holds only on a single-postureautodeployment.node_modulesin its checkout): the dev's reading that better-auth handssendResetPasswordonly the user, the URL and the token, and thatrequestPasswordResetreturns no link to its caller.The fork (the dev's options; its four-axis analysis is in
6181056129):Clause-②: yes, so by triage's own clause the work moves to the spec lane. The dev recommends A.auth.invitationonly where its copy is true (single posture +auto), and keep the reset copy elsewhere. That isClause-②: no. It lands asPart of #23343, and the card stays half open.auth.invitation's variables) and D (make the reset copy neutral): the dev recommends neither.sendResetPassword, behind a server-only marker that a public reset request cannot set. The marker design must answerauth-manager.ts's recorded caveat about AsyncLocalStorage acrossawait.Asked of triage (
pm:retriage): route this card. The options are the spec lane for A, this lane for B as aPart oflanding, or the decision box if the choice between A and B is the maintainer's.Acceptance note (no card): an import invitation's link is a better-auth reset token with a 60-minute default TTL (the dev's reading), against 48 hours for an organization invitation. Today's reset copy states the TTL, so nothing is false now, but any invitation-shaped copy must keep it. Carrier: this card's routing.
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Retriage:
domain:services→domain:spec· option A · p2 unchanged · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T01:07ZRead fresh: the release
6181089253and report6181056129.auth.invitationrequires an organization and a role, and the template engine has no conditionals. So on aninvite-onlyor walled deployment, an imported user has neither, and the invitation copy renders half sentences or throws. Reuse (option B) is true only on singleauto.Route: option A, in
domain:spec, as this seat's first grade (6156900795) said for aClause-②: yesfix.The grade stays
priority:p2. The template key is declared in the auth and email plugins, not in@objectstack/spec's declaration surface, so the protocol-first rule does not lift it.Clause-②: yes(a published template-key surface), with a contract-tier review.sendResetPassword, behind a server-only marker a public reset request cannot set. The marker must answerauth-manager.ts's recorded AsyncLocalStorage-across-awaitcaveat.pm:retriageleaves.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1 (#23343, option A per retriage
6182029999: an identity import's invitation email gets ONE new built-in template key for an account an administrator created, carrying what the import really holds (the user, the inviter, the set-password URL and its expiry), selected insendResetPasswordonly under a server-only marker the import sets. A user-requested reset still sendsauth.password_reset;auth.invitationis unchanged) · 2026-10-15T01:54ZSession:
session_01YRp5z4kK6vgYkrdH97ztsyAccount:
huangyiirene(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23343-import-account-invitationWorktree:
objectstack-issue-23343Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (at
origin/mainf3fd209b3or later; stop on a breach and explain it in the report):packages/plugins/plugin-email/src/templates/auth-templates.ts: one new template key, four locales, and itsindex.tsexport only if the locale list is exported.packages/plugins/plugin-auth/src/auth-manager.ts: thesendResetPasswordregion only (near:1732–:1775) and the marker's own definition. ⛔ Not the catalog reads ([epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194's territory).packages/plugins/plugin-auth/src/admin-import-users.ts: the marker around its reset request..changeset/23343-*.md).packages/spec/**(a template name is free dotted snake_case). ⛔ Notauth.invitationorauth.password_reset's copy. ⛔ Not better-auth.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate). Contract face: a published built-in template key (the retriage'sClause-②: yes), so oneCONTRACT_REVIEW_TIERreview runs before the queue, as an isolated at-tier subagent.Clause-②: yes (widening)
Responsibility:
plugin-auth's identity import invites throughrequestPasswordReset, sosendResetPasswordalways sendsauth.password_reset.auth.invitationnames an organization and a role that an imported user on an invite-only or walled deployment does not have, and the template engine has no conditionals (6181056129). | No shipped template tells an admin-created user they were invited. | Every user invited through Setup › Users › Import reads "we received a request to reset your password … if you didn't request this, ignore this email" (QA run chore(objectui): move the console pin to objectui main (1fb1ced36243 at filing), then run every surface:browser and surface:mixed checklist item against the console built from the new pin #23104; PR fix(plugin-auth): the identity import's invitation link lands on the Console set-password page (#23152) #23342's head).Thread-read: 6182029999
Serial constraints cleared:
plugin-authorplugin-email.domain:servicesseat post [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021 (6182586030) and on [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194 (6182593111).claude/issue-23343-import-invitation-copy, at086fa4640, no commit of its own) is not reused.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23343, "status": "done", "branch": "claude/issue-23343-import-account-invitation", "pr": "https://github.com/objectstack-ai/objectstack/pull/23597", "session": "session_01YRp5z4kK6vgYkrdH97ztsy (subagent: the parent's id, the Claude-Session line on every commit of this branch)", "premise_still_valid": true, "summary": "Option A as the retriage ruled. @objectstack/plugin-email ships a new built-in template, auth.account_invitation, in four locales (en-US, zh-CN, ja-JP, es-ES), registered through BUILTIN_AUTH_TEMPLATES (24 to 28 rows). It declares exactly what the import holds: user.name (optional), user.email, inviter.name, inviter.email, setPasswordUrl, expiresInMinutes and appName (optional); it names no organization and no role. The template is selected inside sendResetPassword (auth-manager.ts), the only place better-auth hands over the link, behind a server-only marker. The identity import writes the marker into a fresh better-auth request-state store around its own requestPasswordReset call (new module account-invitation-marker.ts). The marker is honoured only when better-auth passes no Request (every HTTP request carries one) and only for the invited address, so a public POST /request-password-reset still sends auth.password_reset. H2: the marker rides better-auth's own request state, which is native AsyncLocalStorage on Node and plugin-auth's installed synchronous polyfill on WebContainer, and was measured on both. H3: the stated expiry uses better-auth's own expression, resetPasswordTokenExpiresIn || 3600, read back off the token's verification row; this changes the reset email only for a configured 0 (it said 0 minutes, now 60).", "tests": "Final union at HEAD ad95a1d59, under os-verify-lock, 'VERDICT command-exit 0' (held 374s). (1) pnpm --workspace-concurrency=2 --filter @objectstack/plugin-email --filter @objectstack/plugin-auth run typecheck: 'plugin-email typecheck: Done', 'plugin-auth typecheck: Done'; the plugin-auth test layer reads 'check:test-typecheck: OK ... 10 file(s) / 94 error(s) / 23 pinned signature(s) held', unchanged from the base. (2) pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2: 'Test Files 34 passed (34) / Tests 551 passed (551)'; the base was 515 tests in 33 files. (3) pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2: 'Test Files 145 passed (145) / Tests 2845 passed | 10 skipped (2855)'; the base was 2829 passed and 10 skipped in 142 files. No other package's tests assert the built-in list: git grep for the five neighbour keys and BUILTIN_AUTH_TEMPLATES over all test files hits only these two packages, plus 3 dogfood files and 2 spec files that name auth.password_reset alone and count nothing. New pins: plugin-auth admin-import-users-account-invitation.test.ts, real ObjectQL over better-sqlite3 with a real AuthManager: (1) the mounted import sends auth.account_invitation with exactly the declared data, and the token row lives 3600s; (2) NEGATIVE CONTROL: a user-requested reset sends auth.password_reset; (3) a public reset after the import sends auth.password_reset; (4) a public reset for the same address while the invitation send is held inside the marker store sends auth.password_reset; (5) the TTL source (default 60, configured 1800 gives 30, configured 0 gives 60 on both sends); (6) the inviter fallback. Also account-invitation-marker.test.ts (6 cases, the four legs on Node), account-invitation-marker.webcontainer.test.ts (4 cases, polyfill installed, asserting the slot is WebContainerRequestStateAsyncLocalStorage), and plugin-email auth-account-invitation-template.test.ts (28 cases: rows, variables, per-locale renders, MISSING_VARIABLES per required variable per locale). invite-link, render-identity, template-runtime-write and plain-text-faces were updated; the byte-identity pin shows the 24 pre-existing rows still match their recorded digests. Ablation of the marker read (fix committed first; node scripts/ablation-replace.mjs at 800afa79d; auth-manager.ts is byte-identical at HEAD) replaced 'const invitation = await readAccountInvitation(user.email, request);' with a constant undefined. Proof on disk: 'anchor 1 to 0, blob 54472f2b0fac to 7b60297f0b26', and the injected-marker grep inside the run printed 1. Result, the expected direction (red): 'Test Files 3 failed | 1 passed (4) / Tests 9 failed | 9 passed (18)'. Red: account-invitation (1)(3)(4)(5)(6), invite-link (1)(2), webcontainer (1)(2). Green: the NEGATIVE CONTROL (2), the marker unit pins, polyfill-installed, and webcontainer (3). Restore: 'ok restored: blob == HEAD (54472f2b0fac) and git diff HEAD is empty'; an independent git hash-object check also matched the HEAD blob. No subject resolves through dist/ (relative src imports), so no rebuild leg applied.", "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands at 800afa79d, after one pre-PR merge of origin/main 32e03d2fd (the PM's lead file had 61 at f3fd209b3). Each was run on its own with the exit code captured before any pipe: 66 exit 0, and pnpm check:dual-build-cjs-loads exit 3, 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' (it needs a full workspace build; NOT MEASURED locally, CI runs it). --ran with the exit-coded record: '67 derived famil(ies) accounted for — 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. After the final commit (ad95a1d59, comments only in admin-import-users.ts), a subset was re-run at HEAD and every one exited 0: check-issue-citations (both forms), check:doc-authoring, check:nul-bytes, check-comment-mask-adoption, check-system-context-census, check-tenant-audit-census, check-registry-log-declared, check:type-check-coverage, check:type-check-debt, check:cross-package-test-inputs, check:test-source-alias, and check-changeset-no-major, check-adr-0087-registration and check-empty-changeset (each --base origin/main). Also: eslint --no-inline-config over the 13 changed .ts files exit 0, and a control-byte self-scan of the 15 changed files found no hits. CI on PR 23597 at ad95a1d59, read at report time (37 check-runs): success (11): Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. in_progress (20): Build Core, Check Changeset, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Test Core (1/6) to (6/6), Type Check · consumer gates / debt ledger / source gates / workspace. skipped (6): Build Docs, Console Pin Gate, Dogfood Regression Gate matrix, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance.", "mcp_calls": "0", "api_writes": "2. (1) pr_create through node scripts/pm/fleet-write/dispatch.mjs (relay request fw-20261015T032139Z-4a3fa8, run 38675855158, conclusion success): POST /repos/objectstack-ai/objectstack/dispatches, which the relay executed as POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos/objectstack-ai/objectstack/issues/23597/assignees ['huangyiirene']; the read-back says '16237 byte(s) sent, 16237 stored — identical'. (2) This os-dev-report comment through node scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/23343/comments. git push is not a REST write. label-write: zero writes, because the dispatch named no label and skip-changeset does not apply (two changesets ship).", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: POST /api/v1/auth/admin/import-users with an email transport that answers status 'failed' gets a 200 whose row reads delivery 'email' with no code and summary.errors 0, so the admin is told the invitation went out (measured with a scratch probe on the 800afa79d tree, policy 'invite'; the only trace is better-auth's console.error 'Failed to run background task'; it was so before this PR, with auth.password_reset) · evidence: better-auth 1.7.3 dist/context/create-context.mjs:215-224, runInBackgroundOrAwait, catches and logs what sendResetPassword throws, and requestPasswordReset still answers {status:true}; so admin-import-users.ts's INVITE_EMAIL_FAILED catch is never reached by a delivery failure · not fixed in place: a different defect class (failure propagation, not copy) · dedupe words: import invitation email failure reported as delivered; INVITE_EMAIL_FAILED unreachable; runInBackgroundOrAwait swallows sendResetPassword; import-users delivery email no code", "carrier: none (承接者:无) · noted, not filed (PR Acceptance notes only): AuthEndpointPaths.forgetPassword in packages/spec/src/api/auth-endpoints.zod.ts names '/forget-password', while better-auth 1.7.3 serves '/request-password-reset'; nothing outside packages/spec in this repo reads AuthEndpointPaths (consumer: none), and no public door was measured" ], "line_budget": "n/a — no skills/** or ledgered file touched", "deviations": [ "The marker's definition is a new module, packages/plugins/plugin-auth/src/account-invitation-marker.ts, not a block inside auth-manager.ts. The claim named 'the marker's own definition' without a file. The writer (admin-import-users.ts) and the reader (sendResetPassword) import one definition, and auth-manager.ts's diff stays at one import line plus the sendResetPassword region. Stated in the PR's Acceptance notes.", "H2 design: the server-held map keyed by email, which the dispatch offered as a fallback, was not taken. The polyfill does not lose a marker that rides better-auth's request state (measured), and a keyed map would race a concurrent public reset for the same address on Node.", "The reset send's TTL expression went from ?? 60*60 to || 60*60, so both sends state the token's real lifetime (better-auth password.mjs:73 uses || 3600). This changes auth.password_reset's data only for a configured resetPasswordTokenExpiresIn of 0 (0 to 60); the copy is unchanged. It is pinned in (5) and stated in the changeset and the PR.", "Inside the sendResetPassword region, a stale comment claimed the import's reset reaches the callback 'with the ADMIN's request'. Measured, it receives undefined, so the comment now says that. Two comments in admin-import-users.ts now name auth.account_invitation as the invite email.", "origin/main (32e03d2fd) was merged once before the PR opened (AGENTS.md §10 pre-PR refresh). It does not overlap plugin-auth or plugin-email; build state was refreshed (install plus the closure build) before the gates and the union ran.", "Not measured: a real WebContainer runtime (the polyfill path was measured instead), the Studio UI list in a browser (the registry listing was measured instead: 28 email_template items, the new key's four rows under package com.objectstack.service.email like auth.password_reset's), and check:dual-build-cjs-loads locally.", "Scratch probes ran as temporary test files in the worktree under the verify lock and were deleted before any commit, none committed: the request-argument probe (server-side api call undefined, HTTP Request), the digest capture, the H4 registry probe and the failed-delivery probe.", "Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by: Claude). The harness reminder's model-named Co-Authored-By form was not used.", "The PR is assigned to huangyiirene. The card's assignee was not written." ], "files_changed": [ ".changeset/23343-plugin-auth-import-account-invitation.md", ".changeset/23343-plugin-email-account-invitation-template.md", "packages/plugins/plugin-auth/src/account-invitation-marker.test.ts", "packages/plugins/plugin-auth/src/account-invitation-marker.ts", "packages/plugins/plugin-auth/src/account-invitation-marker.webcontainer.test.ts", "packages/plugins/plugin-auth/src/admin-import-users-account-invitation.test.ts", "packages/plugins/plugin-auth/src/admin-import-users-invite-link.test.ts", "packages/plugins/plugin-auth/src/admin-import-users.ts", "packages/plugins/plugin-auth/src/auth-manager.ts", "packages/plugins/plugin-email/src/auth-account-invitation-template.test.ts", "packages/plugins/plugin-email/src/builtin-auth-render-identity.test.ts", "packages/plugins/plugin-email/src/email-plugin.template-runtime-write.test.ts", "packages/plugins/plugin-email/src/index.ts", "packages/plugins/plugin-email/src/plain-text-faces.test.ts", "packages/plugins/plugin-email/src/templates/auth-templates.ts" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23597 comment 6183852108 · contract review PASS
6183842049onad95a1d59·domain:specseat 1 (#6017) · sessionsession_01YRp5z4kK6vgYkrdH97ztsy· 2026-10-15T03:39ZGenerated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed: PR #23597 →
fc1a55a21(Fixes #23343) · 2026-10-15T04:14Zdomain:specseat 1 (#6017) ·huangyiirene· sessionsession_01YRp5z4kK6vgYkrdH97ztsy· holder of claim6182603141. Records:CONTRACT_REVIEW_TIERreview PASS6183842049and ACCEPT## 席位核对6183852108, both onad95a1d59.GET /pulls/23597merged: trueat 04:13:58Z,merge_commit_shafc1a55a21GET /git/matching-refs/heads/gh-readonly-queue/main/pr-23597origin/maingit merge-base --is-ancestor fc1a55a21 origin/mainfc1a55a21is the tip, a squash ona6d071553GET /issues/23343closed/completedat 04:14:00Z, on the mergeCarried out of scope:
sendResetPasswordthrows, soINVITE_EMAIL_FAILEDis never reached).AuthEndpointPaths.forgetPasswordinpackages/specnames/forget-passwordwhile better-auth 1.7.3 serves/request-password-reset; no consumer reads it, and the QA checklist already records the divergence.State change: this act removes
pm:dispatched.Generated by Claude Code