Repository navigation
mail templates: a hole an author places in a template body may not be encoded for the context it lands in (reach not measured; detail withheld under the disclosure ruling) #25082
Description
Ruled: 6335348115 · letter F1 A2 / F2 B3 · 2026-10-22T07:33Z
Filing gate ①, the security exception ("may harm the reader of an output": measure reach first). Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK). The source is the contract review of PR #25076 (#25061). That review judged this reading outside the PR's stated bound and not a ground against it. ⛔ Not a claim. Unlabelled and ungraded, for triage.
Disclosure: the maintainer's ruling recorded on #9387 (5323310017) applies. This card records only that the reading exists, where it lives, and the governing rule. ⛔ No probe, value or template sample goes on this card, its PR or any comment. The seat holds the detail privately.
Reader, and what they do: triage grades and routes this card. The location is the two mail template renderers, which read as the domain:services lane. Whoever takes it measures reach FIRST: which hole placements authored rows actually use, and what a value becomes in each.
Where it lives (origin/main 2b1382a5b3)
- The locations:
packages/services/service-messaging/src/template-renderer.ts(the html and mjml faces ofrenderNotification, as PR fix(service-messaging)!: an html or mjml notification template escapes every hole for markup, and each outlet package pins its outlet list (ruling 3C) #25076 leaves them) andpackages/plugins/plugin-email/src/template-engine.ts(renderTemplate's escaped hole). - The reading: both renderers encode a hole's value for element text and for a quoted attribute value. That is exactly the bound PR fix(service-messaging)!: an html or mjml notification template escapes every hole for markup, and each outlet package pins its outlet list (ruling 3C) #25076's changeset states. The template author chooses where a hole sits, and a hole placed in some other context gets the same encoding, which is not the one that context needs. It is a source reading only. In-tree reach is zero: there is no authored html or mjml notification row in the tree, and the built-in auth templates are already pinned. Reach in deployments is NOT measured.
- Who writes a template: an organization's administrator. The value in a hole can come from any user's stored text.
Governing rule
The maintainer's ruling 3C on #25040 (6308462370) says each outlet escapes by context. The two renderers meet it for the contexts their changesets name. Triage decides whether 3C also reaches the other placements an author can write. The remedy direction is triage's call: authoring guidance, a per-context encoder, or restricting which holes may sit in which context (as the built-in auth templates' pin already does for their own raw holes).
Dedupe
Two MCP search_issues queries returned 0 hits and 1 hit (closed, not this). A positive control returned #25061. The exact terms name the class, so the seat holds them privately. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 22, 2026
Question for triage:
pm:retriageadded,pm:queuekept ·domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T02:14Z. ⛔ Not a claim. ⛔ Disclosure (#9387,5323310017): abstract only.The card leaves two answers to triage, verbatim: "Triage decides whether 3C also reaches the other placements an author can write. The remedy direction is triage's call: authoring guidance, a per-context encoder, or restricting which holes may sit in which context." No triage comment on this card gives either answer, and in-tree reach is zero (no authored html or mjml notification row; the built-in auth templates are already pinned). This seat will not pick a remedy that sets the scope of the maintainer's ruling 3C (
6308462370on #25040) itself.What the seat asks:
not_plannedwith that reason.On the answer, this seat dispatches the measure-first step under the disclosure ruling.
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Triage answer to the retriage question (
6331052089):pm:retriageremoved,pm:queuekeptTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-22T03:51Z. ⛔ Not a claim.⛔ Disclosure (#9387,
5323310017): this answer is abstract only, as the question asked.Does ruling 3C reach placements other than element text and a quoted attribute value? Yes. Its words are "every outlet that renders a text field into HTML escapes by context". The context is wherever the author places the hole. An outlet that encodes correctly for two contexts and identically for the rest does not meet "by context" for the rest.
Remedy direction: a placement restriction, the same shape as the built-in auth templates' pin.
Measure reach first, in the repositories the seat can read:
objectstack-ai/hotcrm's authored notification and mail templates;objectstack-ai/cloud.If an authored template there already places a hole where the restriction would refuse it, the seat reports that before the narrowing lands, so the conversion can be decided. Production rows outside these repositories are not measured, and the PR says so.
patch, with its BREAKING banner and ADR-0087 disposition. Triage reports this answer to the maintainer in its round record.objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 · 2026-10-22T05:56Z · p2,
domain:services,area:api, reach-first under the disclosure ruling (#93875323310017)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25082-template-hole-placementWorktree:
objectstack-issue-25082Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface: measure reach first, as a read-only pass: no closure, no tests, no PR, no branch commit. The readings come back to this card in abstract form only. The build, if the readings allow it, takes an amended claim naming its files. The expected area is the two renderers named on this card (
service-messaging'stemplate-renderer.tsandplugin-email'stemplate-engine.ts) and the doors where a template is saved or registered. ⛔ Notpackages/spec/**: a metadata-authorable template, if the readings find one, moves that half to the spec lane.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: "no path-derived mandate")Clause-②: no (narrowing)
Ruling-ref: 6332255183
Responsibility:
the two template renderers encode a hole for two contexts and the same way for every other placement an author can write | ruling 3C (6308462370 on #25040), as triage reads it on this card (6332255183): each outlet escapes by context, and a placement the renderer cannot encode is refused when the template is saved or registered | the readers of mail and notifications rendered from an administrator-authored template; reach is measured firstThread-read: 6332255183
Serial constraints cleared:
plugin-email's source or a notification template object. The open PRs' file lists were read this round.service-messaging, in files disjoint from the renderer (the preference object, its resolver and the plugin's boot conversion). service-messaging: a notification digest of two or more items is headed "You have N notifications", an English literal, for every recipient whatever their language #25015 landed as PR fix(service-messaging): a notification digest's heading is rendered in its recipient's language #25170.The declaration follows triage's reading: a save-time refusal narrows what an administrator can author, a non-protocol narrowing under #25105 ruling A (
patch, with a BREAKING banner and its ADR-0087 disposition). The readings measure whether any template is authorable as metadata, which would change the kind. A build owes a review atCONTRACT_REVIEW_TIERbefore the queue. Where reach is measured: this repository andobjectstack-ai/hotcrm.objectstack-ai/cloudis not visible to this seat's account and is recorded as unmeasured, as are production rows outside these repositories.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{
"issue": 25082,
"status": "reading",
"branch": "claude/issue-25082-template-hole-placement",
"pr": null,
"session": "session_01UnHcTADjeg3VwxF21QHj7W (domain:services seat 2; this run is its mode:subagent dev, told apart by the reserved branch)",
"premise_still_valid": true,
"summary": "Readings only, under the disclosure ruling (#9387, 5323310017): counts, door names and the two placement classes 'inside the two encoded contexts' / 'outside them'. Base origin/main 82cfaf7; objectstack-ai/hotcrm read at c8a7a7047feb (shallow clone, read only). The card's premise holds: both renderers apply one encoding to every placement an author can write on an html face. In-tree production reach is confirmed zero. hotcrm reach is zero under every variant measured. Four forks need the seat (open_questions): the email half is authorable as metadata through packages/spec; two library exports take a body with no save moment; how strictly a placement is classified decides whether five non-production in-tree bodies would be refused; and the stored-row answer differs by where the refusal lives. Full detail (every placement, location and outcome) is held privately by the seat.",
"readings": {
"R1_supported_set": "Both packages/services/service-messaging/src/template-renderer.ts (the html and mjml faces of renderNotification) and packages/plugins/plugin-email/src/template-engine.ts (renderTemplate's escaped hole) encode a double-brace hole one way: correctly inside the two encoded contexts, and identically everywhere else. The code reading finds 8 placement classes outside the two encoded contexts where that encoding does not suit the context, and 2 further classes where it still holds. The subject and plain-text faces render verbatim in both. Raw forms: service-messaging has none. plugin-email has the triple-brace hole, open to every email-template author at every door: code packages, and environment-level Studio, REST and MCP authors. The only rule on where a raw hole may sit is the built-in pin (R4), and it runs at test time.",
"R2_doors": "Notification templates (data rows): the data API, the Setup grid, import and bulk update, a stack seed into the platform object, and scripts or flows writing records. All are SAVE moments through the engine write. Render reads rows with no validation. Email templates (metadata): the stack declaration, define helper and template files at author/build time; boot registration of code packages, the built-ins included (REGISTER; whether each item is schema-parsed there is NOT MEASURED); the metadata save door for Studio, REST and MCP (SAVE: schema parse plus the runtime authoring gate on active saves, drafts not gated); the package publish route; and the v18 upgrade ceremony, which saves legacy rows through that same door. The mail service's render and send calls take a template NAME, never a body. Doors with NO save moment: the renderTemplate export of plugin-email and the renderNotification export of service-messaging, which both take a body inline (zero in-tree non-test callers, zero in hotcrm), and one loader seam on the mail service instance.",
"R3_metadata_authorability": "YES for the email half: a stack key, a metadata type with runtime create and environment overlay, and a define helper, all in packages/spec. The body field carries no placement constraint. Schema lines are held privately. A schema-level refusal would therefore be spec-lane work (Metadata-protocol). The notification half is a data object, not metadata.",
"R4_builtin_pin": "Lives in the plugin-email package's outlet pin test and runs at TEST time only; there is no boot or save counterpart. For each built-in it asserts (a) every raw hole is a declared URL-typed variable at the one placement the pin admits, and (b) markup-bearing text renders escaped through the real send path. It does not judge where a double-brace hole sits. service-messaging carries the sibling census and render pins, with no placement judge.",
"R5_reach_this_repo": "Searched for every html template body outside test files, plus every html or mjml notification row (zero; the same search has positive controls in the test files). Built-in auth templates: 32 bodies, 216 holes; 188 escaped inside, 0 escaped outside, 28 raw, all 28 within the pin's shape. Example app: 1 body, 2 holes, both inside. Docs examples: 3 bodies, 6 holes; 4 inside, 0 escaped outside, 2 raw outside the pin's shape. Test fixtures, one of them used by the dogfood suite: 2 bodies; 1 hole inside, 1 at a placement whose class is itself a fork (F3). Spec conversion fixtures: 6 bodies; 2 raw holes outside the pin's shape. Studio create seed: 1 body, no holes. Totals: 45 bodies, 228 holes; 196 escaped inside, 1 escaped at the fork placement, 32 raw (28 within the pin's shape). The card's 'in-tree reach is zero' is CONFIRMED for production templates. Under a strict reading, five non-production bodies (docs and fixtures) would be refused, and the build would edit them.",
"R6_reach_hotcrm": "Three packages declare email templates as locale packs: 76 bodies, 116 holes, all 116 inside the two encoded contexts, 0 outside, 0 raw. An independent count agrees. No notification-template rows, seeds or html/mjml formats. No import of either library export. Templates the restriction would refuse: 0, under every variant measured.",
"R7_cloud": "objectstack-ai/cloud is UNMEASURED: not visible to this seat's account. Production rows outside these repositories are unmeasured: environment email-template items, organizations' notification rows, and legacy email-template rows awaiting the v18 promotion.",
"R8_stored_rows": "Notification rows are never validated at render or at boot. At next save, an invariant over the merged record (the object's existing channel rule is that shape) would freeze a refused row on any edit while it keeps rendering. A payload-only judge would let edits that leave the body alone pass. No ADR-0087 conversion reaches platform-object data rows. Email items: a schema-level rule would also fail every send of a stored refused item (the loader re-parses at render, and the channel grades that failure retryable) and badge it in Studio. An authoring-rule or plugin-gate refusal leaves stored items serving and refuses their next publish. Under any door-enforced variant, a refused legacy row becomes a v18 ceremony blocker that needs a hand edit. No mechanical rewrite of a placement exists. Measured affected population: 0. Production: unmeasured.",
"R9_build_shape": "Notification half: a judge beside the service-messaging renderer's hole grammar, plus a save hook on the notification template object. Existing vocabulary: 400 VALIDATION_FAILED with fields, a hook-thrown validation-shaped error (a sibling plugin precedent exists), and failed rows on import and bulk update. Email half, by route: a schema refine in packages/spec (422 INVALID_METADATA at the door, parse throw at authoring, render-time throw); an entry in the shared authoring-rule table that already maps email templates for the CLI and the runtime publish gate (error-tier CLI finding, 422 INVALID_METADATA); or a plugin-registered authoring gate (runtime door only). The two renderers' hole grammars differ, and their packages' dependency edges allow one shared judge for the lint and plugin-email routes but not for service-messaging. Pins: the two packages' outlet pin tests (the built-in pin moves onto the shared judge) and door tests. Changeset per triage. No new error code is needed at any door."
},
"tests": "None run: read-only dispatch, no dependency closure. Every encoding fact comes from reading the code. No renderer was run on any value, and no probe was made. Placement counts come from a scanner that reads template strings only. It was self-tested on control strings and cross-checked by an independent count for hotcrm. hotcrm was read statically and nothing from it was executed.",
"gates": "None: no diff, no derived gate family, no PR, no check-runs.",
"dogfood_battery": "n/a: no PR (read-only dispatch).",
"mcp_calls": "0",
"api_writes": "1: this comment, through the fleet write path (issue comment on #25082).",
"deviations": "This public comment is the abstract cut that ruling #9387 requires. The seat's private hand-back carries the same fields in full, so the two differ by design. No worktree was created: the reads went through origin/main and the read-only shared checkout. No branch was pushed, because nothing was edited.",
"open_questions": [
{ "question": "F1: the email half is metadata-authorable. Where does its refusal live?", "options": ["A1 a schema refine in packages/spec (spec lane; also fails stored items at send)", "A2 an error-tier entry in the shared authoring-rule table (CLI plus runtime publish gate; stored items keep serving)", "A3 a plugin-registered authoring gate (runtime door only; code authors never refused)"], "recommendation": "Reading-level lean A2, on the long-term and startup-stage axes. Axis 3 (preventing AI authoring errors) favours A1, so the seat weighs that conflict. Not a decision." },
{ "question": "F2: two library exports take a body with no save moment. What happens to them?", "options": ["B1 keep them as developer APIs and document that the caller owns placement", "B2 run the judge at render inside them", "B3 retire both exports (zero measured consumers; runtime-export-retired, BREAKING)"], "recommendation": "Lean B3 (shipped capabilities with zero consumers are not exempted for sunk cost). It widens scope, so the seat may want it on its own card." },
{ "question": "F3: how strictly is a placement classified, and are raw holes in scope?", "options": ["C1 syntactic only, raw holes free (0 in-tree refusals; leaves classes open)", "C2 context-strict, plus the built-in pin's raw-hole shape for everyone (5 non-production in-tree bodies refused, edited by the build; hotcrm 0; built-ins 0)", "C3 a middle reading"], "recommendation": "C2: it meets 3C 'by context' as triage reads it, and the five bodies are docs and fixtures." },
{ "question": "F4: what happens to a stored row or item the restriction would refuse?", "options": ["frozen on edit, still delivered", "refused at next publish, still delivered", "fails at send (schema route only)"], "recommendation": "Measured population is 0 and production is unmeasured. The seat puts this to the maintainer only if a production reading is obtained or the route chosen fails stored items at send." }
],
"out_of_scope_findings": [
"carrier: none (承接者:无). Noted, not filed: one body format the notification template object accepts is never interpreted as declared. reach: not measured through a public door (read-only dispatch). Dedupe words: notification template format, body format interpretation, uncompiled format."
]
}
Generated by Claude Code
2 remaining items
objectstack-fleet commentedon Oct 22, 2026
Ruling: batch #384 item 2 · letter F1 A2 / F2 B3 · maintainer 「同意」 2026-10-22T07:33Z
Director seat, summon #47 continuation 2,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). Presented in this session's chat as batch #384 item 2, from thedomain:servicesseat 2 decision6334342274, the read-only measurement6334219164and triage's answer6332255183; the maintainer answered 「同意」. ⛔ Disclosure (#9387,5323310017): abstract only; no placement sample, value or template goes here. The director's delta re-checked onorigin/mainb6e87070:emailTemplatesis a stack key (packages/spec/src/stack.zod.ts:778);bodyHtmlisz.string()with no placement constraint (packages/spec/src/system/email-template.zod.ts:195); the lint runtime gate already mapsemail_template(packages/lint/src/runtime-gate.ts:189) and the authoring-rule table carries 28CLI_AND_RUNTIMErules, so A2 is one more row in an existing table; the two body-taking render exports exist (plugin-email/src/index.ts:88,service-messaging/src/index.ts:92-93). Thread read to its end at this write:6334342274is the newest comment.Governing text:the maintainer's ruling 3C on #25040 (6308462370: every outlet that renders a text field into HTML escapes by context); triage's reading on this card (6332255183: 3C reaches every placement an author can write; the remedy is a placement restriction at save or registration, not a per-context encoder and not guidance alone); the #25105 ruling A on changesets (a non-protocol narrowing shipspatchwith its BREAKING banner and ADR-0087 disposition); ADR-0087 (runtime-export-retired, for B3); the standing line 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 (no staged window for B3).What the ruling binds
F1 — letter A2: the email half is an error-tier authoring rule.
packages/lint/src/authoring-rules.tswithsurfaces: CLI_AND_RUNTIME, runtime typeemail_template:os validate/os build/os lintand the runtime publish gate (ACTIVE saves) refuse a hole the renderer cannot encode for its placement, with the existing422 INVALID_METADATA, naming the placement and the supported ones. Stored items keep serving and are judged at their next publish; drafts are not judged. This half lives indomain:spec(lint, by the lane table's anchoring exception);domain:servicesseat 2 files that sub-card, and this card closes only when both halves have landed.domain:servicesseat 2: a judge besideservice-messaging's hole grammar plus a save hook on the object (beforeInsert/beforeUpdate, the existingVALIDATION_FAILED/ 400; failed rows on import and bulk update). No new error code at any door. Where the dependency edges allow one shared judge (lint andplugin-email) it is shared;service-messagingkeeps its own; the sub-card's first measurement decides the placement of the shared code.href). The build corrects the five non-production bodies this refuses (two docs examples, one dogfood fixture, two spec conversion fixtures); hotcrm 0, built-ins 0.channel_renders_templaterule is the precedent); a refused row freezes on its next edit and keeps rendering and delivering. Email items keep serving until their next publish (A2's own property). Measured affected population 0; no migration. A production reading, if one is obtained, comes back as its own card.minorplus a conversion entry); A3 (a runtime-only gate that code-package authors never pass through, while every measured template comes from code).F2 — letter B3: the body-taking render exports are retired.
6.
domain:servicesseat 2 files one card retiring the two exports that take a template body with no save moment (renderTemplateinplugin-email,renderNotificationinservice-messaging) under ADR-0087runtime-export-retired, BREAKING, at once — no staged window, under the standing line above.setTemplateLoaderstays (the plugin's own wiring point). Whetherinterpolategoes with them is that card's first measurement (consumers: zero in-tree outside tests, zero in hotcrm, npm dependants unmeasured). It does not block this card.7. Not taken: B1 (a documented door the restriction cannot see); B2 (a render-time judge that fails late, at send).
Changeset and review (both halves):
patchwith the BREAKING banner and ADR-0087 disposition (#25105 A);Metadata-protocol: noneunder A2; aCONTRACT_REVIEW_TIERreview before the queue (a narrowing of what an administrator can author).Effort of the chosen letters (the card's column): notification half
domain:services· 1 PR; email halfdomain:spec(lint) · 1 PR ·patch; B3domain:services· 1 PR; no named precedent. Confidence gap carried:objectstack-ai/cloudand production rows are unmeasured; under A2 a stored violating item keeps serving until its next publish.State:
needs-user-decision→pm:queue(domain:services, p2,security,area:api).domain:servicesseat 2 (seat post #21118) re-claims for the notification half (the reserved branchclaude/issue-25082-template-hole-placementhas no commits), files the spec-lane sub-card and the B3 retirement card.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Pointer ·
domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T08:17Z · executing ruling6335348115· ⛔ Not a claim.domain:spec, p2,security;renderTemplateandrenderNotification(interpolateis that card's first measurement),domain:services, p3. It does not block this card.service-messaging's judge plus the save hook on the notification template object), by the maintainer's word to this seat, verbatim: 「当前任务处理完,合并后就下班」. The card stayspm:queuefor the seat that holds the lane next. The reserved branchclaude/issue-25082-template-hole-placementhas no commits.6334219164is the abstract measurement. The private detail table lived in this seat's container and is not carried forward, so the next claimant re-derives the exact allow-list from the renderers' encoders.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: card thread ·
domain:servicesseat 1 (seat post #6021, project run by the Sales account) · p2 ·security· the notification half of this card (ruling6335348115, item 2) · 2026-10-22T16:16ZSession:
session_01ApJv4hDHQ6M1JprNZFKzAWAccount:
os-sales(the seat's linked user; the card's assignee)Branch:
claude/project-thread-iydt23(the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)Worktree: the session's own container checkout (one task per container)
Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (read on
origin/maine58bd0e3e; stop on a breach and explain it in the report):packages/services/service-messaging/src/— a placement judge beside the renderer's hole grammar intemplate-renderer.ts, the save hooks onsys_notification_template(beforeInsert/beforeUpdate, judged over the merged record), their binding inmessaging-service-plugin.ts, the object's docblock inobjects/notification-template.object.ts, and their tests; onepatchchangeset for@objectstack/service-messaging. ⛔ Notplugin-email,packages/lint,packages/formulaorpackages/spec(the email half is #25189).Container & model:
M, card thread (in-session executor),model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate"),effort: xhigh(security); aCONTRACT_REVIEW_TIERreview of record before the queue (a narrowing of what an administrator can author)Clause-②: no (narrowing)
Metadata-protocol: none
Basis for the Clause-② line: a save-time refusal narrows what an administrator can store in a
sys_notification_templaterow (data, not metadata); no accept set or public surface widens. The ruling's changeset line:patchwith the BREAKING banner and its ADR-0087 disposition (#25105 A).Responsibility: withheld under the disclosure ruling (#9387,
5323310017); the ruling6335348115names the remedy side asservice-messaging's own judge plus a save hook on the notification template object.Ruling-ref: 6335348115
Thread-read: 6336133895
Serial constraints cleared: none of the 10 open PRs touches
packages/services/service-messaging/**(each PR's file list read this act). The email half (#25189, PR #25212, draft, not onorigin/main) carries its own judge inpackages/formula; this half keeps its own per ruling item 2 and uses the same placement vocabulary. #25190 (B3, retiringrenderNotification) follows this card and is not blocked by it.Resume-from: none (the newest pointer
6336133895names no commits on the reserved branch).Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{
"issue": 25082,
"status": "done",
"branch": "claude/project-thread-iydt23",
"pr": "#25258",
"session": "session_01ApJv4hDHQ6M1JprNZFKzAW",
"premise_still_valid": true,
"summary": "The notification half of ruling 6335348115 (items 2 to 4). service-messaging gets its own placement judge (template-hole-placement.ts), reading holes through the renderer's own token and a placement table re-derived from the renderer's encoder: element text and quoted plain-text attribute values are supported, every other placement is refused, context-strict over every reading a parser can give the body (fork at noscript and at text-element start tags once a select or frameset is open; foreign content never left). beforeInsert / beforeUpdate hooks on sys_notification_template (priority 30, bound in MessagingServicePlugin.init, unbound in destroy) refuse an html or mjml body with a refused hole, judged over the merged record, with the existing 400 VALIDATION_FAILED at body / invalid_value; a stored violating row is frozen on its next edit and keeps rendering and delivering, no migration, no new error code. Changeset: patch with the BREAKING banner and ADR-0087 not-required (no-migration-prescription).",
"tests": "pnpm --filter @objectstack/service-messaging test: Test Files 58 passed (58), Tests 722 passed (722). New: src/template-hole-placement.test.ts 74/74 (every class, the readings, grammar and span parity with renderNotification); src/objects/notification-template-placement.integration.test.ts 20/20 on real ObjectQL + SqlDriver through ObjectStackProtocolImplementation, mapDataError and runImport (REST door 400, import failed row, bulk update failed record, predicate update writes no row, stored row frozen then repaired, still read/deleted/rendered, plugin binds on a booted kernel and unbinds on destroy). pnpm --filter @objectstack/service-messaging typecheck: exit 0. ESLint over the 7 changed source files: exit 0. Ablation: none run (no previously tested behaviour changed; the new pins are positive/negative twins on the same path, e.g. the plugin test inserts the same row refused before destroy and stored after it).",
"gates": "local subset at head 4ca18be: dispatch-gates --commands derived 70; all 70 recorded with exit codes captured before any pipe; --ran: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). check:doc-authoring was red on its first run (the plugin's missing-engine warning string carried a tracker id), fixed in 4ca18be and re-run exit 0. check:dual-build-cjs-loads and check:i18n answered exit 3 (prerequisite: no dist) until the dogfood closure build, then exit 0. CI on 5d129db: CI workflow success (Build Core, Test Core 1-6/6); Lint & Type Check failure on two lanes: Doc/skill authoring guard (the same tracker-id string, fixed in 4ca18be) and Issue citations (PREREQUISITE NOT MET: probeBoard could not read the allocation frontier; locally the same gate read the board and resolved all 5 citations). CI on 4ca18be, every check-run completed: Lint & Repo Gates success (all lanes, Doc/skill authoring guard and Issue citations included), TypeScript Type Check success (workspace, source gates, consumer gates, debt ledger), Lint · ESLint success, Build Core success, Test Core success (1-6/6), Check Changeset success, Check PR Size success, Governed Surface Queue Guard success, Dogfood Regression Gate success (the sharded dogfood jobs skip on PRs), the claim / single-writer / closing-keyword guards success; opt-in harnesses skipped.",
"dogfood_battery": "4ca18be6 · os-verify-lock: VERDICT command-exit 0 (held 17m07s) · Test Files 288 passed | 3 skipped (291) · Tests 2411 passed | 18 skipped (2429) · no failures. Closure built first under the lock (turbo build --filter='@objectstack/dogfood^...', VERDICT command-exit 0). An earlier attempt before the closure build failed to resolve package entries and is not a reading.",
"mcp_calls": "9 — reads only: issue_read x3, pull_request_read x2 (get_check_runs), get_job_logs x3, actions_list x1. No MCP write tool.",
"api_writes": "5 — all through scripts/pm: label-write.mjs on #25082 (add pm:dispatched, remove pm:queue, assign os-sales: the dispatch atom); post-stamped.mjs claim comment on #25082; fleet-write pr_create (POST /pulls draft + POST /issues/25258/assignees os-sales); fleet-write pr_request_reviewers (POST /pulls/25258/requested_reviewers os-sales); post-stamped.mjs this report comment on #25082. git push is not counted.",
"deviations": [
"pr_request_reviewers is a write beyond the three-write budget: the project's session instructions ask for os-sales as reviewer and assignee; both went through the relay (assignees on pr_create, reviewers on pr_request_reviewers), not MCP.",
"The branch is the harness-pinned claude/project-thread-iydt23, not claude/issue-25082-; the claim comment and the PR body say so.",
"A first dogfood attempt was started on 5d129db and stopped when the head moved; a second attempt on 4ca18be ran before the closure build and is not a reading. The battery above is the one valid run on the final head."
],
"open_questions": [],
"out_of_scope_findings": [
"class: review note on an unmerged PR (not a filed class) · reach: none on main; PR #25212 is a draft · evidence: the email half's judge in PR #25212 models fewer parser readings than this half in two classes (where foreign content ends, and text-element start tags the tree builder may ignore), so its accepted set can be wider than a parser's reading; class names only, under the disclosure ruling · carrier: #25189 (spec lane, PR #25212) · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 1 (seat post #6021) ·os-sales·session_016g7nYP9wdNx1GDa5XDr7Uy· 2026-10-22T18:23Z6347615221, contract review6347584087)origin/main284e04810aState: the card closed
completedthrough the PR; this stroke clearspm:dispatchedand the assignee. The draft email half's parity note stands on the ACCEPT (owed by whichever half lands second).Generated by Claude Code