Skip to content

feat(plugin-email): every email template can read the deployment address as {{baseUrl}} - #29478

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-fuyq3m
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-fuyq3m

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29095

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:邮件模板的默认上下文新增固定键 baseUrl(与 appName 并列),模板可写 {{baseUrl}}/OBJECT/ID 生成绝对链接。
  • 为什么改:执行裁决 A(director 记录 6573566446):元数据应用此前拿不到部署地址,邮件里无法给出可点的记录链接。
  • 风险与回滚:只增不拒;每封模板邮件的上下文多一个键,发送方自带的 data.baseUrl 仍然优先。回滚即 revert 本 PR。
  • 先看:packages/plugins/plugin-email/src/capability-arg.ts(resolveDeploymentBaseUrl 的优先级链),packages/cli/src/commands/serve-email-base-url-parity.test.ts(与 serve 的地址链对齐的钉子)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-email exec vitest run src/capability-arg.base-url.test.ts,预期 10 条全绿。
  • 需要你做的:无,席位落地。

What changes

  • resolveEmailCapabilityArg (@objectstack/plugin-email) resolves a baseUrl key into the default template context, after the author's context spread, exactly as appName is. Chain: OS_AUTH_URL (its legacy BETTER_AUTH_URL) > OS_BASE_URL > config.email.defaultTemplateContext.baseUrl > http://localhost:PORT (OS_PORT > PORT > 3000, the default serve's --port flag reads). Trailing slashes are trimmed.
  • Mirror, not a second authority. The chain's source is resolveAuthBaseUrl in @objectstack/cli serve.ts, which plugin-email cannot import (the CLI depends on it; no new dependency edge). The helper is module-private and its docblock names the source; the new CLI test serve-email-base-url-parity.test.ts drives both readers over the same environment through resolveCapabilityArgument (the call serve makes) and goes red when either chain moves. The mirror keeps the source's ?? semantics: a set-but-empty OS_AUTH_URL is a value, as in serve (which warns about it at boot).
  • Merge order measured: EmailService spreads the send's data over defaultTemplateContext (email-service.ts, the data object built before requireVars), so a send's own baseUrl still wins. Pinned in the new test.
  • Docs: content/docs/automation/email-templates.mdx gains a section on appName and baseUrl, with one example ({{baseUrl}}/crm_deal/{{deal.id}} in bodyText), saying the path after the prefix is the application's own.
  • No new config key, no generic env carrier for defaultTemplateContext, no spec change (ruling A).

Tests

  • New packages/plugins/plugin-email/src/capability-arg.base-url.test.ts (10 tests): each link of the chain in order, the local default's port, trailing-slash trim from every link, the set-but-empty case, appName unchanged (control), a rendered template producing https://crm.example.com/x/1 in text and html, and a send's own data.baseUrl winning.
  • New packages/cli/src/commands/serve-email-base-url-parity.test.ts (8 rows): resolveAuthBaseUrl and the email capability argument agree on every env combination.
  • Forced test edits (exact-shape toEqual on the context, which now carries baseUrl): packages/cli/src/commands/serve-email-appname-precedence.test.ts (4 expectations gain baseUrl: 'http://localhost:3000', one title renamed from "only appName" to "only the resolved keys"); packages/plugins/plugin-email/src/capability-arg.config-parity.contract.test.ts (1 expectation gains the same key).

