Skip to content

A flow notice cannot name a select field's option by its label in the recipient's language: the mail renderer has no label formatter and the flow passes no option labels to CEL label() #30172

Closed

Description

@objectstack-fleet

Source

Measured by HotCRM on platform 18.3.0 while fixing hotcrm#2391 (the stalled-deal notice printed the stage as needs_analysis). The implementer's report is hotcrm#2391 comment 6640505913; the HotCRM side is hotcrm#2406.

What happens

A notify node's templateData is evaluated once per run, before the recipients fan out, and each recipient is then sent the template of their own locale. A select field's value reaches the template as its stored value. None of the platform's routes turns it into the option label in the recipient's language:

# Route tried on 18.3.0 Result
1 A template formatter, {{stage | label}} Refused as an unknown formatter (known-formatter). The refusal text says to format the value in the caller and pass finished text.
2 CEL label(currentOpp.stage) in templateData Refused at parse: label() takes record.FIELD only.
3 CEL label(record.stage) in a flow Refused: "this evaluation context carries no option labels".
4 A formula field label(record.stage) read by the flow Resolves, but in the reader's locale. A schedule run reads as the system, so a zh-CN recipient got "Needs Analysis".

Seam: NotifyConfigSchema.templateData → the service-automation notify executor → the plugin-email renderer.

What HotCRM does meanwhile

hotcrm#2406 builds, at flow-build time, a CEL map of every shipped locale's option labels from the package's translation bundle (stage_label: { en, zh_CN, es_ES, ja_JP }), and each locale's template reads its own key ({{stage_label.zh_CN}}). It works, but every app has to write it, and it covers only the locales the app ships.

Suggestion

One of these, at the platform's choice:

  • a renderer formatter that labels an option in the template's locale, given the object and field (for example {{stage | option:'crm_opportunity.stage'}}); or
  • the notify executor resolves option labels per recipient when templateData names a field by reference rather than by value.

Either lets an app pass the stored value and get the label each recipient reads.

Filing gate ① a reproduced gap, class b. Filed by the repo:hotcrm seat (huangyiirene, session session_01J8VVAnPkgK6CWrJa6t7Fk5).

Grade: priority:p3. Apps have a workaround; the cost is a hand-built label map per notice. Size: M.

Dedupe: searched open objectstack issues for notify option label locale, templateData label recipient, label formatter email template and known-formatter label. #9205 (closed) localized a notify node's own title and message, not a field's option label; the rest are QA run logs and unrelated mail cards.

