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
Mail templates cannot format an amount in the record's currency: currency:ARG takes only a literal ISO code, and a path argument silently renders USD #26660
Found by the hotclm app (objectstack-ai/hotclm#179, PR #182), measured with EmailService.renderTemplate from @objectstack/plugin-email@18.2.0.
(b) A mail template cannot format an amount in the record's own currency
An app that sends one notice for every contract, such as "instalment overdue", has amounts in many currencies. The record carries the code (currency_code). The currency hole formatter takes its ISO code only as a literal argument in the template ({{ amount | currency:CNY }}, which defaults to USD). So one bundle can only show bare numbers, or hard-code one currency for every record.
Seam: spec email_template hole formatter currency:ARG → plugin-email template-enginerender() → @objectstack/formulaformatValueFORMATTERS.currency. The engine's own comment says it reuses formatValue "so dates, money … render identically to in-app templates", but the in-app renderer formats a currency field with its record's code.
Expected: a template can name the code from the data. For example, the argument resolves a template variable (currency:currency_code), or templateData passes a value the formatter already knows the code of.
(c) A path argument falls back to USD silently, and the gates pass it
{{ planned_amount | currency:currency_code }} passes os validate and os lint (exit 0). At render time, with currency_code = 'eur', it renders US$1,234.50 (zh-CN), $1,234.50 (en) and 1 234,50 $US (fr-FR): the Intl RangeError is caught and USD is used. An author who reaches for the path form gets the wrong currency, with no refusal anywhere.
Expected: either the argument resolves (b), or validate refuses a non-ISO literal and the renderer does not silently substitute USD.
Measured
currency:CNY renders ¥1,234.50 in zh-CN (correct). currency:currency_code with data eur renders USD in all three locales (above). Probe script and output: hotclm PR #182 body.
Dedupe: searched currency formatter / email template / per-record code / silent USD; closest are #8345 (exchange rates, a different subject) and #22727 (formula currency return type, closed).
p2: a path argument to the currency hole formatter silently renders USD. Refuse it at template save until it is supported. Supporting a record path (currency:record.currency_code) widens the template grammar: if that grammar is declared in packages/spec, it is a protocol addition (p1) and comes back to triage. ⛔ Never a silent default currency.
Claim: PM loop round 1 (family dispatch, chain head; members #26660 and #26673)
Session: session_01QSeKYmQXM3rHJcPnrEPk4B
Account: os-justin
Branch: claude/issue-26660-mail-hole-grammar
Worktree: objectstack-issue-26660
Domain: domain:server
Seat: domain:server#3
File surface: packages/plugins/plugin-email/src/template-engine.ts, packages/plugins/plugin-email/src/email-template-door.ts, packages/formula/src/template-engine.ts (the shared hole parser and currency formatter), and the os validate / lint rule that reads mail-template holes if that is the producer of the save-time verdict; their tests; one changeset per touched package (stop on breach; explain in the report). ⛔ No packages/spec/** (lane red line).
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: "no path-derived mandate … default opus"), effort: high
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: the hole parsers (plugin-emailFILTER_RE/parseHole; formula template engine) take everything after the first colon as the argument, and FORMATTERS.currency falls back to USD on any non-ISO code | none: os validate and os lint pass both shapes (measured in the cards) | any mail-template author; hotclm hit both on 18.2.0
Thread-read: 6441491903
Premise: holds (seat re-check on origin/main74db43ef: plugin-email/src/template-engine.tsFILTER_RE = /^(\w+)(?::\s*'?([^']*?)'?)?$/; formula/src/template-engine.ts:75(arg && arg.trim()) || 'USD' and :79 the USD fallback)
Serial constraints cleared: #25805 (in flight) declares packages/lint/**; if this card's save-time verdict lands in packages/lint, merge origin/main before the PR and again after #25805 lands. No other in-flight claim declares plugin-email or formula.
Fold (five gates): ① same defect shape and fix: a mail-template hole's formatter argument is not validated, so a malformed hole renders silently wrong text; the fix is a loud refusal at save / os validate in the one hole grammar. ② same packages (one worktree, one queue slot). ③ both graded by triage (6441491903, 6442560667). ④ each has its own named criterion: #26660 = currency:<non-ISO literal or path> refused, no silent USD; #26673 = a second | refused. ⑤ excluded look-alikes: #25082, #25189, #25351 (hole placement encoding, a different rule). The PR carries one Fixes line per member.
{
"issue": 26660,
"members": [
26660,
26673
],
"status": "done",
"branch": "claude/issue-26660-mail-hole-grammar",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26799",
"session": "session_01QSeKYmQXM3rHJcPnrEPk4B",
"premise_still_valid": true,
"summary": "Both cards' refusals are delivered from one judgement, @objectstack/formula judgeHoleFormatter: one formatter per hole (a second | outside a closed quoted argument), and a currency argument that is a literal ISO 4217 code in three capitals. The mail hole grammar now lives once in formula (mail-hole-grammar.ts: mailHolePattern, parseMailHole, judgeMailHoleGrammar); the placement judge and the plugin-email renderer import it instead of keeping copies. A new gating lint rule email-template-hole-grammar refuses both shapes in subject/bodyText/bodyHtml on os validate/build/lint and the email_template publish door (422 INVALID_METADATA); the renderer throws TEMPLATE_INVALID naming template, locale, hole and rule instead of printing '0 | number:2' or USD; the currency formatter no longer substitutes USD (throws RangeError); the in-app template dialect refuses the same two shapes at compile because the formatter is shared. Both symptoms re-measured on a485b5a3 through EmailService.renderTemplate (the by-name method survives; the module-level body renderer is what was retired) and reproduced exactly as the cards state. Closing lines: 'Fixes #26673' and 'Part of #26660' because item (b), formatting in the record's own currency, is a grammar widening triage sent back and stays owed on that card; the dispatch offered 'Refs #26660 (item c)', os-dev.md prescribes the 'Part of' spelling for a card the merge must not close, so I used that (conflict noted, not a silent pick). PM mechanism assumption falsified: the save door is the lint authoring-rule registry's runtime-publish surface, not email-template-door.ts (that is the closed sys_email_template row door and judges no holes).",
"tests": "New: formula src/template-hole-grammar.test.ts 24/24; plugin-email src/template-hole-grammar.render.test.ts 10/10; lint src/validate-email-template-hole-grammar.test.ts 15/15; metadata-protocol runtime-authoring-gate.email-template-writes.test.ts 5/5 (+2, each asserting code INVALID_METADATA + status 422 + issue rule/path). Full suites at 9fd3fde3: formula Test Files 55 passed (55) Tests 1476 passed; plugin-email 40/40 files, 712 tests; lint 169/169 files, 7445 tests; metadata-protocol 269 passed 3 skipped files, 21618 passed 19 skipped tests; typecheck exit 0 for all four (test layers via check:test-typecheck; tsc configs exclude *.test.ts). eslint on 13 changed .ts files exit 0. 9d428aae changes four docs transcripts only, suites not re-run there. Ablations via scripts/ablation-replace.mjs WRAP, dist legs proven by ablation-dist-preflight (marker present in 2 formula dist files; restore leg rebuilt and both markers absent, tree clean): A currency rule off then formula 11 failed, plugin-email 4, lint 9, metadata-protocol 1, exactly the currency pins (first attempt refused by the tool before running: replacement contained the anchor; re-anchored); B one-formatter rule off then 6/4/6/1 failed, exactly the chain pins; C renderer guard off then plugin-email 8 failed ('Paid 0 | number:2' rendered, the original defect); D USD substitution restored in the formatter then formula 1 failed (the formatter-throws pin). Every leg restored to the HEAD blob, git diff HEAD empty.",
"gates": "Local, derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 90 at 9fd3fde3, 91 at 9d428aae (+ pnpm check:cli-examples-parity). Exit codes captured before any pipe: 86 derived commands :: exit 0 at 9fd3fde3; pnpm check:docs-transcript-drift :: exit 1 at 9fd3fde3 (4 transcripts printed 54 author-time rules, registry derives 55) then fixed in 9d428aae :: exit 0; pnpm check:cli-examples-parity :: exit 0 at 9d428aae; pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (client-react unbuilt) then exit 0 at 9d428aae after the dogfood closure build; pnpm check:dual-build-cjs-loads :: exit 3 (unbuilt packages) then exit 0 at 9d428aae; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 3 NOT MEASURED, reason: its positive control is pinned to commit 621a4876 unreachable from this shallow checkout (checker health only). Extras: pnpm check:error-code-casing :: exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main :: exit 0; pnpm exec eslint (13 changed .ts) :: exit 0. --ran reconciliation: 91 derived, 91 run, 0 UNRUN. CI on PR #26799 head 9d428aae at report time: Check Documentation Links success, Check PR Size success, Governed Surface Queue Guard success, PR-body guards (claim, duplicate, Part-of, single-writer path) success, filter success; Build Core, Check Changeset, Lint (all lanes), Type Check (all lanes), Spec property liveness, Plan Test Core shards, Dogfood Regression Gate: in_progress; Temporal Conformance, Dogfood shards, Dogfood Verify CLI, upgrade harness, tarball smoke: skipped.",
"dogfood_battery": "9d428aae · os-verify-lock: VERDICT command-exit 0 · Test Files 302 passed | 3 skipped (305) · Tests 2507 passed | 20 skipped (2527) · no failures",
"line_budget": "n/a — no skills/** file touched",
"files_changed": [
".changeset/26660-mail-hole-grammar.md",
".changeset/26660-mail-hole-grammar-formula.md",
".changeset/26660-mail-hole-grammar-plugin-email.md",
"content/docs/automation/email-templates.mdx",
"content/docs/deployment/cli.mdx",
"content/docs/deployment/validating-metadata.mdx",
"content/docs/getting-started/build-with-claude-code.mdx",
"content/docs/ui/react-pages.mdx",
"packages/formula/src/index.ts",
"packages/formula/src/mail-hole-grammar.ts",
"packages/formula/src/mail-html-hole-placement.ts",
"packages/formula/src/template-engine.ts",
"packages/formula/src/template-hole-grammar.test.ts",
"packages/lint/src/authoring-rules.ts",
"packages/lint/src/index.ts",
"packages/lint/src/validate-email-template-hole-grammar.ts",
"packages/lint/src/validate-email-template-hole-grammar.test.ts",
"packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts",
"packages/plugins/plugin-email/src/email-service.ts",
"packages/plugins/plugin-email/src/template-engine.ts",
"packages/plugins/plugin-email/src/template-hole-grammar.render.test.ts"
],
"deviations": [
"Landing beyond the claim's named files: the save-time verdict is a new packages/lint rule (the claim allowed it 'if that is the producer'; measured: it is); the door pin is a test-only addition in packages/metadata-protocol beside the placement rule's; four content/docs CLI transcripts carry the derived rule count (54 to 55). email-template-door.ts is unchanged (it is not a hole validator).",
"Bounded in-place extension: the in-app template dialect (formula templateEngine compile) now refuses the same two shapes. Reason: the currency formatter is shared, so removing its USD substitution means a non-code argument must be refused at the in-app compile or evaluate would throw; the in-app chain printed '0 | number:2' too, and both are judged by the same function. Same defect class, file named in the claim's surface, same gate family; Metadata-protocol: narrowed already declared.",
"Closing spelling: 'Part of #26660' rather than the dispatch's 'Refs #26660 (item c)', per os-dev.md's rule for a card the merge must not close.",
"Attribution: commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the PR body the AGENTS.md session-URL footer, not the harness reminder's model-named trailer and emoji footer; AGENTS.md wins.",
"Four commits, not two: one per member (9e59d3de #26660, 44ccaf92 #26673), a merge of origin/main (9fd3fde3), and the transcript-count fix (9d428aae).",
"NOT edited although this PR introduces the code: service-messaging classifyError and the spec IEmailService error lists do not know TEMPLATE_INVALID (outside the claim's surface; 'stop on breach'). See open_questions.",
"PR body error to correct (body is written once, not PATCHed): under Verification, first bullet reads '87 derived commands exit 0 at 9fd3fde3'; the measured count is 86 (90 derived = 86 exit 0 + 1 exit 1 + 3 exit 3). Seat edit: change 87 to 86."
],
"mcp_calls": "0",
"api_writes": "2 — (1) relay stroke pr_create via POST /repos/objectstack-ai/objectstack/dispatches (run 40542544681, executed POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/26799/assignees); (2) this os-dev-report comment on #26660 via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/26660/comments through the relay). git push is not counted.",
"open_questions": [
{
"question": "TEMPLATE_INVALID, the new render refusal, is graded 'retryable' by service-messaging's email and inbox classifyError (email-channel.ts ~line 420, inbox-channel.ts ~line 250 grade only TEMPLATE_NOT_FOUND / TEMPLATE_INACTIVE / MISSING_VARIABLES / TEMPLATE_UNSUPPORTED as permanent), and the IEmailService contract's Errors lists (packages/spec/src/contracts/email-service.ts) do not name it. A template stored before the rule, sent through a notify channel, burns its retry ladder before going dead. Both files are outside this claim's surface.",
"options": [
"A: authorize a follow-up commit on this branch: add TEMPLATE_INVALID to both classifyError regexes with one pin each (service-messaging patch changeset), and route the spec contract doc line to the spec lane",
"B: land as is and file the classification + contract-doc gap as a card for the server lane",
"C: change the render guard to not throw (rejected: it would print a wrong value, which both triages forbid)"
],
"recommendation": "A, because this PR mints the code, the fix is two regex words and two assertions, and a failure the caller cannot repair should go dead at once like its three siblings; the spec doc line is documentation only and can follow in the spec lane."
}
],
"out_of_scope_findings": [
"class: b · reach: named producer — plugin-email EmailService.resolveAndRenderTemplate throws TEMPLATE_INVALID (this PR) for a pre-rule stored template; service-messaging classifyError grades it retryable · Seam: spec:IEmailService.sendTemplate/renderTemplate Errors → runtime:service-messaging email-channel classifyError, inbox-channel classifyError · evidence: the two regexes list four codes only · dedupe words: TEMPLATE_INVALID, classifyError retryable, IEmailService errors list, template render failure permanent",
"carrier: #26660 · item (b): format an amount in the record's own currency (a code read from the data) is a template-grammar widening triage sent back to triage; not built, PR is 'Part of #26660'",
"carrier: #26660 · `{{ amount | currency }}` with no argument still formats in USD for a record in another currency; not decided by these cards, unchanged, belongs to the item-(b) decision",
"carrier: none · other formatter arguments stay unjudged (number:two formats without fixed digits silently); not measured at a door, noted in PR Acceptance notes only",
"carrier: seat · serial: #25805 (packages/lint, still open) adds an authoring rule too; whichever lands second must move the four CLI transcript counts again (check:docs-transcript-drift names them). origin/main merged at 0f3d068c before the PR."
]
}
File surface adds (measured by the dev's report, os-dev-report6447779926):
packages/lint/src/** (the new gating rule email-template-hole-grammar; the claim allowed it as "the os validate / lint rule … if that is the producer"; measured: it is), packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts (test only), and four content/docs/** CLI transcripts (rule count 54 → 55);
this round:packages/services/service-messaging/src/email-channel.ts and inbox-channel.tsclassifyError (grade TEMPLATE_INVALID permanent like its three siblings), their tests, one patch changeset. Reason: this PR mints TEMPLATE_INVALID; graded retryable, a pre-rule stored template burns its retry ladder before going dead. That is a defect this PR introduces, so it is fixed here.
Still out: packages/spec/src/contracts/email-service.ts (the IEmailService Errors lists do not name TEMPLATE_INVALID): server-lane red line; the seat files it for the spec lane.
{
"issue": 26660,
"members": [
26660,
26673
],
"round": "patch round 1 (delta report)",
"status": "done",
"branch": "claude/issue-26660-mail-hole-grammar",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26799",
"head": "dfe8026266",
"session": "session_01QSeKYmQXM3rHJcPnrEPk4B",
"premise_still_valid": true,
"summary": "Ruling A, runtime half only: service-messaging email-channel.ts and inbox-channel.ts classifyError now grade TEMPLATE_INVALID permanent beside TEMPLATE_NOT_FOUND / TEMPLATE_INACTIVE / MISSING_VARIABLES / TEMPLATE_UNSUPPORTED, with the adjacent vocabulary comments updated; one pin per channel, each ablated; one patch changeset on @objectstack/service-messaging. packages/spec untouched. The PR was 15 commits behind origin/main (no conflict), so origin/main was merged first (1cd4b739). The PR body was not PATCHed; the exact edits for the seat are in pr_body_edits, including the 87 to 86 count.",
"tests": "New pins: email-channel.test.ts and inbox-channel.test.ts, +1 each (a 'TEMPLATE_INVALID: ...' failure from sendTemplate / renderTemplate reaches the delivery row with the code first and is graded permanent); the two files 67/67. Ablation per channel via scripts/ablation-replace.mjs WRAP (anchor 'MISSING_VARIABLES|TEMPLATE_INVALID|TEMPLATE_UNSUPPORTED' to the same without the word; the suite reads source, no dist leg): email Tests 1 failed | 41 passed, inbox 1 failed | 24 passed, each the new pin, AssertionError expected 'retryable' to be 'permanent'; both restored to the HEAD blob with git diff HEAD empty. Full suites at dfe80262, test and typecheck exit 0: service-messaging Test Files 61 passed (61) Tests 731 passed; formula 56/56 files, 1484 tests (main moved formula); plugin-email 40/40, 712; lint 169/169, 7445; metadata-protocol 269 passed 3 skipped files, 21618 passed 19 skipped tests. pnpm exec eslint on the 17 changed .ts files: exit 0.",
"gates": "Re-derived at dfe80262 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 93 commands (adds pnpm check:i18n and pnpm check:i18n-stale-fill against the previous 91). Exit codes captured before any pipe: 92 :: exit 0 on the first run, including node scripts/check-plugin-teardown-shape.mjs --self-test (NOT MEASURED last round, measured now); pnpm check:dual-build-cjs-loads :: exit 3 (studio, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge unbuilt in the fresh worktree), then exit 0 after those six were built. Extras: pnpm check:error-code-casing :: exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main :: exit 0; eslint :: exit 0. --ran with exit codes recorded: 93 derived, 93 run, 0 NOT-MEASURED (a derived zero, none exit 3). CI on PR #26799 head dfe80262, concluded: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (8/8 shards), Build Core success, Governed Surface Queue Guard success, Type Check UI success, Test UI success, Build & E2E UI success, Lint UI skipped (path selector); Check Changeset success, Lint lanes all success, Type Check lanes all success, Spec property liveness success, Dogfood Regression Gate success, PR-body guards success; opt-in / push-only jobs skipped.",
"dogfood_battery": "dfe8026266 · os-verify-lock: VERDICT command-exit 0 · Test Files 313 passed | 3 skipped (316) · Tests 2588 passed | 20 skipped (2608) · no failures",
"line_budget": "n/a — no skills/** file touched",
"files_changed": [
".changeset/26660-mail-hole-grammar-messaging.md",
"packages/services/service-messaging/src/email-channel.ts",
"packages/services/service-messaging/src/email-channel.test.ts",
"packages/services/service-messaging/src/inbox-channel.ts",
"packages/services/service-messaging/src/inbox-channel.test.ts"
],
"deviations": [
"Two commits this round, not one: d6a898e3 (a WIP commit pushed before the closure build, message 'wip: grade TEMPLATE_INVALID permanent in the messaging channels') and dfe80262 (the changeset, with the full message). Not squashed: a force-push on a branch with an open PR is outside AGENTS.md section 3's five conditions as I can verify them.",
"Execution posture: the closure build (7m02s), the suites (7m53s), the gate run and the dogfood battery (18m24s under the lock) run longer than the container's 10-minute foreground cap, so each ran under nohup and was waited on in the foreground with tail --pid, blocking in this turn, as os-dev.md prescribes for over-cap work. No wake-up was waited on.",
"Fresh worktree: the previous one was removed after the first report, so this round re-created it from origin/claude/issue-26660-mail-hole-grammar at 9d428aae (BASE) and rebuilt the closure."
],
"mcp_calls": "0",
"api_writes": "1 — this delta os-dev-report comment on #26660 via scripts/pm/post-stamped.mjs (relay; POST /repos/objectstack-ai/objectstack/issues/26660/comments). git push is not counted. The PR body was not written.",
"pr_body_edits": "EDIT 1 — \"## What changes\": after the `@objectstack/plugin-email` bullet, insert this bullet:\n\n- **`@objectstack/service-messaging`** (patch round 1): the email and inbox channels' `classifyError` grade `TEMPLATE_INVALID` permanent, beside `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`. A pre-rule stored template that fails this way now goes dead at once with the code on its delivery row, instead of spending its retry ladder. Patch changeset `26660-mail-hole-grammar-messaging.md` (`Clause-②: no`, `Metadata-protocol: none`).\n\nEDIT 2 — \"## Tests\": append this bullet:\n\n- `packages/services/service-messaging/src/email-channel.test.ts` and `inbox-channel.test.ts` (+1 each): a `TEMPLATE_INVALID: …` failure from `sendTemplate` / `renderTemplate` reaches the delivery row with the code at its front and is graded `permanent`. Ablated per channel (the word removed from the regex): each went red on exactly that pin, `expected 'retryable' to be 'permanent'`, and was restored to the HEAD blob.\n\nEDIT 3 — \"## Acceptance notes\": replace the whole bullet that begins \"**`TEMPLATE_INVALID` is a new render failure code**\" with:\n\n- **`TEMPLATE_INVALID` is a new render failure code.** The messaging email and inbox channels grade it permanent (patch round 1). The `IEmailService` contract's error lists in `packages/spec/src/contracts/email-service.ts` still name three codes; that documentation line is the spec lane's, filed by the seat.\n\nEDIT 4 — \"## Verification\", first bullet of the \"Gate families\" block: change \"87 derived commands exit 0\" to \"86 derived commands exit 0\".\n\nEDIT 5 — \"## Verification\": insert this block after the \"Dogfood battery at 9d428aae\" details block:\n\n<details><summary>Patch round 1 at dfe80262 (origin/main merged at 1cd4b739, the PR was 15 behind)</summary>\n\n- Full suites and typecheck exit 0: `@objectstack/service-messaging` Test Files 61 passed (61), Tests 731 passed; `@objectstack/formula` 56 files, 1484 tests; `@objectstack/plugin-email` 40 files, 712 tests; `@objectstack/lint` 169 files, 7445 tests; `@objectstack/metadata-protocol` 269 passed and 3 skipped files, 21618 passed and 19 skipped tests. `pnpm exec eslint` on the 17 changed TypeScript files: exit 0.\n- Gates re-derived with dispatch-gates --commands (no paths) at dfe80262: 93 commands (adds `pnpm check:i18n` and `pnpm check:i18n-stale-fill`). 92 exit 0 on the first run; `pnpm check:dual-build-cjs-loads` exit 3 (six packages unbuilt in the fresh worktree), exit 0 once they were built. Extras `pnpm check:error-code-casing` and `node scripts/check-platform-checklist.mjs --changed-from origin/main`: exit 0. `--ran` reconciliation with exit codes recorded: 93 derived, 93 run, 0 NOT-MEASURED.\n- Dogfood battery at dfe80262: `VERDICT command-exit 0`, Test Files 313 passed, 3 skipped (316), Tests 2588 passed, 20 skipped (2608).\n</details>\n\nEDIT 6 — the \"Commits:\" line: replace it with:\n\nCommits: one per member (`9e59d3de` the currency literal, `44ccaf92` one formatter per hole), merges of `origin/main` (`9fd3fde3`, `1cd4b739`), `9d428aae` the transcript counts, and patch round 1 (`d6a898e3` the messaging grade, `dfe80262` its changeset).\n",
"open_questions": [],
"out_of_scope_findings": [
"carrier: seat · the spec IEmailService Errors-list doc line for TEMPLATE_INVALID (packages/spec/src/contracts/email-service.ts), per the ruling the seat files it for the spec lane; untouched here",
"carrier: #26660 · item (b) and the no-argument USD default, unchanged from the first report",
"carrier: seat · serial: #25805 (packages/lint) still adds a rule; whichever lands second moves the four CLI transcript counts (check:docs-transcript-drift names them); it stayed green at dfe80262"
]
}
Claim: PM loop round 3 · item (b) · 2026-10-27T20:26Z
Session: session_01WGufzgbJhFoXYsLSe5WSPP
Account: os-support-ai
Branch: claude/project-thread-wonj87 (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-26660-<slug>)
Worktree: objectstack-issue-26660
Domain: domain:spec
Seat: domain:spec#3
File surface: the spec describes that document the mail hole grammar (email_template schema; flow-text-slot-template.ts / action-copy-template.ts / bulk-action.zod.ts only if their text changes), packages/formula/src/template-engine.ts + mail-hole-grammar.ts, packages/plugins/plugin-email/src/template-engine.ts / email-service.ts, packages/lint/src/validate-email-template-hole-grammar.ts, their tests, docs and skills that teach the currency formatter, generated output, one changeset per touched published package (stop on breach; explain in the report)
Container & model: M, mode:thread, model: TIER_DEFAULT — --tier: no path-derived mandate (protocol addition; one CONTRACT_REVIEW_TIER review owed before the queue, run by the seat), effort: high
Clause-②: yes
Metadata-protocol: added
Responsibility: the spec lane owns the template hole grammar (retriage 6452088840: item (b) is a protocol addition); the formula formatter, the mail renderer and the lint rule ride the same vertical PR
Thread-read: 6452088840
Premise: holds on origin/mainc4a4aefc — after PR #26799 (dabe77182) a currency: argument must be a literal ISO 4217 code; a path such as currency:currency_code is refused (currency-code-literal, packages/formula/src/template-engine.ts ~:227), so no template can format an amount in the record's own currency
Serial constraints cleared: no open PR touches formula's template engine, plugin-email, or the email-template lint rule (search 20:25Z: only #26861, unrelated region, and the version-packages PR); a skills/** edit, if any, makes this PR governed (Tier H); seat 1 told at 20:20Z
os-dev-report
{
"issue": 26660,
"item": "b",
"status": "done",
"branch": "claude/project-thread-wonj87",
"pr": "#26937",
"head": "511e09b12",
"session": "session_01WGufziJbRgRHSN9KrbF46h",
"premise_still_valid": true,
"summary": "A mail template formats an amount in the record's own currency with currency:@path ({{ planned_amount | currency:@currency_code }}). Rule, one sentence, exported as CURRENCY_ARGUMENT_RULE and quoted by the spec email_template face descriptions, the lint hint and every renderer refusal: a currency argument is either a literal ISO 4217 code in three capital letters (currency:EUR) or, in a mail template only, @ followed by a path into the send's data whose value is such a code (currency:@currency_code). The @ mark is unambiguous against the literal for every input, so every #26799 refusal pin (currency:currency_code, currency:record.currency_code, eur, empty) stays green unchanged. Dialects: mail accepts; the in-app engine (flow text slots, action and bulk-action copy, titleFormat, promptTemplate) refuses it by name, new rule currency-code-path-mail-only, because its closed-scope root judges read a hole's path and never an argument. Save time: os validate / build / lint and the email_template publish door accept a well-formed marked path and, when the template declares variables, refuse an undeclared one (currency-code-path-undeclared, 422 INVALID_METADATA at the door). Render: plugin-email resolves the path; a missing, null, lowercase or other value throws TEMPLATE_INVALID naming template, locale, hole, path and value, never USD. Premise re-measured on 6090bb7 through EmailService.renderTemplate and the lint rule: the path form was refused at save (1 finding) and render (TEMPLATE_INVALID) for both 'eur' and 'EUR'.",
"tests": "New: formula src/template-currency-code-path.test.ts 38 (incl. a spec-describe parity pin against CURRENCY_ARGUMENT_RULE); plugin-email src/template-currency-code-path.render.test.ts 12 (one bundle, en-US and zh-CN rows, EUR and CNY records: en-US €1,234.50 / CN¥1,234.50, zh-CN €1,234.50 / ¥1,234.50 in every face; unresolvable path, 'eur' and null each refused in both locales, TEMPLATE_INVALID prefix asserted, no $ in the message, nothing sent); lint src/validate-email-template-hole-grammar.currency-path.test.ts 13; metadata-protocol runtime-authoring-gate.email-template-writes.test.ts +1 (code INVALID_METADATA + status 422 + issue). #26799 pins unchanged and green. Full suites at 511e09b under os-verify-lock, one && chain, VERDICT command-exit 0: spec Test Files 738 passed (738) Tests 22101 passed 1 todo; formula 57/57, 1522; plugin-email 41/41, 724; lint 170/170, 7483; metadata-protocol 273 passed 4 skipped (277), 21694 passed 26 skipped; service-messaging 61/61, 734; typecheck exit 0 for all six. Same six green at d1fffa1 before the merge. eslint --no-inline-config on the 11 changed .ts files exit 0. Ablations via scripts/ablation-replace.mjs, each landed (anchor 1 -> 0, blob changed) and restored to the HEAD blob with git diff HEAD empty: renderer value guard dropped -> plugin-email 4 failed | 8 passed (the refusal pins; the unresolvable path then printed USD); in-app dialect check off -> formula 2 failed | 36 passed; declared-variable check off -> formula 1 failed | 37 passed. Suites under ablation read source (formula's own suite, plugin-email's renderer), so no dist leg; the lint and door legs read formula dist and were not ablated.",
"gates": "Derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), 114 commands, serial, exit captured before any pipe. At 511e09b: 114 :: exit 0; --ran: 114 derived, 114 run, 0 NOT-MEASURED. At d1fffa1: 112 exit 0, pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 and pnpm check:dual-build-cjs-loads :: exit 3 (unbuilt), both exit 0 after pnpm build. Extras at d1fffa1: check:dev-prereqs 0, check:skill-frame-freshness 0, check-platform-checklist --changed-from origin/main 0, check:error-code-casing 0, check-skills-token-ratchet 0, spec check:generated 0 after --fix (content/docs/references/system/email-template.mdx) and 0 again after the merge. CI on PR #26937 at 511e09b at report time: Governed Surface Queue Guard success, Check PR Size success, PR-body guards success, filter success; Build Core, Check Changeset, Lint lanes, Type Check lanes, Plan Test Core shards, Spec property liveness, Dogfood Regression Gate, Build Docs: in_progress; Temporal Conformance, Dogfood shards, Dogfood Verify CLI, upgrade harness, tarball smoke: skipped.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"line_budget": "skills/objectstack-formula/SKILL.md 471 -> 473 lines, 5627 -> 5668 tokens (ceiling 5997, headroom 329 after); shipped SKILL.md set 4606 -> 4608 lines; bundle 213518 -> 213559 tokens. Within the ceiling, no ceiling raised. PR is Tier H (skills/**): maintainer approval owed before landing.",
"declarations": "Clause-②: yes · Metadata-protocol: added (both copied from claim 6452747733 into the PR body and all four changesets; re-judged from the diff: a form refused at save and render until now becomes accepted, nothing narrows — the in-app dialect refused currency:@x before as currency-code-literal and still refuses it) · Skills-docs: follow-up owed — skills/objectstack-formula/evals/cel-predicates-formulas.json · ADR-0087: not-required — nothing stored changes meaning: the new form was refused at save and render until now, so no stored template or built artifact carries it",
"files_changed": [
".changeset/26660-currency-code-path-formula.md",
".changeset/26660-currency-code-path-lint.md",
".changeset/26660-currency-code-path-plugin-email.md",
".changeset/26660-currency-code-path-spec.md",
"content/docs/automation/email-templates.mdx",
"content/docs/references/system/email-template.mdx",
"packages/formula/src/index.ts",
"packages/formula/src/mail-hole-grammar.ts",
"packages/formula/src/template-currency-code-path.test.ts",
"packages/formula/src/template-engine.ts",
"packages/lint/src/validate-email-template-hole-grammar.currency-path.test.ts",
"packages/lint/src/validate-email-template-hole-grammar.ts",
"packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts",
"packages/plugins/plugin-email/src/email-service.ts",
"packages/plugins/plugin-email/src/template-currency-code-path.render.test.ts",
"packages/plugins/plugin-email/src/template-engine.ts",
"packages/spec/src/system/email-template.zod.ts",
"skills/objectstack-formula/SKILL.md"
],
"deviations": [
"File surface: packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts (+1 test, test only, no changeset) is beyond the claim's named files; it is the publish-door host pin the order's 'lint/publish-door tests' asks for, beside #26799's cases.",
"No eval added (order: say what it should cover): see open_questions / the PR's Acceptance notes.",
"One merge of origin/main (511e09b, via os-regen-merge.sh, no conflict) before opening the PR, because main moved spec, lint and metadata-protocol; rebuilt, re-ran the six suites and the 114 gates on the merge head.",
"Branch is the platform-assigned claude/project-thread-wonj87 (as the claim states), reset to origin/main 6090bb7 before work; never pushed empty.",
"Reviewer request for os-support-ai not made: the order forbids MCP writes and the relay pr_create carries assignees only; assignee os-support-ai read back. The relay's governed-review follow-up handles Tier H approvers.",
"PR activity not subscribed: os-dev.md's dispatch contract overrides the platform's auto-subscribe posture; patch rounds come back through the seat."
],
"mcp_calls": "6 — reads only: issue_read get x2 and get_comments x2 on #26660, pull_request_read get_comments on #26799, pull_request_read get_check_runs on #26937. No MCP write.",
"api_writes": "2 — (1) relay pr_create (fleet-write dispatch fw-20261027T221554Z-bae3e6, run 40593465479: POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos//issues/26937/assignees), body read back 15609 bytes identical; (2) this os-dev-report comment on #26660 via the relay comment op. git push not counted.",
"open_questions": [
{
"question": "currency with no argument still formats in USD. Census at 6090bb7 (no-arg | currency holes, generated, CHANGELOG and release files excluded): examples 0, seeds 0, skills 0, apps 0; docs 2 (automation/flows.mdx:472, data-modeling/formulas.mdx:79, both in-app); spec describes/comments 4 (flow-text-slot-template.ts:23, io-node-config.zod.ts:161/:632/:686); service-automation 2 (notify-node.ts:810, builtin/template.ts:104); example prose 2 (formula invalid-hole message, migration 18.flow-text-slot-single-brace-refused.ts:24); tests pinning the USD output 4 (formula template-formatters.test.ts:19, template-hole-grammar.test.ts:114, plugin-email template-engine.test.ts:79, template-hole-grammar.render.test.ts:110) plus 3 fixtures carrying the hole. No shipped template relies on it.",
"options": [
"A: a separate narrowing card: refuse a no-argument currency at authoring (both dialects) with the prescription currency:USD (or, in mail, currency:@path); BREAKING with a one-line D3 note; migrate the 2 docs, 4 describes and 4 pins",
"B: keep USD as the documented default (status quo)",
"C: default to the tenant localization.currency (a semantic change; needs a context channel in both engines and the mail renderer)"
],
"recommendation": "A, because an implicit USD is the silent default currency triage ruled out (6441491903), and nothing shipped depends on it; the work is small and mechanical. Seat rules."
},
{
"question": "Eval owed (#26369 B), not in this PR: what it should cover.",
"options": [
"A: skills/objectstack-formula/evals/cel-predicates-formulas.json gains one case: 'write an email_template that shows the overdue amount in each contract's own currency' expecting {{ amount | currency:@currency_code }} (not a bare path, not a hard-coded code), the code stored in capitals, and the path declared when the template declares variables"
],
"recommendation": "A, filed by the seat (it moves the same Tier H surface)."
}
],
"out_of_scope_findings": [
"carrier: none · in-app dialects cannot read a currency code from data: accepting currency:@path there needs the closed-scope root judges (spec closedScopeTemplateRefusal, lint flow-path rules) to read an argument's path; noted in Acceptance notes, not filed (no defect: refused by name)"
]
}
Contract review round 2 PASS (6456767827) on head 49258dec. Tier H, because the PR touches skills/**. It is waiting for an authorized approval from os-zhuang or hotlong, and needs-user-decision is on the PR. The seat lands it after the approval. Item (b) of this card is in this PR; item (c) landed in #26799.
A mail template formats an amount in the record's own currency with {{ amount | currency:@path }}. The code is read from the send data, and a missing, null or lowercase code renders TEMPLATE_INVALID. It never falls back to US dollars.
The in-app template dialects (flow notifications, action copy) refuse currency:@path by name. Every form accepted before renders as it did.
Owed elsewhere: #26940 (skills follow-up), #26939 (a no-argument currency still formats in USD; spec seat 1), #27225 (non-word formatter text in mail holes). pm:dispatched is removed here; the card is closed by the merge.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
enhancement·priority:p2·domain:server(lane merge:domain:services→domain:server) ·area:i18n·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T12:07Z⛔ Not a claim, ⛔ not a dispatch.
currencyhole formatter silently renders USD. Refuse it at template save until it is supported. Supporting a record path (currency:record.currency_code) widens the template grammar: if that grammar is declared inpackages/spec, it is a protocol addition (p1) and comes back to triage. ⛔ Never a silent default currency.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1 (family dispatch, chain head; members #26660 and #26673)
Session:
session_01QSeKYmQXM3rHJcPnrEPk4BAccount:
os-justinBranch:
claude/issue-26660-mail-hole-grammarWorktree:
objectstack-issue-26660Domain:
domain:serverSeat:
domain:server#3File surface:
packages/plugins/plugin-email/src/template-engine.ts,packages/plugins/plugin-email/src/email-template-door.ts,packages/formula/src/template-engine.ts(the shared hole parser andcurrencyformatter), and theos validate/ lint rule that reads mail-template holes if that is the producer of the save-time verdict; their tests; one changeset per touched package (stop on breach; explain in the report). ⛔ Nopackages/spec/**(lane red line).Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: "no path-derived mandate … default opus"),effort: highClause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: the hole parsers (
plugin-emailFILTER_RE/parseHole;formulatemplate engine) take everything after the first colon as the argument, andFORMATTERS.currencyfalls back to USD on any non-ISO code | none:os validateandos lintpass both shapes (measured in the cards) | any mail-template author; hotclm hit both on 18.2.0Thread-read: 6441491903
Premise: holds (seat re-check on
origin/main74db43ef:plugin-email/src/template-engine.tsFILTER_RE = /^(\w+)(?::\s*'?([^']*?)'?)?$/;formula/src/template-engine.ts:75(arg && arg.trim()) || 'USD'and:79the USD fallback)Serial constraints cleared: #25805 (in flight) declares
packages/lint/**; if this card's save-time verdict lands inpackages/lint, mergeorigin/mainbefore the PR and again after #25805 lands. No other in-flight claim declaresplugin-emailorformula.Fold (five gates): ① same defect shape and fix: a mail-template hole's formatter argument is not validated, so a malformed hole renders silently wrong text; the fix is a loud refusal at save /
os validatein the one hole grammar. ② same packages (one worktree, one queue slot). ③ both graded by triage (6441491903,6442560667). ④ each has its own named criterion: #26660 =currency:<non-ISO literal or path>refused, no silent USD; #26673 = a second|refused. ⑤ excluded look-alikes: #25082, #25189, #25351 (hole placement encoding, a different rule). The PR carries oneFixesline per member.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26660, "members": [ 26660, 26673 ], "status": "done", "branch": "claude/issue-26660-mail-hole-grammar", "pr": "https://github.com/objectstack-ai/objectstack/pull/26799", "session": "session_01QSeKYmQXM3rHJcPnrEPk4B", "premise_still_valid": true, "summary": "Both cards' refusals are delivered from one judgement, @objectstack/formula judgeHoleFormatter: one formatter per hole (a second | outside a closed quoted argument), and a currency argument that is a literal ISO 4217 code in three capitals. The mail hole grammar now lives once in formula (mail-hole-grammar.ts: mailHolePattern, parseMailHole, judgeMailHoleGrammar); the placement judge and the plugin-email renderer import it instead of keeping copies. A new gating lint rule email-template-hole-grammar refuses both shapes in subject/bodyText/bodyHtml on os validate/build/lint and the email_template publish door (422 INVALID_METADATA); the renderer throws TEMPLATE_INVALID naming template, locale, hole and rule instead of printing '0 | number:2' or USD; the currency formatter no longer substitutes USD (throws RangeError); the in-app template dialect refuses the same two shapes at compile because the formatter is shared. Both symptoms re-measured on a485b5a3 through EmailService.renderTemplate (the by-name method survives; the module-level body renderer is what was retired) and reproduced exactly as the cards state. Closing lines: 'Fixes #26673' and 'Part of #26660' because item (b), formatting in the record's own currency, is a grammar widening triage sent back and stays owed on that card; the dispatch offered 'Refs #26660 (item c)', os-dev.md prescribes the 'Part of' spelling for a card the merge must not close, so I used that (conflict noted, not a silent pick). PM mechanism assumption falsified: the save door is the lint authoring-rule registry's runtime-publish surface, not email-template-door.ts (that is the closed sys_email_template row door and judges no holes).", "tests": "New: formula src/template-hole-grammar.test.ts 24/24; plugin-email src/template-hole-grammar.render.test.ts 10/10; lint src/validate-email-template-hole-grammar.test.ts 15/15; metadata-protocol runtime-authoring-gate.email-template-writes.test.ts 5/5 (+2, each asserting code INVALID_METADATA + status 422 + issue rule/path). Full suites at 9fd3fde3: formula Test Files 55 passed (55) Tests 1476 passed; plugin-email 40/40 files, 712 tests; lint 169/169 files, 7445 tests; metadata-protocol 269 passed 3 skipped files, 21618 passed 19 skipped tests; typecheck exit 0 for all four (test layers via check:test-typecheck; tsc configs exclude *.test.ts). eslint on 13 changed .ts files exit 0. 9d428aae changes four docs transcripts only, suites not re-run there. Ablations via scripts/ablation-replace.mjs WRAP, dist legs proven by ablation-dist-preflight (marker present in 2 formula dist files; restore leg rebuilt and both markers absent, tree clean): A currency rule off then formula 11 failed, plugin-email 4, lint 9, metadata-protocol 1, exactly the currency pins (first attempt refused by the tool before running: replacement contained the anchor; re-anchored); B one-formatter rule off then 6/4/6/1 failed, exactly the chain pins; C renderer guard off then plugin-email 8 failed ('Paid 0 | number:2' rendered, the original defect); D USD substitution restored in the formatter then formula 1 failed (the formatter-throws pin). Every leg restored to the HEAD blob, git diff HEAD empty.", "gates": "Local, derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 90 at 9fd3fde3, 91 at 9d428aae (+ pnpm check:cli-examples-parity). Exit codes captured before any pipe: 86 derived commands :: exit 0 at 9fd3fde3; pnpm check:docs-transcript-drift :: exit 1 at 9fd3fde3 (4 transcripts printed 54 author-time rules, registry derives 55) then fixed in 9d428aae :: exit 0; pnpm check:cli-examples-parity :: exit 0 at 9d428aae; pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (client-react unbuilt) then exit 0 at 9d428aae after the dogfood closure build; pnpm check:dual-build-cjs-loads :: exit 3 (unbuilt packages) then exit 0 at 9d428aae; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 3 NOT MEASURED, reason: its positive control is pinned to commit 621a4876 unreachable from this shallow checkout (checker health only). Extras: pnpm check:error-code-casing :: exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main :: exit 0; pnpm exec eslint (13 changed .ts) :: exit 0. --ran reconciliation: 91 derived, 91 run, 0 UNRUN. CI on PR #26799 head 9d428aae at report time: Check Documentation Links success, Check PR Size success, Governed Surface Queue Guard success, PR-body guards (claim, duplicate, Part-of, single-writer path) success, filter success; Build Core, Check Changeset, Lint (all lanes), Type Check (all lanes), Spec property liveness, Plan Test Core shards, Dogfood Regression Gate: in_progress; Temporal Conformance, Dogfood shards, Dogfood Verify CLI, upgrade harness, tarball smoke: skipped.", "dogfood_battery": "9d428aae · os-verify-lock: VERDICT command-exit 0 · Test Files 302 passed | 3 skipped (305) · Tests 2507 passed | 20 skipped (2527) · no failures", "line_budget": "n/a — no skills/** file touched", "files_changed": [ ".changeset/26660-mail-hole-grammar.md", ".changeset/26660-mail-hole-grammar-formula.md", ".changeset/26660-mail-hole-grammar-plugin-email.md", "content/docs/automation/email-templates.mdx", "content/docs/deployment/cli.mdx", "content/docs/deployment/validating-metadata.mdx", "content/docs/getting-started/build-with-claude-code.mdx", "content/docs/ui/react-pages.mdx", "packages/formula/src/index.ts", "packages/formula/src/mail-hole-grammar.ts", "packages/formula/src/mail-html-hole-placement.ts", "packages/formula/src/template-engine.ts", "packages/formula/src/template-hole-grammar.test.ts", "packages/lint/src/authoring-rules.ts", "packages/lint/src/index.ts", "packages/lint/src/validate-email-template-hole-grammar.ts", "packages/lint/src/validate-email-template-hole-grammar.test.ts", "packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts", "packages/plugins/plugin-email/src/email-service.ts", "packages/plugins/plugin-email/src/template-engine.ts", "packages/plugins/plugin-email/src/template-hole-grammar.render.test.ts" ], "deviations": [ "Landing beyond the claim's named files: the save-time verdict is a new packages/lint rule (the claim allowed it 'if that is the producer'; measured: it is); the door pin is a test-only addition in packages/metadata-protocol beside the placement rule's; four content/docs CLI transcripts carry the derived rule count (54 to 55). email-template-door.ts is unchanged (it is not a hole validator).", "Bounded in-place extension: the in-app template dialect (formula templateEngine compile) now refuses the same two shapes. Reason: the currency formatter is shared, so removing its USD substitution means a non-code argument must be refused at the in-app compile or evaluate would throw; the in-app chain printed '0 | number:2' too, and both are judged by the same function. Same defect class, file named in the claim's surface, same gate family; Metadata-protocol: narrowed already declared.", "Closing spelling: 'Part of #26660' rather than the dispatch's 'Refs #26660 (item c)', per os-dev.md's rule for a card the merge must not close.", "Attribution: commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the PR body the AGENTS.md session-URL footer, not the harness reminder's model-named trailer and emoji footer; AGENTS.md wins.", "Four commits, not two: one per member (9e59d3de #26660, 44ccaf92 #26673), a merge of origin/main (9fd3fde3), and the transcript-count fix (9d428aae).", "NOT edited although this PR introduces the code: service-messaging classifyError and the spec IEmailService error lists do not know TEMPLATE_INVALID (outside the claim's surface; 'stop on breach'). See open_questions.", "PR body error to correct (body is written once, not PATCHed): under Verification, first bullet reads '87 derived commands exit 0 at 9fd3fde3'; the measured count is 86 (90 derived = 86 exit 0 + 1 exit 1 + 3 exit 3). Seat edit: change 87 to 86." ], "mcp_calls": "0", "api_writes": "2 — (1) relay stroke pr_create via POST /repos/objectstack-ai/objectstack/dispatches (run 40542544681, executed POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/26799/assignees); (2) this os-dev-report comment on #26660 via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/26660/comments through the relay). git push is not counted.", "open_questions": [ { "question": "TEMPLATE_INVALID, the new render refusal, is graded 'retryable' by service-messaging's email and inbox classifyError (email-channel.ts ~line 420, inbox-channel.ts ~line 250 grade only TEMPLATE_NOT_FOUND / TEMPLATE_INACTIVE / MISSING_VARIABLES / TEMPLATE_UNSUPPORTED as permanent), and the IEmailService contract's Errors lists (packages/spec/src/contracts/email-service.ts) do not name it. A template stored before the rule, sent through a notify channel, burns its retry ladder before going dead. Both files are outside this claim's surface.", "options": [ "A: authorize a follow-up commit on this branch: add TEMPLATE_INVALID to both classifyError regexes with one pin each (service-messaging patch changeset), and route the spec contract doc line to the spec lane", "B: land as is and file the classification + contract-doc gap as a card for the server lane", "C: change the render guard to not throw (rejected: it would print a wrong value, which both triages forbid)" ], "recommendation": "A, because this PR mints the code, the fix is two regex words and two assertions, and a failure the caller cannot repair should go dead at once like its three siblings; the spec doc line is documentation only and can follow in the spec lane." } ], "out_of_scope_findings": [ "class: b · reach: named producer — plugin-email EmailService.resolveAndRenderTemplate throws TEMPLATE_INVALID (this PR) for a pre-rule stored template; service-messaging classifyError grades it retryable · Seam: spec:IEmailService.sendTemplate/renderTemplate Errors → runtime:service-messaging email-channel classifyError, inbox-channel classifyError · evidence: the two regexes list four codes only · dedupe words: TEMPLATE_INVALID, classifyError retryable, IEmailService errors list, template render failure permanent", "carrier: #26660 · item (b): format an amount in the record's own currency (a code read from the data) is a template-grammar widening triage sent back to triage; not built, PR is 'Part of #26660'", "carrier: #26660 · `{{ amount | currency }}` with no argument still formats in USD for a record in another currency; not decided by these cards, unchanged, belongs to the item-(b) decision", "carrier: none · other formatter arguments stay unjudged (number:two formats without fixed digits silently); not measured at a door, noted in PR Acceptance notes only", "carrier: seat · serial: #25805 (packages/lint, still open) adds an authoring rule too; whichever lands second must move the four CLI transcript counts again (check:docs-transcript-drift names them). origin/main merged at 0f3d068c before the PR." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim amendment — file surface, patch round 1 ·
domain:serverseat 3 ·session_01QSeKYmQXM3rHJcPnrEPk4B· 2026-10-27T16:42Z · same claim (6445327635), same branchclaude/issue-26660-mail-hole-grammar.File surface adds (measured by the dev's report,
os-dev-report6447779926):packages/lint/src/**(the new gating ruleemail-template-hole-grammar; the claim allowed it as "theos validate/ lint rule … if that is the producer"; measured: it is),packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts(test only), and fourcontent/docs/**CLI transcripts (rule count 54 → 55);packages/services/service-messaging/src/email-channel.tsandinbox-channel.tsclassifyError(gradeTEMPLATE_INVALIDpermanent like its three siblings), their tests, onepatchchangeset. Reason: this PR mintsTEMPLATE_INVALID; graded retryable, a pre-rule stored template burns its retry ladder before going dead. That is a defect this PR introduces, so it is fixed here.Still out:
packages/spec/src/contracts/email-service.ts(theIEmailServiceErrors lists do not nameTEMPLATE_INVALID): server-lane red line; the seat files it for the spec lane.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26660, "members": [ 26660, 26673 ], "round": "patch round 1 (delta report)", "status": "done", "branch": "claude/issue-26660-mail-hole-grammar", "pr": "https://github.com/objectstack-ai/objectstack/pull/26799", "head": "dfe8026266", "session": "session_01QSeKYmQXM3rHJcPnrEPk4B", "premise_still_valid": true, "summary": "Ruling A, runtime half only: service-messaging email-channel.ts and inbox-channel.ts classifyError now grade TEMPLATE_INVALID permanent beside TEMPLATE_NOT_FOUND / TEMPLATE_INACTIVE / MISSING_VARIABLES / TEMPLATE_UNSUPPORTED, with the adjacent vocabulary comments updated; one pin per channel, each ablated; one patch changeset on @objectstack/service-messaging. packages/spec untouched. The PR was 15 commits behind origin/main (no conflict), so origin/main was merged first (1cd4b739). The PR body was not PATCHed; the exact edits for the seat are in pr_body_edits, including the 87 to 86 count.", "tests": "New pins: email-channel.test.ts and inbox-channel.test.ts, +1 each (a 'TEMPLATE_INVALID: ...' failure from sendTemplate / renderTemplate reaches the delivery row with the code first and is graded permanent); the two files 67/67. Ablation per channel via scripts/ablation-replace.mjs WRAP (anchor 'MISSING_VARIABLES|TEMPLATE_INVALID|TEMPLATE_UNSUPPORTED' to the same without the word; the suite reads source, no dist leg): email Tests 1 failed | 41 passed, inbox 1 failed | 24 passed, each the new pin, AssertionError expected 'retryable' to be 'permanent'; both restored to the HEAD blob with git diff HEAD empty. Full suites at dfe80262, test and typecheck exit 0: service-messaging Test Files 61 passed (61) Tests 731 passed; formula 56/56 files, 1484 tests (main moved formula); plugin-email 40/40, 712; lint 169/169, 7445; metadata-protocol 269 passed 3 skipped files, 21618 passed 19 skipped tests. pnpm exec eslint on the 17 changed .ts files: exit 0.", "gates": "Re-derived at dfe80262 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 93 commands (adds pnpm check:i18n and pnpm check:i18n-stale-fill against the previous 91). Exit codes captured before any pipe: 92 :: exit 0 on the first run, including node scripts/check-plugin-teardown-shape.mjs --self-test (NOT MEASURED last round, measured now); pnpm check:dual-build-cjs-loads :: exit 3 (studio, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge unbuilt in the fresh worktree), then exit 0 after those six were built. Extras: pnpm check:error-code-casing :: exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main :: exit 0; eslint :: exit 0. --ran with exit codes recorded: 93 derived, 93 run, 0 NOT-MEASURED (a derived zero, none exit 3). CI on PR #26799 head dfe80262, concluded: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (8/8 shards), Build Core success, Governed Surface Queue Guard success, Type Check UI success, Test UI success, Build & E2E UI success, Lint UI skipped (path selector); Check Changeset success, Lint lanes all success, Type Check lanes all success, Spec property liveness success, Dogfood Regression Gate success, PR-body guards success; opt-in / push-only jobs skipped.", "dogfood_battery": "dfe8026266 · os-verify-lock: VERDICT command-exit 0 · Test Files 313 passed | 3 skipped (316) · Tests 2588 passed | 20 skipped (2608) · no failures", "line_budget": "n/a — no skills/** file touched", "files_changed": [ ".changeset/26660-mail-hole-grammar-messaging.md", "packages/services/service-messaging/src/email-channel.ts", "packages/services/service-messaging/src/email-channel.test.ts", "packages/services/service-messaging/src/inbox-channel.ts", "packages/services/service-messaging/src/inbox-channel.test.ts" ], "deviations": [ "Two commits this round, not one: d6a898e3 (a WIP commit pushed before the closure build, message 'wip: grade TEMPLATE_INVALID permanent in the messaging channels') and dfe80262 (the changeset, with the full message). Not squashed: a force-push on a branch with an open PR is outside AGENTS.md section 3's five conditions as I can verify them.", "Execution posture: the closure build (7m02s), the suites (7m53s), the gate run and the dogfood battery (18m24s under the lock) run longer than the container's 10-minute foreground cap, so each ran under nohup and was waited on in the foreground with tail --pid, blocking in this turn, as os-dev.md prescribes for over-cap work. No wake-up was waited on.", "Fresh worktree: the previous one was removed after the first report, so this round re-created it from origin/claude/issue-26660-mail-hole-grammar at 9d428aae (BASE) and rebuilt the closure." ], "mcp_calls": "0", "api_writes": "1 — this delta os-dev-report comment on #26660 via scripts/pm/post-stamped.mjs (relay; POST /repos/objectstack-ai/objectstack/issues/26660/comments). git push is not counted. The PR body was not written.", "pr_body_edits": "EDIT 1 — \"## What changes\": after the `@objectstack/plugin-email` bullet, insert this bullet:\n\n- **`@objectstack/service-messaging`** (patch round 1): the email and inbox channels' `classifyError` grade `TEMPLATE_INVALID` permanent, beside `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`. A pre-rule stored template that fails this way now goes dead at once with the code on its delivery row, instead of spending its retry ladder. Patch changeset `26660-mail-hole-grammar-messaging.md` (`Clause-②: no`, `Metadata-protocol: none`).\n\nEDIT 2 — \"## Tests\": append this bullet:\n\n- `packages/services/service-messaging/src/email-channel.test.ts` and `inbox-channel.test.ts` (+1 each): a `TEMPLATE_INVALID: …` failure from `sendTemplate` / `renderTemplate` reaches the delivery row with the code at its front and is graded `permanent`. Ablated per channel (the word removed from the regex): each went red on exactly that pin, `expected 'retryable' to be 'permanent'`, and was restored to the HEAD blob.\n\nEDIT 3 — \"## Acceptance notes\": replace the whole bullet that begins \"**`TEMPLATE_INVALID` is a new render failure code**\" with:\n\n- **`TEMPLATE_INVALID` is a new render failure code.** The messaging email and inbox channels grade it permanent (patch round 1). The `IEmailService` contract's error lists in `packages/spec/src/contracts/email-service.ts` still name three codes; that documentation line is the spec lane's, filed by the seat.\n\nEDIT 4 — \"## Verification\", first bullet of the \"Gate families\" block: change \"87 derived commands exit 0\" to \"86 derived commands exit 0\".\n\nEDIT 5 — \"## Verification\": insert this block after the \"Dogfood battery at 9d428aae\" details block:\n\n<details><summary>Patch round 1 at dfe80262 (origin/main merged at 1cd4b739, the PR was 15 behind)</summary>\n\n- Full suites and typecheck exit 0: `@objectstack/service-messaging` Test Files 61 passed (61), Tests 731 passed; `@objectstack/formula` 56 files, 1484 tests; `@objectstack/plugin-email` 40 files, 712 tests; `@objectstack/lint` 169 files, 7445 tests; `@objectstack/metadata-protocol` 269 passed and 3 skipped files, 21618 passed and 19 skipped tests. `pnpm exec eslint` on the 17 changed TypeScript files: exit 0.\n- Gates re-derived with dispatch-gates --commands (no paths) at dfe80262: 93 commands (adds `pnpm check:i18n` and `pnpm check:i18n-stale-fill`). 92 exit 0 on the first run; `pnpm check:dual-build-cjs-loads` exit 3 (six packages unbuilt in the fresh worktree), exit 0 once they were built. Extras `pnpm check:error-code-casing` and `node scripts/check-platform-checklist.mjs --changed-from origin/main`: exit 0. `--ran` reconciliation with exit codes recorded: 93 derived, 93 run, 0 NOT-MEASURED.\n- Dogfood battery at dfe80262: `VERDICT command-exit 0`, Test Files 313 passed, 3 skipped (316), Tests 2588 passed, 20 skipped (2608).\n</details>\n\nEDIT 6 — the \"Commits:\" line: replace it with:\n\nCommits: one per member (`9e59d3de` the currency literal, `44ccaf92` one formatter per hole), merges of `origin/main` (`9fd3fde3`, `1cd4b739`), `9d428aae` the transcript counts, and patch round 1 (`d6a898e3` the messaging grade, `dfe80262` its changeset).\n", "open_questions": [], "out_of_scope_findings": [ "carrier: seat · the spec IEmailService Errors-list doc line for TEMPLATE_INVALID (packages/spec/src/contracts/email-service.ts), per the ruling the seat files it for the spec lane; untouched here", "carrier: #26660 · item (b) and the no-argument USD default, unchanged from the first report", "carrier: seat · serial: #25805 (packages/lint) still adds a rule; whichever lands second moves the four CLI transcript counts (check:docs-transcript-drift names them); it stayed green at dfe80262" ] }Generated by Claude Code
7 remaining items
objectstack-fleet commentedon Oct 27, 2026
Retriage answer: item (b) is a protocol addition —
domain:server→domain:spec, p1 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T19:53Z⛔ Not a claim.
pm:retriageremoved.currency:argument is refused, and USD is never substituted. Item (b), formatting an amount in the record's own currency (a code read from the data), widens a hole grammar the contract review reads as spec-documented (6450017524). So it is a protocol addition:domain:specfirst, with the renderer half riding the same vertical PR. Under [Decision] Triage priority caliber for protocol additions: keep 「协议新增定 p1」 for every addition, or grade additions an app asks for at p2 unless a release needs them #26368 A, p2 → p1. Skills, docs and eval are owed ([Decision] pm-dispatch: which PRs owe aSkills-docs:line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B).Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 3 · item (b) · 2026-10-27T20:26Z
Session:
session_01WGufzgbJhFoXYsLSe5WSPPAccount:
os-support-aiBranch:
claude/project-thread-wonj87(card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead ofclaude/issue-26660-<slug>)Worktree:
objectstack-issue-26660Domain:
domain:specSeat:
domain:spec#3File surface:
the spec describes that document the mail hole grammar (email_template schema; flow-text-slot-template.ts / action-copy-template.ts / bulk-action.zod.ts only if their text changes), packages/formula/src/template-engine.ts + mail-hole-grammar.ts, packages/plugins/plugin-email/src/template-engine.ts / email-service.ts, packages/lint/src/validate-email-template-hole-grammar.ts, their tests, docs and skills that teach the currency formatter, generated output, one changeset per touched published package(stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT — --tier: no path-derived mandate (protocol addition; one CONTRACT_REVIEW_TIER review owed before the queue, run by the seat),effort: highClause-②: yes
Metadata-protocol: added
Responsibility: the spec lane owns the template hole grammar (retriage 6452088840: item (b) is a protocol addition); the formula formatter, the mail renderer and the lint rule ride the same vertical PR
Thread-read: 6452088840
Premise: holds on
origin/mainc4a4aefc— after PR #26799 (dabe77182) acurrency:argument must be a literal ISO 4217 code; a path such ascurrency:currency_codeis refused (currency-code-literal,packages/formula/src/template-engine.ts~:227), so no template can format an amount in the record's own currencySerial constraints cleared:
no open PR touches formula's template engine, plugin-email, or the email-template lint rule (search 20:25Z: only #26861, unrelated region, and the version-packages PR); a skills/** edit, if any, makes this PR governed (Tier H); seat 1 told at 20:20Zobjectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26660,
"item": "b",
"status": "done",
"branch": "claude/project-thread-wonj87",
"pr": "#26937",
"head": "511e09b12",
"session": "session_01WGufziJbRgRHSN9KrbF46h",
"premise_still_valid": true,
"summary": "A mail template formats an amount in the record's own currency with
currency:@path({{ planned_amount | currency:@currency_code }}). Rule, one sentence, exported as CURRENCY_ARGUMENT_RULE and quoted by the spec email_template face descriptions, the lint hint and every renderer refusal: a currency argument is either a literal ISO 4217 code in three capital letters (currency:EUR) or, in a mail template only, @ followed by a path into the send's data whose value is such a code (currency:@currency_code). The @ mark is unambiguous against the literal for every input, so every #26799 refusal pin (currency:currency_code, currency:record.currency_code, eur, empty) stays green unchanged. Dialects: mail accepts; the in-app engine (flow text slots, action and bulk-action copy, titleFormat, promptTemplate) refuses it by name, new rule currency-code-path-mail-only, because its closed-scope root judges read a hole's path and never an argument. Save time: os validate / build / lint and the email_template publish door accept a well-formed marked path and, when the template declares variables, refuse an undeclared one (currency-code-path-undeclared, 422 INVALID_METADATA at the door). Render: plugin-email resolves the path; a missing, null, lowercase or other value throws TEMPLATE_INVALID naming template, locale, hole, path and value, never USD. Premise re-measured on 6090bb7 through EmailService.renderTemplate and the lint rule: the path form was refused at save (1 finding) and render (TEMPLATE_INVALID) for both 'eur' and 'EUR'.","tests": "New: formula src/template-currency-code-path.test.ts 38 (incl. a spec-describe parity pin against CURRENCY_ARGUMENT_RULE); plugin-email src/template-currency-code-path.render.test.ts 12 (one bundle, en-US and zh-CN rows, EUR and CNY records: en-US €1,234.50 / CN¥1,234.50, zh-CN €1,234.50 / ¥1,234.50 in every face; unresolvable path, 'eur' and null each refused in both locales, TEMPLATE_INVALID prefix asserted, no $ in the message, nothing sent); lint src/validate-email-template-hole-grammar.currency-path.test.ts 13; metadata-protocol runtime-authoring-gate.email-template-writes.test.ts +1 (code INVALID_METADATA + status 422 + issue). #26799 pins unchanged and green. Full suites at 511e09b under os-verify-lock, one && chain, VERDICT command-exit 0: spec Test Files 738 passed (738) Tests 22101 passed 1 todo; formula 57/57, 1522; plugin-email 41/41, 724; lint 170/170, 7483; metadata-protocol 273 passed 4 skipped (277), 21694 passed 26 skipped; service-messaging 61/61, 734; typecheck exit 0 for all six. Same six green at d1fffa1 before the merge. eslint --no-inline-config on the 11 changed .ts files exit 0. Ablations via scripts/ablation-replace.mjs, each landed (anchor 1 -> 0, blob changed) and restored to the HEAD blob with git diff HEAD empty: renderer value guard dropped -> plugin-email 4 failed | 8 passed (the refusal pins; the unresolvable path then printed USD); in-app dialect check off -> formula 2 failed | 36 passed; declared-variable check off -> formula 1 failed | 37 passed. Suites under ablation read source (formula's own suite, plugin-email's renderer), so no dist leg; the lint and door legs read formula dist and were not ablated.",
"gates": "Derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), 114 commands, serial, exit captured before any pipe. At 511e09b: 114 :: exit 0; --ran: 114 derived, 114 run, 0 NOT-MEASURED. At d1fffa1: 112 exit 0, pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 and pnpm check:dual-build-cjs-loads :: exit 3 (unbuilt), both exit 0 after pnpm build. Extras at d1fffa1: check:dev-prereqs 0, check:skill-frame-freshness 0, check-platform-checklist --changed-from origin/main 0, check:error-code-casing 0, check-skills-token-ratchet 0, spec check:generated 0 after --fix (content/docs/references/system/email-template.mdx) and 0 again after the merge. CI on PR #26937 at 511e09b at report time: Governed Surface Queue Guard success, Check PR Size success, PR-body guards success, filter success; Build Core, Check Changeset, Lint lanes, Type Check lanes, Plan Test Core shards, Spec property liveness, Dogfood Regression Gate, Build Docs: in_progress; Temporal Conformance, Dogfood shards, Dogfood Verify CLI, upgrade harness, tarball smoke: skipped.",
"dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm","line_budget": "skills/objectstack-formula/SKILL.md 471 -> 473 lines, 5627 -> 5668 tokens (ceiling 5997, headroom 329 after); shipped SKILL.md set 4606 -> 4608 lines; bundle 213518 -> 213559 tokens. Within the ceiling, no ceiling raised. PR is Tier H (skills/**): maintainer approval owed before landing.",
"declarations": "Clause-②: yes · Metadata-protocol: added (both copied from claim 6452747733 into the PR body and all four changesets; re-judged from the diff: a form refused at save and render until now becomes accepted, nothing narrows — the in-app dialect refused currency:@x before as currency-code-literal and still refuses it) · Skills-docs: follow-up owed — skills/objectstack-formula/evals/cel-predicates-formulas.json · ADR-0087: not-required — nothing stored changes meaning: the new form was refused at save and render until now, so no stored template or built artifact carries it",
"files_changed": [
".changeset/26660-currency-code-path-formula.md",
".changeset/26660-currency-code-path-lint.md",
".changeset/26660-currency-code-path-plugin-email.md",
".changeset/26660-currency-code-path-spec.md",
"content/docs/automation/email-templates.mdx",
"content/docs/references/system/email-template.mdx",
"packages/formula/src/index.ts",
"packages/formula/src/mail-hole-grammar.ts",
"packages/formula/src/template-currency-code-path.test.ts",
"packages/formula/src/template-engine.ts",
"packages/lint/src/validate-email-template-hole-grammar.currency-path.test.ts",
"packages/lint/src/validate-email-template-hole-grammar.ts",
"packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts",
"packages/plugins/plugin-email/src/email-service.ts",
"packages/plugins/plugin-email/src/template-currency-code-path.render.test.ts",
"packages/plugins/plugin-email/src/template-engine.ts",
"packages/spec/src/system/email-template.zod.ts",
"skills/objectstack-formula/SKILL.md"
],
"deviations": [
"File surface: packages/metadata-protocol/src/runtime-authoring-gate.email-template-writes.test.ts (+1 test, test only, no changeset) is beyond the claim's named files; it is the publish-door host pin the order's 'lint/publish-door tests' asks for, beside #26799's cases.",
"No eval added (order: say what it should cover): see open_questions / the PR's Acceptance notes.",
"One merge of origin/main (511e09b, via os-regen-merge.sh, no conflict) before opening the PR, because main moved spec, lint and metadata-protocol; rebuilt, re-ran the six suites and the 114 gates on the merge head.",
"Branch is the platform-assigned claude/project-thread-wonj87 (as the claim states), reset to origin/main 6090bb7 before work; never pushed empty.",
"Reviewer request for os-support-ai not made: the order forbids MCP writes and the relay pr_create carries assignees only; assignee os-support-ai read back. The relay's governed-review follow-up handles Tier H approvers.",
"PR activity not subscribed: os-dev.md's dispatch contract overrides the platform's auto-subscribe posture; patch rounds come back through the seat."
],
"mcp_calls": "6 — reads only: issue_read get x2 and get_comments x2 on #26660, pull_request_read get_comments on #26799, pull_request_read get_check_runs on #26937. No MCP write.",
"api_writes": "2 — (1) relay pr_create (fleet-write dispatch fw-20261027T221554Z-bae3e6, run 40593465479: POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos//issues/26937/assignees), body read back 15609 bytes identical; (2) this os-dev-report comment on #26660 via the relay comment op. git push not counted.",
"open_questions": [
{
"question": "
currencywith no argument still formats in USD. Census at 6090bb7 (no-arg| currencyholes, generated, CHANGELOG and release files excluded): examples 0, seeds 0, skills 0, apps 0; docs 2 (automation/flows.mdx:472, data-modeling/formulas.mdx:79, both in-app); spec describes/comments 4 (flow-text-slot-template.ts:23, io-node-config.zod.ts:161/:632/:686); service-automation 2 (notify-node.ts:810, builtin/template.ts:104); example prose 2 (formula invalid-hole message, migration 18.flow-text-slot-single-brace-refused.ts:24); tests pinning the USD output 4 (formula template-formatters.test.ts:19, template-hole-grammar.test.ts:114, plugin-email template-engine.test.ts:79, template-hole-grammar.render.test.ts:110) plus 3 fixtures carrying the hole. No shipped template relies on it.","options": [
"A: a separate narrowing card: refuse a no-argument currency at authoring (both dialects) with the prescription currency:USD (or, in mail, currency:@path); BREAKING with a one-line D3 note; migrate the 2 docs, 4 describes and 4 pins",
"B: keep USD as the documented default (status quo)",
"C: default to the tenant localization.currency (a semantic change; needs a context channel in both engines and the mail renderer)"
],
"recommendation": "A, because an implicit USD is the silent default currency triage ruled out (6441491903), and nothing shipped depends on it; the work is small and mechanical. Seat rules."
},
{
"question": "Eval owed (#26369 B), not in this PR: what it should cover.",
"options": [
"A: skills/objectstack-formula/evals/cel-predicates-formulas.json gains one case: 'write an email_template that shows the overdue amount in each contract's own currency' expecting {{ amount | currency:@currency_code }} (not a bare path, not a hard-coded code), the code stored in capitals, and the path declared when the template declares variables"
],
"recommendation": "A, filed by the seat (it moves the same Tier H surface)."
}
],
"out_of_scope_findings": [
"carrier: none · in-app dialects cannot read a currency code from data: accepting currency:@path there needs the closed-scope root judges (spec closedScopeTemplateRefusal, lint flow-path rules) to read an argument's path; noted in Acceptance notes, not filed (no defect: refused by name)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
ACCEPT → PR #26937 comment 6456811179 ·
domain:specseat 3 · 2026-10-28T00:05ZContract review round 2 PASS (6456767827) on head
49258dec. Tier H, because the PR touchesskills/**. It is waiting for an authorized approval fromos-zhuangorhotlong, andneeds-user-decisionis on the PR. The seat lands it after the approval. Item (b) of this card is in this PR; item (c) landed in #26799.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Landed ·
domain:specseat 3 ·os-support-ai· 2026-10-28T13:35Zos-zhuang(Tier H) and merged at 2026-10-28T09:58Z, merge commit2205863525origin/maingit merge-base --is-ancestor)Delivered (item (b); item (c) landed in #26799):
{{ amount | currency:@path }}. The code is read from the send data, and a missing, null or lowercase code rendersTEMPLATE_INVALID. It never falls back to US dollars.currency:@pathby name. Every form accepted before renders as it did.Owed elsewhere: #26940 (skills follow-up), #26939 (a no-argument
currencystill formats in USD; spec seat 1), #27225 (non-word formatter text in mail holes).pm:dispatchedis removed here; the card is closed by the merge.