Repository navigation
docs(skills): objectstack-automation teaches the {{ }} delimiter in flow text slots - #22475
Merged
os-zhuang merged 1 commit intoOct 9, 2026
Merged
Conversation
…low text slots
The notify example, the dialect paragraph and eval id 2 restate the text
slots (notify title / message, screen title / description, end message) in
the {{ }} delimiter the build and save doors read, and name which slots keep
the single-brace template.
Claude-Session: https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA
Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Contributor
Author
维护者速读 — PR #22475(#22454)— skills 席 1,2026-10-09T12:02Z
|
This was referenced Oct 9, 2026
os-zhuang
approved these changes
Oct 9, 2026
os-zhuang
marked this pull request as ready for review
October 9, 2026 12:24
os-zhuang
enabled auto-merge
October 9, 2026 12:24
os-zhuang
deleted the
claude/issue-22454-automation-skill-text-slot-delimiter
branch
October 9, 2026 13:02
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #22454
Clause-②: no
What changed, and why
PR #22315 made a flow's TEXT slots — a
notifynode'stitle/message, ascreennode'stitle/description, a refusingendnode'smessage— read ADR-0032 §3's{{ }}delimiter. The door's own rule (packages/spec/src/automation/flow-text-slot-template.ts,TEXT_SLOT_TEMPLATE_REFUSAL): 「A flow text slot reads{{ }}template holes (ADR-0032 §3), not the single-brace{…}dialect: a{…}token here is no placeholder any more and would be sent as literal text, so it is refused.」 The published skill still taught the single brace at three sites. Each is restated to whatmainenforces today, nothing beyond it:SKILL.mdnotify example (base:108–109):title: 'Done: {{ record.title }}'. The oldmessage: 'Closed by {$User.Id}'has no mechanical flip —{$User.Id}is not a variable, so no hole spells it; the door says to compute it into a variable first — so the body is now'Closed {{ record.closed_at | date:long }}', a path with a formatter, which the door accepts and renders (measured below). Therecipients/sourceIdlines keep their single brace: value-like positions still go throughinterpolate().SKILL.mddialect paragraph (base:238–244): three statements, one door each — text slots render{{ }}holes (a variable path, optional formatter, no logic) andos validate/registerFlowrefuse a{…}token there with its{{ }}spelling;{$User.Id}/{NOW()}have no hole and{{ $User.Id }}renders blank (measured) — assign them to a variable first; every other slot (recipients,inputs,http,filter) keeps the single-brace template. The clause "{{x}}is the template-field dialect" is deleted —{{ }}is the text-slot dialect itself.expected_outputnamestitle: 'Won: {{ record.name }}'(a spelling the door accepts);must_containgains"{{";must_not_containdrops"{{", which pinned the old dialect and would have failed the correct answer. No single-brace negative is added:recipients: '{record.owner_id}'is a legitimate single-brace value on the same node, so any{record.substring would false-fire, and no runner in this repository reads these assertions (Acceptance notes).Two sentences in § Filter tokens that listed text slots among the single-brace positions were re-read: "in
title,messageandurla bare{current_year_start}is a nonsense reference" keeps onlyurl(a text slot reads no single-brace token at all); "(message body,httpurl) only renders an empty string — a warning" now says "(a{{ }}hole,httpurl)" —packages/lint/src/validate-flow-template-paths.tsjudges a{{ record.x }}hole's path exactly like a bare token, so the warning half still holds in the hole spelling.Every slot the paragraph names, re-read against
mainatd87dff67maindoestitle/message, screentitle/description, endmessage{{ }}holes; a{…}token refused atNotifyConfigSchema/ScreenConfigSchema/EndConfigSchema, atregisterFlowand atobjectstack validate(error), then the holes compiled byvalidateExpression('template', …)flow-text-slot-template.ts;service-automation/src/engine.ts(registerFlow);lint/src/validate-expressions.tsrecipients,actionUrl,sourceId,payload,templateDatavaluesinterpolate()service-automation/src/builtin/notify-node.tsscript.inputsvalues interpolate {token} templates)spec/src/automation/schemaless-node-config.zod.ts;screen-nodes.tshttp(the whole config)The whole config is interpolate()d before reading)spec/src/automation/io-node-config.zod.ts;http-nodes.tsfilterfields.*/assignments.*(the skill's item 1, untouched){…}refused, CEL envelope prescribed;{NOW()}/{TODAY() ± N}/{$User.*}still accepted — #19939's C half landed in #22259, the card itself stayspm:queueflow-value-slot-template.tsMeasured on the built dists (lint closure built at
057ce436)NotifyConfigSchema.safeParse: the new example ACCEPTED;title: 'Done: {record.title}'REFUSED — "WriteDone: {record.title}asDone: {{ record.title }}";message: 'Closed by {$User.Id}'REFUSED — "{$User.Id}is not a variable, so no hole spells it: compute it into a variable with anassignmentnode … and write{{ v }}here"; eval id 2's'Won: {{ record.name }}'ACCEPTED;'By {{ $User.Id }}'ACCEPTED (it is syntactically a hole).validateExpression('template', …)from@objectstack/formula: all four strings compile.templateEngine.evaluate:Done: {{ record.title }}→Done: Fix login;Closed {{ record.closed_at | date:long }}→Closed October 9, 2026;By {{ $User.Id }}→Bywithok: true— the blank the paragraph now names.Enumeration
git grep -n -Pfor atitle/message/descriptionkey whose quoted value holds a{with no{on either side (a lookbehind and a lookahead; the dispatch's\{[^{]spelling also matches the second brace of a{{hole, so the lookaround form is the pin) overskills/objectstack-automationatd87dff67→SKILL.md:108,:109(exactly the seat's reading); on this head → 0 hits; over all ofskills/on this head → 0 hits.{{mention underskills/**isskills/objectstack-i18n/SKILL.md:246(i18n bundle placeholders, not flow text slots) — not this package, untouched.{{under this package:SKILL.md1 → 5 sites, the eval 1 → 2.{TODAY()}tokens inreferences/examples-flows.mdsit infilterpositions — single-brace onmain, untouched.Line and token budget
check-skills-token-ratchettokens (ceiling)SKILL.mdevals/flows-triggers-approvals.jsonskills/objectstack-automation/**Net lines −1, no line added. The text-slot sentences are paid by deleting the § Filter tokens lede ("The one slot where two
{…}dialects meet, and the one whose failure widens a query instead of narrowing it" — restated by the two bullets under it and by the routing table's "a dropped filter condition widens the query") and the now-false{{x}}clause; no re-wrap paid a line. No ceiling was moved:scripts/check-skills-token-ratchet.mjsis outside the claimed file surface (the eval's ceiling could be lowered 1,255 → 1,253 by a later edit).Gates, head
057ce436node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(change set from the merge base, no paths given) derived 24 commands; each was run with its exit captured before any pipe;--ranreconciliation: 「24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN」. All 24 exit 0 with their own verdict lines read — among themcheck-skills-token-ratchet(54 authored files within ceilings),check:skill-identifier-liveness,check:skill-docs,check:doc-formula-expressions,check:skill-compatibility,check:skill-frame-sync,check:nul-bytes,check:doc-authoring,check:corpus-claim-drift,check:pm-governed-merges. The list derived from the two paths (the dispatch's command) is identical. The derivation printed a STALE TREE note — 3 commits behindorigin/main, two derivation inputs changed (scripts/check-durability-degradation-log-level.mjs,scripts/measure-durability-swallow-family.mjs), neither in this list;origin/mainwas not merged in because those commits carrymerge=os-regenspec artifacts, which would hang a regeneration commit on a skills-only PR. The lint dependency closure was built once under the verify lock (129 s) socheck:skill-docsandcheck:doc-formula-expressionsread a freshdist/.Skill eval run: NOT MEASURED — no runner in this repository reads
must_contain/must_not_contain(a tree grep finds only the eval JSONs and two READMEs); the eval is a fixture for a model-driven grader. The siblingobjectui/scripts/check-skill-eval-tokens.mjsmatches a token case-sensitively as a whole token, degrading to an exact substring at a non-identifier end — so"{{"is a plain substring there.Changeset: none.
skills/**is in no published package'sfiles[]— 69 published packages, 0 nameskills, positive control 69 namedist— and the catalog ships from git bynpx skills add;skip-changesetapplied.Acceptance notes
SKILL.md:154("The handler reads{NODE_ID.error}(or run-wide{$error})") names no slot: in a text slot the spelling is now{{ n1.error }}/{{ $error.message }}, in a value-like position it stays single-brace. Left as is (budget; the door names the spelling loudly). carrier: 承接者:无 — noted, not filed.scripts/check-skills-token-ratchet.mjs, outside this card's file surface. carrier: 承接者:无 — noted, not filed.{{ $User.Id }}renders blank withok: trueand passesobjectstack validate(validate-flow-template-paths.tsskips an unresolvable root;lint-flow-patterns.tsreads a bare$only outside holes). A read-only inference about a shape no in-tree flow authors; the paragraph now warns. Not filed — class (c) needs a named producer and there is none.{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 as "the value-slot dialect", not landed: its C half (fields.*/assignments.*refusal) landed in feat(spec)!: flow value slots refuse the{…}template dialect, naming the CEL spelling of each token (#19939, C half) #22259; the card stayspm:queuefor its remaining passes. The skill's item 1 already matched that half and is untouched.维护者速读(草稿)
改了什么 — 把
objectstack-automation技能里仍在教「通知文案用单花括号{token}」的三处改成平台现在真正接受的{{ }}写法:通知示例、「文本槽 vs 其它槽」那段说明、以及评测用例 2。顺带把 Filter 一节里两句把文本槽列为单花括号位置的话改准确。净行数 −1,token 棘轮内(SKILL.md 余量 2)。为什么改 — #22315 落地后,
notify的title/message(以及screen的title/description、end的message)只读{{ }},单花括号在构建门、注册门和存盘门都会被拒。技能是随npx skills add原样装到客户项目里的,照它写出来的流必定被拒,错误到了门口才看见。改后技能说的和门做的一致;其它槽位(recipients、inputs、http、filter)仍是单花括号,照main实测写明,不多说一句。风险与代价(含回滚) — 风险低:只改发布技能的文字和评测断言,不碰代码与契约;新示例已在构建产物上实测被接受并渲染。代价是 SKILL.md 多 77 字节(棘轮内)。回滚:revert 本 PR 即可,无其它依赖。
席位意见 — (留空)
你要做的 — Tier H:
skills/**是受管面,本 PR 保持 draft,由您的 APPROVED review 批准后落地;不需要您做其它操作。Generated by Claude Code