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
os dev --seed-admin on an email-verification-armed boot logs ERROR "TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService" — the seed sign-up fires sendVerificationEmail before plugin-email wires its template loader #23946
Filing gate ①: a product defect with a named landing point. Reach: public CLI entry, objectstack dev --seed-admin (the default) on a boot with email verification required. It was measured on two separate fresh-DB boots: OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, and OS_AUTH_AUDIENCE_POSTURE=open, which forces verification. Each printed the error once at boot.
Reader who acts: the domain:identity / plugin-email execution seat. Fix the ordering, or skip the send. Either:
the dev seed does not fire sendOnSignUp for an identity it immediately marks verified; or
the verification send waits for or tolerates the template loader that EmailServicePlugin.start installs, instead of throwing into better-auth's background-task logger.
What happens
On a stock showcase boot with verification armed, the boot log shows this line before the server is ready:
ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService
at EmailService.resolveAndRenderTemplate (plugin-email/dist/index.mjs)
at EmailService.sendTemplate (plugin-email/dist/index.mjs)
at Object.sendVerificationEmail (plugin-auth/dist/index.mjs)
Cost: every verification-armed dev boot opens with a red ERROR and a stack trace for a send nobody needed. The seeded admin is marked emailVerified: true.
That verification send leaves no sys_email row.
Any identity created through better-auth before EmailServicePlugin.start runs would lose its verification mail the same way.
Later sends on the same boot (sign-up, resend) render and persist fine, so the window is boot-time only.
Reproduction
At 2034975a, from examples/app-showcase:
OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true OS_PORT=P pnpm exec objectstack dev --ui --seed-admin -p P -d file:/tmp/fresh/data.db on a fresh DB.
Read the boot log. The ERROR line above appears once, before the serving line.
Contrast: the same boot without the verification variable prints no such line.
Expected: no ERROR on a healthy boot. The seed either sends nothing, or sends successfully once templates resolve. Actual: the seed's verification send throws TEMPLATE_NOT_FOUND into better-auth's background-task error log.
Mechanism
The dev seed (packages/cli/src/commands/dev.ts, --seed-admin, default on) signs the admin up in-process. With emailVerification.sendOnSignUp, better-auth runs sendVerificationEmail (plugin-auth) as a background task.
packages/plugins/plugin-email/src/email-service.ts:1315: resolveAndRenderTemplate throws when options.templateLoader is unset.
packages/plugins/plugin-email/src/email-plugin.ts (about lines 386 and 629–638) wires persistence and setTemplateLoader(...) only in start, so a send issued before that has no loader.
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Why domain:services:plugin-email and plugin-auth are domain:services.
Why p3: measured as one boot-time ERROR on a verification-armed dev boot, for a send nobody needed. The seeded admin is already verified.
The card's wider case (an identity created before EmailServicePlugin.start loses its mail) is inferred, not measured. If the claimant measures it on a non-dev boot, re-grade: lost verification mail is p2.
Claim: PM loop round 15 · 2026-10-17T00:01Z
Session: session_018VqJPGA1NDNSCZsnecsChP (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: os-elon-musk (the seat's linked user, as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-23946-email-template-loader-boot-order
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface (at origin/main8a83cebc15 or later; stop on a breach and explain it in the report):
packages/plugins/plugin-email/src/email-plugin.ts: where the template loader is installed (setTemplateLoader, :638, inside start at :400) and the init / start split (:353, :400);
packages/plugins/plugin-email/src/email-service.ts: resolveAndRenderTemplate's TEMPLATE_NOT_FOUND throw (:1315), if the fix holds a send rather than installing the loader earlier;
only if neither of those can fix it, plugin-auth's sendVerificationEmail callback (auth-manager.ts, around :1591);
the tests beside them;
one patch changeset for the published packages touched.
Exclusions:
⛔ Not packages/cli/src/commands/dev.ts, which is domain:cli. The card's first option, the seed not firing the send, is the cli lane's; if the platform fix is right, that option is moot. Say so in the report.
os-dev-report
{
"issue": 23946,
"status": "done",
"branch": "claude/issue-23946-email-template-loader-boot-order",
"pr": "#24113",
"session": "session_018VqJPGA1NDNSCZsnecsChP — the dispatching seat's id, shared in mode:subagent; this run's identity is the branch",
"premise_still_valid": true,
"summary": "Option (a): EmailServicePlugin.init now installs sys_email persistence and the registry template loader before it registers the email service; the kernel:ready hook calls the same wireEngine method and only wires a host whose engine registered later. Premise reproduced on 8a83ceb: AuthPlugin.maybeSeedDevAdmin (plugin-auth kernel:ready hook; Auth starts before EmailServicePlugin) runs signUpEmail, better-auth awaits sendVerificationEmail via runInBackgroundOrAwait, and sendTemplate throws because the loader was installed only in the email plugin's own later kernel:ready. (b) was not chosen: that send is awaited inside an earlier kernel:ready hook and the kernel runs those hooks one at a time, so an awaiting hold blocks until its timeout on every armed dev boot, and a non-awaiting hold answers queued with no row behind it. The card's cli option is moot (the send is now rendered and persisted, with no ERROR) and is misplaced: dev.ts only forwards OS_SEED_ADMIN, and the sign-up runs in plugin-auth. Three notes with no carrier are in the PR's Acceptance notes and none was filed; the main one is that a send in this window uses the boot transport, because the mail settings are applied at kernel:ready.",
"tests": "REPRO at 8a83ceb (fresh DB, OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, objectstack dev --ui --seed-admin from examples/app-showcase): one line 'ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService', trace 'at Object.sendVerificationEmail (plugin-auth)' to 'at async AuthPlugin.maybeSeedDevAdmin'; DB read: 'sys_email rows: 0', admin email_verified 1. PIN (email-plugin.outbox-sweep.test.ts, real LiteKernel, stand-in for the seed registered ahead of EmailServicePlugin, sends auth.verify_email from its kernel:ready; reuses the file's existing engine double, ledger untouched): 'pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2 src/email-plugin.outbox-sweep.test.ts' gave 'Tests 14 passed (14)'. ABLATION (fix committed first; node scripts/ablation-replace.mjs deleted the init call 'if (engine) this.wireEngine(ctx, engine);'): anchor x1 to x0, marker x0 to x1, blob 8e8ca0e6d6a6 to 855acf5ca71b; result RED 'AssertionError: expected Error: TEMPLATE_NOT_FOUND: no templateLoa... to be undefined', 'Tests 1 failed | 13 passed (14)'; restore: 'blob == HEAD (8e8ca0e6d6a6) and git diff HEAD is empty', git status clean. Direction: turned red, as expected. The subject is a relative src import, so there was no dist leg. PACKAGE at d879052: 'pnpm --filter @objectstack/plugin-email test' gave 'Test Files 34 passed (34) / Tests 552 passed (552)'; typecheck: tsc --noEmit ok and 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'; tsc -p tsconfig.test.json --listFiles includes the edited test file (count 1). LIVE (fixed dist, same boot command, fresh DB): ERROR 0, TEMPLATE_NOT_FOUND 0, 'Failed to run background task' 0, 'Server is ready' 1, 'Dev admin ... seeded on empty DB' present; DB 'sys_email rows: 1' status sent, subject 'Verify your ObjectStack email address', related_object sys_user, related_id = the seeded admin's id; admin email_verified 1. So the verification send is rendered and persisted (logged-only through the dev LogTransport) and the admin is also stamped verified. Both boot process groups were killed by recorded PGID and their ports confirmed free. FINAL HEAD 404db56 merges origin/main be076da (hono adapter, showcase report, ADR note; no plugin-email or dependency change), so the package suite was not re-run there (AGENTS.md section 10 scoped re-check). Gates were re-run there.",
"gates": "LOCAL at 404db56: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 64 commands (30 pnpm, 34 node), the same list as before the merge; all 64 were run, each exit captured before any pipe, all exit 0. 'dispatch-gates --ran': 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Includes check:engine-double-contract, check:nul-bytes, check:test-source-alias, check:cross-package-test-inputs, check:durability-log-level, check:plugin-teardown-shape, check:published-files, check:dual-build-cjs-loads and check:type-check-coverage. An earlier run of the 64 at pre-merge d879052 was also all 0: check:dual-build-cjs-loads first exited 3 PREREQUISITE NOT MET (8 packages had no dist; not counted as a measurement), the 8 were built (turbo cache), and the rerun gave exit 0. No platform-checklist run was needed: no exported symbol was renamed or deleted. CI check-runs on 404db56 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 were in_progress (Lint · CI, release & engine gates; Lint · PM dispatch-gates; Lint · repo & tooling gates; Lint · ESLint; Lint · docs & code guards; Lint · scoped x3; Type Check · source gates / consumer gates / debt ledger / workspace; Check Changeset; Check PR Size; Governed Surface Queue Guard; The card this PR closes must claim this branch; Part-of PR must not also close its card; No other open PR may claim the same single-writer path; Flag docs affected by code changes; Check Documentation Links; filter). These are recorded as in_progress and were not waited on, per the contract.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 platform writes in 2 fleet-write relay strokes (each stroke is one POST /repos/objectstack-ai/objectstack/dispatches under the session token, executed as objectstack-fleet[bot]): (1) stroke fw-20261017T010500Z-6ab5c0, pr_create: POST /repos/objectstack-ai/objectstack/pulls (#24113, draft forced) + POST /repos//issues/24113/assignees (os-elon-musk), read back with the body byte-identical (9673 bytes); (2) this report: POST /repos//issues/23946/comments via scripts/pm/post-stamped.mjs. 0 label writes: the dispatch named no label and the diff ships a patch changeset, so skip-changeset does not apply. The 5 git pushes are not REST writes.",
"open_questions": [],
"out_of_scope_findings": []
}
Landed: PR #24113 (Fixes #23946) merged through the merge queue as 33228c5b6c. That commit is on origin/main (git merge-base --is-ancestor). Timeline: merged 2026-10-17T01:57Z. ACCEPT 6227946488.
Probe on origin/main
Reading
plugin-email/src/email-plugin.ts: init wires the engine before registerService('email') (if (engine) this.wireEngine(ctx, engine);)
present, line 427
What an operator sees: a verification email sent during boot, before kernel:ready, now finds its template and persists its sys_email row. Before, it threw TEMPLATE_NOT_FOUND. An engine registered only later is still wired at kernel:ready, once per engine.
pm:dispatched is cleared and the assignee is released in this act.
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p3·domain:services·area:devpath·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T14:06ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:services:plugin-emailandplugin-autharedomain:services.EmailServicePlugin.startloses its mail) is inferred, not measured. If the claimant measures it on a non-dev boot, re-grade: lost verification mail is p2.objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 15 · 2026-10-17T00:01Z
Session:
session_018VqJPGA1NDNSCZsnecsChP(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
os-elon-musk(the seat's linked user, asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23946-email-template-loader-boot-orderDomain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (at
origin/main8a83cebc15or later; stop on a breach and explain it in the report):packages/plugins/plugin-email/src/email-plugin.ts: where the template loader is installed (setTemplateLoader,:638, insidestartat:400) and theinit/startsplit (:353,:400);packages/plugins/plugin-email/src/email-service.ts:resolveAndRenderTemplate'sTEMPLATE_NOT_FOUNDthrow (:1315), if the fix holds a send rather than installing the loader earlier;plugin-auth'ssendVerificationEmailcallback (auth-manager.ts, around:1591);patchchangeset for the published packages touched.Exclusions:
packages/cli/src/commands/dev.ts, which isdomain:cli. The card's first option, the seed not firing the send, is the cli lane's; if the platform fix is right, that option is moot. Say so in the report.Container & model:
S–M: one boot-ordering fix in plugin-email with a pin that boots the real plugin order ⇒ S–M,mode:subagent,model: opus(TIER_DEFAULT; no path-derived mandate)Clause-②: no. A verification send issued before
EmailServicePlugin.startstops throwingTEMPLATE_NOT_FOUND. No export, type or accept set moves.Responsibility:
plugin-email (this lane): a send issued during boot, before start installs the template loader, renders and persists, or is held until it can, never dropped behind a background-task ERROR | the dev seed in cli is unchanged | a later send behaves as todayThread-read:
6217271273Ruling-ref: none. Triage's first grade is
6217271273.Serial constraints: no open PR touches
email-plugin.ts,email-service.tsorauth-manager.ts.Premise to falsify first: on
origin/main,setTemplateLoaderruns only instart. Find what a send before it does, and whether--seed-adminon a verification-armed boot firessendVerificationEmailbeforeEmailServicePlugin.start. Reproduce the boot-log ERROR once, then decide the shape:initif its dependencies are ready there;start.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{
"issue": 23946,
"status": "done",
"branch": "claude/issue-23946-email-template-loader-boot-order",
"pr": "#24113",
"session": "session_018VqJPGA1NDNSCZsnecsChP — the dispatching seat's id, shared in mode:subagent; this run's identity is the branch",
"premise_still_valid": true,
"summary": "Option (a): EmailServicePlugin.init now installs sys_email persistence and the registry template loader before it registers the email service; the kernel:ready hook calls the same wireEngine method and only wires a host whose engine registered later. Premise reproduced on 8a83ceb: AuthPlugin.maybeSeedDevAdmin (plugin-auth kernel:ready hook; Auth starts before EmailServicePlugin) runs signUpEmail, better-auth awaits sendVerificationEmail via runInBackgroundOrAwait, and sendTemplate throws because the loader was installed only in the email plugin's own later kernel:ready. (b) was not chosen: that send is awaited inside an earlier kernel:ready hook and the kernel runs those hooks one at a time, so an awaiting hold blocks until its timeout on every armed dev boot, and a non-awaiting hold answers queued with no row behind it. The card's cli option is moot (the send is now rendered and persisted, with no ERROR) and is misplaced: dev.ts only forwards OS_SEED_ADMIN, and the sign-up runs in plugin-auth. Three notes with no carrier are in the PR's Acceptance notes and none was filed; the main one is that a send in this window uses the boot transport, because the mail settings are applied at kernel:ready.",
"tests": "REPRO at 8a83ceb (fresh DB, OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, objectstack dev --ui --seed-admin from examples/app-showcase): one line 'ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService', trace 'at Object.sendVerificationEmail (plugin-auth)' to 'at async AuthPlugin.maybeSeedDevAdmin'; DB read: 'sys_email rows: 0', admin email_verified 1. PIN (email-plugin.outbox-sweep.test.ts, real LiteKernel, stand-in for the seed registered ahead of EmailServicePlugin, sends auth.verify_email from its kernel:ready; reuses the file's existing engine double, ledger untouched): 'pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2 src/email-plugin.outbox-sweep.test.ts' gave 'Tests 14 passed (14)'. ABLATION (fix committed first; node scripts/ablation-replace.mjs deleted the init call 'if (engine) this.wireEngine(ctx, engine);'): anchor x1 to x0, marker x0 to x1, blob 8e8ca0e6d6a6 to 855acf5ca71b; result RED 'AssertionError: expected Error: TEMPLATE_NOT_FOUND: no templateLoa... to be undefined', 'Tests 1 failed | 13 passed (14)'; restore: 'blob == HEAD (8e8ca0e6d6a6) and git diff HEAD is empty', git status clean. Direction: turned red, as expected. The subject is a relative src import, so there was no dist leg. PACKAGE at d879052: 'pnpm --filter @objectstack/plugin-email test' gave 'Test Files 34 passed (34) / Tests 552 passed (552)'; typecheck: tsc --noEmit ok and 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'; tsc -p tsconfig.test.json --listFiles includes the edited test file (count 1). LIVE (fixed dist, same boot command, fresh DB): ERROR 0, TEMPLATE_NOT_FOUND 0, 'Failed to run background task' 0, 'Server is ready' 1, 'Dev admin ... seeded on empty DB' present; DB 'sys_email rows: 1' status sent, subject 'Verify your ObjectStack email address', related_object sys_user, related_id = the seeded admin's id; admin email_verified 1. So the verification send is rendered and persisted (logged-only through the dev LogTransport) and the admin is also stamped verified. Both boot process groups were killed by recorded PGID and their ports confirmed free. FINAL HEAD 404db56 merges origin/main be076da (hono adapter, showcase report, ADR note; no plugin-email or dependency change), so the package suite was not re-run there (AGENTS.md section 10 scoped re-check). Gates were re-run there.",
"gates": "LOCAL at 404db56: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 64 commands (30 pnpm, 34 node), the same list as before the merge; all 64 were run, each exit captured before any pipe, all exit 0. 'dispatch-gates --ran': 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Includes check:engine-double-contract, check:nul-bytes, check:test-source-alias, check:cross-package-test-inputs, check:durability-log-level, check:plugin-teardown-shape, check:published-files, check:dual-build-cjs-loads and check:type-check-coverage. An earlier run of the 64 at pre-merge d879052 was also all 0: check:dual-build-cjs-loads first exited 3 PREREQUISITE NOT MET (8 packages had no dist; not counted as a measurement), the 8 were built (turbo cache), and the rerun gave exit 0. No platform-checklist run was needed: no exported symbol was renamed or deleted. CI check-runs on 404db56 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 were in_progress (Lint · CI, release & engine gates; Lint · PM dispatch-gates; Lint · repo & tooling gates; Lint · ESLint; Lint · docs & code guards; Lint · scoped x3; Type Check · source gates / consumer gates / debt ledger / workspace; Check Changeset; Check PR Size; Governed Surface Queue Guard; The card this PR closes must claim this branch; Part-of PR must not also close its card; No other open PR may claim the same single-writer path; Flag docs affected by code changes; Check Documentation Links; filter). These are recorded as in_progress and were not waited on, per the contract.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 platform writes in 2 fleet-write relay strokes (each stroke is one POST /repos/objectstack-ai/objectstack/dispatches under the session token, executed as objectstack-fleet[bot]): (1) stroke fw-20261017T010500Z-6ab5c0, pr_create: POST /repos/objectstack-ai/objectstack/pulls (#24113, draft forced) + POST /repos//issues/24113/assignees (os-elon-musk), read back with the body byte-identical (9673 bytes); (2) this report: POST /repos//issues/23946/comments via scripts/pm/post-stamped.mjs. 0 label writes: the dispatch named no label and the diff ships a patch changeset, so skip-changeset does not apply. The 5 git pushes are not REST writes.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
ACCEPT → PR #24113 ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-17T01:27Z. Landing through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Landed ·
domain:servicesseat 2 (#21118, stay-behind) ·os-elon-musk·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-17T01:58Z.Landed: PR #24113 (
Fixes #23946) merged through the merge queue as33228c5b6c. That commit is onorigin/main(git merge-base --is-ancestor). Timeline:merged2026-10-17T01:57Z. ACCEPT6227946488.origin/mainplugin-email/src/email-plugin.ts:initwires the engine beforeregisterService('email')(if (engine) this.wireEngine(ctx, engine);)What an operator sees: a verification email sent during boot, before
kernel:ready, now finds its template and persists itssys_emailrow. Before, it threwTEMPLATE_NOT_FOUND. An engine registered only later is still wired atkernel:ready, once per engine.pm:dispatchedis cleared and the assignee is released in this act.Generated by Claude Code