Filed by the repo:hotcrm lane seat from hotcrm#2391's out-of-scope finding.

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p3 kept · domain:server · pm:queue · 2026-11-04T20:55Z

    ⛔ Not a claim.

    • Read fresh. A flow notice cannot print a select field's option label in each recipient's language. templateData is evaluated once, before the recipients fan out, and neither a template formatter nor CEL label() reaches it. HotCRM measured it, and a stored value prints instead. p3 kept.
    • Fix site: service-automation's notify and the template renderer. The option's label belongs to the recipient's locale, so it resolves per recipient.

    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 4, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

    Claim: PM loop round 1
    Session: session_01Utp6CXCS98uhsFQYk64epE (seat) · dev = project card thread session_01Utp6CbWG4NyLTjvVq3tE4U
    Account: os-bill
    Branch: claude/project-thread-0q8hex (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30172
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/services/service-automation/src/**, and only if the label lookup belongs there packages/plugin-email/src/** or packages/formula/src/** (CEL evaluation context, no grammar change), with tests; content/docs/** only for sentences this PR makes false (not releases/**); one changeset. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/**, .claude/**, AGENTS.md (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus; dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: yes (widening) — provisional on route P: a flow that is refused today (an option label in a notify) would evaluate. The dev corrects the arm from the diff
    Metadata-protocol: semantic (provisional) — what a notify's select value renders as per recipient; the dev confirms or corrects
    Responsibility: service-automation's notify evaluates templateData once before fan-out, and neither the renderer nor CEL label() reaches option labels there (measured by HotCRM on 18.3.0) | none: apps hand-build a per-locale label map (hotcrm#2406) | every app that sends a notice naming a select field to recipients of more than one language
    Thread-read: 6641888761
    Premise: holds per the HotCRM measurement; the dev measures route P first and reports the route before writing code. A packages/spec need (template grammar, NotifyConfigSchema key, CEL label() contract) stops as needs_decision.
    Serial constraints cleared: none (no open PR or claim on service-automation notify at claim time; dev re-checks)

    Order: project file server-seat6/dispatch-30172.md, delivered to the card thread.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
    os-dev-report

    决策: 流程通知里的选项值,按每个收件人的语言显示标签,落在哪条路。状态 needs_decision,无 PR,未写代码。dev = 卡线程 session_01Utp6CbWG4NyLTjvVq3tE4U,分支 claude/project-thread-0q8hex(零提交)。

    一句话问题

    同一封流程通知发给中文和英文同事,选项字段(如商机阶段)印出的是存储值 needs_analysis,平台没有一条路能让每个人读到自己语言的标签;派单令首选的「仅 server 内」路线覆盖不了卡上复现的场景。

    三条读数(origin/main f273c9f)

    1. 收件人语言只在投递端出现。 notify 执行器对整个受众 emit 一次(受众可含 role:/team: 选择器),templateData 在 packages/services/service-automation/src/builtin/notify-node.ts L988 求值一次。收件人的 locale 第一次出现在 packages/services/service-messaging/src/email-channel.ts L383(resolveRecipient 返回 locale)和 inbox-channel.ts L198(resolveRecipientLocale),两者都把 data + locale 交给 plugin-email 的 sendTemplate / renderTemplate。执行器里拿不到任何一位收件人的语言。
    2. 唯一已声明的「此值是对象 O 的字段 F」是 CEL label(record.FIELD)。 宏只认 record 作接收者(packages/formula/src/stdlib.ts optionLabelFieldOf L429–434),对象由 record 所属对象隐含。写进 templateData 能过 parse,运行时因流程的 CEL 上下文不带 optionLabels 被拒(stdlib.ts L502–510,即卡上第 3 行)。按语言构造标签的机制已存在:packages/objectql/src/formula-option-labels.ts(readerOptionLabels,与 /meta/object 同一条翻译路径)。
    3. 卡上复现的是循环变量。 HotCRM 的滞留商机通知是定时流程里遍历记录,写的是 label(currentOpp.stage),parse 即拒(卡上第 2 行)。定时 tick 没有触发记录,record 不存在。要让它工作,必须改 label() 的契约(允许非 record 接收者并说明对象),派单令把这一条列为停手条件。

    Governing text

    • packages/spec/src/system/email-template.zod.ts L107–123 MAIL_TEMPLATE_HOLES:「A formatter … is one the template engine knows (an unknown name … is refused …)」。新增 formatter 即改此句,属 spec 车道。
    • packages/formula/src/types.ts L200–214 EvalContext.optionLabels:「The option labels of the record's object, in the reader's locale」,label(record.FIELD) 只读它。
    • packages/spec/src/contracts/email-service.ts SendTemplateInput.data:「Render context — placeholders in subject/body are resolved against this object.」本报告的选项均不改这句。

    协议声明:P1 不改 spec 文字;F 改模板文法(spec);L 改 label() 契约(formula,影响 lint 与技能所教)。

    选项

    选项 做什么 客户可感知后果 工作量
    P1 执行器识别调用 label(record.F) 的 templateData 项,对 i18n 服务持有的每个 locale 各求值一次(无 locale 用声明标签),payload 带每个 locale 的变体;email / inbox 两个 channel 按收件人 locale 用 resolveBundleLocale 挑一份 记录触发和 time-relative 扫描的通知变好;HotCRM 这类定时遍历的通知不变,仍要自建标签表 server 车道 · 1 PR;需把文件面扩到 packages/services/service-messaging/src/**;无实测先例
    F 模板新增 formatter,如 {{stage | option:'crm_opportunity.stage'}},plugin-email 渲染时(已持收件人 locale)按对象元数据加翻译包取标签 所有通知都对,含定时、循环、记录流;作者传存储值即可 spec 车道改文法 + formula 模板引擎认这个 formatter + lint 作者规则 + plugin-email 渲染;约 2 PR(spec 先、server 后);先例形态是 currency:@path 参数,行数未实测
    L 扩 label():如 label(VAR.FIELD, 'object'),再加 P1 的 channel 改动 与 F 覆盖相同,但作者要在流程里写对象名,且 CEL 和模板两处都要懂 formula 契约 + lint + 技能 + server 两包;≥2 PR;无先例

    业务含义直译:P1 像只给「从某张单据发起」的通知配翻译,批量巡检发出的仍是原始代码;F 像在邮件模板里放一个「按读者语言显示这一项」的占位符,谁发都一样;L 像让每个发信流程自己声明「这是哪张表的哪一列」再翻译。

    四轴分析

    • 实际业务需求: 今天撞上的是 HotCRM 定时遍历通知(hotcrm#2391,变通方案 hotcrm#2406 手建四语标签表)。P1 覆盖不到这一真实场景;F 与 L 覆盖。记录触发流程的需求目前无具名用户。
    • 项目长远合理性: 标签属于读者语言,渲染点是唯一同时知道「值、对象字段、收件人语言」的地方。F 把它放在 plugin-email 渲染器一处,email 与 inbox 共用(inbox 也调 renderTemplate)。P1 在执行器把每个 locale 预算一遍再让投递端挑,多一条两包之间的隐式 payload 约定,且只覆盖一半。L 让 CEL 与模板两处都背契约。
    • 防 AI 犯错: F 的 formatter 带对象.字段参数,作者规则可在保存时响亮拒绝不存在的对象/字段/无选项字段(同 label-field-no-options 的拒法)。P1 下 AI 写 label(currentOpp.stage) 仍被拒,但写在定时流程里的 label(record.stage) 会在运行时失败,作者在保存时看不到。L 让 AI 必须写对两个参数,错拼对象名是运行时问题。
    • 创业阶段不扩散: F 新增一个 formatter 名,是一个永久声明;P1 不新增作者面但新增内部 payload 约定;L 扩一个已发布函数的签名。三者都不新增门禁。

    os-decision-facets

    • ① 项目长远合理性:F 收拢到一个渲染点,P1 增加一条两包隐式约定,L 让两处契约同时增生。
    • ② 实际业务拉动:HotCRM 定时遍历通知今天撞上;P1 不覆盖它,F/L 覆盖。
    • ③ 防 AI 犯错:F 可在保存时按对象.字段响亮拒绝;P1 留下运行时才暴露的失败;L 错拼对象名到运行时才见。
    • ④ 创业阶段不扩散:F 一个新 formatter 名,L 改已发布签名,P1 无作者面但有内部约定;均不加门禁。

    Prior rulings read: "option label" formatter → 16 hits, "label(record" → 919 hits (top 6 read), known-formatter → 42 hits; none is a ruling on this question; ADR none; thread: none

    推荐

    推荐 F。只看①选 F:两年后的样子是模板占位符本身声明「这是哪个对象字段的选项」,渲染器按读者语言出标签,主流 CRM 的邮件模板合并字段同样以对象.字段引用写成(如 Salesforce 的 {!Opportunity.StageName};其是否按收件人语言出标签未实测)。②③④ 是否翻转:否。置信缺口:F 的具体拼写(option:'obj.field' 还是别的词)未与 spec 席位对齐,属 spec 车道裁定。P1 不取:覆盖不到真实场景。L 不取:两处契约同时扩。

    裁后执行

    • 选 F:本卡转 spec 车道立文法卡(MAIL_TEMPLATE_HOLES 加 formatter 句,formula 模板引擎认名,lint 作者规则),server 半卡挂 Blocked-by: 该卡,做 plugin-email 渲染 + 两收件人 en/zh-CN pin + 定时流 pin + 非选项值对照 pin。本线程可接 server 半。
    • 选 P1:席位把文件面扩到 packages/services/service-messaging/src/**,本线程按派单令先红后绿开草稿 PR,卡用 Part of,循环场景留卡。
    • 选 L:formula 车道先改 label() 契约与技能,server 再按 P1 做。
    {
      "issue": 30172,
      "status": "needs_decision",
      "branch": "claude/project-thread-0q8hex",
      "pr": null,
      "session": "session_01Utp6CbWG4NyLTjvVq3tE4U",
      "premise_still_valid": true,
      "summary": "Measured route P on origin/main f273c9fe before any code. The recipient locale first exists in service-messaging's email and inbox channels after fan-out, never in the notify executor, and the only declared field reference is CEL label(record.FIELD), whose receiver must be record; the card's reproduced case is a schedule loop variable (label(currentOpp.stage)), refused at parse. A per-recipient route P needs the service-messaging surface and still misses that case; covering it needs the template grammar (spec) or the label() contract, both stop conditions.",
      "tests": "none run: measurement only, no code written",
      "gates": "none run: no diff",
      "dogfood_battery": "n/a: no PR",
      "mcp_calls": "0",
      "api_writes": "1: POST issues/30172/comments (this report, via post-stamped)",
      "open_questions": [
        {
          "question": "Where does a select option label in a flow notice resolve in each recipient's language?",
          "options": [
            "P1: notify executor evaluates label(record.F) entries once per held locale, channels pick by recipient locale; needs service-messaging surface; record-triggered flows only",
            "F: a template formatter naming object.field, resolved by the plugin-email renderer in the recipient's locale; spec grammar change; covers every flow",
            "L: widen label() to name a non-record receiver and its object, plus P1's channel change"
          ],
          "recommendation": "F, because it is the only option that covers the reproduced schedule-loop notice and resolves at the one render point that already holds the recipient locale"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Report taken 2026-11-05T12:41Z.


    Generated by Claude Code

  5. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 5, 2026
  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

    Release: session_01Utp6CXCS98uhsFQYk64epE · reason: seat clock-out · next: needs-user-decision
    Resume-from: none

    The server lane closes this seat on the maintainer's word 「当前任务处理完,合并后只留一个 server 席位」 (2026-11-05T16:41Z); the assignee is cleared and pm:dispatched removed 2026-11-05T18:52Z. The card waits on the maintainer's letter (decision analysis 6658266694; the seat recommended F, which is spec-lane work first). No code was written on the card branch. On the ruling, domain:server#2 (the remaining server seat) claims it afresh, or routes F's spec half to domain:spec.

  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Ruling confirmed: letter F · recorded by domain:server#2; routed to domain:spec first (seat's split: the spec half is the gate); the server half is cut as its own card Blocked-by this one when the spec half lands. The card branch claude/project-thread-0q8hex has no commits and is dropped.

    Maintainer: 「同意」 to the recommendation, in the decision thread, 2026-11-05T21:43Z (project files decisions/30172-流程通知选项标签按收件人语言-呈批-复审.md, seven fixed parameters).

    1. A mail-template formatter {{ path | option:'object.field' }}: the argument is a literal object.field (spelling is the spec seat's call); no @path.
    2. Like currency:@path, mail templates only; in-app title / message / confirmText refuse it on save as an unknown formatter.
    3. plugin-email builds a label table per object.field in the template with the recipient's locale (the readerOptionLabels translation path) before a synchronous render; no locale ⇒ declared labels; a value outside the options prints as is; a missing object/field or one with no options ⇒ TEMPLATE_INVALID, the whole send refused.
    4. lint checks object.field on save.
    5. Split: spec first (the MAIL_TEMPLATE_HOLES grammar sentence + the formula allow-list + the lint rule); the server card is Blocked-by it (plugin-email prefetch + render; pins: en and zh-CN recipients each see their own labels; a scheduled flow running as system still renders per recipient; a non-option value prints as is).
    6. objectstack-automation skill gains one line: option labels in notices use the formatter, not label() in templateData.
    7. label()'s contract does not move; no P1.

    State change: needs-user-decision and domain:server removed; domain:spec and pm:queue added.

  8. 11 remaining items

  9. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgUtH2CXA8Qb8bDBRom
    os-dev-report

    PR #30727 patch round after the delta review (draft, head 030a4930): the ladder's rung four. An option hole naming a platform-prefixed object that no rung above carries is advised at warning, its field unjudged, at every door; formula and lint answer it alike. 56928a5b before it changed only the three changesets' Skills-docs: line to follow-up #30731. Revised body at /mnt/project-files/briefs/30727-body-r3.md; the PR body was not patched.

    {
      "issue": 30172,
      "round": "patch round 2 (delta review PASS with one should-fix, comment 6673435245)",
      "status": "done",
      "branch": "claude/project-thread-d8v0qv",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30727",
      "head": "030a4930",
      "session": "session_01L6bhgUtH2CXA8Qb8bDBRom",
      "premise_still_valid": true,
      "summary": "SHOULD-FIX (rung four): formula mail-hole-grammar gains optionObjectUnregisteredPlatform(ref, objects), true when the objects do not carry the name and it has a platform prefix (hasPlatformObjectPrefix: sys_, cloud_, ai_); optionFieldRefusal returns null for it, so judgeMailHoleGrammar neither refuses nor judges it. email-template-hole-grammar adds a warning for each such hole under the same rule id, naming the hole and object, the hint pointing at os explain object-reference-unregistered-platform and quoting OPTION_ARGUMENT_RULE; EmailTemplateHoleGrammarFinding.severity widens to error or warning. The sys_userz pin moved from error to warning, with an unprefixed userz control refused at error, and os validate and the publish door both answer it as a warning (publish errors empty, advisories carry it). A send that reaches such a hole with no label table still throws optionTableRefusal. Changesets (formula, lint) and email-templates.mdx updated. Item 2 (Skills-docs follow-up #30731 in all three changesets, word for word with the body) landed in 56928a5b.",
      "tests": "head 030a4930, formula rebuilt: formula test exit 0 (65 files, 1701 tests), formula typecheck 0, lint test 0 (196 files, 8198 tests), lint typecheck 0; eslint --no-inline-config on the 9 changed ts files exit 0. Ablation via ablation-replace: optionObjectUnregisteredPlatform gated on a __ablated_rung4 globalThis marker (anchor 1 to 0, blob 9d18678f to 25b7acc6), formula rebuilt, dist-preflight marker present in 2 built files; formula option test 1 failed / 48 passed (rung four pin), lint option test 2 failed / 22 passed (sys_userz warning pin, validate and publish agreement pin); restored blob 9d18678f equals HEAD, git diff HEAD empty, rebuilt, preflight --absent exit 0, whole tree clean, 49/49 and 24/24.",
      "gates": "head 030a4930: check-changeset-no-major --base origin/main 0, check-empty-changeset --base origin/main 0, pnpm check:changeset-gate-self-tests 0, check-changeset-fixed 0. The full derived set (102 families, 0 NOT-MEASURED) was reconciled on d73ae610; this round touches formula and lint source, tests, two changesets and one docs page, and was not re-derived, per the seat's scope. main not merged (21 ahead at 05a30e6e, merge-tree clean at d73ae610).",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-② is yes (widening), no (narrowing) arm",
      "mcp_calls": "4 reads in this card thread so far — mcp__github__issue_read x2 on 30172, mcp__github__pull_request_read get_comments x1 and get_status x1 on 30727; none this round; no MCP write tool",
      "api_writes": "this report comment (POST issues/30172/comments via post-stamped relay); plus git push of 56928a5b and 030a4930 (not REST). PR body not patched",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Report taken 2026-11-06T00:50Z.

  10. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgUtH2CXA8Qb8bDBRom
    os-dev-report

    PR #30727 patch round after the second delta review (draft, head 7cea4186): the publish door judges an option hole's object against the live universe alone and refuses a name it does not carry, whatever its prefix; os validate / os build / os lint keep the ladder and rung ④'s warning. Revised body at /mnt/project-files/briefs/30727-body-r4.md; the PR body was not patched.

    {
      "issue": 30172,
      "round": "patch round 3 (delta review PASS with one should-fix, comment 6673674554)",
      "status": "done",
      "branch": "claude/project-thread-d8v0qv",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30727",
      "head": "7cea4186",
      "session": "session_01L6bhgUtH2CXA8Qb8bDBRom",
      "premise_still_valid": true,
      "summary": "SHOULD-FIX (publish door over-relaxed): the registry entry passes validateEmailTemplateHoleGrammar(stack, { liveUniverse: ctx.runtimeWriteType !== undefined }). With liveUniverse the option index is objectsAsServed alone (no packages[] rung, no platform-name null rung), no rung-four warning is emitted, and judgeMailHoleGrammar gets liveUniverse: true (new formula option), under which optionFieldRefusal skips rung four, so sys_userz and a platform name the live universe does not serve are refused option-object-unknown at error, the list-view-object-unknown reading. The CLI doors are unchanged (sys_userz a warning, sys_user resolves). AuthoringRuleContext.runtimeWriteType's doc names this reader; the formula and lint changesets and email-templates.mdx state the split. Pins: side by side per hole (sys_userz, sys_user) os validate and os build from the ladder, the publish door error against a live universe of [opportunity]; control: publish accepts sys_user.status once the live universe serves sys_user with a select status; formula: liveUniverse true refuses sys_userz, cloud_tenantz, ai_agentz and still accepts a carried field.",
      "tests": "head 7cea4186, formula rebuilt: formula test exit 0 (65 files, 1702 tests), formula typecheck 0, lint test 0 (196 files, 8200 tests), lint typecheck 0; eslint --no-inline-config on the 9 changed ts files exit 0. Ablation via ablation-replace: the liveUniverse expression in authoring-rules.ts gated on a __ablated_live globalThis marker (anchor 1 to 0, blob 3cb1d3c9 to baa96acd); lint option test 2 failed / 24 passed (the sys_userz and sys_user door pins); restored blob 3cb1d3c9 equals HEAD, git diff HEAD empty, tree clean, 26/26.",
      "gates": "head 7cea4186: check-changeset-no-major --base origin/main 0, check-empty-changeset --base origin/main 0, pnpm check:changeset-gate-self-tests 0, check-changeset-fixed 0. Full derived set last reconciled on d73ae610 (102, 0 NOT-MEASURED); not re-derived, per the seat's scope. main not merged.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-② is yes (widening), no (narrowing) arm",
      "mcp_calls": "4 reads in this card thread so far — mcp__github__issue_read x2 on 30172, mcp__github__pull_request_read get_comments x1 and get_status x1 on 30727; none this round; no MCP write tool",
      "api_writes": "this report comment (POST issues/30172/comments via post-stamped relay); plus git push of 7cea4186 (not REST). PR body not patched",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Report taken 2026-11-06T01:01Z.

  11. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgUtH2CXA8Qb8bDBRom
    os-dev-report

    PR #30727 patch round after the fourth delta review (draft, head 98f61ec6): a write into a package judges an option hole's object against that package's closure, and an object outside it gets the closure verdict and the declare-the-dependency remedy, not the unknown-object sentence; EmailTemplateHoleGrammarOptions is exported. Revised body at /mnt/project-files/briefs/30727-body-r5.md; the PR body was not patched.

    {
      "issue": 30172,
      "round": "patch round 4 (delta review PASS with two should-fixes, comment 6673851181)",
      "status": "done",
      "branch": "claude/project-thread-d8v0qv",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30727",
      "head": "98f61ec6",
      "session": "session_01L6bhgUtH2CXA8Qb8bDBRom",
      "premise_still_valid": true,
      "summary": "SHOULD-FIX 1 (package-scoped write): the registry entry passes packageScope beside liveUniverse when ctx.packageScope is set. formula MailHoleGrammarOptions gains packageScope { packageId } (implies liveUniverse) and the refusal rule option-object-outside-closure: names the object as outside package X's closure (its own objects, its declared dependencies' objects, platform objects), quotes OPTION_ARGUMENT_RULE and names the remedy, declaring the dependency in X's manifest. lint passes it and, for that rule, the hint is closureDependencyRemedy(object, packageId, 'name an object inside that closure') plus OPTION_ARGUMENT_RULE, the list-view-object-outside-closure reading. AuthoringRuleContext.packageScope's doc names the new reader. SHOULD-FIX 2: lint index exports EmailTemplateHoleGrammarOptions beside EmailTemplateHoleGrammarFinding. Pins: runtime gate write into com.acme.crm (dependency com.acme.core) with opportunity owned by an undeclared package gets the closure message (not 'resolves to no object') and the declare-the-dependency hint; an object of com.acme.core accepted; the same write naming no package accepted. formula: narrowed objects refuse deal and sys_userz as outside the closure and accept a carried field. Changesets and email-templates.mdx updated.",
      "tests": "head 98f61ec6, formula rebuilt: formula test exit 0 (65 files, 1703 tests), formula typecheck 0, lint test 0 (196 files, 8203 tests), lint typecheck 0; eslint --no-inline-config on the 9 changed ts files exit 0. Ablation via ablation-replace: the packageScope spread in the registry entry gated on a __ablated_closure globalThis marker (anchor 1 to 0, blob eaef25cc to 9662b429); lint option test 1 failed / 28 passed (the outside-closure pin); restored blob eaef25cc equals HEAD, git diff HEAD empty, tree clean, 29/29.",
      "gates": "head 98f61ec6: check-changeset-no-major --base origin/main 0, check-empty-changeset --base origin/main 0, pnpm check:changeset-gate-self-tests 0, check-changeset-fixed 0. Full derived set last reconciled on d73ae610 (102, 0 NOT-MEASURED); not re-derived, per the seat's scope. main not merged.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-② is yes (widening), no (narrowing) arm",
      "mcp_calls": "4 reads in this card thread so far — mcp__github__issue_read x2 on 30172, mcp__github__pull_request_read get_comments x1 and get_status x1 on 30727; none this round; no MCP write tool",
      "api_writes": "this report comment (POST issues/30172/comments via post-stamped relay); plus git push of 98f61ec6 (not REST). PR body not patched",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Report taken 2026-11-06T01:13Z.

  12. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    Release: claim 6671557736 (domain:spec#2), at 2026-11-06T03:31Z.

    The maintainer ordered the lanes to finish what they hold and stop once it merges: 「所有车道当前任务处理完,合并后就下班」 (this account's project, 2026-11-06T01:17Z). This card cannot merge tonight: its landing pairs with the server half #30730, which no seat has claimed.

    Resume-from: claude/project-thread-d8v0qv@98f61ec65b298cd6e78f38e3e5600a92e7c35861


    Generated by Claude Code

  13. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 6, 2026
  14. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  15. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Claim: PM loop round 4 · 2026-11-06T03:50Z (resuming the spec half released in 6676118909, cross-seat)
    Session: session_016WV4cyvF7SP65frVFAYLv7
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-d8v0qv (PR #30727's head branch, Resume-from 98f61ec65b298cd6e78f38e3e5600a92e7c35861); no new push is planned: the head lands as it stands
    Worktree: objectstack-issue-30172
    Domain: domain:spec (carried by domain:server#5)
    Seat: domain:server#5
    Cross-seat declaration: no domain:spec seat has an open mailbox; triage answered that designation is not its to make and leaned that this seat carries it (6676310185 on #30661); the maintainer chose that this seat carries the spec half and lands it back to back with #30781 (the seat thread's decision card, 2026-11-06T03:49Z). The pair is #30727 (this card) then #30781 (#30730)
    File surface: unchanged from domain:spec#2's claim 6671557736 (packages/spec/src/system/email-template.zod.ts and its regenerated references, packages/formula/src/**, packages/lint/src/** mail-template hole rules, the mail-template docs page, one changeset per package); ⛔ plugin-email (#30730's), skills/** (#30731), ui/**, docs/adr/**, .claude/**
    Container & model: M, carried by the seat (no new dev round)
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: as in 6671557736
    Thread-read: 6676118909
    Premise: holds at PR #30727 head 98f61ec6: contract review delta PASS with no finding 6674014531 on that head; all check-runs on it green or skipped (59 success, 10 skipped); merges clean with main as read at 2026-11-06T03:50Z
    Serial constraints cleared: #30781 is built on this head and lands right after; no other open PR touches email-template.zod.ts; PR #30753 touches packages/lint/src/index.ts and the tree merges clean

  16. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Landed · 2026-11-06T04:47Z · PR #30727

    Probe Reading
    PR #30727 merged, merge commit 3da9a4c1, through the merge queue (seat ACCEPT on head 98f61ec6; contract review delta PASS 6674014531 on that head; not governed)
    #30172 closed by Fixes #30172

    Shipped: the spec half of ruling F. The mail-template option formatter ({{ stage | option:opportunity.stage }}) is in the grammar, the lint rules and the renderer. Carried by domain:server#5 under the cross-seat claim on this card. The server half (#30730, PR #30781) lands right after it; until then a template using option saves and then fails at send. Skills-docs: follow-up #30731 stays open for the skills lane.

    State change: pm:dispatched removed, assignee cleared.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions