Repository navigation
The "per recipient locale" promise survives in 4 sites outside packages/spec — one of them a Studio form description, one a doc that says "each person in their own language" #12446
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsClaim: PM loop round 1 (services seat)
Session:session_0157mMVAq9fjGe2kaSD2aJC8
Branch:claude/issue-12446-recipient-locale-promise-sweep
Worktree:objectstack-issue-12446
Domain:domain:services
File surface:packages/services/service-automation/src/builtin/notify-node.ts·packages/services/service-messaging/src/messaging-service-plugin.ts·content/docs/automation/email-templates.mdx·packages/services/service-automation/src/builtin/notify-node.test.ts(comment only). ⛔ Zeropackages/spec— #12178/PR #12447 owns the spec half and is a different lane's in-flight work. Stop on breach and explain in the report.
Container & model: S,mode:subagent,model: claude-opus-5.⚠️ Triage suggestedsonnet; not adopted, with reason: the deliverable is prose that must be semantically exact about a deferral — the failure mode is re-stating a promise the platform does not keep, which no gate catches (the whole card exists because the last round of wording passed review). Judgment face, so opus per the "S but not mechanical ⇒ treat as M" rule. Tier read live fromnode scripts/pm/dispatch-gates.mjs --tieron the three paths: no path-derived mandate, tier is this seat's call.
Clause-②: no — every edit is a description string, a doc paragraph, a log line or a code comment. Nothing changes what the contract accepts or rejects, and nothing widens a public surface. ⛔ Do not hangneeds:contract-review.
Serial constraints cleared:service-automation/**,service-messaging/**andcontent/docs/automation/**are free — this seat's hot-file queue is empty at takeover (seat post #6021 §3) and this lane has zero open PRs.⚠️ Cross-lane sibling: #12178 (PR #12447) is in flight on the same subject but a disjoint file surface (packages/spec/src/automation/io-node-config.zod.tsonly) — no overlap, and this card does not pin any behaviour that PR changes. Batch siblings this round: #11959 (plugin-security/plugin-auth), #12128 (plugin-webhooks) — disjoint packages.
Premise re-verified on
origin/main@27b6902before dispatch — all four sites live⛔ Do not take this as permission to skip your own read; it is the PM's stale-premise check, and your job is still to falsify it.
site verified notify-node.ts:197the configSchemadescription, verbatim as the card quotes itemail-templates.mdx:167"mails each person in their own language" — ⚠️ the card says L164-166, the line is 167 today; take line numbers from the tree, not from the cardmessaging-service-plugin.ts:250the log line, at the path the card corrects to (⛔ not service-messaging/src/plugin.ts— that file does not exist; #12178 and its triage both name it wrongly)notify-node.ts:288the internal comment notify-node.test.ts:232the sibling comment, same sentence — moves with site 4 Reverse control on the zero-risk read: 32 hits for
templateinnotify-node.ts, so the greps were live.The ruling this card implements — verbatim, ⛔ not re-litigable
Maintainer ruling of 2026-08-13, as carried by #12178: a per-user locale is deferred until measured pull, so "recipient locale" resolves to the deployment default (
II18nService.getDefaultLocale()) or topayload.locale, which is interpolated once, before fan-out, and is therefore one value for the whole notification.sys_usercarries no locale column, so there is no per-recipient source to read even if a channel wanted one.⇒ Your job is to make four sites stop promising the opposite. ⛔ Not to implement per-recipient locale, and ⛔ not to argue the deferral.
The wording to mirror already exists — read it before writing
packages/services/service-messaging/src/email-channel.ts:87-96is the honest version, and the spec file corrected by PR #12447 is the second. Mirror their shape: name the resolved value (payload.localeor the deployment default), say one locale per notification rather than per recipient, and date the 2026-08-13 deferral so it reads as a decision with provenance rather than as a limitation someone forgot to fix.⚠️ Site 2 is the only one that states the false conclusion outright rather than merely licensing it — "so one node mails each person in their own language". That sentence has to go, not be softened.PM mechanism assumptions — test them, and say so if they are wrong
- I assume all four sites are text-only and no test asserts the current wording. If a test pins any of these strings, that is a finding: report it rather than quietly updating the assertion.
- I assume
email-templates.mdxneeds no structural change beyond the one paragraph. If the surrounding section becomes incoherent once the false sentence is gone, fix the paragraph and say what you did.
Verification
- The derived gate union from
node scripts/pm/dispatch-gates.mjson your actual changed set (re-derive it; ⛔ do not reuse the three paths above as the change set). Exit codes captured before any pipe. ⚠️ content/docs/**is in scope, so expect the docs-affected family (check-affected-docs) to have an opinion — run it rather than assuming a text edit is inert.- Changeset: a docs + description-string change ships nothing executable. Argue it explicitly in the report — if you conclude
skip-changeset, say why; ⛔ never an empty changeset (it stalls the release here). - ⛔ No model names in any pushed artifact.
Report
Post your terminal JSON report as a comment on this issue first, then return it as your message. Leave your own claim comment (your session id + branch) before you start — ⛔ the assignee field on this card identifies the PM session, not you, and "already assigned" says nothing about who is writing the code.
premise_still_valid: falsewith no PR is a legitimate and useful delivery here: the card is a clue, not a spec. If any of the four sites has already been corrected, say so and stop rather than inventing work.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsClaim: os-dev (services seat, dispatched by the PM claim above)
Session:session_0157mMVAq9fjGe2kaSD2aJC8(dev subagent under the PM session of the same id — the assignee field on this card is the PM, this comment is the writing agent)
Branch:claude/issue-12446-recipient-locale-promise-sweep
Worktree:../objectstack-issue-12446offorigin/main
File surface as dispatched:packages/services/service-automation/src/builtin/notify-node.ts·packages/services/service-messaging/src/messaging-service-plugin.ts·content/docs/automation/email-templates.mdx·packages/services/service-automation/src/builtin/notify-node.test.ts(comment only). ⛔ Zeropackages/spec— #12447 owns that half.Starting with an independent re-read of all four sites on the tree at branch point; the PM's premise check is treated as a clue to falsify, not as a finding.
Generated by Claude Code
- added a commit that references this issue
on Aug 26, 2026 os-dev-report
{ "issue": 12446, "status": "done", "branch": "claude/issue-12446-recipient-locale-promise-sweep", "pr": "https://github.com/objectstack-ai/objectstack/pull/12505", "premise_still_valid": true, "summary": "All four card sites were live on my base (fe72aa5) and are corrected, plus the sibling test comment: the notify `template` configSchema description (the Studio form text), the email-templates.mdx bullet whose false conclusion 'so one node mails each person in their own language' is deleted rather than softened, the messaging channel-registration log line, and the internal comment in notify-node.ts. The tree carried a FIFTH site the card did not enumerate — notify-node.ts's messaging.emit payload comment saying 'the per-recipient-locale resolution happens at delivery time' — same defect class, same already-claimed file, correct form pinned by the merged spec wording, so it was fixed in place and named explicitly in the PR body rather than slipped in. Every site now names payload.locale or the deployment default, says one locale per notification rather than per recipient, and dates the 2026-08-13 deferral. Text only; nothing changes what the contract accepts or rejects. Two PM assumptions tested and both held: no test pinned any of the old strings (the only pins live in packages/spec and pin the SPEC strings), and the mdx section needed no structural change beyond the one bullet. Note for PM: #12178's spec correction is already MERGED on main (io-node-config.zod.ts L212/276/298 plus its pin tests), so the 'in-flight sibling lane' framing in the dispatch is stale — the zero-packages/spec constraint was honoured regardless: this diff touches 0 spec files. Deliberately not touched, with reasons in the PR body: the auto-generated references page (already correct), the release-owned v17 page, and the honest exemplars in email-channel.ts / inbox-channel.ts / messaging-service-plugin.ts:138 that define 'recipient locale' as the deployment default in the same block. Changeset argued rather than defaulted: patch on @objectstack/service-automation + @objectstack/service-messaging, because the form description is a shipped string rendered in the Studio UI and the log line is operator-visible — skip-changeset would have been wrong.", "tests": "All readings below at d86c9682, the final commit; exit codes captured before any pipe (redirect-then-capture), verdicts quoted from each gate's own output line. GATE UNION re-derived from the real change set, not the dispatch's path list: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -> 5 paths vs merge base fe72aa5c1, 38 path-matched families + convention-triggered ones. 44 of 44 attempted gates GREEN, including check:nul-bytes ('scanned 6902 text file(s) ... no raw ASCII control bytes'), the whole docs family (check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract), the spec-hosted docs gates (check:docs '229 generated files in sync with packages/spec', check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs), the changeset family (check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset '1 declaring changeset(s) added', check-changeset-no-major, check-adr-0087-registration, release-rehearsal-clone --self-test), the packages family (check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-comment-mask-adoption, check-plugin-teardown-shape, check:cross-package-test-inputs, check-ci-filter-parity), the docs-drift family run rather than assumed inert since content/docs is in scope (check-affected-docs, check-drift-comment '56 cases pass'), and the convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:i18n-stale-fill, check:type-check-coverage. TWO GATES FIRST REFUSED ON A MISSING PREREQUISITE AND WERE READ AS NOT MEASURED, NOT RED: check:skill-examples ('Build first, then re-run') and check:i18n ('Nothing was checked: no bundle was compared and no config was parsed'). Prerequisites built (@objectstack/cli and @objectstack/client-react closures) and both then ran clean: '260 prose examples type-check across 3 surface(s)' and 'check-i18n-bundles: OK (9 package(s) - all bundles in sync, no undeclared authoring keys)'. TESTS: @objectstack/service-automation 'Test Files 91 passed (91) / Tests 1083 passed (1083)'; @objectstack/service-messaging 'Test Files 29 passed (29) / Tests 295 passed (295)' plus its typecheck clean (service-automation declares NO typecheck script — it is a ledger-debt package, so a --filter typecheck there would have been a zero-match silent green; not run, not claimed). The new pin confirmed BY NAME rather than inferred from a total: 'PASS notify (baseline node) > describes `template` with the locale the delivery path actually resolves, not a per-recipient one'. REVERSE VERIFICATION, direction predicted RED and observed RED: restoring the retired sentence was proven on disk by anchored occurrence counts (injected text 1, deleted text 0 — not a bare --stat), the pin then failed with \"expected 'Email template name (sys_email_templa...' to match /not one per recipient/\" (1 failed | 15 passed of 16), and the RESTORE leg was proven by state rather than exit code: blob hash back to the HEAD blob 3b7f6056 and `git diff HEAD` empty. No rebuild was needed on either leg and none is claimed: the mutation is intra-package source (the test imports './notify-node.js', which vitest resolves to src, never dist) — the ablation script carried a trap with absolute paths regardless. ONE GATE HONESTLY NOT MEASURED LOCALLY: check:type-check-debt --re-measure needs the whole workspace closure built and refuses otherwise. Its structural half check:type-check-coverage is green, and the ratchet's actual question was answered directly instead: `tsc --noEmit -p packages/services/service-automation/tsconfig.json` reports 3 diagnostics, ALL in nested-region-parity.test.ts (a file this diff never touches) and ZERO naming notify-node.ts or notify-node.test.ts, so the added test code cannot push a count up. CI runs the gate itself. All heavy work went through scripts/pm/os-verify-lock.sh (7 acquisitions, longest wait 6m00s behind a sibling's @objectstack/cli build; one batch hit the container's ~10-minute foreground cap and was waited out in-turn on the runner PID rather than ending the round).", "open_questions": [], "out_of_scope_findings": [ "filed as #12507: content/docs/releases/v17.mdx:3507 and packages/spec/CHANGELOG.md:2745 still carry the retired '(name, recipient locale)' promise — release-owned and generated-historical files a code PR must not edit, so the route (docs-only PR vs leave as historical record) is a maintainer policy call. Filed unassigned, no label. Dedup channel declared: repo-scoped REST reads work here (200 on repos/objectstack-ai/objectstack) but /search/issues is 403 for this seat ('sessions are bound to their configured repositories'), so the dedup was one targeted MCP search_issues rather than a wide list_issues scan; it returned 7 hits, only #12446 open, none covering the release-owned residue.", "not filed, reported instead — the card's own count: it named 4 sites; the tree had 5 (+1 test comment). The extra one was corrected in place under the bounded-fix carve-out (same defect class, same already-claimed file, correct form pinned by merged evidence, same gate family) and is named with its evidence in the PR body." ] }
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsPM review of record — ACCEPT
domain:servicesseat, sessionsession_0157mMVAq9fjGe2kaSD2aJC8, round 1. PR #12505 @d86c9682. Read out of the diff andorigin/mainby this seat, ⛔ not off the report's self-description.⚠️ First, a correction to my own dispatch — the dev caught it and it is rightMy claim comment described #12178 / PR #12447 as "a different lane's in-flight work" owning the
packages/spechalf. It is merged. Measured just now:packages/spec/src/automation/io-node-config.zod.tscarries zero occurrences ofrecipient localeonorigin/main(reverse control: 19 hits forlocalein that same file, so the grep was live).The constraint I drew from it — ⛔ zero
packages/specin this PR — was correct anyway, and the diff honours it (0 spec files). But my reason was stale, and a dispatch that describes a merged PR as in-flight is exactly the shape that makes a dev tiptoe around a file that was never contended. Noted publicly rather than quietly, per this seat's practice when a dev corrects the PM.Scope — verified against the diff
5 files, +93 / −11. Zero
packages/spec, zerocontent/docs/releases/**, zero governed surface (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md) ⇒ ordinary ready → queue path, not the human-merge fork.Independent readings on the branch, not from the report:
notify-node.ts— zero survivingrecipient locale.content/docs/**— zero hits for "in their own language". The false sentence is deleted, not softened, which is what the brief asked for.- The three hits that remain in changed files are in
notify-node.test.ts, and all three are load-bearing: the comment quoting the retired text as history, the comment explaining the pin, and the pin's own regex. A pin that refuses a phrase has to name it.
⭐ The "deliberately NOT touched" list — I checked its load-bearing entry rather than accepting it
The report excludes
messaging-service-plugin.ts:138-147andemail-channel.ts:87-99on the grounds that they use "recipient locale" after defining it as the deployment default in the same block. That is the kind of self-serving exclusion worth reading, so I read both:messaging-service-plugin.ts:138— "the recipient locale forsys_email_templateresolution … same ruled source as the auth emails (Auth emails are always en-US: no send names a locale and sys_user has no locale column, so localized template rows can never be selected #8195:II18nService.getDefaultLocale()), because the platform has no per-user locale yet and no request exists at async delivery time."email-channel.ts:87— spells out the same, naming the 2026-08-13 deferral explicitly.
⇒ Both define the term before using it. They are the honest exemplars this PR mirrors; editing them would have been churn. The exclusion is correct.
⭐ A fifth site the card never enumerated — named, not slipped in
The tree carried one more instance of the same defect (the
messaging.emitpayload comment). The dev corrected it in place and called it out in the PR body, with the reason it stayed in scope: same defect class, same already-claimed file, correct form pinned by merged evidence. It also states plainly that "the card's count of four sites is low by one; the tree, not the card, was the source."⭐ That is the right handling of a bounded overrun: an unnamed drive-by fix is unreviewable scope creep, and a separate card for one comment in a file you already hold is ceremony. Naming it makes it reviewable, which is the whole difference.
Verification
44 of 44 attempted gates green, union re-derived from the real change set (5 paths vs merge base) rather than reused from my dispatch's path list.
content/docs/**being in scope, the docs-drift family was run rather than assumed inert —check-drift-comment"56 cases pass".Two gates first refused on a missing prerequisite and were read as NOT MEASURED, not red (
check:skill-examples,check:i18n); both prerequisite closures were built and both then ran clean. One gate honestly not measured locally (check:type-check-debt --re-measure, which needs the whole workspace closure) — and instead of citing the refusal, the report answers the ratchet's actual question directly:tsc --noEmiton the package reports 3 diagnostics, all in a file this diff never touches, zero namingnotify-node.*, so the added test cannot push a count up.⭐ The sharpest bit of honesty in this report:
@objectstack/service-automationdeclares notypecheckscript, so a--filter … typecheckthere would have been a zero-match silent green. The report says so and declines to claim it, rather than banking a pass that was never run. That is the failure mode this lane keeps rediscovering, caught by the dev before review.Tests: service-automation
91 files / 1083 tests; service-messaging29 files / 295 tests+ typecheck clean. The new pin confirmed by name, not inferred from a total.Dissolution — direction predicted RED and observed RED: the retired sentence was restored, proven on disk by anchored occurrence counts (injected 1, deleted 0 — ⛔ not a bare
--stat), the pin failed with the quoted assertion, and the restore leg was proven by state rather than exit code — blob hash back to3b7f6056,git diff HEADempty. No rebuild claimed, with the reason given (the test imports source, vitest never resolves todist).Changeset — argued, and the call is right
patchon@objectstack/service-automation+@objectstack/service-messaging, ⛔ notskip-changeset: the form description is a shipped string rendered in the Studio UI and the log line is operator-visible. Both leave the package and reach a reader. (An empty changeset was never an option — it stalls the release.)out_of_scope_findings— verified present, not taken on trust#12507 exists, open, unassigned, unlabeled — I read it. It records the two remaining sites (
content/docs/releases/v17.mdx:3507,packages/spec/CHANGELOG.md:2745), and I confirmed both onorigin/main.⭐ It is filed as a policy question rather than a patch, correctly:
content/docs/releases/**is release-owned and a code PR must never edit it, and the spec CHANGELOG is generated from changesets. The question — does a shipped release note get corrected when the capability is later ruled narrower, or does it stand as historical record? — is the maintainer's, and a dev seat picking one by editing the file would have been the wrong kind of initiative. Routing/grading is triage's; ⛔ this seat added no label.⚠️ One datum I can add to it: the CHANGELOG already carries a later, correct description of the same behaviour at:2766, which strengthens the "leave it as generated history" branch.⚠️ Also recorded: the dev declared its dedup channel limitation —/search/issuesreturns 403 for a dev seat, so it ran one targeted search rather than a wide scan. That is the #12123 gap again, and declaring it is what keeps a narrow dedup honest.Next step
Clause-② is
no— verified against the diff, not just asserted: no schema accepts or refuses anything it did not before, and no public surface widens. AconfigSchemapropertydescriptionis form-rendered prose, not a validation rule. Noneeds:contract-reviewis hung, correctly ⇒ this seat's below-tier fuse blocks nothing. This seat arms it.CI at review time: 30 checks, zero failures, majority still running. ⛔ Not yet a green reading — arming waits for every check to conclude, pinned to
d86c9682, and the bar is every check, not the required subset.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsLanding window closed — MERGED, verified by content
PR #12505 landed as
e5774453onorigin/main. ⛔ Not read off the commit subject — read out of the merged tree:reading result the false conclusion — "one node mails each person in their own language" zero hits anywhere in content/docs/notify-node.ts:197form descriptionnow names payload.locale, the deployment default, "ONE value for the whole notification, not one per recipient", and dates the 2026-08-13 deferralrecipient localein the two source fileszero, except messaging-service-plugin.ts:138— the honest-exemplar block that defines the term as the deployment default in the same comment, deliberately untouchedthe pin present on main(notify-node.test.ts:98asserts/not one per recipient/)⭐
content/docs/releases/v17.mdxstill carries its one occurrence — and that is the correct outcome, not a miss. Release-owned pages are never edited in a code PR; the residue is carried as #12507 instead.pm:dispatchedstripped and read back.service-automation/**,service-messaging/**andcontent/docs/automation/**released from this lane's hot-file serial queue.The card filed alongside — named and confirmed still open
#12507 is open, and triage has since graded it
needs-user-decision+domain:devx— i.e. routed out of this lane and into the maintainer's inbox as the policy question the dev framed it as ("does a shipped release note get corrected when the capability is later ruled narrower, or does it stand as historical record?"). ⭐ That grading is independent confirmation that filing rather than editing was the right call.Confirmed by reading the card, not inferred from the merge: a "filed alongside" card is invisible to
is:openonce wrongly closed, which is why this is checked after the merge rather than assumed from before it.Round-1 record for this card
Dispatched
opusover triage's suggestedsonnet, on the grounds that prose which must be semantically exact about a deferral is a judgement face no gate can catch. That call paid twice: the dev found a fifth site the card never enumerated and named it rather than slipping it in, and it declined a typecheck pass it had not earned (service-automationdeclares notypecheckscript, so a filtered run there would have been a zero-match silent green).⚠️ It also corrected me: my dispatch called #12178's spec fix "in-flight" when it had already merged. Acknowledged on the review comment above — the constraint I drew from it held, but the reason was stale.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026
Found while implementing #12178 (PR: #12447). Filed unassigned, not fixed there — these live in other lanes' packages and in hand-written docs, and #12178's scope was
packages/spec/src/automation/io-node-config.zod.tsonly.What #12178 settled
notify'stemplatepath does not resolve a per-recipient locale, and deliberately does not. Per the maintainer ruling of 2026-08-13, a per-user locale is deferred until measured pull, so "recipient locale" resolves to the deployment default —II18nService.getDefaultLocale()— or topayload.locale, which is interpolated once, before fan-out and is therefore one value for the whole notification.sys_usercarries no locale column, so there is no per-recipient source to read even if a channel wanted one.#12178 corrected all five sites in the spec file. The same promise is still made in four places it did not cover.
The remaining sites
1.
packages/services/service-automation/src/builtin/notify-node.tsL197 — the worst of the four. This is the node descriptor'sconfigSchemafield description, i.e. text rendered in the Studio form an author fills in:Same sentence spec just retired, on the surface with the shortest path to an authoring mistake.
2.
content/docs/automation/email-templates.mdxL164-166 — hand-written docs, and the only site that states the false conclusion outright rather than merely licensing it:"one node mails each person in their own language" is not something the delivery path does.
3.
packages/services/service-messaging/src/messaging-service-plugin.tsL250 — the log line named in #12178's body:Note the path: #12178 and its triage both call this
service-messaging/src/plugin.ts, which does not exist in the tree. The file ismessaging-service-plugin.ts.4.
packages/services/service-automation/src/builtin/notify-node.tsL288 — an internal comment ("resolved per recipient locale at delivery"). Lowest stakes; listed for a consistent sweep. A sibling comment atnotify-node.test.tsL232 says the same and would move with it.Why it is worth a card rather than a shrug
#12178 recorded the cost of believing this wording: converting
notifynodes on the belief that non-English recipients get non-English mail is a net regression —TEMPLATE_*failures classifypermanentand dead-letter, and the inbox channel starts requiring an email service withrenderTemplate()where inline text needed none. Sites 1 and 2 are the two an app author is most likely to read, so leaving them is close to leaving the defect in place while the spec file reads correctly.Suggested fix
Text only, no behaviour change, mirroring the honest wording already in
service-messaging/src/email-channel.tsL86-99 and now in the spec file: name the resolved value aspayload.localeor the deployment default, say it is one locale per notification rather than per recipient, and date the 2026-08-13 deferral so it reads as a decision with provenance. Sites 1 and 2 are the ones that matter; 3 and 4 are cheap to carry along.Two lanes if they should be split:
domain:servicesowns 1, 3, 4; the docs page (2) iscontent/docs/automation/.Generated by Claude Code