Acceptance notes

  • bodyHtml cannot build an anchor href from baseUrl. Measured with judgeMailHtmlHoles (@objectstack/formula): an href value of {{baseUrl}}/x/{{id}} is judged url-attribute, unsupported, and so is {{{baseUrl}}}/x/1 (not the whole-value type: 'url' shape); the authoring doors refuse both (email-template-hole-placement). So the prefix delivers a link in bodyText and subject, and as element text in bodyHtml; an anchor link still needs the whole URL in a type: 'url' variable. The docs say exactly this rather than advertise an href use. Whether the placement rule should admit a baseUrl prefix is a spec/lint-lane question, raised in the report.
  • The generated reference content/docs/references/system/email-config.mdx (from the spec's .describe() on defaultTemplateContext) says the context is "passed through unchanged except appName"; it now also resolves baseUrl. That text lives in packages/spec (spec lane), so it is not edited here; raised in the report.
  • Not mirrored, because the email reader cannot know them: the https tail of an in-process TLS listener and a dev port that auto-shifted off a busy one. Both affect only the unconfigured local default.

Verification

Ablations (each red, restored, proved)
  • Leg A (src, plugin-email suite): deleted the baseUrl: line from the context via ablation-replace.mjs (anchor 1 -> 0, blob 48162550 -> f7afc7db); capability-arg.base-url.test.ts went 7 failed, e.g. expected 'https://config.example.com' to be 'https://auth.example.com', expected undefined to be 'http://localhost:3000'. Restored by the tool, git diff HEAD empty.
  • Leg B (dist, CLI parity pin): reordered the env links to OS_BASE_URL ?? OS_AUTH_URL ?? BETTER_AUTH_URL, rebuilt plugin-email, ablation-dist-preflight marker present in 2 built files; serve-email-base-url-parity.test.ts 3 failed | 5 passed (expected 'https://base.example.com' vs auth/legacy). Restored (blob back to 48162550), forced rebuild, preflight --absent: marker absent from all 6 built files, tree clean.
Local runs
  • Tree: 891d74ec (last code commit) for the full sweep; head f224fedf is that plus a merge of origin/main (13 commits, none touching plugin-email, serve.ts, capability-composition.ts or email-templates.mdx); scoped re-checks below ran on f224fedf.
  • pnpm --filter @objectstack/plugin-email test: Test Files 48 passed, Tests 785 passed. typecheck for plugin-email and cli: exit 0 (after a full-dts closure build).
  • New and edited test files on f224fedf: plugin-email 2 files, 18 tests passed; cli 3 files (parity, appname-precedence, auth-app-name contract), 26 tests passed.
  • dispatch-gates --commands: 92 derived families, all run, --ran reconciliation: 92 run, 0 NOT-MEASURED, 0 UNRUN. Four first answered exit 3 (prerequisites: the teardown self-test's pinned fixture commit, spec dts, client-react/studio dist, example dist) and re-ran exit 0 after fetching the fixture and building packages and examples. Derived list identical on f224fedf; ratchet and changeset families re-run there, exit 0.
  • pnpm lint (root eslint, --no-inline-config): exit 0. check:nul-bytes: exit 0.
  • dogfood battery: n/a (no ! in the title, no (narrowing) arm).

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c49dbccb19f065263384647d331226132fdd3764 — the merge of head f224fedf2b078ea457730b7a7544b7fbc22d24af into base cafe8ee62eb2513cbb36ae83e73aa84b2bf84714, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c49dbccb19f065263384647d331226132fdd3764 && git checkout c49dbccb19f065263384647d331226132fdd3764
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 f224fedf2b078ea457730b7a7544b7fbc22d24af && git checkout -B drift-repro cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 && git merge --no-ff f224fedf2b078ea457730b7a7544b7fbc22d24af

node scripts/docs-audit/affected-docs.mjs --json cafe8ee62eb2513cbb36ae83e73aa84b2bf84714

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head f224fed · 2026-11-02T16:10Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract review

Served-tier: TIER_DEFAULT
Head-sha: f224fedf2b078ea457730b7a7544b7fbc22d24af
Local-runs: none

Inputs read: card #29095 (body; triage 6572007785; director 6573083963 and ruling A 6573566446; the seat's claim 6580514849; dev report 6581879408 with its three open_questions), the PR #29478 body and its 7-file list, the net diff origin/main...pr29478 (04e0c2c0...f224fedf, +300/-5), serve.ts / env.ts / email-service.ts / port-contract.ts / mail-html-hole-placement.ts / email-config.zod.ts on main, and the check-runs on the head.

Check-runs on the head (48): 26 success, 12 skipped (UI, opt-in and scheduled lanes, as the path filter selects them), and 10 still in progress: Test Core (1/8) through Test Core (8/8), Type Check · workspace, Type Check · consumer gates. None has failed. Among those that finished green: Check Changeset, Lint & Repo Gates (all Lint · lanes, including PM dispatch-gates), Governed Surface Queue Guard, PR-body guards, Build Core, Build Docs, Check Documentation Links, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Dogfood Regression Gate. Landing waits on the 10 in-progress runs.

① Derived judgments

  1. New baseUrl key in the default template context (packages/plugins/plugin-email/src/capability-arg.ts, resolveDeploymentBaseUrl, set after the ...cfgTemplateContext spread next to appName). Right. It is the key ruling A asks for (6573566446), with no new config key, no env carrier for defaultTemplateContext, and no spec change. It widens what every template receives and refuses nothing.
  2. Precedence chain. The code reads OS_AUTH_URL ?? BETTER_AUTH_URL ?? OS_BASE_URL ?? cfgEmail.defaultTemplateContext?.baseUrl ?? http://localhost:PORT, with PORT = OS_PORT ?? PORT ?? '3000', then String(value).replace(/\/+$/, ''). Right, and it matches the ruling's chain. Env wins over config, as cli: config.email.defaultTemplateContext.appName 压过 OS_APP_NAME —— 与「env 逐项覆盖」的声明相反 #5448 settled for appName. The trim runs once, on the resolved value, so it covers every link (pinned by the "trailing slash is trimmed, from every link" test). BETTER_AUTH_URL is not named in the ruling text, but the ruling binds "the chain serve already uses", and serve reads BETTER_AUTH_URL as the legacy spelling of OS_AUTH_URL. Including it is right.
  3. A send's own data.baseUrl still wins. Right. On main, EmailService builds data = { ...defaultTemplateContext, ...input.data } before requireVars, so per-call data overrides the context. The new test pins this (https://tenant.example.com). The one in-repo caller that already passes baseUrl (plugin-auth's phone invite) is the SMS path and is unaffected anyway.
  4. Is the mirror faithful to serve? Read against resolveAuthBaseUrl on main (serve.ts about line 6802): readEnvWithDeprecation('OS_AUTH_URL','BETTER_AUTH_URL') ?? process.env.OS_BASE_URL ?? boundProtocol://localhost:port. readEnvWithDeprecation returns the first variable that is !== undefined, so ?? over the raw env gives identical values, including a set-but-empty value. The port mirrors serve's --port default (readEnvWithDeprecation('OS_PORT','PORT') ?? '3000'). A --port on argv reaches the mirror because portFlagEnv exports OS_PORT and PORT before the config loads and before capability composition, which passes env: process.env.
    • The mirror differs from serve in four ways, and each is deliberate or harmless. (a) It adds the config link between OS_BASE_URL and the default, as the ruling asks. (b) It trims the trailing slash, as the ruling asks; serve returns its value untrimmed. (c) It does not follow the https tail of an in-process TLS listener. (d) It does not follow an auto-shifted dev port. The PR body and the docblock disclose (c) and (d), and both affect only the unconfigured local default.
    • Verdict on this item: right.
  5. Would the CLI parity pin catch drift? Mostly, and it overclaims slightly. serve-email-base-url-parity.test.ts drives the real resolveAuthBaseUrl and the email reader (through resolveCapabilityArgument, the call serve makes) against fixed expected values over 8 env rows.
    • It catches a reorder, a rename or removal of any of the three variables, and a change in how empty values are read. The dev's dist ablation B (reorder, 3 red of 8) shows this.
    • It does not catch two things. First, a variable added to serve's chain: no row sets it, yet the docblock lists "a variable added". Second, a change to serve's port default: serveBaseUrl() computes the port with the mirror's own expression instead of reading serve's.
    • Advisory, not blocking: one assertion that AUTH_BASE_URL_ENV_NAMES equals the mirror's three names would close the addition gap.
  6. Does output change for a caller that never set baseUrl?
    • A template that never references baseUrl: the extra context key does not change its output. Right.
    • A template that already used a baseUrl hole and relied on the context to supply it (no in-repo email template does; git grep finds none). It used to render empty, or fail MISSING_VARIABLES if the variable was declared required. It now renders the deployment address. This is a widening, as declared.
    • A deployment that already wrote defaultTemplateContext.baseUrl in config. That value used to pass through verbatim. Now OS_AUTH_URL/OS_BASE_URL override it, and a trailing slash is trimmed. Both behaviours are bound by ruling A (env first, no trailing slash) and stated in the changeset ("env first", "It never ends in a slash"). This is a value change on deployment config, not on a metadata author's surface. Right as ruled; named here so the seat sees it.
    • The existing exact-shape tests gain baseUrl: 'http://localhost:3000' (4 in serve-email-appname-precedence.test.ts, 1 in capability-arg.config-parity.contract.test.ts). These are forced, additive edits; no assertion was loosened.
  7. Public surface. resolveDeploymentBaseUrl is module-private and is not exported from index.ts. The exported resolveEmailCapabilityArg keeps its signature; only its output object gains a key. Right.
  8. Docs page edit (content/docs/automation/email-templates.mdx, new section on appName and baseUrl). Right.
    • The appName chain it states matches resolveDeploymentAppName on main, and the baseUrl chain matches the code.
    • The bodyText example uses the application's own path, as the ruling asks.
    • The href caveat is accurate against mail-html-hole-placement.ts on main: href is a URL attribute, so the placement is url-attribute, and only a whole-value type: 'url' triple-stash is supported. The caveat points at the placement rule earlier on the same page (about lines 155-161). Check Documentation Links and Build Docs are green.
    • The docs omit BETTER_AUTH_URL from the stated chain. That is harmless, since the legacy name is deprecated, and the changeset names it.
  9. Generated reference staleness. content/docs/references/system/email-config.mdx (from packages/spec/src/system/email-config.zod.ts about line 224) still reads "Passed through unchanged except appName". It is now incomplete. Not edited here, correctly: the text is in the spec lane and ruling A bars a spec change. See ③(2).

② Semver level

Clause-②: yes (widening)
Metadata-protocol none — both declarations sit line-initial in the PR body (lines 5-6) and in the changeset body (lines 7-8), the clause key exactly once in each.

  • none is right. No spec key, accepted value or metadata type changes; the template context stays a free-form Record. The value change in item ⑥ is on deployment config (config.email), not on what a metadata author writes.
  • By AGENTS.md's rule, a non-protocol widening ships patch. The changeset .changeset/29095-email-template-base-url.md names exactly one package, '@objectstack/plugin-email': patch. Right.
  • The @objectstack/cli files in the diff are test-only and publish nothing, so leaving cli out of the changeset is right.
  • No major, no BREAKING banner, and no ADR-0087 marker are owed for a widening. Check Changeset passed on the head.

③ Boundary flags

open_questions (dev report 6581879408):

  1. href hole rule. Confirmed: A for this card. The placement rule stays as it is. The docs and the changeset tell authors to pass a whole type: 'url' variable for an anchor, and they do not advertise an href use. Admitting a {{{baseUrl}}} prefix in a double-quoted href would change what an author may write. Any such widening is a separate spec/lint-lane card, filed only on a consumer's need (hotcrm plans bodyText links, which work today).
  2. Stale spec .describe() on defaultTemplateContext. Confirmed: A. File a one-line spec-lane follow-up card to amend the describe text at email-config.zod.ts about line 224 (name baseUrl beside appName) and regenerate email-config.mdx. It does not block this card, because ruling A bars a spec change here.
  3. Empty OS_AUTH_URL. Confirmed: A, keep serve parity. Serve's own docblock on resolveAuthBaseUrl records that treating empty as unset inside readEnvWithDeprecation "is a separate, deliberate decision". Serve already warns at boot (formatUnusableAuthBaseUrlDiagnostic). If the mail reader read empty values differently, a second meaning of the same variable would appear. A parity row and a unit test pin the shipped behaviour.

Deviations:

  • The forced test edits are accepted. They are exact-shape toEqual assertions that gain the new key, plus one renamed title; none is weakened.
  • BETTER_AUTH_URL in the chain is accepted (see ① item 2).
  • The OS_SKIP_DTS rebuild note does not touch the diff; typecheck was re-measured. Accepted.

Raised by this review (non-blocking):

  • The parity pin's docblock says it catches "a variable added", which it does not (① item 5). It is advisory: tighten the pin in a follow-up, or reword the docblock.
  • Ruled behaviour to carry forward for hotcrm and operators: if a deployment's Console origin differs from OS_AUTH_URL/OS_BASE_URL, a config-set baseUrl is now overridden by env. Only a per-send data.baseUrl can supply a different prefix. This is ruling A as written; no escalation.
  • The 10 in-progress check-runs (Test Core 1-8, Type Check · workspace, Type Check · consumer gates) gate the landing, not this verdict.

Implemented-by: claude/project-thread-fuyq3m
Reviewed-by: session_01ADHkmQJdgarUjPLNgFBY67

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

席位核对 · ACCEPT · PR #29478 · 2026-11-02T16:16Z

Head-sha: f224fedf2b078ea457730b7a7544b7fbc22d24af

维护者速读(席位终稿)

  • 改了什么:邮件模板上下文在 appName 旁新增固定的 baseUrl。取值链:OS_AUTH_URL(旧名 BETTER_AUTH_URL)> OS_BASE_URL > config.email.defaultTemplateContext.baseUrl > http://localhost:PORT,去掉末尾斜杠。单次发送自带的 data.baseUrl 仍然优先。
  • 为什么改:plugin-email: the email template context has no deployment base URL, so a template cannot render a clickable record link #29095(总监裁定 A,6573566446)。以前模板拿不到部署地址,拼不出可点击的记录链接。
  • 风险与回滚:低。只是每个模板多拿到一个键,什么都不拒绝。行为变化只有一处:已在配置里写了 defaultTemplateContext.baseUrl 的部署,现在会被环境变量覆盖、末尾斜杠被去掉,这正是裁定 A 的写法,changeset 已写明。回滚即 revert。
  • 先看:packages/plugins/plugin-email/src/capability-arg.ts(resolveDeploymentBaseUrl)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-email exec vitest run src/capability-arg.base-url.test.ts,预期 10 个全绿。
  • 需要你做的:无。席位按常设授权落地,合并即关 plugin-email: the email template context has no deployment base URL, so a template cannot render a clickable record link #29095。
  • 席位意见:可落,本 head CI 跑完全绿即入队。

核对六项

  1. 形态:draft → main,Fixes #29095,与认领 6580514849 一致。7 个文件 +300/-5,都在申报面内(插件读取与测试、CLI 对齐钉、文档页、本卡 changeset)。被迫测试改动 5 处,都是精确形状断言多一个键,没有放宽。
  2. 前提实测:声明面契约复审 PASS(本 PR 上的 ## Contract review 记录,TIER_DEFAULT,独立会话)。复审对照 main 上 serve 的 resolveAuthBaseUrl 逐项核过,镜像与之等值,差异只有裁定要求的配置环节与去斜杠,外加两处只影响本地默认值、已在 docblock 写明的差异(TLS 尾部、自动换端口)。
  3. 门禁:dev 报告(6581879408):plugin-email 48 文件 785 测试绿;新测试 10 个 + CLI 对齐 8 行;typecheck plugin-email + cli exit 0;消融 A(删 baseUrl 行)7 红、复原干净,消融 B(换环境变量顺序)对齐钉转红;dispatch-gates 92/92,0 未测。入队前席位读本 head 全部 check-run。
  4. 接受的偏差:链中含 BETTER_AUTH_URL(serve 读它作 OS_AUTH_URL 的旧名,裁定绑的是 serve 的链)。OS_SKIP_DTS 重建说明不涉及 diff。复审提示对齐钉 docblock 说能抓「新增变量」,实际抓不到;这是注释过宽,不影响行为,接受,不为此重跑 CI。
  5. 开放问题裁定:(1) A,保留 href 洞位规则,文档已说锚点请传整值 type: 'url' 变量;若日后有消费方需要在 href 里用 baseUrl 前缀,另由 spec/lint 车道立卡。(2) A,已立 spec: the email.defaultTemplateContext describe() still says only appName is resolved; baseUrl is now resolved too (#29095) #29485(建议 p3):spec 上 defaultTemplateContext 的 describe() 补写 baseUrl 并重生成参考文档。(3) A,空 OS_AUTH_URL 保持与 serve 一致(?? 语义),serve 启动时已有告警。
  6. 落地条款:Clause-②: yes (widening),Metadata-protocol: none,PR 正文与 changeset 都有。semver:@objectstack/plugin-email patch(CLI 改动只有测试,不发布)。check-governed-merges --pr 29478:NOT governed(7 个路径全清)。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 16:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 73c2439 Nov 2, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-fuyq3m branch November 2, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-email: the email template context has no deployment base URL, so a template cannot render a clickable record link

1 participant