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
approvals: approval.requested / reminders / outcomes are emitted with no channels, so they are inbox-only; request e-mail too, with an approval e-mail template that deep-links to the approval page (objectstack-ai/cloud#2549 ruling B) #30509
Filing gate: executing a recorded ruling. The maintainer ruled objectstack-ai/cloud#2549 B; their selection, verbatim, is 「B 只让租户自配邮箱」 (ruling record on that card). This card is item ②. Item ③ is a sibling card. The cloud half stays on objectstack-ai/cloud#2549 as pm:blocked.
Owner. The repo:cloud lane (repo:cloud#3, #30278) follows it to close. The code is pure objectstack, and triage routes it. Priority p3, inherited from objectstack-ai/cloud#2549.
维护者速读
审批的提醒(有新审批、催办、结果)现在只发站内信,因为审批插件发通知时没指定渠道,消息服务默认只发站内信。改成同时发站内信和邮件,并加一个审批邮件模板,邮件里放一个跳转到应用内审批页的链接。不做「一键审批」令牌(PR #24620 有意拿掉的)。环境没配发信时,由 ③ 那张卡保证如实显示「未配置」。
Measured (objectstack origin/main48231b02, the seat's read-only reading)
packages/plugins/plugin-approvals/src/approval-service.ts: notify() (~:2072) emits through this.messaging.emit({...}) with no channels; channels occurs 0 times in the file. The approval.requested call site is :4759. The reminder comment (:6735-6745): "a reminder names no channel, messaging's default materializes it in the in-app inbox".
ApprovalNotificationPayload (:433-450) is closed (title, body, actionUrl). actionUrl is /system/approvals?request=<id> (:2126-2130).
The e-mail channel falls back to payload.title/body when no sys_notification_template row exists (email-channel.ts ~:480). plugin-email/src/templates/auth-templates.ts holds auth templates only.
On staging (objectstack-ai/cloud#2549, comment 6647625229), an approval request's sys_notification had a single sys_notification_delivery: channel: inbox.
The ruled design
The three approval topics (approval.requested, the reminder, the outcome) request channels: ['inbox', 'email']. Preferences still filter, as today.
An approval e-mail template (in plugin-email templates, or as seeded sys_notification_template rows; the lane chooses and states why). Its link is the absolute ${publicBaseUrl}/system/approvals?request=<id>.
A test proves an approval request on a kernel with a configured transport yields an email delivery row and a rendered message with the deep link.
A test proves a kernel with no transport yields the honest refusal from item ③, not a fake send.
The existing approval and messaging tests stay green.
Dedupe (issues, open and closed, 2026-11-05): repo:objectstack-ai/objectstack approval notification email channel is:issue returned 89 hits, and the nearest three (#27314, #30208, #27287) are not this; none carries approvals requesting e-mail.
Executes item ② of ruling B on objectstack-ai/cloud#2549. Approval request, reminder and outcome notices are emitted with no channels, so they are inbox-only. Wanted: e-mail as well, with an approval template that deep-links to the approval page, and no one-click approve token.
Claim: PM loop round 3
Session: session_01Utp6CXCS98uhsFQYk64epE (seat) · dev = project card thread session_01Utp6CX9BC4BTFLAJJaaAHh
Account: os-bill
Branch: claude/project-thread-1sf9m9 (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30509
Domain: domain:server
Seat: domain:server#6
File surface: packages/plugins/plugin-approvals/src/** (notify call sites, payload), packages/plugins/plugin-email/src/templates/** or the sys_notification_template seed (the dev chooses and states why), their tests; content/docs/** only for sentences this PR makes false (not releases/**); one changeset. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/**, .claude/**, AGENTS.md, email-channel.ts, email-plugin.ts, **/mail.manifest.ts, packages/rest/** (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus), effort: high
Clause-②: no (provisional) — no accept set or published surface moves; approval topics request one more channel. The dev corrects it from the diff
Metadata-protocol: none (provisional) — becomes a named value if templates are seeded as metadata rows
Responsibility: plugin-approvals approval-service.ts notify() emits with no channels, and messaging-service.ts:1163 defaults that to ['inbox'], so approval requests, reminders and outcomes never reach e-mail | none: approvals never named a channel | every approver who does not watch the in-app inbox (cloud#2549 ruling B item ②)
Thread-read: 6659996085
Premise: holds (card's reading on 48231b02; seat confirmed messaging-service.ts default and no channels in approval-service.ts on origin/main). ⛔ No ADR-0043 action token in any stored payload (#24620 pins stay green).
Serial constraints cleared: sibling #30510 (seat 6) owns email-channel.ts / email-plugin.ts; seat 5's #30212 owns mail.manifest.ts; seat 4's PR #30491 owns rest approvals-routes.ts — all excluded here
Order: project file server-seat6/dispatch-30509.md, delivered to the card thread.
Card thread dev session session_01Utp6CX9BC4BTFLAJJaaAHh, reading taken 2026-11-05T14:27Z on origin/mainfa9c0aa2.
{
"issue": 30509,
"status": "needs_decision",
"branch": "claude/project-thread-1sf9m9",
"pr": null,
"session": "session_01Utp6CX9BC4BTFLAJJaaAHh",
"premise_still_valid": true,
"summary": "Measured on origin/main fa9c0aa2 before any code. Item 1 (approval topics request inbox + email) holds. Item 2's two named template locations both collide with in-tree constraints, so the ruling's intent needs a route choice: (1) seeded sys_notification_template rows are organization-owned and the store reads by organization_id, so a seed means one row set per organization, which ADR-0131 section 1.4 rules out verbatim: 「在组织没有提出编辑诉求前,强制为每个组织 seed 也是很蠢。我宁可先不让他编辑。」 (the plugin-auth SMS seed retired for this, phone-sms-texts.ts header). (2) A plugin-email template reached through payload.template on the single emit hijacks the inbox too: inbox-channel.ts (~:167-230) renders payload.template through email.renderTemplate, so the inbox row's title and body become the e-mail template's text, and on a kernel with no email service (or one without renderTemplate) the inbox delivery fails TEMPLATE_UNSUPPORTED, losing the approval inbox notice outright; both channels also leave the per-recipient keyedText catalog path (#24978). EmitInput has no per-channel payload (messaging-service.ts ~:83-121). Built-in e-mail templates register in email-plugin.ts:488, which this order excludes. Absolute link: plugin-approvals' publicBaseUrl option (approvals-plugin.ts:57) is passed by no composition in the tree, so it is always empty and every link it builds is relative; plugin-email's template context carries baseUrl (capability-arg.ts resolveDeploymentBaseUrl) on the sendTemplate path only. No code written, no PR.",
"tests": "none run: needs_decision before code. Readings are git show / git grep against origin/main fa9c0aa2.",
"gates": "not run: no diff.",
"dogfood_battery": "n/a — no diff",
"mcp_calls": "6 — issue_read x4 (get 30509, get_comments 30509 x2, get 30510), pull_request_read x1 (get 24620), plus 0 write tools",
"api_writes": "1 — this report comment via post-stamped (relay)",
"open_questions": [
{
"question": "Where does the approval e-mail's template and absolute link live, given that seeded sys_notification_template rows are ruled out by ADR-0131 and a payload.template on the shared emit takes over the inbox?",
"options": [
"A — two emits per notice: the inbox emit unchanged (channels inbox), plus an e-mail-only emit (channels email) whose payload names an approval email_template declared in plugin-approvals' own package, link baseUrl + /system/approvals?request=ID from plugin-email's template context. Cost: two sys_notification rows per notice; the approval sentences rewritten per locale (en-US, zh-CN, ja-JP, es-ES x requested/reminder/approved/rejected, with and without comment) in the template bundle, duplicating the catalog; departs from one emit requesting both channels. In file surface. Size M+.",
"B — a built-in rung in messaging's NotificationTemplateStore: code-declared (topic, email, locale) templates read after the organization's own row and before the generic fallback, nothing written to the database (the plugin-auth SMS walk's shape, ADR-0131 compatible). plugin-approvals declares the four topics: subject title, body = per-recipient body + one localized open-the-request line + the absolute link carried in the payload. One emit requests inbox + email; the inbox is unchanged; e-mail keeps per-recipient localization. Cost: leaves the file surface (service-messaging template-renderer.ts + its plugin wiring, a new public seam) and needs plugin-approvals' publicBaseUrl wired to the deployment base URL (cli/runtime composition). Size L.",
"C — no template: the absolute link written into the catalog body sentence; one emit. Cost: the inbox body shows a raw URL too; drops the ruled e-mail template; publicBaseUrl still unwired. Size S."
],
"recommendation": "B, because it is the only route that keeps all four of: ruling item 1 as written (one emit, two channels), the inbox byte-unchanged, #24978 per-recipient localization on the e-mail, and ADR-0131's no-seed rule. Its cost is a wider file surface (service-messaging template store + one composition line), which needs a re-issued order; the store is not a file #30510 edits."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: #30509 decision — plugin-approvals' publicBaseUrl having no caller is dormant (zero pull since #24620 removed the token links), so it is not filed; it is costed inside option B"
]
}
Ruling: fable decision thread (needs_decision report 6660484535) · letter B · maintainer 「同意」 · recorded 2026-11-05T14:46Z
Ruling confirmed. The maintainer answered in the decision thread at 2026-11-05T14:45Z. The presentation recommended B ("B 追认") with fixed parameters; the maintainer's one word accepts it. Not Tier H. Fixed parameters (presentation §6):
One emit: the four approval topics (approval.requested, approval.reminder, approval.approved, approval.rejected) request channels: ['inbox','email']; preferences filter as today. The payload carries no template, so the inbox is unchanged.
service-messaging's NotificationTemplateStore gains a code-declared built-in template tier, not stored. Per locale step: tenant row first, built-in second, then the next step; the generic fallback last (the plugin-auth SMS pattern, ADR-0131 §1.4: no per-organization seed).
The registration seam is an optional method on the messaging service; approvals calls it through its package-local ApprovalMessagingSurface. No packages/spec change; if one proves necessary, stop and report.
The e-mail template: subject is the notice title; body is the notice body, one localized "open the approval" line and the absolute link. The absolute link lives in a NEW payload field; payload.actionUrl stays relative.
When approvals' publicBaseUrl is empty it resolves in plugin-email's order (OS_AUTH_URL > OS_BASE_URL > config), without the localhost step; when nothing resolves it falls back to the relative link and warns once at boot.
The file surface widens to service-messaging/src/template-renderer.ts, the file holding the messaging service seam, and the approvals address wiring. email-channel.ts, email-plugin.ts, packages/spec/**, ui/** stay out.
Open check carried to the build: with the address wired, ADR-0043's actionLinkUrl becomes absolute too (the option's documented purpose); the dev checks every pin of the relative shape and reports each.
席位核对 · Amendment · 2026-11-05T14:47Z · claim on #30509, file surface widened by the ruling
Ruling-ref: 6660948916
File surface: the claim's surface plus packages/services/service-messaging/src/template-renderer.ts, the messaging service plugin wiring (messaging-service-plugin.ts) and index.ts exports for an optional built-in-template registration method on the messaging service, and the approvals publicBaseUrl resolution and its composition point. Still out: email-channel.ts, email-plugin.ts, **/mail.manifest.ts, packages/rest/**, packages/spec/**, ui/**. Same lane (domain:server). Size L.
Clause-②: no (provisional) for the served inbox text; the messaging service gains one optional method (additive). The dev states the arm from the diff (yes (widening) if the method is exported public surface), and the seat reviews the declaration at TIER_DEFAULT.
Everything else in the claim stands. Order: server-seat6/dispatch-30509.md Amendment 1.
Card thread dev session session_01Utp6CX9BC4BTFLAJJaaAHh, reading taken 2026-11-05T16:14Z at head 9751540c (branch with origin/main merged at b8ab4808).
{
"issue": 30509,
"status": "done",
"branch": "claude/project-thread-1sf9m9",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30567",
"head": "9751540c1a9cc35e0a1e82455b0e388b01a965aa",
"session": "session_01Utp6CX9BC4BTFLAJJaaAHh",
"premise_still_valid": true,
"summary": "Ruling B built as amended. One emit requests channels inbox+email on approval.requested / reminder / approved / rejected; every other approval topic stays inbox-only; no payload.template, so the inbox is unchanged. NotificationTemplateStore reads a code-declared tier (BuiltinNotificationTemplates) at every rung of its locale ladder: org row, else built-in, else next rung, else the generic fallback; nothing is written to the database (ADR-0131 section 1.4). Seam: MessagingService.registerBuiltinTemplates, reached through approvals' local ApprovalMessagingSurface (optional member); no packages/spec change. Templates (en, zh-CN, ja-JP, es-ES x 4 topics): subject {{title}}, body {{body}} + one localized open-the-approval line with {{approvalUrl}}, a new payload member set only on the four emailed topics; actionUrl stays relative. publicBaseUrl resolves option > OS_AUTH_URL (legacy BETTER_AUTH_URL) > OS_BASE_URL, never localhost; unresolved or not an absolute http(s) URL means relative links plus one warn at start. Composition point: ApprovalsServicePlugin.start in plugin-approvals/src/approvals-plugin.ts.",
"tests": "New approval-email-notification.integration.test.ts (real ObjectQL over SqlDriver, real MessagingServicePlugin outbox+dispatcher, real ApprovalsServicePlugin, recording email transport): 5 tests green; on origin/main sources 4 failed 1 passed (the inbox-only control passes). Ablation of the channels request in notify(): 4 failed, restored to HEAD blob a372f16a, git diff HEAD empty. plugin-approvals: 114 files, 1482 tests passed, typecheck exit 0. service-messaging: 66 files, 828 tests passed (at b8ab4808; later commit touches plugin-approvals only), typecheck exit 0. New unit pins: public-base-url.test.ts, approval-email-templates.test.ts, template-renderer.test.ts + template-declared-fallback.test.ts top-level describes.",
"gates": "dispatch-gates --commands: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN at 9751540c, all exit 0. Three scans timed out at a 240 s cap and exited 0 uncapped (check-comment-mask-corpus, check:query-options-erasure, check:slot-lookup). check:i18n and check-plugin-teardown-shape --self-test first answered PREREQUISITE NOT MET (exit 3) and exited 0 once the build closure was built / fixture commit fetched. Changed-file eslint --no-inline-config: exit 0. Repo-wide pnpm lint left to CI Lint & Repo Gates.",
"line_budget": "Size L ordered. 19 files, +953 / -22: source +380 / -19 (8 files), tests +559 / -3 (10 files), changeset +14.",
"deviations": [
"Ruling item 2's two named locations both collide (ADR-0131 no-seed; payload.template re-renders the inbox), so the third location ruled on #30509 is used; stated in the PR body verbatim.",
"messaging-service.ts (service-messaging) carries the seam method; it sits inside service-messaging but was not on the amended file list. public-base-url.ts is a new file in plugin-approvals.",
"PR body order: os-dev.md puts Fixes right after the attribution block, the dispatch put the signature line first; os-dev wins on conflict, noted in the body.",
"The relay pr_create run 42061923926 ended failure (HTTP 502 on POST /pulls) but GitHub created PR #30567; confirmed by a read of pulls?head=, not re-dispatched."
],
"files_changed": [
".changeset/30509-approval-notices-email.md",
"packages/plugins/plugin-approvals/src/approval-email-templates.ts",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approvals-plugin.ts",
"packages/plugins/plugin-approvals/src/public-base-url.ts",
"packages/services/service-messaging/src/index.ts",
"packages/services/service-messaging/src/messaging-service-plugin.ts",
"packages/services/service-messaging/src/messaging-service.ts",
"packages/services/service-messaging/src/template-renderer.ts",
"packages/plugins/plugin-approvals/src/approval-email-notification.integration.test.ts",
"packages/plugins/plugin-approvals/src/approval-email-templates.test.ts",
"packages/plugins/plugin-approvals/src/public-base-url.test.ts",
"packages/plugins/plugin-approvals/src/approval-service.test.ts",
"packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts",
"packages/plugins/plugin-approvals/src/approval-requested-notification.integration.test.ts",
"packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts",
"packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts",
"packages/services/service-messaging/src/template-renderer.test.ts",
"packages/services/service-messaging/src/template-declared-fallback.test.ts"
],
"pins_changed": "ADR-0043 actionLinkUrl relative-shape pins: none exist (git grep finds only its definition). Exact-payload pins updated to add approvalUrl on the emailed topics, none loosened: approval-service.test.ts (remind, also asserts channels), approval-notification-body (per-topic declared fields), approval-requested-notification, approval-decision-outcome-notification (2), reminder-action-link-at-rest (exact payload; storage scan and no-mint assertions unchanged).",
"clause_2": "yes (widening): MessagingService.registerBuiltinTemplates, BuiltinNotificationTemplate type, ApprovalNotificationPayload.approvalUrl, ApprovalMessagingSurface optional members. Changeset patch for both packages.",
"dogfood_battery": "n/a: no UI surface; the integration file runs the real kernel composition end to end.",
"known_side_effect": "A kernel with no email service logs one warn per emailed approval notice (channel 'email' is not registered); the inbox delivery is unaffected. #30510 owns the mounted-channel not-configured answer.",
"mcp_calls": "8 reads, 0 writes: issue_read x4, pull_request_read x2 (24620, 30567), list_pull_requests x1, get_job_logs x1",
"api_writes": "3: needs_decision report comment 6660484535 (post-stamped), pr_create via relay (run failed 502, PR created), this report comment (post-stamped)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: PR #30567 acceptance notes: content/docs/automation/approvals.mdx could say these notices also go by e-mail and that OS_BASE_URL makes the link absolute; no sentence there became false, left for the seat."
]
}
merged, merge commit a0c68aeb2c, merged at 2026-11-05T17:03Z through the merge queue (contract review 6663511556, seat record 6663529355; ruling 6660948916)
State change: pm:dispatched removed; card closed; card thread resolved. Items ② (this card) and ③ (#30510, PR #30547) of ruling B on objectstack-ai/cloud#2549 are both delivered in objectstack.
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p3 ·
domain:server· 2026-11-05T14:05Z⛔ Not a claim.
repo:cloud#3follows it.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Claim: PM loop round 3
Session:
session_01Utp6CXCS98uhsFQYk64epE(seat) · dev = project card threadsession_01Utp6CX9BC4BTFLAJJaaAHhAccount:
os-billBranch:
claude/project-thread-1sf9m9(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30509Domain:
domain:serverSeat:
domain:server#6File surface:
packages/plugins/plugin-approvals/src/**(notify call sites, payload),packages/plugins/plugin-email/src/templates/**or thesys_notification_templateseed (the dev chooses and states why), their tests;content/docs/**only for sentences this PR makes false (notreleases/**); one changeset. ⛔packages/spec/**,ui/**,skills/**,docs/adr/**,.claude/**,AGENTS.md,email-channel.ts,email-plugin.ts,**/mail.manifest.ts,packages/rest/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus),effort: highClause-②: no (provisional) — no accept set or published surface moves; approval topics request one more channel. The dev corrects it from the diff
Metadata-protocol: none (provisional) — becomes a named value if templates are seeded as metadata rows
Responsibility:
plugin-approvals approval-service.ts notify() emits with no channels, and messaging-service.ts:1163 defaults that to ['inbox'], so approval requests, reminders and outcomes never reach e-mail | none: approvals never named a channel | every approver who does not watch the in-app inbox (cloud#2549 ruling B item ②)Thread-read: 6659996085
Premise: holds (card's reading on
48231b02; seat confirmedmessaging-service.tsdefault and nochannelsinapproval-service.tsonorigin/main). ⛔ No ADR-0043 action token in any stored payload (#24620 pins stay green).Serial constraints cleared:
sibling #30510 (seat 6) owns email-channel.ts / email-plugin.ts; seat 5's #30212 owns mail.manifest.ts; seat 4's PR #30491 owns rest approvals-routes.ts — all excluded hereOrder: project file
server-seat6/dispatch-30509.md, delivered to the card thread.objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
os-dev-report
Card thread dev session
session_01Utp6CX9BC4BTFLAJJaaAHh, reading taken 2026-11-05T14:27Z onorigin/mainfa9c0aa2.{ "issue": 30509, "status": "needs_decision", "branch": "claude/project-thread-1sf9m9", "pr": null, "session": "session_01Utp6CX9BC4BTFLAJJaaAHh", "premise_still_valid": true, "summary": "Measured on origin/main fa9c0aa2 before any code. Item 1 (approval topics request inbox + email) holds. Item 2's two named template locations both collide with in-tree constraints, so the ruling's intent needs a route choice: (1) seeded sys_notification_template rows are organization-owned and the store reads by organization_id, so a seed means one row set per organization, which ADR-0131 section 1.4 rules out verbatim: 「在组织没有提出编辑诉求前,强制为每个组织 seed 也是很蠢。我宁可先不让他编辑。」 (the plugin-auth SMS seed retired for this, phone-sms-texts.ts header). (2) A plugin-email template reached through payload.template on the single emit hijacks the inbox too: inbox-channel.ts (~:167-230) renders payload.template through email.renderTemplate, so the inbox row's title and body become the e-mail template's text, and on a kernel with no email service (or one without renderTemplate) the inbox delivery fails TEMPLATE_UNSUPPORTED, losing the approval inbox notice outright; both channels also leave the per-recipient keyedText catalog path (#24978). EmitInput has no per-channel payload (messaging-service.ts ~:83-121). Built-in e-mail templates register in email-plugin.ts:488, which this order excludes. Absolute link: plugin-approvals' publicBaseUrl option (approvals-plugin.ts:57) is passed by no composition in the tree, so it is always empty and every link it builds is relative; plugin-email's template context carries baseUrl (capability-arg.ts resolveDeploymentBaseUrl) on the sendTemplate path only. No code written, no PR.", "tests": "none run: needs_decision before code. Readings are git show / git grep against origin/main fa9c0aa2.", "gates": "not run: no diff.", "dogfood_battery": "n/a — no diff", "mcp_calls": "6 — issue_read x4 (get 30509, get_comments 30509 x2, get 30510), pull_request_read x1 (get 24620), plus 0 write tools", "api_writes": "1 — this report comment via post-stamped (relay)", "open_questions": [ { "question": "Where does the approval e-mail's template and absolute link live, given that seeded sys_notification_template rows are ruled out by ADR-0131 and a payload.template on the shared emit takes over the inbox?", "options": [ "A — two emits per notice: the inbox emit unchanged (channels inbox), plus an e-mail-only emit (channels email) whose payload names an approval email_template declared in plugin-approvals' own package, link baseUrl + /system/approvals?request=ID from plugin-email's template context. Cost: two sys_notification rows per notice; the approval sentences rewritten per locale (en-US, zh-CN, ja-JP, es-ES x requested/reminder/approved/rejected, with and without comment) in the template bundle, duplicating the catalog; departs from one emit requesting both channels. In file surface. Size M+.", "B — a built-in rung in messaging's NotificationTemplateStore: code-declared (topic, email, locale) templates read after the organization's own row and before the generic fallback, nothing written to the database (the plugin-auth SMS walk's shape, ADR-0131 compatible). plugin-approvals declares the four topics: subject title, body = per-recipient body + one localized open-the-request line + the absolute link carried in the payload. One emit requests inbox + email; the inbox is unchanged; e-mail keeps per-recipient localization. Cost: leaves the file surface (service-messaging template-renderer.ts + its plugin wiring, a new public seam) and needs plugin-approvals' publicBaseUrl wired to the deployment base URL (cli/runtime composition). Size L.", "C — no template: the absolute link written into the catalog body sentence; one emit. Cost: the inbox body shows a raw URL too; drops the ruled e-mail template; publicBaseUrl still unwired. Size S." ], "recommendation": "B, because it is the only route that keeps all four of: ruling item 1 as written (one emit, two channels), the inbox byte-unchanged, #24978 per-recipient localization on the e-mail, and ADR-0131's no-seed rule. Its cost is a wider file surface (service-messaging template store + one composition line), which needs a re-issued order; the store is not a file #30510 edits." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: #30509 decision — plugin-approvals' publicBaseUrl having no caller is dormant (zero pull since #24620 removed the token links), so it is not filed; it is costed inside option B" ] }objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Ruling: fable decision thread (needs_decision report 6660484535) · letter B · maintainer 「同意」 · recorded 2026-11-05T14:46Z
Ruling confirmed. The maintainer answered in the decision thread at 2026-11-05T14:45Z. The presentation recommended B ("B 追认") with fixed parameters; the maintainer's one word accepts it. Not Tier H. Fixed parameters (presentation §6):
approval.requested,approval.reminder,approval.approved,approval.rejected) requestchannels: ['inbox','email']; preferences filter as today. The payload carries notemplate, so the inbox is unchanged.service-messaging'sNotificationTemplateStoregains a code-declared built-in template tier, not stored. Per locale step: tenant row first, built-in second, then the next step; the generic fallback last (the plugin-auth SMS pattern, ADR-0131 §1.4: no per-organization seed).messagingservice; approvals calls it through its package-localApprovalMessagingSurface. Nopackages/specchange; if one proves necessary, stop and report.payload.actionUrlstays relative.publicBaseUrlis empty it resolves in plugin-email's order (OS_AUTH_URL > OS_BASE_URL > config), without the localhost step; when nothing resolves it falls back to the relative link and warns once at boot.service-messaging/src/template-renderer.ts, the file holding the messaging service seam, and the approvals address wiring.email-channel.ts,email-plugin.ts,packages/spec/**,ui/**stay out.Open check carried to the build: with the address wired, ADR-0043's
actionLinkUrlbecomes absolute too (the option's documented purpose); the dev checks every pin of the relative shape and reports each.objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
席位核对 · Amendment · 2026-11-05T14:47Z · claim on #30509, file surface widened by the ruling
Ruling-ref: 6660948916
File surface: the claim's surface plus
packages/services/service-messaging/src/template-renderer.ts, the messaging service plugin wiring (messaging-service-plugin.ts) andindex.tsexports for an optional built-in-template registration method on themessagingservice, and the approvalspublicBaseUrlresolution and its composition point. Still out:email-channel.ts,email-plugin.ts,**/mail.manifest.ts,packages/rest/**,packages/spec/**,ui/**. Same lane (domain:server). Size L.Clause-②: no (provisional) for the served inbox text; the messaging service gains one optional method (additive). The dev states the arm from the diff (
yes (widening)if the method is exported public surface), and the seat reviews the declaration at TIER_DEFAULT.Everything else in the claim stands. Order:
server-seat6/dispatch-30509.mdAmendment 1.objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
os-dev-report
Card thread dev session
session_01Utp6CX9BC4BTFLAJJaaAHh, reading taken 2026-11-05T16:14Z at head9751540c(branch withorigin/mainmerged atb8ab4808).{ "issue": 30509, "status": "done", "branch": "claude/project-thread-1sf9m9", "pr": "https://github.com/objectstack-ai/objectstack/pull/30567", "head": "9751540c1a9cc35e0a1e82455b0e388b01a965aa", "session": "session_01Utp6CX9BC4BTFLAJJaaAHh", "premise_still_valid": true, "summary": "Ruling B built as amended. One emit requests channels inbox+email on approval.requested / reminder / approved / rejected; every other approval topic stays inbox-only; no payload.template, so the inbox is unchanged. NotificationTemplateStore reads a code-declared tier (BuiltinNotificationTemplates) at every rung of its locale ladder: org row, else built-in, else next rung, else the generic fallback; nothing is written to the database (ADR-0131 section 1.4). Seam: MessagingService.registerBuiltinTemplates, reached through approvals' local ApprovalMessagingSurface (optional member); no packages/spec change. Templates (en, zh-CN, ja-JP, es-ES x 4 topics): subject {{title}}, body {{body}} + one localized open-the-approval line with {{approvalUrl}}, a new payload member set only on the four emailed topics; actionUrl stays relative. publicBaseUrl resolves option > OS_AUTH_URL (legacy BETTER_AUTH_URL) > OS_BASE_URL, never localhost; unresolved or not an absolute http(s) URL means relative links plus one warn at start. Composition point: ApprovalsServicePlugin.start in plugin-approvals/src/approvals-plugin.ts.", "tests": "New approval-email-notification.integration.test.ts (real ObjectQL over SqlDriver, real MessagingServicePlugin outbox+dispatcher, real ApprovalsServicePlugin, recording email transport): 5 tests green; on origin/main sources 4 failed 1 passed (the inbox-only control passes). Ablation of the channels request in notify(): 4 failed, restored to HEAD blob a372f16a, git diff HEAD empty. plugin-approvals: 114 files, 1482 tests passed, typecheck exit 0. service-messaging: 66 files, 828 tests passed (at b8ab4808; later commit touches plugin-approvals only), typecheck exit 0. New unit pins: public-base-url.test.ts, approval-email-templates.test.ts, template-renderer.test.ts + template-declared-fallback.test.ts top-level describes.", "gates": "dispatch-gates --commands: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN at 9751540c, all exit 0. Three scans timed out at a 240 s cap and exited 0 uncapped (check-comment-mask-corpus, check:query-options-erasure, check:slot-lookup). check:i18n and check-plugin-teardown-shape --self-test first answered PREREQUISITE NOT MET (exit 3) and exited 0 once the build closure was built / fixture commit fetched. Changed-file eslint --no-inline-config: exit 0. Repo-wide pnpm lint left to CI Lint & Repo Gates.", "line_budget": "Size L ordered. 19 files, +953 / -22: source +380 / -19 (8 files), tests +559 / -3 (10 files), changeset +14.", "deviations": [ "Ruling item 2's two named locations both collide (ADR-0131 no-seed; payload.template re-renders the inbox), so the third location ruled on #30509 is used; stated in the PR body verbatim.", "messaging-service.ts (service-messaging) carries the seam method; it sits inside service-messaging but was not on the amended file list. public-base-url.ts is a new file in plugin-approvals.", "PR body order: os-dev.md puts Fixes right after the attribution block, the dispatch put the signature line first; os-dev wins on conflict, noted in the body.", "The relay pr_create run 42061923926 ended failure (HTTP 502 on POST /pulls) but GitHub created PR #30567; confirmed by a read of pulls?head=, not re-dispatched." ], "files_changed": [ ".changeset/30509-approval-notices-email.md", "packages/plugins/plugin-approvals/src/approval-email-templates.ts", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/approvals-plugin.ts", "packages/plugins/plugin-approvals/src/public-base-url.ts", "packages/services/service-messaging/src/index.ts", "packages/services/service-messaging/src/messaging-service-plugin.ts", "packages/services/service-messaging/src/messaging-service.ts", "packages/services/service-messaging/src/template-renderer.ts", "packages/plugins/plugin-approvals/src/approval-email-notification.integration.test.ts", "packages/plugins/plugin-approvals/src/approval-email-templates.test.ts", "packages/plugins/plugin-approvals/src/public-base-url.test.ts", "packages/plugins/plugin-approvals/src/approval-service.test.ts", "packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts", "packages/plugins/plugin-approvals/src/approval-requested-notification.integration.test.ts", "packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts", "packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts", "packages/services/service-messaging/src/template-renderer.test.ts", "packages/services/service-messaging/src/template-declared-fallback.test.ts" ], "pins_changed": "ADR-0043 actionLinkUrl relative-shape pins: none exist (git grep finds only its definition). Exact-payload pins updated to add approvalUrl on the emailed topics, none loosened: approval-service.test.ts (remind, also asserts channels), approval-notification-body (per-topic declared fields), approval-requested-notification, approval-decision-outcome-notification (2), reminder-action-link-at-rest (exact payload; storage scan and no-mint assertions unchanged).", "clause_2": "yes (widening): MessagingService.registerBuiltinTemplates, BuiltinNotificationTemplate type, ApprovalNotificationPayload.approvalUrl, ApprovalMessagingSurface optional members. Changeset patch for both packages.", "dogfood_battery": "n/a: no UI surface; the integration file runs the real kernel composition end to end.", "known_side_effect": "A kernel with no email service logs one warn per emailed approval notice (channel 'email' is not registered); the inbox delivery is unaffected. #30510 owns the mounted-channel not-configured answer.", "mcp_calls": "8 reads, 0 writes: issue_read x4, pull_request_read x2 (24620, 30567), list_pull_requests x1, get_job_logs x1", "api_writes": "3: needs_decision report comment 6660484535 (post-stamped), pr_create via relay (run failed 502, PR created), this report comment (post-stamped)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: PR #30567 acceptance notes: content/docs/automation/approvals.mdx could say these notices also go by e-mail and that OS_BASE_URL makes the link absolute; no sentence there became false, left for the seat." ] }objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
ACCEPT → PR #30567 comment 6663529355 · 2026-11-05T16:38Z · head
9751540c:全绿,契约复核 TIER_DEFAULT PASS(6663511556),已 ready + auto-merge。objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Landed · 2026-11-05T17:04Z · PR #30567
merged, merge commita0c68aeb2c, merged at 2026-11-05T17:03Z through the merge queue (contract review 6663511556, seat record 6663529355; ruling 6660948916)closed·completedby the PR'sFixesState change:
pm:dispatchedremoved; card closed; card thread resolved. Items ② (this card) and ③ (#30510, PR #30547) of ruling B on objectstack-ai/cloud#2549 are both delivered in objectstack.