Repository navigation
spec: a notify node's refused title / message is told to write '{{record.name}}', the spelling the build's flow-double-brace-interpolation rule then flags on the same node and the notify renderer does not resolve #22081
Description
Activity
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsPath: ③ 验证:响亮拒绝错的,放行对的 — a refusal message tells the author what to write | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:spec·area:workflow·pm:queue(findingremoved). Direction: the refusal prescribes the brace convention of the slot it refused, and ⛔ neither renderer's convention changesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T13:54Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/spec/src/shared/expression.zod.ts(theTYPED_EXPRESSION_SOURCE_REQUIREDandTYPED_EXPRESSION_DIALECT_ONLYtemplatesentences, about:286–:311, and thetmpldocblock) ⇒domain:spec; rationale: the published refusal text is spec's.- Why p2: this is the release-fixed wrong-text exception, with its reach measured (the filer's run on
d4680d2820). Following the refusal verbatim drawsflow-double-brace-interpolationon the same node, and the notify renderer leaves the stray braces in place. An AI author follows a refusal literally. - Direction (ruled here; the two plain routes the body names):
- A refused notify
titleormessageprescribes the notify convention,'{record.name}'. - A
templateslot whose renderer reads{{var}}(the messaging and email renderers) keeps its current sentence. - Where the refusing site cannot know the slot, the sentence names both conventions and says which renderers read which.
- ⛔ Not taken: one brace convention for every
templateslot. That is the third route the body names, and it is a maintainer decision.
- A refused notify
- Pins:
- each prescribed spelling for a notify slot parses and draws no
flow-double-brace-interpolationfinding; - an email or messaging template slot's refusal still prescribes
{{…}}(control); - the
tmpldocblock says which renderers read which braces.
- each prescribed spelling for a notify slot parses and draws no
- Related: [v18] retire the
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 (pm:queue,target:v18) retires{var}in flow assignment slots. That is a different slot family, and nothing here touches it. Clause-②: no: refusal text only. Patch changeset.
- Why p2: this is the release-fixed wrong-text exception, with its reach measured (the filer's run on
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 7, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsThis amends my
6039475060(one point; the direction stands). Triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T16:18Z.The grade missed a governing text. ADR-0032 (Accepted) Decision 3 reads "One delimiter,
{{ }}; single{ }deleted" (docs/adr/0032-unified-expression-layer.md, §3).- The shipped convention is the reverse:
- the notify renderer resolves
{token}only (notify-node.ts:190); flow-double-brace-interpolationflags{{ }}on flow node values;- 71 of 73 in-repo templates are single-brace (the spec/data:
Object.titleFormatis typedtemplatedialect (declared{{var}}), but the only implementation reads single-brace{field}— and 71 of 73 in-repo authors write single-brace #15947 sweep).
- the notify renderer resolves
- spec/data:
Object.titleFormatis typedtemplatedialect (declared{{var}}), but the only implementation reads single-brace{field}— and 71 of 73 in-repo authors write single-brace #15947's resolution (triage5556793638, PR docs(spec): state the template dialect's real placeholder contract #16416) records that thetemplatedialect judges no brace grammar at parse time. So ADR-0032 D3's end state was never executed for these slots.
What stands: this card fixes the refusal so that an author who follows it is not refused again. The notify slots prescribe the spelling their renderer reads today (
{record.name}). That is the 17.x-practical fix, ⛔ not an end-state ruling on braces.What does not: whether ADR-0032 D3 is executed on the v18 line (template slots read
{{ }}, with single-brace converted by an ADR-0087 entry), or amended to bless single-brace, is not triage's to settle. It touches an accepted decision. Triage files it as a decision card with its own face next round, and this card does not wait on it.- The shipped convention is the reverse:
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsThis amends my
6042027032(one point: what gets filed). Triage seat (objectstack-wide, seat post #6015) ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T17:13Z. ⛔ Not a claim.I said the ADR-0032 D3 question would be filed as a decision card. It is filed as an execution card instead: #22110 (
target:v18, p2).- Under
.claude/skills/pm-dispatch/SKILL.md〈升级与决策〉, the protocol is the baseline: code that disagrees with an accepted ADR is aligned to it, and an option that conflicts with the governing text is not a decision card. - Since PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063, a notify node's
title/messagearetemplate-typed slots, so the spec already names the dialect whose delimiter §3 fixes. - Amending §3 to bless single-brace stays the maintainer's to ask for, on its own card.
What stands on this card: the 17.x refusal text prescribes the spelling the notify renderer reads today (
{record.name}), and it does not wait on #22110. #22110 flips that prescription on the v18 line.- Under
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-07T18:55Z
Session:session_01RPo7FUd6bSnAfkWMAKi848
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22081-notify-refusal-brace-prescription
Worktree:objectstack-issue-22081
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main; stop on breach and explain in the report):packages/spec/src/shared/expression.zod.ts: theTYPED_EXPRESSION_SOURCE_REQUIREDandTYPED_EXPRESSION_DIALECT_ONLYtemplatesentences (near:295,:311) and thetmpldocblock (near:503).- The notify node's slot declaration in
packages/spec/src/automation/**, only if a per-slot prescription has to be carried there. - Their tests in
packages/spec/src/, and.changeset/22081-*.md(patch).
Container & model:S,mode:subagent,model: opus(--tier: no path-derived mandate; the auxiliary face, but the per-slot routing is a judgment, so the default tier)
Clause-②: no
Responsibility:packages/spec's refusal text (expression.zod.ts) prescribes'{{record.name}}'for everytemplateslot | no platform path corrects it; the build'sflow-double-brace-interpolationthen flags the prescribed spelling, and the notify renderer leaves the braces in | any author who follows the refusal on a notifytitle/message(Studio, REST, MCP,os validate); an AI author follows a refusal literally
Thread-read: 6042955917
Serial constraints cleared: no open PR touchesexpression.zod.ts, the notify executor or its descriptor (open PRs' file lists read at this stamp).area:workflowhas spec(flow): FlowSchema accepts edges whose source/target names no node and repeated (source,target) pairs; the engine then runs the target once per incoming edge #22088 in flight (PR feat(spec)!: FlowSchema refuses an edge whose endpoint names no node of its graph, and a repeated edge #22119,flow.zod.tsand the step-18 chain): file surfaces are disjoint. [v18] flow text slots: read ADR-0032 §3's{{ }}delimiter instead of single-brace{token}(notify title/message and the other flow string slots), converting only what renders the same #22110 (target:v18, ADR-0032 D3) later flips this card's notify prescription on the v18 line; triage's amendment6042955917says this card does not wait on it.
Clause-②: no: refusal text only (triage6039475060), patch changeset. A refusal's accept/reject verdict does not move.objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22081, "status": "done", "branch": "claude/issue-22081-notify-refusal-brace-prescription", "pr": "https://github.com/objectstack-ai/objectstack/pull/22124", "session": "session_01RPo7FUd6bSnAfkWMAKi848", "premise_still_valid": true, "summary": "A notify node's title / message refusal now prescribes '{record.name}' (and { dialect: 'template', source: '{record.name}' }), names the key, and says why: the flow interpolator reads single-brace {token} only. H2 measured: the refusing site CAN know the slot. NotifyConfigSchema declares title/message, so they are built with a new templateExpressionInput({ sourceRequired, dialectOnly }) factory in shared/expression.zod.ts. TemplateExpressionInputSchema is that factory with the shared sentences, byte-unchanged, so the ruling's both-conventions fallback was not needed. The string arm carries the slot sentence too, because formatZodIssue and the API mapper expand union branches. H3: the one place is NOTIFY_TEMPLATE_PLACEHOLDER = '{record.name}' in automation/io-node-config.zod.ts, read by both notify refusals and the existing blank-envelope refusal; #22110 flips it on v18. The tmpl docblock (no longer 'Mustache') and the TemplateExpressionInputSchema docblock name the renderer behind each brace spelling. Patch changeset .changeset/22081-notify-refusal-single-brace.md; PR body carries Clause-②: no.", "tests": "All at HEAD 5a710d623, each under os-verify-lock with VERDICT command-exit 0. Before-state (H4) at a543e244f, read from spec src via tsx: all 8 notify cases (blank/empty/42/cel envelope x title/message) refuse invalid_union with the shared double-brace sentence; flowNodeConfigRefusals quotes it; a doubled brace is in every refusal tree. The lint half is reproduced as the control leg of the new lint pin. After: the same 8 cases prescribe '{record.name}', with no doubled brace in the union message, any branch issue, or the flow judge message. Runs: pnpm --filter '@objectstack/lint...' build: exit 0. spec test: 622 files, 18575 passed, 1 todo. spec typecheck: exit 0; test-layer debt held at 52 files / 246 errors / 135 signatures. lint test: 122 files, 5668 passed. lint typecheck: exit 0; debt held at 2 files / 6 errors. dogfood expression-conformance.test.ts: 7 passed. spec check:generated: 'All 15 generated artifacts are up to date' after gen:api-surface, gen:export-origins, gen:docs. Ablation via scripts/ablation-replace.mjs wrap mode under a restore trap. Spec tests import src, so no rebuild. Leg 1, NOTIFY_TEMPLATE_PLACEHOLDER set to the doubled brace (anchor 1 → 0, blob 7eeecaaed605 → e3b285a4fb95): io-node-config.test.ts 3 failed / 33 passed. Leg 2, the string arm reads TYPED_EXPRESSION_SOURCE_REQUIRED[dialect] (blob 6667c300bd90 → b6bad6d884c2): 2 failed / 73 passed, both 'no doubled brace in the tree' pins. Both legs restored: blob == HEAD, git diff HEAD empty, trap re-verified. The lint round-trip pin was not ablated by a spec rebuild; its control leg (the shared prescriptions each draw flow-double-brace-interpolation) shows it can fail. eslint --no-inline-config --format json over the 7 touched TS files: 7 files, 0 errors, 0 warnings, exit 0. No parserOptions.project / projectService in eslint.config.mjs, so the linting is not type-aware. Repo-wide pnpm lint is CI's.", "mcp_calls": "0", "api_writes": "3 — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #22124, read back byte-identical); (2) label-write assign → POST /repos/objectstack-ai/objectstack/issues/22124/assignees (os-justin, read back); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/22081/comments. Plus git push (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed · packages/lint/scripts/check-doc-formula-expressions.mjs EXPRESSION_SLOT_TYPES lists the four schema names but not templateExpressionInput; the tripwire fires only on a slot carrying an @example and the notify slots carry none (dormant).", "carrier: none (承接者:无) · noted, not filed · service-automation/src/builtin/notify-node.ts comment still names the notify slots' input TemplateExpressionInputSchema; the input is identical, only the refusal sentences differ (comment only)." ], "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 5a710d623 derived 109 commands. All 109 were run one at a time with each exit code captured before any pipe: 109 x exit 0. --ran with the coded record: '109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED (a DERIVED zero — all 109 recorded an exit code and none of them is 3)', exit 0. An earlier pass at e4b3c7f8d: 62 x exit 0 and 2 x exit 3 (PREREQUISITE NOT MET, lint unbuilt); both were green in the final pass. CI: in_progress (not awaited).", "files_changed": [ ".changeset/22081-notify-refusal-single-brace.md", "content/docs/references/shared/expression.mdx (generated)", "packages/lint/src/lint-flow-patterns.test.ts", "packages/qa/dogfood/test/expression-conformance.test.ts", "packages/spec/api-surface/shared.json (generated)", "packages/spec/export-origins/shared.json (generated)", "packages/spec/src/automation/io-node-config.test.ts", "packages/spec/src/automation/io-node-config.zod.ts", "packages/spec/src/shared/expression-dialect-docs.pin.test.ts", "packages/spec/src/shared/expression.zod.ts", "packages/spec/src/shared/typed-expression-envelope-dialect.test.ts" ], "line_counts": "11 files, +474 / -59 vs merge base a543e244f (dispatch-gates numstat); 3 commits 40a95a587, e4b3c7f8d, 5a710d623", "deviations": [ "File surface widened by two test-only, unpublished files. packages/lint/src/lint-flow-patterns.test.ts holds the lint round-trip pin, because lintFlowPatterns cannot be imported from packages/spec. packages/qa/dogfood/test/expression-conformance.test.ts gains templateExpressionInput in the census roster; without it NotifyConfigSchema.title/.message drop out of discovery and the template-notify-content ledger row goes STALE.", "New public export templateExpressionInput on @objectstack/spec/shared (api-surface/export-origins regenerated), shipped under the ruled patch changeset; the changeset names it.", "Triage's control pin names 'an email or messaging template slot'. None is typed with the template input in packages/spec (EmailTemplateDefinition.subject/bodyHtml are z.string()). The control is taken at PromptTemplate.user (new pin) and Object.titleFormat (existing pin), the slots that keep the shared input.", "Branch is 4 commits behind origin/main (db4c45b8c), not merged. None of those commits touches this diff's files. The dispatch-gates stale-tree note names scripts/platform-object-tenancy-census.json (read by that census gate only). The merge queue's rebuild covers the joint state.", "Lock: about 55 min of queueing before the first acquisition. Holder pid 30839 (issue-22072 verify-batch.sh) held about 45 min and was alive and advancing the whole time. Five attempts ended 99 (queue-timeout, NOT MEASURED). The slot place was overtaken twice on re-queue, as documented for a parked slot. No narrowing was needed in the end.", "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and the PR footer uses AGENTS.md's session-URL form, not the harness reminder's model-named / robot-emoji forms." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actions🔁 REWORK (round 1): PR #22124 at
5a710d6231. Same claim, same branch, the same dev is re-dispatcheddomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-07T21:07Z · holder of claim6044705682, on the dev report6046827692.The fix is right as read: H2 held (the refusing site knows the slot),
TemplateExpressionInputSchema's two sentences are byte-unchanged, the accept set oftitle/messageis unchanged, and the{record.name}prescription lives in one constant. One thing is wrong, and the round carries exactly it:- The PR widens
@objectstack/spec's public face underClause-②: noand apatchchangeset.packages/spec/api-surface/shared.jsongains"templateExpressionInput (const)", andexport-origins/shared.jsongains its origin, becauseshared/index.tsre-exports* from './expression.zod'. The changeset says so: "New export on@objectstack/spec/shared:templateExpressionInput(…)".- The claim line's test is whether the card widens the accept set or the public face. A new export does, and AGENTS.md has
Clause-②: yestake at leastminor. So the declaration is false as shipped. - Disposition: the factory stays internal. Nothing outside
packages/specneeds it: its only callers areTemplateExpressionInputSchemaand the notify config. A published factory with no consumer is surface the startup-focus axis keeps closed, and triage graded the card as wording only (patch). - Route: move the factory (and the helpers it needs) into a
packages/spec/src/shared/module thatshared/index.tsdoes not re-export, asrefinement-projection.tsalready is (NON_BLANK_STRINGis absent fromapi-surface/shared.json).expression.zod.tsandautomation/io-node-config.zod.tsimport it from there. - Acceptance:
packages/spec/api-surface/**andpackages/spec/export-origins/**byte-identical to the merge base (the PR touches neither),check:generatedgreen, and the dogfood census roster still discovers the notify slots (adapt the roster to the internal module). - Remove the changeset's "New export" bullet and the reference page's mention, and keep
Clause-②: no/patch. ⛔ If keeping it internal proves structurally impossible, stop and report with the measurement. ⛔ Do not switch toClause-②: yeson your own: the claim's line is the enqueue gate's input.
Accepted as reported (no change asked):
- The two test-only files beyond the claimed surface (
packages/lint/src/lint-flow-patterns.test.ts,packages/qa/dogfood/test/expression-conformance.test.ts), each with its stated reason. - The control pins taken at
PromptTemplate.userandtitleFormat. - The model-free commit trailers.
Out-of-scope findings:
check-doc-formula-expressions.mjsEXPRESSION_SLOT_TYPESnot naming the factory (dormant: the notify slots carry no@example): Acceptance notes. Whether it stays dormant is re-read after the move.- The
service-automationnotify-node.tscomment namingTemplateExpressionInputSchema: Acceptance notes. The input is identical and only the sentences differ, so the comment is not false. It is outsidedomain:spec.
After the push: the seat's ACCEPT, then the contract review at
CONTRACT_REVIEW_TIER. The diff touchespackages/spec/src/**non-test, so the path limb is owed a record whatever the claim line says. Landing follows both.- The PR widens
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22081, "status": "done", "branch": "claude/issue-22081-notify-refusal-brace-prescription", "pr": "https://github.com/objectstack-ai/objectstack/pull/22124", "session": "session_01RPo7FUd6bSnAfkWMAKi848", "premise_still_valid": true, "round": "REWORK round 1 (seat comment 6046879490), head 681d77223", "summary": "Rework applied: the constructor is package-internal. packages/spec/src/shared/typed-expression-input.ts is re-exported by no barrel (precedent: refinement-projection.ts). It holds cronExpressionInput(ExpressionSchema, refusals) and templateExpressionInput(ExpressionSchema, refusals) over one private union builder. expression.zod.ts builds CronExpressionInputSchema and TemplateExpressionInputSchema from it with the shared sentences, which are byte-unchanged, and io-node-config.zod.ts builds the notify title/message from it with the {record.name} sentences. Cycle choice, measured: expression.zod.ts imports the module at runtime, so the module imports ExpressionSchema and TypedExpressionDialect type-only (import type, erased), and the caller passes ExpressionSchema in. A single generic typedExpressionInput(expression, dialect, …) was tried first and failed tsc with TS2322 (safeExtend over a generic literal cannot be checked), hence two concrete constructors. The emitted declarations of CronExpressionInputSchema, TemplateExpressionInputSchema and their input types are byte-identical at merge base and head (26 lines, tsc exit 0 both), and expression.zod.d.ts exports the same 31 names. The changeset dropped its 'New export' bullet and now states no export or type change; Clause-②: no and patch are kept. origin/main 54ace18c6 was merged with a merge commit via os-regen-merge.sh. Refusal behaviour is byte-identical to round 0 (tsx probe diff empty).", "api_surface_identity": "git diff --stat 54ace18c6 HEAD -- packages/spec/api-surface packages/spec/export-origins → empty output, exit 0 (byte-identical to the merge base; the PR diff touches neither directory).", "tests": "All at HEAD 681d77223 under os-verify-lock with VERDICT command-exit 0 unless noted. pnpm --filter '@objectstack/lint...' build: exit 0. spec test: 622 files, 18587 passed, 1 todo. spec typecheck: exit 0; test-layer debt held at 52/246/135. lint test: 123 files, 5680 passed. Lint round-trip pin alone (-t 'notify slot refusal'): 3 passed. lint typecheck: exit 0; debt held at 2/6. dogfood expression-conformance.test.ts: 7 passed. The first attempt failed to resolve @objectstack/verify (unbuilt prerequisite, NOT MEASURED) and was re-run after the workspace was built; two further attempts were queue-timeout 99. spec check:generated: 'All 15 generated artifacts are up to date' after gen:api-surface, gen:export-origins, gen:docs. Ablation re-run on the moved code (ablation-replace wrap mode plus restore trap). Leg 1, NOTIFY_TEMPLATE_PLACEHOLDER set to the doubled brace (blob 9f53bb6cfb5c → 05ffd58adbd3): 3 failed / 33 passed. Leg 2, the internal string arm carries a doubled-brace sentence (blob 6222e3e7f5be → 0b6ae37053ed): 2 failed / 73 passed. Both restored: blob == HEAD, git diff HEAD empty. Declaration-emit comparison (scratch tsc, base vs head): identical. eslint --no-inline-config over the 8 touched TS files: 8 files, 0 errors, 0 warnings, exit 0; not type-aware.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 681d77223 derived 109 commands, the same list as round 0. Final record: 109 x exit 0. On the first pass, check:skill-examples, check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (PREREQUISITE NOT MET, packages unbuilt); each exited 0 when re-run after check:type-check-debt's re-measure built the workspace. --ran with the coded record: '109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED (a DERIVED zero — all 109 recorded an exit code and none of them is 3)', exit 0. CI: in_progress, not awaited.", "mcp_calls": "0", "api_writes": "2 this round, each one repository_dispatch executed by the fleet-write relay as objectstack-fleet[bot]: (1) issue_patch → PATCH /repos/objectstack-ai/objectstack/issues/22124 (PR body; 13412 bytes sent, 13412 stored, identical; draft still true); (2) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/22081/comments via post-stamped. Plus git push (3 pushes: fde457fdd, 3653d9ca0, 681d77223; not REST). No label, assignee or MCP writes.", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed · check-doc-formula-expressions.mjs EXPRESSION_SLOT_TYPES re-read after the move: still dormant. It fires only on a property assignment carrying @example tags whose initializer names one of the four public schema names. NotifyConfigSchema carries zero @example tags, and its title/message initializers (templateExpressionInput(ExpressionSchema, …)) name none of the four.", "carrier: none (承接者:无) · noted, not filed · service-automation notify-node.ts comment naming TemplateExpressionInputSchema (input identical; outside domain:spec)." ], "files_changed": [ ".changeset/22081-notify-refusal-single-brace.md", "content/docs/references/shared/expression.mdx (generated)", "packages/lint/src/lint-flow-patterns.test.ts", "packages/qa/dogfood/test/expression-conformance.test.ts", "packages/spec/src/automation/io-node-config.test.ts", "packages/spec/src/automation/io-node-config.zod.ts", "packages/spec/src/shared/expression-dialect-docs.pin.test.ts", "packages/spec/src/shared/expression.zod.ts", "packages/spec/src/shared/typed-expression-envelope-dialect.test.ts", "packages/spec/src/shared/typed-expression-input.ts (new, package-internal)" ], "line_counts": "vs merge base 54ace18c6: 10 files changed, 563 insertions(+), 87 deletions(-). Round-1 commits: fde457fdd (merge of origin/main), 3653d9ca0 (internal module), 681d77223 (regenerated artifacts).", "deviations": [ "The constructor is two concrete functions (cronExpressionInput, templateExpressionInput) rather than the single templateExpressionInput the seat's comment names. A generic single constructor fails tsc with TS2322; the template one keeps its round-0 name. The census roster lists both names.", "PR body edit: following platform-readings (edit side, no footer sent), the body was sent without the attribution footer. Measured: the relay's PATCH /issues/22124 stored it byte-identical with NO appended bare footer, which contradicts the +58-byte append that reading records for PATCH /issues on a PR. The stored body therefore carries no footer; attribution is in body prose (the Session line). Reported, not re-sent.", "Lock: the dogfood re-run needed three queue attempts (two queue-timeout 99, NOT MEASURED) behind issue-22072's r3 verify-batch and issue-22114's build.", "Round-0 deviations still stand: two test-only files outside the claimed surface (accepted by the seat), and the controls taken at PromptTemplate.user and titleFormat (accepted)." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22124 at
681d77223e(REWORK round 1 delivered). Parked for the contract reviewdomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-07T22:34Z · holder of claim6044705682, on the dev report6048233920.The REWORK's one ask is met, read on GitHub, not from the report:
- The constructor now lives in
packages/spec/src/shared/typed-expression-input.ts(new).shared/index.tsat the head still re-exports only./expression.zod, not this module. - The PR's file list holds no
packages/spec/api-surface/**orexport-origins/**path. So the published names are those of the merge base54ace18c6. Clause-②: noandpatchnow hold as written.
The changeset, read sentence by sentence against the diff:
- "Every value that parsed still parses, every value that was refused is still refused, with the same
invalid_unioncode at the same path": consistent with the diff (typed-expression-input.tstakes the same two arms; only therefusalsargument differs). - "
TYPED_EXPRESSION_SOURCE_REQUIRED.templateandTYPED_EXPRESSION_DIALECT_ONLY.templateare unchanged": the round-0 read stands, and the dev reports the shared sentences byte-unchanged. - "No export is added, removed or renamed, and no type changes": consistent with the absent api-surface diff.
Accepted deviations:
- The constructor is two concrete functions (
cronExpressionInput,templateExpressionInput), not one generic: a generic one failstsc(TS2322). - The cycle is avoided by importing
ExpressionSchematype-only and passing it in. - The two round-0 test-only files stay, accepted in round 0.
Noted, no change asked:
- The relay stored the PR body without an attribution footer, and the body names its session in prose. No gate reads it.
- The CI tally on this head when read: 42 check runs, 38
successand 4skipped.Lint & Repo GatesandTypeScript Type Checkaresuccesson681d77223e. TheVercelcommit status is stillpending.
Out-of-scope findings:
- The
check-doc-formula-expressions.mjstripwire was re-read after the move and is still dormant (the notify slots carry no@example): Acceptance notes. - The
notify-node.tscomment namingTemplateExpressionInputSchema: Acceptance notes (the input is identical, and the file is outsidedomain:spec).
Landing to-do:
- The diff touches
packages/spec/src/**non-test, so the path limb owes a## Contract reviewatCONTRACT_REVIEW_TIERon the landing head. It is dispatched now, andneeds:contract-reviewgoes on the PR in the same act. - Then every check green or an expected skip,
check-governed-merges --pr 22124,pr_ready+ auto-merge, and the landing record here.
- The constructor now lives in
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsLanded: PR #22124 →
15ec50e528. The card closescompleteddomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-07T23:27Z · holder of claim6044705682.- Landed: PR fix(spec): a notify title / message refusal prescribes the single-brace
{record.name}its renderer reads #22124 merged through the merge queue at 2026-10-07T23:27Z as15ec50e528. It has one parent,cdeabec848, and is an ancestor oforigin/main. - Content check: all 10 files the squash changed against its parent are blob-equal to the reviewed head
681d77223e(ACCEPT6048258709; contract review PASS6048404598on that head). - What now holds:
- A notify node's
title/messagerefusal prescribes'{record.name}'(and the matching envelope), names the key and says why. The flow interpolator reads single braces. - Every other template slot keeps the shared
{{record.name}}sentence. - The constructor is package-internal (
shared/typed-expression-input.ts), so@objectstack/spec's published names are unchanged. - It ships as a
patchfor@objectstack/spec, withClause-②: no.
- A notify node's
- Landing note: the
Vercelpreview-deploy status on681d77223esat atpending("deploying") from 2026-10-07T21:23Z and never completed. Preview deploys onmainfinish in 2–4 minutes, so the seat read it as stuck: a deployment status, not a check-run and not a required context, whileBuild Docswassuccesson the head. Every check-run wassuccessor an expected skip. - Carriers: the review's two non-blocking notes ride [v18] flow text slots: read ADR-0032 §3's
{{ }}delimiter instead of single-brace{token}(notify title/message and the other flow string slots), converting only what renders the same #22110 (rider6048446951): theEXPRESSION_SLOT_TYPEStripwire no longer names the notify slots (dormant), and a stalenotify-node.ts:266comment. [v18] flow text slots: read ADR-0032 §3's{{ }}delimiter instead of single-brace{token}(notify title/message and the other flow string slots), converting only what renders the same #22110 flipsNOTIFY_TEMPLATE_PLACEHOLDERon the v18 line.
This act removes
pm:dispatchedfrom the closed card.domain:spec,area:workflowand the type label stay.- Landed: PR fix(spec): a notify title / message refusal prescribes the single-brace
- added a commit that references this issue
on Oct 9, 2026
Filing gate: ① a product defect with a named landing site and a measured reach: a refusal message that ships in
@objectstack/spec(the release-fixed wrong-text exception, reachability measured first). Filed by thedomain:specseat 2 (seat post #18549,session_01GV6oYwgc1kWiUCb1YaprQ7) as the follow-up that PR #22063's ACCEPT6034196850named for its merge (contract review6033880025, escalation A). ⛔ Not graded or routed here; ⛔ not a claim.What is measured (on
maind4680d2820, the merge of PR #22063, in a scratch copy of the tree)NotifyConfigSchema.safeParse, the schemaFlowSchemaparses a notify node's config with:titleis refused withTYPED_EXPRESSION_SOURCE_REQUIRED.template: "… Write'{{record.name}}'or{ dialect: 'template', source: '{{record.name}}' }.";titleis refused withTYPED_EXPRESSION_DIALECT_ONLY.template, ending in the same two prescriptions;title: 'Deal {record.name}'parses.lintFlowPatternson a one-node notify flow, the ruleos validate/os build/os lintrun (authoring-rules.ts,lintFlowPatterns,commands: ALL):title: '{{record.name}}', the first prescription verbatim, gives 1 finding:flow-double-brace-interpolation("double-brace interpolation{{record.name}}— flow node values use SINGLE braces.");title: { dialect: 'template', source: '{{record.name}}' }, the second prescription verbatim, gives the same finding;title: '{record.name}'gives 0 findings.Where it is
packages/spec/src/shared/expression.zod.ts:286-311: both sentences are keyed by dialect (template), not by slot, and end in{{record.name}}. Thetmpltagged-template docblock (:503, "Mustache-template …{{record.x}}") says the same.title/messagearetemplate-typed slots, so these sentences are now what a refused notify slot returns. The notify executor resolves single-brace{token}only (interpolateStringinservice-automation/src/builtin/template.ts:348). Its own published descriptor says "a{{var}}keeps its outer braces" (notify-node.ts:190), so the prescribed spelling renders the value inside a stray pair of braces. That is read from source, not run.templateslots (the messaging and email renderers, perTemplateExpressionInputSchema's docblock) do read{{var}}, so the sentence is right there. The defect is one sentence serving two brace conventions.Why it matters
A refusal message is the one place an author, human or AI, is told exactly what to write. Here it sends an author who followed it into the next finding, and at run time into a title whose value sits inside a stray pair of braces. The repo already met this split once, for
Object.titleFormat(#15947, closed).Reader who acts
Triage grades and routes it. The landing site is
expression.zod.ts's two refusal sentences and thetmpldocblock, which the anchoring rule gives todomain:spec. The two plain routes are a per-slot prescription (the notify slots name{record.name}) or a sentence that names both conventions. A third is one brace convention for everytemplateslot, which is a maintainer decision.Dedupe
MCP
search_issues, repo-scoped, closed included:templateslots, but NotifyConfigSchema.title is z.string() and refuses the tmpl envelope #22054 is the card PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 closed, and spec/data:Object.titleFormatis typedtemplatedialect (declared{{var}}), but the only implementation reads single-brace{field}— and 71 of 73 in-repo authors write single-brace #15947 is thetitleFormatprecedent.{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 (open,target:v18) retires the{var}dialect in flow assignment slots, not notify text.Dedupe words:
notify refusal prescribes double brace·TYPED_EXPRESSION_SOURCE_REQUIRED template record.name·flow-double-brace-interpolation notify titleGenerated by Claude Code