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
Triage: lands in packages/plugins/plugin-email/src/templates/ ⇒ domain:services.
On origin/mainb1cbd927, auth-templates.ts:194 renders {{verificationUrl}} (double braces, HTML-escaped) in bodyText.
The HTML href at :188 uses {{{verificationUrl}}}.
The same holds for every locale variant (:79, :112, :136, :160).
Rationale: a plain-text client, or a user who copies the text link, loses callbackURL, so a verified invitee lands on / instead of the accept page. It runs but answers wrong ⇒ p2 (NORTH-STAR 〈优先级〉 rule 2). Most clients render the HTML part, which is why it is not higher.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T04:57Z. ⛔ Not a claim, ⛔ not a dispatch.
Direction confirmed: the engine renders bodyText without HTML escaping.
HTML escaping belongs to the HTML part only. A plain-text body never needs it, so fixing the engine covers every template once, including the reset and invitation ones and any authored template.
Patching per template with {{{…}}} leaves the next text template wrong.
Duplicate check. Same corpus. &|html escap|bodyText|body_text together with email|template gives 3 hits, all closed PRs (#16239, #13956, #1521). None is this.
Related, separate: objectui#10893 (sign-up passes no callbackURL). That is a different repo and a different mechanism. Both matter for the invite path.
Pins.
The verify, reset and invite emails' body_text carry a literal & in the link.
The HTML part is unchanged.
Control: an authored value with < renders escaped in bodyHtml and literally in bodyText.
Epic objectstack-ai/cloud#2440 (session 786273a0-0246-4bb2-b026-bb37ba5d7295) tracks this card as a sub-issue only. Central triage has already routed it to the domain:services queue, so the epic does not reserve or dispatch it here. A pm:epic label added at 05:2xZ was removed within minutes to avoid the pm:epic + pm:queue half-state.
Claim: PM loop round 1
Session: session_01TEah6PeJGjxJfbHaySJjLQ
Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-20374-plain-text-unescaped
Worktree: objectstack-issue-20374
Domain: domain:services
Seat: domain:services#1
Ruling-ref: 5863748439
File surface: packages/plugins/plugin-email/src/: template-engine.ts (and its render entry, which triage says is where the fix belongs), email-service.ts or the render path only if that is where the text body is rendered, templates/auth-templates.ts only if the engine fix needs no template edit (measure first), new or extended tests beside them, and .changeset/20374-*.md. ⛔ No packages/spec. ⛔ No change to the HTML part's escaping. (Stop on breach; explain in the report.)
Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5863965959
Serial constraints cleared: read at 2026-09-28T05:26Z on origin/mainc577e666. None of the 15 open PRs touches packages/plugins/plugin-email/. The last commit on the package is 28f92779 (#19685). #20380 (this seat's) is in the landing window and is disjoint (service-analytics). The epic's note 5863965959 reserves nothing.
Clause-②: no: the plain-text body stops carrying HTML entities. No accept set or public surface widens; the HTML part is unchanged. Triage direction (5863748439): the engine renders bodyText without HTML escaping, covering every template once, rather than per-template triple braces.
os-dev-report
{
"issue": 20374,
"status": "done",
"branch": "claude/issue-20374-plain-text-unescaped",
"pr": "#20392",
"session": "session_01TEah6PeJGjxJfbHaySJjLQ — the harness-stamped id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_01TEah6PeJGjxJfbHaySJjLQ); subagent = parent's",
"premise_still_valid": true,
"summary": "Premise held on c577e66 (measured via the real EmailService over the seeded BUILTIN_AUTH_TEMPLATES rows): body_text rendered ?token=abc&callbackURL=%2F for verify / reset / invitation / magic_link. Fix at the engine, per the triage direction: template-engine.ts gains an internal render() with a per-face hole encoder. renderTemplate (the HTML face and the exported helper) is unchanged. The new module-only renderPlainTextTemplate renders every hole verbatim. The one render entry, email-service.ts resolveAndRenderTemplate (behind sendTemplate and IEmailService.renderTemplate), uses it for body_text AND the subject. The package entry does not re-export it: dist/index.d.ts names it 0 times, so the public surface is unchanged and Clause-② stays no. No template edit. The invitation template lives in this package and is covered. Two deviations are declared in the PR body. (1) The subject was fixed in place under the bounded in-place exemption: same defect class, measured O'Brien invited you to R&D, same line of the same render entry, same test family. (2) CONFLICT between the claim's file surface and os-dev.md: content/docs/automation/email-templates.mdx said a double-brace hole is HTML-escaped in 'Subject and both bodies', which this change makes false. It was edited per os-dev.md ('本轮改动令其变假…必修'), which overrides the dispatch; this is the only file outside packages/plugins/plugin-email/src/ plus the changeset. Worktree node_modules removed and the worktree removed. CI not awaited (in_progress at report time).",
"tests": "HEAD eb169bd. pnpm --filter @objectstack/plugin-email test: 31 files / 510 tests passed (run before the ablations and again after them, from the committed tree). typecheck: tsc --noEmit exit 0; check:test-typecheck 'OK … 0 error(s)'. New src/plain-text-faces.test.ts has 36 cases: 4 link templates x 4 locales x (body_text literal & + HTML unchanged), plus 4 controls (built-in and authored rows: less-than and & escaped in body_html, literal in body_text and the subject; render-only renderTemplate; no-body_text fallback). template-engine.test.ts has +6 renderPlainTextTemplate cases, including a control that the HTML face still escapes. template-locale-resolution.test.ts subject expectations dropped a vacuous esc() wrapper; html still uses esc(). HTML byte-identity, one-time: BASE c577e66 renderTemplate vs HEAD renderTemplate on 24 built-in bodyHtml plus 2 hole-shape extras with hostile data: 0 of 26 differ; control: 24 of 24 bodyText renders differ in text mode. ABLATION via scripts/ablation-replace.mjs WRAP on the committed fix (anchor 1 hit to 0, blob changed on disk). Tests import src relatively, so no dist is in the path and no rebuild legs apply. A1 (body_text back to renderTemplate): 19 failed / 48 passed; received …?token=TOK123&callbackURL=…. A2 (subject back): 3 failed / 64 passed; received O'Brien invited you to R&D <Lab>. A3 (plain-text encoder made to escape): 23 failed / 44 passed. Every restore: blob == HEAD (email-service.ts e5a222f83fb1, template-engine.ts 86d358762d50), git diff HEAD empty, status clean. GATES: dispatch-gates --repo objectstack-ai/objectstack --commands (no paths; 7 paths vs merge base c577e66) gives 90 commands (dispatch-time list 61; the delta is the docs families from the .mdx edit). Each command ran with its exit captured after a single redirect. --ran verdict: '90 derived famil(ies) accounted for — 88 run, 2 NOT-MEASURED'. check:skill-examples first exited 3 (client-react unbuilt); after building the @objectstack/client-react... closure it exited 0 (259 examples). NOT MEASURED: check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (51 packages without dist; needs a whole-repo build). Declared narrowing: no package.json, exports or tsup change; plugin-email dist CJS require and ESM import both load, with 93 exports and an identical key set. NOT MEASURED: check:type-check-debt, reason: exit 3 PREREQUISITE NOT MET (16 unbuilt deps). Declared narrowing: the DEBT entries are cloud-connection, hono, observability and the root; none of the three packages depends on plugin-email, and the root program is scripts/ plus configs, which are untouched. Lint narrowed: eslint --no-inline-config --format json on the 5 touched .ts files gives 5 files, 0 errors, 0 warnings, none ignored (each resolves a config via --print-config). Invariance: eslint.config.mjs enables no type-aware linting (:326-328). Repo-wide pnpm lint is left to CI. Only @objectstack/plugin-email was tested; its public surface bytes are unchanged, so it owes no downstream-consumer sweep.",
"mcp_calls": "0 — none",
"api_writes": "3 — each one relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204) executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #20392); (2) label-write --assign, POST /repos//issues/20392/assignees [os-litant], read back as matching; (3) post-stamped, POST /repos//issues/20374/comments (this report). Not REST: git push x2 (the empty-branch probe, then eb169bd). Reads: single-card REST GETs of #20374, its comments and PR #20392.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR #20392 Acceptance notes, not filed. The messaging inbox channel stores rendered.text as body_md, so an authored value carrying markup characters now reaches that markdown field raw instead of entity-encoded through the declared body_text path. It already did so through the htmlToText fallback, and RenderTemplateResult.text is contracted as plain text. How the objectui console renders body_md (raw HTML allowed or not) is NOT MEASURED, because no objectui checkout is in the container."
]
}
domain:services seat (#6021) · session_01TEah6PeJGjxJfbHaySJjLQ · 2026-09-28T06:15Z. Review of report 5864466905 against GitHub and origin/main.
Review items
PR shape. Draft; base main. The head lines are Fixes #20374 / Clause-②: no. A full-body scan finds that as the only closing keyword, and there is one footer.
Scope. 7 files, +332 / −21:
template-engine.ts and email-service.ts;
one new test file and two edited ones;
the changeset (patch, Clause-②: no);
one doc page.
There is no packages/spec, no content/docs/releases/**, and no template edit (templates/ diff is empty against origin/main). The merge with origin/main15bf186f is clean, and no os-regen path is touched. check-governed-merges: NOT governed, 353 lines.
Surface revision, accepted:content/docs/automation/email-templates.mdx. It stated that double-brace holes are HTML-escaped in the subject and both bodies, and this change makes that false. Correcting it is owed under the dev rules, and it is recorded here as the revised claim surface.
The premise, re-read on the base c577e666.
resolveAndRenderTemplate, the one render entry behind sendTemplate and IEmailService.renderTemplate, rendered subject and body_text through the HTML escaper.
The fix renders those two faces through a module-only renderPlainTextTemplate, which leaves every hole as-is. body_html and the exported renderTemplate helper are byte-for-byte unchanged. The dev's one-time proof: 0 of 26 HTML renders differ, against a positive control of 24 of 24.
Declared deviation: the subject is fixed in place. Accepted. It is the same defect class on the same line of the same render entry, and the dev measured O'Brien / R&D in subjects. The fix is the same call, and it is pinned (ablation A2 goes red). A subject never reaches markup on the send path: it goes to send() and to the transports' Subject field only.
Tests.
plain-text-faces.test.ts (36 cases) runs the real EmailService over the seeded rows: verify, reset, invitation and magic link in 4 locales, text part literal and HTML part unchanged, plus per-face controls. There are also 6 new engine cases.
The edited template-locale-resolution expectations drop only the escape wrapper on the subject; html keeps it.
Ablations A1 (19 red), A2 (3 red) and A3 (23 red) all go red, and every restore is proven.
plugin-email: 31 files / 510 tests.
Gates: 90 derived, 88 run, 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt, both exit 3 on missing prerequisites), each with a declared narrowing. The CI checks on this head answer them.
The card's acceptance. (1) The text part of the verify, reset and invitation emails (and magic link) keeps a literal &. (2) The unit pins show no & in the plain-text face. Both hold per the tests above.
CI on eb169bd4 at review time: still running. This is not a landing read.
The dev's out-of-scope note stays in the PR's Acceptance notes; it is not filed. The messaging inbox stores rendered.text as body_md, so authored values now arrive raw on the template path. The seat's reading: the inline notify path already writes interpolated record values into body_md without HTML encoding (service-automationbuiltin/notify-node.ts:260). So this change makes the template path match an existing path and opens no new class. How the console renders body_md is unmeasurable here. With no reach:, the note is not filed.
Consumer. The reporter's stack (cloud, epic objectstack-ai/cloud#2440) consumes a framework commit pin. This lane's landing criterion is MERGED on origin/main, and the cloud side picks the fix up with its next pin bump.
Landing to-dos, recorded now:
The at-tier contract review on the CI-green head. It is owed on this surface: .changeset prose and a doc page.
Only on PASS: re-run check-governed-merges.mjs --pr 20392, read the merge state, then pr_ready + automerge_enable through the relay, confirmed by added_to_merge_queue.
Landed. PR #20392 merged through the merge queue as df3ba164a5 on origin/main. domain:services seat · session_01TEah6PeJGjxJfbHaySJjLQ · 2026-09-28T06:59Z
Verified on origin/main, not from the PR-closed event.
The squash has parent count 1, on 681868ca, and it is an ancestor of origin/main.
Content control: renderPlainTextTemplate is present in plugin-email/src/template-engine.ts at the merge and absent on its parent.
The merge-queue branch is gone.
The card's acceptance is delivered. The auth emails' plain-text faces (body_text and the subject) render values unencoded, so the link in the text part of the verify, reset, invitation and magic-link mails keeps a literal & and its callbackURL. body_html renders exactly as before. The switch is in the renderer, so it also covers declared and Studio-authored templates, with no template edit. The at-tier contract review is PASS, record 5864762528, on head eb169bd4.
Consumer. The reporter's stack (cloud, epic objectstack-ai/cloud#2440) consumes a framework commit pin, so it picks this up with its next pin bump to a commit at or after the merge. The sign-up side (邀请注册链路丢失 redirect:signUp 不传 callbackURL,被邀请人验证邮箱后被带去「创建工作区」 objectui#10893, no callbackURL passed) is a separate defect in another repo, and it is untouched here.
Board. The closing keyword closed this card completed. pm:dispatched and the assignee os-litant are removed in the same stroke as this note. The claim is discharged, and there is no remainder on this card.
Not filed, on record in the PR's Acceptance notes: the inbox body_md column now receives template values unencoded on the template path. The inline notify path already did this, so it is not a new class. How the console renders body_md is not measurable from this repo, and there is no reach:.
Activity
objectstack-fleet commentedon Sep 28, 2026
Path: identity · 第一次装好就有主人:零用户首跑、注册闸门、邮箱验证 | 缺项 (no item reads the text part of an auth email's link) | P2
Triage: first grade —
bug·priority:p2·domain:services·area:identity·pm:queueTriage: lands in
packages/plugins/plugin-email/src/templates/⇒domain:services.origin/mainb1cbd927,auth-templates.ts:194renders{{verificationUrl}}(double braces, HTML-escaped) inbodyText.hrefat:188uses{{{verificationUrl}}}.:79,:112,:136,:160).Rationale: a plain-text client, or a user who copies the text link, loses
callbackURL, so a verified invitee lands on/instead of the accept page. It runs but answers wrong ⇒ p2 (NORTH-STAR 〈优先级〉 rule 2). Most clients render the HTML part, which is why it is not higher.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T04:57Z. ⛔ Not a claim, ⛔ not a dispatch.Direction confirmed: the engine renders
bodyTextwithout HTML escaping.{{{…}}}leaves the next text template wrong.Duplicate check. Same corpus.
&|html escap|bodyText|body_texttogether withemail|templategives 3 hits, all closed PRs (#16239, #13956, #1521). None is this.callbackURL). That is a different repo and a different mechanism. Both matter for the invite path.Pins.
body_textcarry a literal&in the link.<renders escaped inbodyHtmland literally inbodyText.Size/model suggestion: S.
hotlong commentedon Sep 28, 2026
main 上复现(2026-09-28,objectstack main
862b6ce8,由 cloud 本地栈驱动):新用户的验证邮件body_text里仍然是…verify-email?token=…&callbackURL=%2F。hotlong commentedon Sep 28, 2026
Epic objectstack-ai/cloud#2440 (session
786273a0-0246-4bb2-b026-bb37ba5d7295) tracks this card as a sub-issue only. Central triage has already routed it to thedomain:servicesqueue, so the epic does not reserve or dispatch it here. Apm:epiclabel added at 05:2xZ was removed within minutes to avoid thepm:epic+pm:queuehalf-state.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_01TEah6PeJGjxJfbHaySJjLQAccount:
os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20374-plain-text-unescapedWorktree:
objectstack-issue-20374Domain:
domain:servicesSeat:
domain:services#1Ruling-ref: 5863748439
File surface:
packages/plugins/plugin-email/src/:template-engine.ts(and its render entry, which triage says is where the fix belongs),email-service.tsor the render path only if that is where the text body is rendered,templates/auth-templates.tsonly if the engine fix needs no template edit (measure first), new or extended tests beside them, and.changeset/20374-*.md. ⛔ Nopackages/spec. ⛔ No change to the HTML part's escaping. (Stop on breach; explain in the report.)Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5863965959
Serial constraints cleared: read at 2026-09-28T05:26Z on
origin/mainc577e666. None of the 15 open PRs touchespackages/plugins/plugin-email/. The last commit on the package is28f92779(#19685). #20380 (this seat's) is in the landing window and is disjoint (service-analytics). The epic's note5863965959reserves nothing.Clause-②: no: the plain-text body stops carrying HTML entities. No accept set or public surface widens; the HTML part is unchanged. Triage direction (5863748439): the engine rendersbodyTextwithout HTML escaping, covering every template once, rather than per-template triple braces.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20374,
"status": "done",
"branch": "claude/issue-20374-plain-text-unescaped",
"pr": "#20392",
"session": "session_01TEah6PeJGjxJfbHaySJjLQ — the harness-stamped id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_01TEah6PeJGjxJfbHaySJjLQ); subagent = parent's",
"premise_still_valid": true,
"summary": "Premise held on c577e66 (measured via the real EmailService over the seeded BUILTIN_AUTH_TEMPLATES rows): body_text rendered
?token=abc&callbackURL=%2Ffor verify / reset / invitation / magic_link. Fix at the engine, per the triage direction: template-engine.ts gains an internal render() with a per-face hole encoder. renderTemplate (the HTML face and the exported helper) is unchanged. The new module-only renderPlainTextTemplate renders every hole verbatim. The one render entry, email-service.ts resolveAndRenderTemplate (behind sendTemplate and IEmailService.renderTemplate), uses it for body_text AND the subject. The package entry does not re-export it: dist/index.d.ts names it 0 times, so the public surface is unchanged and Clause-② stays no. No template edit. The invitation template lives in this package and is covered. Two deviations are declared in the PR body. (1) The subject was fixed in place under the bounded in-place exemption: same defect class, measuredO'Brien invited you to R&D, same line of the same render entry, same test family. (2) CONFLICT between the claim's file surface and os-dev.md: content/docs/automation/email-templates.mdx said a double-brace hole is HTML-escaped in 'Subject and both bodies', which this change makes false. It was edited per os-dev.md ('本轮改动令其变假…必修'), which overrides the dispatch; this is the only file outside packages/plugins/plugin-email/src/ plus the changeset. Worktree node_modules removed and the worktree removed. CI not awaited (in_progress at report time).","tests": "HEAD eb169bd.
pnpm --filter @objectstack/plugin-email test: 31 files / 510 tests passed (run before the ablations and again after them, from the committed tree). typecheck: tsc --noEmit exit 0; check:test-typecheck 'OK … 0 error(s)'. New src/plain-text-faces.test.ts has 36 cases: 4 link templates x 4 locales x (body_text literal & + HTML unchanged), plus 4 controls (built-in and authored rows: less-than and & escaped in body_html, literal in body_text and the subject; render-only renderTemplate; no-body_text fallback). template-engine.test.ts has +6 renderPlainTextTemplate cases, including a control that the HTML face still escapes. template-locale-resolution.test.ts subject expectations dropped a vacuous esc() wrapper; html still uses esc(). HTML byte-identity, one-time: BASE c577e66 renderTemplate vs HEAD renderTemplate on 24 built-in bodyHtml plus 2 hole-shape extras with hostile data: 0 of 26 differ; control: 24 of 24 bodyText renders differ in text mode. ABLATION via scripts/ablation-replace.mjs WRAP on the committed fix (anchor 1 hit to 0, blob changed on disk). Tests import src relatively, so no dist is in the path and no rebuild legs apply. A1 (body_text back to renderTemplate): 19 failed / 48 passed; received…?token=TOK123&callbackURL=…. A2 (subject back): 3 failed / 64 passed; receivedO'Brien invited you to R&D <Lab>. A3 (plain-text encoder made to escape): 23 failed / 44 passed. Every restore: blob == HEAD (email-service.ts e5a222f83fb1, template-engine.ts 86d358762d50), git diff HEAD empty, status clean. GATES: dispatch-gates --repo objectstack-ai/objectstack --commands (no paths; 7 paths vs merge base c577e66) gives 90 commands (dispatch-time list 61; the delta is the docs families from the .mdx edit). Each command ran with its exit captured after a single redirect. --ran verdict: '90 derived famil(ies) accounted for — 88 run, 2 NOT-MEASURED'. check:skill-examples first exited 3 (client-react unbuilt); after building the @objectstack/client-react... closure it exited 0 (259 examples). NOT MEASURED: check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (51 packages without dist; needs a whole-repo build). Declared narrowing: no package.json, exports or tsup change; plugin-email dist CJS require and ESM import both load, with 93 exports and an identical key set. NOT MEASURED: check:type-check-debt, reason: exit 3 PREREQUISITE NOT MET (16 unbuilt deps). Declared narrowing: the DEBT entries are cloud-connection, hono, observability and the root; none of the three packages depends on plugin-email, and the root program is scripts/ plus configs, which are untouched. Lint narrowed: eslint --no-inline-config --format json on the 5 touched .ts files gives 5 files, 0 errors, 0 warnings, none ignored (each resolves a config via --print-config). Invariance: eslint.config.mjs enables no type-aware linting (:326-328). Repo-wide pnpm lint is left to CI. Only @objectstack/plugin-email was tested; its public surface bytes are unchanged, so it owes no downstream-consumer sweep.","mcp_calls": "0 — none",
"api_writes": "3 — each one relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204) executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #20392); (2) label-write --assign, POST /repos//issues/20392/assignees [os-litant], read back as matching; (3) post-stamped, POST /repos//issues/20374/comments (this report). Not REST: git push x2 (the empty-branch probe, then eb169bd). Reads: single-card REST GETs of #20374, its comments and PR #20392.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR #20392 Acceptance notes, not filed. The messaging inbox channel stores rendered.text as body_md, so an authored value carrying markup characters now reaches that markdown field raw instead of entity-encoded through the declared body_text path. It already did so through the htmlToText fallback, and RenderTemplateResult.text is contracted as plain text. How the objectui console renders body_md (raw HTML allowed or not) is NOT MEASURED, because no objectui checkout is in the container."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
ACCEPT: PR #20392 at
eb169bd4(closes #20374)domain:servicesseat (#6021) ·session_01TEah6PeJGjxJfbHaySJjLQ· 2026-09-28T06:15Z. Review of report5864466905against GitHub andorigin/main.Review items
main. The head lines areFixes #20374/Clause-②: no. A full-body scan finds that as the only closing keyword, and there is one footer.template-engine.tsandemail-service.ts;patch,Clause-②: no);There is no
packages/spec, nocontent/docs/releases/**, and no template edit (templates/diff is empty againstorigin/main). The merge withorigin/main15bf186fis clean, and noos-regenpath is touched.check-governed-merges: NOT governed, 353 lines.content/docs/automation/email-templates.mdx. It stated that double-brace holes are HTML-escaped in the subject and both bodies, and this change makes that false. Correcting it is owed under the dev rules, and it is recorded here as the revised claim surface.c577e666.resolveAndRenderTemplate, the one render entry behindsendTemplateandIEmailService.renderTemplate, renderedsubjectandbody_textthrough the HTML escaper.renderPlainTextTemplate, which leaves every hole as-is.body_htmland the exportedrenderTemplatehelper are byte-for-byte unchanged. The dev's one-time proof: 0 of 26 HTML renders differ, against a positive control of 24 of 24.O'Brien/R&Din subjects. The fix is the same call, and it is pinned (ablation A2 goes red). A subject never reaches markup on the send path: it goes tosend()and to the transports' Subject field only.plain-text-faces.test.ts(36 cases) runs the realEmailServiceover the seeded rows: verify, reset, invitation and magic link in 4 locales, text part literal and HTML part unchanged, plus per-face controls. There are also 6 new engine cases.template-locale-resolutionexpectations drop only the escape wrapper on the subject;htmlkeeps it.plugin-email: 31 files / 510 tests.check:dual-build-cjs-loads,check:type-check-debt, both exit 3 on missing prerequisites), each with a declared narrowing. The CI checks on this head answer them.&. (2) The unit pins show no&in the plain-text face. Both hold per the tests above.eb169bd4at review time: still running. This is not a landing read.The dev's out-of-scope note stays in the PR's Acceptance notes; it is not filed. The messaging inbox stores
rendered.textasbody_md, so authored values now arrive raw on the template path. The seat's reading: the inlinenotifypath already writes interpolated record values intobody_mdwithout HTML encoding (service-automationbuiltin/notify-node.ts:260). So this change makes the template path match an existing path and opens no new class. How the console rendersbody_mdis unmeasurable here. With noreach:, the note is not filed.Consumer. The reporter's stack (cloud, epic objectstack-ai/cloud#2440) consumes a framework commit pin. This lane's landing criterion is MERGED on
origin/main, and the cloud side picks the fix up with its next pin bump.Landing to-dos, recorded now:
.changesetprose and a doc page.check-governed-merges.mjs --pr 20392, read the merge state, thenpr_ready+automerge_enablethrough the relay, confirmed byadded_to_merge_queue.origin/main: close out plugin-email:auth 邮件纯文本部分把链接里的 & 转义成 &,纯文本链接丢失 callbackURL #20374.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Landed. PR #20392 merged through the merge queue as
df3ba164a5onorigin/main.domain:servicesseat ·session_01TEah6PeJGjxJfbHaySJjLQ· 2026-09-28T06:59Zorigin/main, not from the PR-closed event.681868ca, and it is an ancestor oforigin/main.renderPlainTextTemplateis present inplugin-email/src/template-engine.tsat the merge and absent on its parent.body_textand the subject) render values unencoded, so the link in the text part of the verify, reset, invitation and magic-link mails keeps a literal&and itscallbackURL.body_htmlrenders exactly as before. The switch is in the renderer, so it also covers declared and Studio-authored templates, with no template edit. The at-tier contract review is PASS, record5864762528, on headeb169bd4.callbackURLpassed) is a separate defect in another repo, and it is untouched here.completed.pm:dispatchedand the assigneeos-litantare removed in the same stroke as this note. The claim is discharged, and there is no remainder on this card.body_mdcolumn now receives template values unencoded on the template path. The inlinenotifypath already did this, so it is not a new class. How the console rendersbody_mdis not measurable from this repo, and there is noreach:.Generated by Claude Code
fix(auth): sign-up carries the invitation redirect into the verificat…
fix(plugin-email): the subject and body_text faces render without HTM…
docs(plugin-email): re-anchor the dead tracker citations to the commi…