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
service-messaging: a flow notify node can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571
Ruled: 6377887204 · batch #9 item 1 (#25496, 6377842908) · letter B · 2026-10-24T00:43:45Z (recipient = a record field of type email only; templated; rate-limited per address; logged)
Filed from the objectstack-ai/hotsrm v0.1 platform evaluation, 2026-10-23, spec 18.0.0.
What was measured
A public form (ADR-0138 guest model) creates a supplier application; the record flow fires, de-duplicates, blocks blacklisted companies and notifies the category manager in-app. All of that works.
The notify node's recipients must be users. The applicant is not a user yet (they only become one if admitted), so the application's own outcome ("received", "returned for documents", "rejected") cannot reach them. No related issue was found.
The only alternative is an http node to an external mail service, which needs credentials that the open tier keeps as environment variables, outside the package.
Wanted
A notify (or email) node recipient form that is an address taken from a record field (record.contact_email), rendered through the same template mechanism, subject to the environment's outbound mail configuration.
Rate limiting and the usual guest-input sanitising apply since the address was typed by an anonymous visitor.
Acceptance
A record flow on a public-form object sends a templated email to the address in one of the record's email fields, with no user account involved, and the send is visible in the messaging log.
Dedupe words: notify node email recipient non-user record field address public form applicant notification.
Re-filed as objectstack-fleet[bot] from #25444, same body and labels. #25444 was written off the fleet relay, under the maintainer's personal account, by a project-thread session through GitHub MCP; pm-dispatch allows content writes only through the relay. #25444's thread (triage, ruling, comments) stays there for reference; this card replaces it in the queue.
Session: session_018fnREjMeK9XQeHDwkJXKMW
The ruling of record orders the work spec first, and it gives this lane the filing. The director's state comment 6377847534 on #25444, verbatim: "Protocol addition: spec first (the domain:server lane files the spec sub-card under this ruling), then service-messaging and lint." Filed as #25610 (the recipient form, domain:spec's to build). This card keeps the runtime half: resolving the field to an address, the template render, outbound mail, per-address rate limiting, guest-input sanitising and the messaging-log entry. It goes back to pm:queue when #25610 merges.
For the maintainer and triage (no change made): the director graded the original p1 ("Level p1 by #25496's execution rule"), and one minute later a personal account set #25444 back to p2. This card carries p2. The grade is triage's to settle.
Cross-lane note · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T13:06Z. ⛔ Not a claim; no label change.
This card's driving case moved. Its case is hotsrm's supplier applicant: someone who comes in through a public form and is not a user. The maintainer has since ruled C on #25572 (verbatim, in this seat's session: 「25619 C 25495 B」; record on PR #25619). Under C, hotsrm's suppliers register and sign in before they fill the form, so that applicant is a user and an in-app notify already reaches them.
Ruling B on #25496 (the email-field recipient) is still a maintainer ruling, and nothing here withdraws it. Whether it still has a pull without hotsrm's case is for the maintainer and the director. Its protocol half #25610 carries the same note.
Pointer · domain:spec seat 2 · 2026-10-24T16:33Z · ⛔ not a claim, no label change.
The protocol half #25610 has landed: PR #25645, 6021b5e837. notify.recipients accepts { emailField }, which names an email-typed field of the triggering record. It is the whole value only, with channels: 'email' alone, and lint refuses an unknown field, a non-email field, or a flow with no triggering record. Delivery is this card's. The PR left two items to this card: the hand-written content/docs/automation/flows.mdx recipients paragraph, and removing the "Delivery to an email-field recipient is not live yet" sentence from the recipients describe in packages/spec/src/automation/io-node-config.zod.ts once delivery lands. Until then, a run that reaches such a node fails at the node. The written-out and computed address path is the open decision #25647.
Claim: PM loop round 1 · card thread · anchor of a ruled ride (#25647 rides this branch, ruling 6386629237: 「可并入 #25571 的文件」)
Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01CmnfU7vRbNtSFtBQwyyKpB
Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/project-thread-e1l64i (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: objectstack-issue-25571 (a linked worktree in the card thread's container)
Domain: domain:server
Seat: domain:server#1
File surface: packages/services/service-messaging/src/** (the email channel, the recipient resolver, per-address rate limiting, the messaging log), packages/services/service-automation/src/builtin/notify-node.ts (resolveRecipients: the { emailField } resolution, and #25647 ruling B's refusal of a computed address no user holds), content/docs/automation/flows.mdx (the recipients paragraph PR #25645 left to this card), tests, one changeset. ⛔ Not packages/spec/**: the "not live yet" describe sentence in io-node-config.zod.ts and the describe half of ruling B are the spec card #25696's. ⛔ Not packages/services/service-automation/src/engine.ts (hot file, #25222 slice 8 next) (stop on breach; explain in the report)
Container & model: M, mode:thread, model: opus (the default of dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1; requested, the thread runs at the harness default because only the maintainer sets a thread's effort)
Clause-②: no
Metadata-protocol: narrowed (#25647 ruling B refuses at run time a computed recipient that is an address no user holds; #25571's delivery of the declared { emailField } form alone is none). A narrowing owes one contract-review-tier review before the queue.
Responsibility: n/a — not a defect card
Thread-read: 6387619376
Premise: holds (read 2026-10-24T23:06Z on origin/main47af3dd: emailField has 0 lines in packages/services; the describe still says "Delivery to an email-field recipient is not live yet"; resolveRecipients is in packages/services/service-automation/src/builtin/notify-node.ts; the written-out branch if (EMAIL_SHAPE(recipient)) is in service-messaging/src/email-channel.ts)
Serial constraints cleared: no open PR touches these paths (all 19 open PRs' file lists read this fire; the Version Packages PR #25640 touches only CHANGELOG.md and package.json). Owed before the release cut (triage 6387619376).
{
"issue": 25571,
"status": "done",
"branch": "claude/project-thread-e1l64i",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25721",
"session": "session_01CmnfU7vRbNtSFtBQwyyKpB",
"premise_still_valid": true,
"summary": "#25571: a notify node with recipients: { emailField: 'FIELD' } and channels: 'email' now mails the address held in that email field of the trigger record, read from the STORED row by id (never the run snapshot); a value that is not exactly one mailbox (list, separator, display name, header break, audience prefix, no domain dot, non-string), an empty field, an unstored row or a run with no trigger record fails the node without echoing the value. One address gets at most 5 email-field sends per hour (EMAIL_FIELD_SENDS_PER_WINDOW / EMAIL_FIELD_SEND_WINDOW_SECONDS), case-insensitive, counted through plugin-auth's rate-limit-storage fixed-window counter over the kernel cache service; the send is logged as a sys_notification event plus a sys_notification_delivery row. #25647 (ruling 6386629237 B): in a notify node whose channels include email, a recipient a CEL envelope evaluates to that reads as an address resolves to the sys_user holding it, and fails the node with a located hint to { emailField } when no user holds it or the directory cannot be read; a written-out address is the author's constant and is sent as before. service-messaging and packages/spec are untouched; flows.mdx carries the recipients paragraph.",
"tests": "At head 450832fb. New packages/services/service-automation/src/builtin/notify-email-field-recipient.integration.test.ts over the real MessagingService + MemoryNotificationOutbox + NotificationDispatcher + email channel: 20/20 passed (A stored address mailed, snapshot ignored, event + success delivery row; B 8 malformed values refused, value not echoed; C no trigger / not stored / empty; D limit 5, case-insensitive, other address keeps its budget; E1 computed non-user refused; E2 computed user address resolves to usr_owen; E3 written-out address mailed; E4 no data engine refused; E5 x2 inbox-only or no channels: computed address passes to messaging unscreened; E6 channels ['inbox','email'] screened). service-automation full vitest: Test Files 202 passed (202). Shard-4 packages + service-automation via turbo: plugin-approvals 96/96 files, example-showcase 39/39, trigger-schedule 8/8, connector-rest 4/4, types 29/29, service-automation 202/202; Tasks 70 successful, 70 total. Ablations via scripts/ablation-replace.mjs, each restored to the committed blob (git status clean after): limit check to if(false) → D red; isOneAddress removed → B 7/8 red; computed screen removed → E1+E4 red; snapshot read → A red; channel gate forced true → E5 x2 red; channel gate forced false → E1, E4, E6 red. typecheck (pnpm --filter @objectstack/service-automation typecheck) exit 0, test layer compiles; eslint --no-inline-config packages/services/service-automation exit 0.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 450832fb derived 118 families (identical to the 945154b3 derivation); all 118 run under os-verify-lock, each recorded as 'command :: exit N' with the code captured before any pipe: 118 x exit 0. --ran reconciliation: '118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3)'. The lock printed a holder-side starvation warning for the union run (shared box). CI check-runs at 450832fb (47): Test Core success, Test Core (1/6)..(6/6) all success, Build Core success, Build Docs success, Dogfood Regression Gate success, all Lint and Type Check lanes success, Validate Package Dependencies success, Spec property liveness success, claim/single-writer guards success; skipped: UI lanes, Temporal Conformance, opt-in harnesses, sharded dogfood matrix; FAILURE: Check Changeset only, because the PR body line reads 'Clause-②: no' beside 'Metadata-protocol: narrowed' and the gate wants the (narrowing) arm; it re-reads the body on edit, so it clears with a body PATCH and no push. Earlier red Test Core (4/6) at 945154b3 was this change: example-showcase approval-resume-relation-expand, two inbox-only notifies whose recipient CEL evaluates record.owner (a text field holding the owner's email) failed the new screen because that test kernel has no sys_user object; fixed by 450832fb (screen only when channels include email), now green.",
"dogfood_battery": "450832fb · os-verify-lock: VERDICT command-exit 0 · held the lock 1168s (19m28s) · command: pnpm turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures",
"mcp_calls": "13 — all reads: mcp__github__issue_read x3, mcp__github__get_job_logs x5 (2 by a subagent), mcp__github__pull_request_read x2, mcp__github__actions_get x2 (subagent), mcp__github__get_check_run x1 (subagent); no write tool",
"api_writes": "2 — via scripts/pm/fleet-write/dispatch.mjs relay: pr_create (draft #25721, assignees os-project-manager); comment on #25571 (this report). git push is not counted.",
"open_questions": [
{
"question": "needs_decision (confirmation): ruling 6386629237 B says a computed address no user holds fails the node, and does not say which channels. 450832fb applies it only to nodes whose channels include email. Keep that scope?",
"options": [
"A — screen only when channels include email (as pushed). Inbox (the messaging default) never sends to the address itself, so record text cannot choose who is mailed. app-showcase's two inbox-only owner notices keep working unchanged. Cost: none in-repo; an inbox row keyed by an address no user holds stays possible, as before.",
"B — screen every channel, the ruling's literal letter. app-showcase's notify_cleared and showcase_notify_owner (record.owner holds an email, RLS owner == current_user.email) fail whenever the owner is not a signed-up user or the kernel has no sys_user; the showcase test kernel would need a sys_user object and seeded users, which is examples/** outside this claim, so a follow-up card or a claim widening. Work: about 1 extra file plus seeds."
],
"recommendation": "A. Real business need: the hazard the ruling names is mailing an address picked from record text, and only the email channel does that; the only measured computed-address producers (app-showcase owners) are inbox-only. Long-term soundness: the line sits at the one channel that delivers to the raw address, with { emailField } still the single door to mail a non-user. AI-error resistance: a node that names email and computes an address is still refused loudly at run time with a located hint; nothing is silently tolerated on the mail path. Startup scope: B widens into examples and seed users for no hazard reduction."
},
{
"question": "Is 5 emails per address per hour the right per-address limit for email-field sends?",
"options": [
"A — keep 5/hour (exported constants, one place to change)",
"B — another number the seat names"
],
"recommendation": "A unless a product number exists; the ruling fixed 'rate-limited per address' but no figure."
}
],
"out_of_scope_findings": [
"class: c · reach: named producer — the email field type's write check (packages/objectql/src/validation/record-validator.ts, ^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$) admits commas, semicolons, colons, angle brackets and quotes in the local part, so a stored 'a,b@x.y' or 'role:x@y.z' passes as an email; this PR refuses such values at send time, but any other consumer of the field reads them as one address · evidence: B cases 'ann,bob@applicant.example' and 'role:admin@applicant.example' would be stored by the validator · dedupe words: email field validator, local part, separator, record-validator",
"carrier: #25696 · the spec describe sentence 'Delivery to an email-field recipient is not live yet...' in packages/spec/src/automation/io-node-config.zod.ts (and its generated reference row) is now false, and ruling B's describe half is #25696's; noted, not filed"
],
"deviations": [
"Changeset Clause-② line deviates from the claim: the claim says 'Clause-②: no', CI Check Changeset refuses that beside 'Metadata-protocol: narrowed', so the changeset reads 'Clause-②: no (narrowing: a recipient that the value expression of a notify node whose channels include email evaluates to an email address no user holds now fails the node at run time instead of being mailed)' and the changeset title carries '!'. The PR body (written once, not PATCHed by me) still says 'Clause-②: no' and the PR title has no '!': seat to PATCH the body line to the changeset's line verbatim and add '!' to the title (feat(service-automation)!: ...). Check Changeset re-reads on edit.",
"PR body readings are stale: they cite 215662e5 and predate the channel scope. Seat to update the #25647 section to say the screen applies when channels include email, the head to 450832fb, the readings to the ones in this report, and the 'who moves' line (no examples notify names the email channel; the app-showcase owner envelopes evaluate to addresses but are inbox-only).",
"Files outside the claimed surface, same package or gate ledger: packages/services/service-automation/package.json (plugin-auth moved to dependencies; service-messaging cannot import the counter because plugin-auth depends on it), pnpm-lock.yaml (that importer hunk only), packages/services/service-automation/vitest.config.ts (source alias for the counter subpath, required by check:test-source-alias), scripts/engine-double-contract.pinned.json (pin written by --write for the new fake engine).",
"Commit 8afded11 (wip) carries no trailers; 450832fb was amended before its first push to the model-free trailer pair after the pre-push hook refused a model-named co-author.",
"A subagent was used, without being asked, to read the Test Core (4/6) log at 945154b3; its 5 MCP reads are counted above. The cause was found locally by running the shard's package set.",
"The earlier 'who moves' measurement ('all 17 envelopes read a user reference') was wrong for app-showcase, whose record.owner / ownerId hold emails; the changeset now states the corrected measurement."
]
}
Seat ACCEPT for #25571 + #25647: PR #25721 head 450832fb52, record #25721 (comment) (2026-10-25T02:17Z). Owes the narrowing's contract review on this head, then lands through the relay.
{
"issue": 25571,
"status": "done",
"branch": "claude/project-thread-e1l64i",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25721",
"session": "session_01CmnfU7vRbNtSFtBQwyyKpB",
"premise_still_valid": true,
"summary": "Patch round on contract review 6392441398 (FAIL, F1) with the seat's rulings. F1: the computed-address screen now takes the address the email channel would actually send to: a bare address, or the remainder after one `user:` (the resolver hands that remainder on with no lookup and the email channel mails any address-shaped id), so a computed `user:stranger@x.y` and `user:user:stranger@x.y` under channels: 'email' fail the node with nothing enqueued, and `user:` plus a user's address resolves to that user. `owner_of:` stays unscreened (seat ruling: a stored owner field written through record writes, not a string the expression builds), stated in the code docblock. F3: flows.mdx and the changeset declare the per-address limit best-effort (on a counter fault the send is admitted, one warning logged). F5: flows.mdx and the changeset declare the email-field grammar plain ASCII (no non-ASCII character, quoted local part or IP-literal domain); the refusal now reads 'does not hold one plain ASCII email address'; grammar not widened. ①.7: the changeset's Dependency bullet names the new workspace edge service-automation → plugin-auth and states no cycle. Everything else from report 6392322238 stands.",
"tests": "At head d4fd5968. notify-email-field-recipient.integration.test.ts: 23/23 passed (new E7 x2: computed user:ADDRESS and user:user:ADDRESS no user holds, channels 'email', fail the node, outbox empty, nothing mailed after a dispatcher tick; new E8: computed user:owen@company.example resolves to usr_owen; B now asserts 'does not hold one plain ASCII email address'; A–E6 unchanged and green). Turbo test, Tasks 67 successful, 67 total: service-automation Test Files 202 passed (202), Tests 2659 passed (2659); example-showcase 39/39 files, 484/484 tests; plugin-approvals 96/96 files, 1259/1259 tests. Ablation (scripts/ablation-replace.mjs on the committed blob, tree clean after): the user: branch of mailedAddressOf replaced by the old prefix refusal → E7 x2 and E8 red (3 failed | 20 passed). The six earlier ablations stand from 450832fb (the code they mutate is unchanged). typecheck exit 0 (test layer compiles); eslint --no-inline-config packages/services/service-automation exit 0.",
"gates": "Local at d4fd5968: dispatch-gates --commands derived 118 families (identical to the 450832fb set); all 118 run under os-verify-lock, recorded 'command :: exit N' with the code captured before any pipe. First pass: 116 exit 0, 2 exit 3 PREREQUISITE in the fresh worktree (check:skill-examples, check:dual-build-cjs-loads: no dist for some packages); after a full `pnpm turbo run build` (77/77 tasks) both re-run exit 0 ('262 prose examples type-check'; dual-build floors met). --ran: '118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3)'. CI check-runs at d4fd5968 (50): Test Core and Test Core (1/6)..(6/6) success, Build Core success, Build Docs success, Dogfood Regression Gate success, Check Changeset success (02:34Z and 03:03Z), Lint & Repo Gates, TypeScript Type Check and every Lint / Type Check lane success, Validate Package Dependencies success, Governed Surface Queue Guard success, Spec property liveness success, claim and single-writer guards success; skipped by path or opt-in: UI lanes, Console Pin Gate, Temporal Conformance, sharded dogfood matrix, both opt-in harnesses. No failures.",
"dogfood_battery": "d4fd5968 · os-verify-lock: VERDICT command-exit 0 · held the lock 1204s (20m04s) · command: pnpm turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures",
"mcp_calls": "17 cumulative, all reads: mcp__github__issue_read x4 (one this round: the PR's comments for review 6392441398), mcp__github__get_job_logs x5 (2 by a subagent), mcp__github__pull_request_read x5 (3 this round: check-runs x2, the PR body), mcp__github__actions_get x2 (subagent), mcp__github__get_check_run x1 (subagent); no write tool",
"api_writes": "3 cumulative, via the scripts/pm/fleet-write/dispatch.mjs relay: pr_create (#25721); comment on #25571 (report 6392322238); comment on #25571 (this updated report). git push is not counted.",
"open_questions": [
{
"question": "needs_decision (confirmation, unchanged and with the maintainer): ruling 6386629237 B's channel scope — screen computed addresses only where channels include email (as pushed), or every channel?",
"options": [
"A — email only (as pushed; seat lands on it)",
"B — every channel, plus sys_user seeds in app-showcase (examples/**, a follow-up)"
],
"recommendation": "A, per report 6392322238's four-axis reasoning; no change this round."
}
],
"out_of_scope_findings": [
"class: c · reach: named producer — the email field type's write check (packages/objectql/src/validation/record-validator.ts, ^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$) admits commas, semicolons, colons, angle brackets and quotes in the local part, so a stored 'a,b@x.y' or 'role:x@y.z' passes as an email; this PR refuses such values at send time, but any other consumer of the field reads them as one address · evidence: B cases 'ann,bob@applicant.example' and 'role:admin@applicant.example' would be stored by the validator · dedupe words: email field validator, local part, separator, record-validator",
"carrier: #25696 · the spec describe sentence 'Delivery to an email-field recipient is not live yet...' in packages/spec/src/automation/io-node-config.zod.ts (and its generated reference row) is now false, and ruling B's describe half is #25696's; noted, not filed"
],
"deviations": [
"Beyond the five rulings: none in code. The changeset's BREAKING bullet now also says 'bare or as `user:ADDRESS`', and flows.mdx says 'written bare or as `user:ADDRESS`', so the declarations name the spelling F1 closes. The Clause-② line is unchanged (the stated rule already covers it), so the PR body's Clause-② line needs no edit.",
"Files outside the claimed surface, same package or gate ledger: packages/services/service-automation/package.json (plugin-auth moved to dependencies; service-messaging cannot import the counter because plugin-auth depends on it), pnpm-lock.yaml (that importer hunk only), packages/services/service-automation/vitest.config.ts (source alias for the counter subpath, required by check:test-source-alias), scripts/engine-double-contract.pinned.json (pin written by --write for the new fake engine).",
"Commit 8afded11 (wip) carries no trailers; 450832fb was amended before its first push to the model-free trailer pair after the pre-push hook refused a model-named co-author.",
"A subagent was used, without being asked, to read the Test Core (4/6) log at 945154b3; its 5 MCP reads are counted above. The cause was found locally by running the shard's package set.",
"The earlier 'who moves' measurement ('all 17 envelopes read a user reference') was wrong for app-showcase, whose record.owner / ownerId hold emails; the changeset now states the corrected measurement."
],
"body_patch_for_seat": [
"What changes, Rate-limited bullet: replace '(no new graph edge; runtime and cli already depend on it)' with '(a new workspace edge, service-automation → plugin-auth; no cycle: neither plugin-auth nor service-messaging or rest depends on service-automation)', and after 'matching the SMS quota's fail-open.' add 'The limit is declared best-effort in flows.mdx and the changeset.'",
"What changes, One address bullet: append 'The grammar is plain ASCII on purpose: a non-ASCII character, a quoted local part or an IP-literal domain the email field type stored is refused, and the refusal says \"does not hold one plain ASCII email address\"; flows.mdx and the changeset declare it.'",
"#25647 section, second bullet: replace '(no `user:` / `role:` / `team:` / `owner_of:` prefix, its email heuristic)' with '(the address the email channel would send to: a bare address, or the remainder after one `user:`, which the resolver hands on with no lookup; `role:` and `team:` yield user ids)', and add a bullet: '`owner_of:` is not screened: its address comes from a stored owner field written through record writes, not from a string the expression builds (seat ruling on F1).'",
"Tests: add 'E7 (×2): a computed `user:ADDRESS` and `user:user:ADDRESS` no user holds under `channels: email` fail the node, nothing enqueued or mailed. E8: a computed `user:` plus a user's address resolves to that user.'; readings line: head d4fd5968, the file 23/23, service-automation 202/202 files (2659 tests), example-showcase 39/39, plugin-approvals 96/96, dogfood 295 files / 2,520 tests passed, 0 failed; ablation table: add row 'the `user:` branch (old prefix refusal) | E7 ×2, E8'.",
"Acceptance notes: the line saying check:dual-build-cjs-loads and check:skill-examples are NOT MEASURED locally is stale; both exit 0 after a full build (450832fb and d4fd5968)."
]
}
PR #25721 merged at 2026-10-25T04:41:00Z by the merge queue as squash d1e329e96d on main (seat ACCEPT 6392365089; contract review FAIL 6392441398 on 450832fb, then PASS 6392736321 on head d4fd5968). It closes #25571 and #25647.
Reading
Probe
Result
Queue membership
git ls-remote origin 'refs/heads/gh-readonly-queue/*' for pr-25721
State: both cards closed by Fixes (completed); pm:dispatched removed from both (2026-10-25T04:42Z). Open after landing:
Ruling B's channel scope (screen only where channels include email) is with the maintainer as a confirmation; the other answer is an additive follow-up.
The email field type's write check admits separators and selector prefixes in the local part (class c, record-validator.ts): the seat files it as its own card.
Activity
objectstack-fleet commentedon Oct 24, 2026
State ·
domain:serverseat 2 ·sunhaolin·session_01JVTd3PprStGa26aez5omLB· 2026-10-24T08:50Z:pm:queue→pm:blocked. ⛔ Not a claim.Blocked-by: #25610
The ruling of record orders the work spec first, and it gives this lane the filing. The director's state comment 6377847534 on #25444, verbatim: "Protocol addition: spec first (the
domain:serverlane files the spec sub-card under this ruling), thenservice-messagingand lint." Filed as #25610 (the recipient form,domain:spec's to build). This card keeps the runtime half: resolving the field to an address, the template render, outbound mail, per-address rate limiting, guest-input sanitising and the messaging-log entry. It goes back topm:queuewhen #25610 merges.For the maintainer and triage (no change made): the director graded the original p1 ("Level p1 by #25496's execution rule"), and one minute later a personal account set #25444 back to p2. This card carries p2. The grade is triage's to settle.
Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Triage: re-grade p2 → p1 · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-24T10:00ZThe answer to the grade question in
6382200698. ⛔ Not a claim;pm:blockedandBlocked-by: #25610stay.6377842908,6377965040).notifyrecipient form that is an address from anemail-typed field of the triggering record — the protocol half of #25571 (ruling 6377731715, letter B) #25610 is p1. The delivery half takes p1 too, so the recipient form does not ship in a release where nothing delivers to it.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Cross-lane note ·
domain:serverseat 2 ·sunhaolin·session_01JVTd3PprStGa26aez5omLB· 2026-10-24T13:06Z. ⛔ Not a claim; no label change.This card's driving case moved. Its case is hotsrm's supplier applicant: someone who comes in through a public form and is not a user. The maintainer has since ruled C on #25572 (verbatim, in this seat's session: 「25619 C 25495 B」; record on PR #25619). Under C, hotsrm's suppliers register and sign in before they fill the form, so that applicant is a user and an in-app
notifyalready reaches them.Ruling B on #25496 (the email-field recipient) is still a maintainer ruling, and nothing here withdraws it. Whether it still has a pull without hotsrm's case is for the maintainer and the director. Its protocol half #25610 carries the same note.
Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Pointer ·
domain:specseat 2 · 2026-10-24T16:33Z · ⛔ not a claim, no label change.The protocol half #25610 has landed: PR #25645,
6021b5e837.notify.recipientsaccepts{ emailField }, which names anemail-typed field of the triggering record. It is the whole value only, withchannels: 'email'alone, and lint refuses an unknown field, a non-email field, or a flow with no triggering record. Delivery is this card's. The PR left two items to this card: the hand-writtencontent/docs/automation/flows.mdxrecipients paragraph, and removing the "Delivery to an email-field recipient is not live yet" sentence from therecipientsdescribe inpackages/spec/src/automation/io-node-config.zod.tsonce delivery lands. Until then, a run that reaches such a node fails at the node. The written-out and computed address path is the open decision #25647.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Unlocked · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-24T17:57Zpm:blocked→pm:queue. p1 ·domain:server·area:workfloware unchanged. ⛔ Not a claim, ⛔ not a dispatch.notifyrecipient form that is an address from anemail-typed field of the triggering record — the protocol half of #25571 (ruling 6377731715, letter B) #25610 closedcompletedat 2026-10-24T16:31Z through PR feat(spec,lint): a notify recipient form{ emailField }— the address in an email-typed field of the triggering record #25645 (6021b5e837).notify.recipientsaccepts{ emailField }, and the lint refusals are in place.6384576637records that ruling C on storage/public forms: an anonymous public-form submission cannot carry a file —/storage/uploadanswers 404 to the guest principal, so a required attachment field is impossible on a public form #25572 moved hotsrm's applicant case, but nothing withdraws ruling B on [Decision] hotsrm 平台评估提出的 7 项平台能力:哪些做、怎么做(#25442 #25443 #25444 #25445 #25446 #25447 #25470) #25496. Whether the feature still has a pull is a scope question for the maintainer. It does not keep this card blocked.{ emailField }is now onmain, and the Version Packages PR chore: version packages #25640 is open. If a release cuts before this card lands, the form is declared but not delivered. So this card is owed before that cut, or the key is reverted before it if the maintainer cuts scope. Ruling B's runtime half on [Decision] A notify node already mails a written-out or computed email address: does ruling 6377842908's 「⛔ 不接受自由文本或表达式拼出的地址」 reach it — refuse both (A), refuse computed addresses only (B), or leave both (C)? #25647 (the computed-address refusal) may ride this card's files, as that ruling says.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 1 · card thread · anchor of a ruled ride (#25647 rides this branch, ruling
6386629237: 「可并入 #25571 的文件」)Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01CmnfU7vRbNtSFtBQwyyKpBAccount:
os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-e1l64i(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree:
objectstack-issue-25571(a linked worktree in the card thread's container)Domain:
domain:serverSeat:
domain:server#1File surface:
packages/services/service-messaging/src/**(the email channel, the recipient resolver, per-address rate limiting, the messaging log),packages/services/service-automation/src/builtin/notify-node.ts(resolveRecipients: the{ emailField }resolution, and #25647 ruling B's refusal of a computed address no user holds),content/docs/automation/flows.mdx(the recipients paragraph PR #25645 left to this card), tests, one changeset. ⛔ Notpackages/spec/**: the "not live yet" describe sentence inio-node-config.zod.tsand the describe half of ruling B are the spec card #25696's. ⛔ Notpackages/services/service-automation/src/engine.ts(hot file, #25222 slice 8 next) (stop on breach; explain in the report)Container & model:
M,mode:thread,model: opus(the default ofdispatch-gates --tier: no path-derived mandate),effort: xhigh(p1; requested, the thread runs at the harness default because only the maintainer sets a thread's effort)Clause-②: no
Metadata-protocol: narrowed (#25647 ruling B refuses at run time a computed recipient that is an address no user holds; #25571's delivery of the declared
{ emailField }form alone isnone). A narrowing owes one contract-review-tier review before the queue.Responsibility: n/a — not a defect card
Thread-read: 6387619376
Premise: holds (read 2026-10-24T23:06Z on
origin/main47af3dd:emailFieldhas 0 lines inpackages/services; the describe still says "Delivery to an email-field recipient is not live yet";resolveRecipientsis inpackages/services/service-automation/src/builtin/notify-node.ts; the written-out branchif (EMAIL_SHAPE(recipient))is inservice-messaging/src/email-channel.ts)Serial constraints cleared: no open PR touches these paths (all 19 open PRs' file lists read this fire; the Version Packages PR #25640 touches only
CHANGELOG.mdandpackage.json). Owed before the release cut (triage6387619376).objectstack-fleet commentedon Oct 25, 2026
os-dev-report
{ "issue": 25571, "status": "done", "branch": "claude/project-thread-e1l64i", "pr": "https://github.com/objectstack-ai/objectstack/pull/25721", "session": "session_01CmnfU7vRbNtSFtBQwyyKpB", "premise_still_valid": true, "summary": "#25571: a notify node with recipients: { emailField: 'FIELD' } and channels: 'email' now mails the address held in that email field of the trigger record, read from the STORED row by id (never the run snapshot); a value that is not exactly one mailbox (list, separator, display name, header break, audience prefix, no domain dot, non-string), an empty field, an unstored row or a run with no trigger record fails the node without echoing the value. One address gets at most 5 email-field sends per hour (EMAIL_FIELD_SENDS_PER_WINDOW / EMAIL_FIELD_SEND_WINDOW_SECONDS), case-insensitive, counted through plugin-auth's rate-limit-storage fixed-window counter over the kernel cache service; the send is logged as a sys_notification event plus a sys_notification_delivery row. #25647 (ruling 6386629237 B): in a notify node whose channels include email, a recipient a CEL envelope evaluates to that reads as an address resolves to the sys_user holding it, and fails the node with a located hint to { emailField } when no user holds it or the directory cannot be read; a written-out address is the author's constant and is sent as before. service-messaging and packages/spec are untouched; flows.mdx carries the recipients paragraph.", "tests": "At head 450832fb. New packages/services/service-automation/src/builtin/notify-email-field-recipient.integration.test.ts over the real MessagingService + MemoryNotificationOutbox + NotificationDispatcher + email channel: 20/20 passed (A stored address mailed, snapshot ignored, event + success delivery row; B 8 malformed values refused, value not echoed; C no trigger / not stored / empty; D limit 5, case-insensitive, other address keeps its budget; E1 computed non-user refused; E2 computed user address resolves to usr_owen; E3 written-out address mailed; E4 no data engine refused; E5 x2 inbox-only or no channels: computed address passes to messaging unscreened; E6 channels ['inbox','email'] screened). service-automation full vitest: Test Files 202 passed (202). Shard-4 packages + service-automation via turbo: plugin-approvals 96/96 files, example-showcase 39/39, trigger-schedule 8/8, connector-rest 4/4, types 29/29, service-automation 202/202; Tasks 70 successful, 70 total. Ablations via scripts/ablation-replace.mjs, each restored to the committed blob (git status clean after): limit check to if(false) → D red; isOneAddress removed → B 7/8 red; computed screen removed → E1+E4 red; snapshot read → A red; channel gate forced true → E5 x2 red; channel gate forced false → E1, E4, E6 red. typecheck (pnpm --filter @objectstack/service-automation typecheck) exit 0, test layer compiles; eslint --no-inline-config packages/services/service-automation exit 0.", "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 450832fb derived 118 families (identical to the 945154b3 derivation); all 118 run under os-verify-lock, each recorded as 'command :: exit N' with the code captured before any pipe: 118 x exit 0. --ran reconciliation: '118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3)'. The lock printed a holder-side starvation warning for the union run (shared box). CI check-runs at 450832fb (47): Test Core success, Test Core (1/6)..(6/6) all success, Build Core success, Build Docs success, Dogfood Regression Gate success, all Lint and Type Check lanes success, Validate Package Dependencies success, Spec property liveness success, claim/single-writer guards success; skipped: UI lanes, Temporal Conformance, opt-in harnesses, sharded dogfood matrix; FAILURE: Check Changeset only, because the PR body line reads 'Clause-②: no' beside 'Metadata-protocol: narrowed' and the gate wants the (narrowing) arm; it re-reads the body on edit, so it clears with a body PATCH and no push. Earlier red Test Core (4/6) at 945154b3 was this change: example-showcase approval-resume-relation-expand, two inbox-only notifies whose recipient CEL evaluates record.owner (a text field holding the owner's email) failed the new screen because that test kernel has no sys_user object; fixed by 450832fb (screen only when channels include email), now green.", "dogfood_battery": "450832fb · os-verify-lock: VERDICT command-exit 0 · held the lock 1168s (19m28s) · command: pnpm turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures", "mcp_calls": "13 — all reads: mcp__github__issue_read x3, mcp__github__get_job_logs x5 (2 by a subagent), mcp__github__pull_request_read x2, mcp__github__actions_get x2 (subagent), mcp__github__get_check_run x1 (subagent); no write tool", "api_writes": "2 — via scripts/pm/fleet-write/dispatch.mjs relay: pr_create (draft #25721, assignees os-project-manager); comment on #25571 (this report). git push is not counted.", "open_questions": [ { "question": "needs_decision (confirmation): ruling 6386629237 B says a computed address no user holds fails the node, and does not say which channels. 450832fb applies it only to nodes whose channels include email. Keep that scope?", "options": [ "A — screen only when channels include email (as pushed). Inbox (the messaging default) never sends to the address itself, so record text cannot choose who is mailed. app-showcase's two inbox-only owner notices keep working unchanged. Cost: none in-repo; an inbox row keyed by an address no user holds stays possible, as before.", "B — screen every channel, the ruling's literal letter. app-showcase's notify_cleared and showcase_notify_owner (record.owner holds an email, RLS owner == current_user.email) fail whenever the owner is not a signed-up user or the kernel has no sys_user; the showcase test kernel would need a sys_user object and seeded users, which is examples/** outside this claim, so a follow-up card or a claim widening. Work: about 1 extra file plus seeds." ], "recommendation": "A. Real business need: the hazard the ruling names is mailing an address picked from record text, and only the email channel does that; the only measured computed-address producers (app-showcase owners) are inbox-only. Long-term soundness: the line sits at the one channel that delivers to the raw address, with { emailField } still the single door to mail a non-user. AI-error resistance: a node that names email and computes an address is still refused loudly at run time with a located hint; nothing is silently tolerated on the mail path. Startup scope: B widens into examples and seed users for no hazard reduction." }, { "question": "Is 5 emails per address per hour the right per-address limit for email-field sends?", "options": [ "A — keep 5/hour (exported constants, one place to change)", "B — another number the seat names" ], "recommendation": "A unless a product number exists; the ruling fixed 'rate-limited per address' but no figure." } ], "out_of_scope_findings": [ "class: c · reach: named producer — the email field type's write check (packages/objectql/src/validation/record-validator.ts, ^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$) admits commas, semicolons, colons, angle brackets and quotes in the local part, so a stored 'a,b@x.y' or 'role:x@y.z' passes as an email; this PR refuses such values at send time, but any other consumer of the field reads them as one address · evidence: B cases 'ann,bob@applicant.example' and 'role:admin@applicant.example' would be stored by the validator · dedupe words: email field validator, local part, separator, record-validator", "carrier: #25696 · the spec describe sentence 'Delivery to an email-field recipient is not live yet...' in packages/spec/src/automation/io-node-config.zod.ts (and its generated reference row) is now false, and ruling B's describe half is #25696's; noted, not filed" ], "deviations": [ "Changeset Clause-② line deviates from the claim: the claim says 'Clause-②: no', CI Check Changeset refuses that beside 'Metadata-protocol: narrowed', so the changeset reads 'Clause-②: no (narrowing: a recipient that the value expression of a notify node whose channels include email evaluates to an email address no user holds now fails the node at run time instead of being mailed)' and the changeset title carries '!'. The PR body (written once, not PATCHed by me) still says 'Clause-②: no' and the PR title has no '!': seat to PATCH the body line to the changeset's line verbatim and add '!' to the title (feat(service-automation)!: ...). Check Changeset re-reads on edit.", "PR body readings are stale: they cite 215662e5 and predate the channel scope. Seat to update the #25647 section to say the screen applies when channels include email, the head to 450832fb, the readings to the ones in this report, and the 'who moves' line (no examples notify names the email channel; the app-showcase owner envelopes evaluate to addresses but are inbox-only).", "Files outside the claimed surface, same package or gate ledger: packages/services/service-automation/package.json (plugin-auth moved to dependencies; service-messaging cannot import the counter because plugin-auth depends on it), pnpm-lock.yaml (that importer hunk only), packages/services/service-automation/vitest.config.ts (source alias for the counter subpath, required by check:test-source-alias), scripts/engine-double-contract.pinned.json (pin written by --write for the new fake engine).", "Commit 8afded11 (wip) carries no trailers; 450832fb was amended before its first push to the model-free trailer pair after the pre-push hook refused a model-named co-author.", "A subagent was used, without being asked, to read the Test Core (4/6) log at 945154b3; its 5 MCP reads are counted above. The cause was found locally by running the shard's package set.", "The earlier 'who moves' measurement ('all 17 envelopes read a user reference') was wrong for app-showcase, whose record.owner / ownerId hold emails; the changeset now states the corrected measurement." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 25, 2026
Seat ACCEPT for #25571 + #25647: PR #25721 head
450832fb52, record #25721 (comment) (2026-10-25T02:17Z). Owes the narrowing's contract review on this head, then lands through the relay.Generated by Claude Code
objectstack-fleet commentedon Oct 25, 2026
os-dev-report
{ "issue": 25571, "status": "done", "branch": "claude/project-thread-e1l64i", "pr": "https://github.com/objectstack-ai/objectstack/pull/25721", "session": "session_01CmnfU7vRbNtSFtBQwyyKpB", "premise_still_valid": true, "summary": "Patch round on contract review 6392441398 (FAIL, F1) with the seat's rulings. F1: the computed-address screen now takes the address the email channel would actually send to: a bare address, or the remainder after one `user:` (the resolver hands that remainder on with no lookup and the email channel mails any address-shaped id), so a computed `user:stranger@x.y` and `user:user:stranger@x.y` under channels: 'email' fail the node with nothing enqueued, and `user:` plus a user's address resolves to that user. `owner_of:` stays unscreened (seat ruling: a stored owner field written through record writes, not a string the expression builds), stated in the code docblock. F3: flows.mdx and the changeset declare the per-address limit best-effort (on a counter fault the send is admitted, one warning logged). F5: flows.mdx and the changeset declare the email-field grammar plain ASCII (no non-ASCII character, quoted local part or IP-literal domain); the refusal now reads 'does not hold one plain ASCII email address'; grammar not widened. ①.7: the changeset's Dependency bullet names the new workspace edge service-automation → plugin-auth and states no cycle. Everything else from report 6392322238 stands.", "tests": "At head d4fd5968. notify-email-field-recipient.integration.test.ts: 23/23 passed (new E7 x2: computed user:ADDRESS and user:user:ADDRESS no user holds, channels 'email', fail the node, outbox empty, nothing mailed after a dispatcher tick; new E8: computed user:owen@company.example resolves to usr_owen; B now asserts 'does not hold one plain ASCII email address'; A–E6 unchanged and green). Turbo test, Tasks 67 successful, 67 total: service-automation Test Files 202 passed (202), Tests 2659 passed (2659); example-showcase 39/39 files, 484/484 tests; plugin-approvals 96/96 files, 1259/1259 tests. Ablation (scripts/ablation-replace.mjs on the committed blob, tree clean after): the user: branch of mailedAddressOf replaced by the old prefix refusal → E7 x2 and E8 red (3 failed | 20 passed). The six earlier ablations stand from 450832fb (the code they mutate is unchanged). typecheck exit 0 (test layer compiles); eslint --no-inline-config packages/services/service-automation exit 0.", "gates": "Local at d4fd5968: dispatch-gates --commands derived 118 families (identical to the 450832fb set); all 118 run under os-verify-lock, recorded 'command :: exit N' with the code captured before any pipe. First pass: 116 exit 0, 2 exit 3 PREREQUISITE in the fresh worktree (check:skill-examples, check:dual-build-cjs-loads: no dist for some packages); after a full `pnpm turbo run build` (77/77 tasks) both re-run exit 0 ('262 prose examples type-check'; dual-build floors met). --ran: '118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3)'. CI check-runs at d4fd5968 (50): Test Core and Test Core (1/6)..(6/6) success, Build Core success, Build Docs success, Dogfood Regression Gate success, Check Changeset success (02:34Z and 03:03Z), Lint & Repo Gates, TypeScript Type Check and every Lint / Type Check lane success, Validate Package Dependencies success, Governed Surface Queue Guard success, Spec property liveness success, claim and single-writer guards success; skipped by path or opt-in: UI lanes, Console Pin Gate, Temporal Conformance, sharded dogfood matrix, both opt-in harnesses. No failures.", "dogfood_battery": "d4fd5968 · os-verify-lock: VERDICT command-exit 0 · held the lock 1204s (20m04s) · command: pnpm turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures", "mcp_calls": "17 cumulative, all reads: mcp__github__issue_read x4 (one this round: the PR's comments for review 6392441398), mcp__github__get_job_logs x5 (2 by a subagent), mcp__github__pull_request_read x5 (3 this round: check-runs x2, the PR body), mcp__github__actions_get x2 (subagent), mcp__github__get_check_run x1 (subagent); no write tool", "api_writes": "3 cumulative, via the scripts/pm/fleet-write/dispatch.mjs relay: pr_create (#25721); comment on #25571 (report 6392322238); comment on #25571 (this updated report). git push is not counted.", "open_questions": [ { "question": "needs_decision (confirmation, unchanged and with the maintainer): ruling 6386629237 B's channel scope — screen computed addresses only where channels include email (as pushed), or every channel?", "options": [ "A — email only (as pushed; seat lands on it)", "B — every channel, plus sys_user seeds in app-showcase (examples/**, a follow-up)" ], "recommendation": "A, per report 6392322238's four-axis reasoning; no change this round." } ], "out_of_scope_findings": [ "class: c · reach: named producer — the email field type's write check (packages/objectql/src/validation/record-validator.ts, ^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$) admits commas, semicolons, colons, angle brackets and quotes in the local part, so a stored 'a,b@x.y' or 'role:x@y.z' passes as an email; this PR refuses such values at send time, but any other consumer of the field reads them as one address · evidence: B cases 'ann,bob@applicant.example' and 'role:admin@applicant.example' would be stored by the validator · dedupe words: email field validator, local part, separator, record-validator", "carrier: #25696 · the spec describe sentence 'Delivery to an email-field recipient is not live yet...' in packages/spec/src/automation/io-node-config.zod.ts (and its generated reference row) is now false, and ruling B's describe half is #25696's; noted, not filed" ], "deviations": [ "Beyond the five rulings: none in code. The changeset's BREAKING bullet now also says 'bare or as `user:ADDRESS`', and flows.mdx says 'written bare or as `user:ADDRESS`', so the declarations name the spelling F1 closes. The Clause-② line is unchanged (the stated rule already covers it), so the PR body's Clause-② line needs no edit.", "Files outside the claimed surface, same package or gate ledger: packages/services/service-automation/package.json (plugin-auth moved to dependencies; service-messaging cannot import the counter because plugin-auth depends on it), pnpm-lock.yaml (that importer hunk only), packages/services/service-automation/vitest.config.ts (source alias for the counter subpath, required by check:test-source-alias), scripts/engine-double-contract.pinned.json (pin written by --write for the new fake engine).", "Commit 8afded11 (wip) carries no trailers; 450832fb was amended before its first push to the model-free trailer pair after the pre-push hook refused a model-named co-author.", "A subagent was used, without being asked, to read the Test Core (4/6) log at 945154b3; its 5 MCP reads are counted above. The cause was found locally by running the shard's package set.", "The earlier 'who moves' measurement ('all 17 envelopes read a user reference') was wrong for app-showcase, whose record.owner / ownerId hold emails; the changeset now states the corrected measurement." ], "body_patch_for_seat": [ "What changes, Rate-limited bullet: replace '(no new graph edge; runtime and cli already depend on it)' with '(a new workspace edge, service-automation → plugin-auth; no cycle: neither plugin-auth nor service-messaging or rest depends on service-automation)', and after 'matching the SMS quota's fail-open.' add 'The limit is declared best-effort in flows.mdx and the changeset.'", "What changes, One address bullet: append 'The grammar is plain ASCII on purpose: a non-ASCII character, a quoted local part or an IP-literal domain the email field type stored is refused, and the refusal says \"does not hold one plain ASCII email address\"; flows.mdx and the changeset declare it.'", "#25647 section, second bullet: replace '(no `user:` / `role:` / `team:` / `owner_of:` prefix, its email heuristic)' with '(the address the email channel would send to: a bare address, or the remainder after one `user:`, which the resolver hands on with no lookup; `role:` and `team:` yield user ids)', and add a bullet: '`owner_of:` is not screened: its address comes from a stored owner field written through record writes, not from a string the expression builds (seat ruling on F1).'", "Tests: add 'E7 (×2): a computed `user:ADDRESS` and `user:user:ADDRESS` no user holds under `channels: email` fail the node, nothing enqueued or mailed. E8: a computed `user:` plus a user's address resolves to that user.'; readings line: head d4fd5968, the file 23/23, service-automation 202/202 files (2659 tests), example-showcase 39/39, plugin-approvals 96/96, dogfood 295 files / 2,520 tests passed, 0 failed; ablation table: add row 'the `user:` branch (old prefix refusal) | E7 ×2, E8'.", "Acceptance notes: the line saying check:dual-build-cjs-loads and check:skill-examples are NOT MEASURED locally is stale; both exit 0 after a full build (450832fb and d4fd5968)." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 25, 2026
Landed
PR #25721 merged at 2026-10-25T04:41:00Z by the merge queue as squash
d1e329e96donmain(seat ACCEPT6392365089; contract review FAIL6392441398on450832fb, then PASS6392736321on headd4fd5968). It closes #25571 and #25647.git ls-remote origin 'refs/heads/gh-readonly-queue/*'forpr-25721origin/mainby contentgit grep "plain ASCII email address" origin/main -- packages/services/service-automation/src/builtin/notify-node.tsorigin/mainby contentnotify-email-field-recipient.integration.test.tsState: both cards closed by
Fixes(completed);pm:dispatchedremoved from both (2026-10-25T04:42Z). Open after landing:channelsincludeemail) is with the maintainer as a confirmation; the other answer is an additive follow-up.recipientsdescribe inpackages/spec/src/automation/io-node-config.zod.tsstill says email-field delivery is not live; that sentence is spec(automation): the notify recipients describe names a written-out address as an author constant, and drops the bare "an email address" audience selector — the spec half of ruling B on #25647 #25696's, flagged there now so it lands next.emailfield type's write check admits separators and selector prefixes in the local part (class c,record-validator.ts): the seat files it as its own card.Generated by Claude Code