Skip to content

docs(skills): objectstack-automation teaches the {{ }} delimiter in flow text slots - #22475

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-22454-automation-skill-text-slot-delimiter
Oct 9, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-22454-automation-skill-text-slot-delimiter

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22454
Clause-②: no

What changed, and why

PR #22315 made a flow's TEXT slots — a notify node's title / message, a screen node's title / description, a refusing end node's message — 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 what main enforces today, nothing beyond it:

  • SKILL.md notify example (base :108–109): title: 'Done: {{ record.title }}'. The old message: '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). The recipients / sourceId lines keep their single brace: value-like positions still go through interpolate().
  • SKILL.md dialect paragraph (base :238–244): three statements, one door each — text slots render {{ }} holes (a variable path, optional formatter, no logic) and os validate / registerFlow refuse 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.
  • eval id 2: expected_output names title: 'Won: {{ record.name }}' (a spelling the door accepts); must_contain gains "{{"; must_not_contain drops "{{", 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, message and url a bare {current_year_start} is a nonsense reference" keeps only url (a text slot reads no single-brace token at all); "(message body, http url) only renders an empty string — a warning" now says "(a {{ }} hole, http url)" — packages/lint/src/validate-flow-template-paths.ts judges 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 main at d87dff67

slot what main does measured in
notify title / message, screen title / description, end message {{ }} holes; a {…} token refused at NotifyConfigSchema / ScreenConfigSchema / EndConfigSchema, at registerFlow and at objectstack validate (error), then the holes compiled by validateExpression('template', …) flow-text-slot-template.ts; service-automation/src/engine.ts (registerFlow); lint/src/validate-expressions.ts
recipients, actionUrl, sourceId, payload, templateData values single-brace interpolate() service-automation/src/builtin/notify-node.ts
script.inputs single-brace (values interpolate {token} templates) spec/src/automation/schemaless-node-config.zod.ts; screen-nodes.ts
http (the whole config) single-brace (The whole config is interpolate()d before reading) spec/src/automation/io-node-config.zod.ts; http-nodes.ts
filter single-brace with the placeholder hand-off unchanged — § Filter tokens
fields.* / 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 stays pm:queue flow-value-slot-template.ts

Measured on the built dists (lint closure built at 057ce436)

  • NotifyConfigSchema.safeParse: the new example ACCEPTED; title: 'Done: {record.title}' REFUSED — "Write Done: {record.title} as Done: {{ 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 an assignment node … 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 }} → By with ok: true — the blank the paragraph now names.

Enumeration

  • Single-brace TEXT-slot examples: git grep -n -P for a title / message / description key 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) over skills/objectstack-automation at d87dff67 → SKILL.md:108, :109 (exactly the seat's reading); on this head → 0 hits; over all of skills/ on this head → 0 hits.
  • Cross-skill: the only other {{ mention under skills/** is skills/objectstack-i18n/SKILL.md:246 (i18n bundle placeholders, not flow text slots) — not this package, untouched.
  • {{ under this package: SKILL.md 1 → 5 sites, the eval 1 → 2.
  • The {TODAY()} tokens in references/examples-flows.md sit in filter positions — single-brace on main, untouched.

Line and token budget

file lines bytes check-skills-token-ratchet tokens (ceiling)
SKILL.md 439 → 438 (−1) 23,052 → 23,129 (+77) 5,763 → 5,783 (5,785 — headroom 2)
evals/flows-triggers-approvals.json 55 → 55 (0) 5,018 → 5,012 (−6) 1,255 → 1,253 (1,255)
package skills/objectstack-automation/** 1,092 → 1,091 (−1)

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.mjs is outside the claimed file surface (the eval's ceiling could be lowered 1,255 → 1,253 by a later edit).

Gates, head 057ce436

node 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; --ran reconciliation: 「24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN」. All 24 exit 0 with their own verdict lines read — among them check-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 behind origin/main, two derivation inputs changed (scripts/check-durability-degradation-log-level.mjs, scripts/measure-durability-swallow-family.mjs), neither in this list; origin/main was not merged in because those commits carry merge=os-regen spec 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) so check:skill-docs and check:doc-formula-expressions read a fresh dist/.

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 sibling objectui/scripts/check-skill-eval-tokens.mjs matches 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's files[] — 69 published packages, 0 name skills, positive control 69 name dist — and the catalog ships from git by npx skills add; skip-changeset applied.

Acceptance notes

维护者速读(草稿)

改了什么 — 把 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

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 057ce4365b6bec13a932962c05c744dbe04ac6eb
Local-runs: none

① Derived judgments

  • Accept set: none — the diff is published skill text and one eval file; no package code, no contract. The surface is the published catalog (npx skills add objectstack-ai/objectstack/skills), and the text now says what main's door does: FLOW_NODE_TEXT_SLOTS is exactly five positions (notify title / message, screen title / description, end message), judged by flow-text-slot-template.ts at registerFlow and objectstack validate.
  • The notify example (SKILL.md 108–109): title: 'Done: {{ record.title }}' and message: 'Closed {{ record.closed_at | date:long }}'. Measured by the dev on the built dists: both accepted by NotifyConfigSchema, compiled by validateExpression('template', …), rendered by the template engine; the old lines refused with the door's own remedies (Done: {record.title} → Done: {{ record.title }}; {$User.Id} "is not a variable, so no hole spells it: compute it into a variable with an assignment node"). Right: the message example could not be flipped mechanically — the old token has no hole spelling — so it became a path-plus-formatter a reader can copy.
  • The dialect paragraph (old 236–241): three statements, one door each — text slots render {{ }} holes (a path, an optional formatter, no logic) and os validate / registerFlow refuse a {…} token there with its {{ }} spelling; {$User.Id} / {NOW()} have no hole and {{ $User.Id }} renders blank, so assign a variable first; every other slot (recipients, inputs, http, filter) keeps the single-brace template. The false "{{x}} is the template-field dialect" clause is gone. Right against main: the five text slots are the whole set, and the value-slot half (feat(spec)!: flow value slots refuse the {…} template dialect, naming the CEL spelling of each token (#19939, C half) #22259, landed) already matches item 1 of the same list.
  • Eval id 2: expected_output names title: 'Won: {{ record.name }}'; must_contain gains {{; must_not_contain drops {{ (a pin of the old dialect that would have failed the right answer) and keeps trigger:; no single-brace pin added, since recipients keeps the single brace. Right.
  • Beyond the three named sites, inside the claimed file: the § Filter tokens bullets (old 207–208 and 213–214) no longer list title / message as single-brace positions ("in url a bare {current_year_start} is a nonsense reference"; "outside a filter (a {{ }} hole, http url)"), and the section's two-line lede was deleted to pay the token ratchet — its substance survives at lines 161–162 ("a dropped filter condition widens the query") and in the second bullet (the condition is DROPPED and the node refuses to execute). Right.
  • Budget: SKILL.md 439 → 438 lines, 23,052 → 23,129 bytes; the eval 55 → 55 lines; the published catalog has a TOKEN ratchet, not a line ratchet (scripts/check-skills-token-ratchet.mjs, shrink-only) — on the head SKILL.md 5,783 / 5,785 and the eval 1,253 / 1,255, exit 0, no ceiling moved. No re-wrap. Operative text carries no issue number.
  • Pin: a title / message / description value holding a { with no { on either side (the lookaround form; the dispatch's \{[^{] also matched the second brace of a hole) — base: SKILL.md 108 and 109; head: 0 hits across all of skills/. Right.
  • Public surface: the published skill only; Clause-②: no is correct.

② Semver level

Clause-②: no on the claim and the PR body; skip-changeset is the correct declaration — skills/** is in no published package's files[]. No .changeset/*.md touched.

③ Boundary flags

  • Mechanism assumptions corrected by measurement: Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 is half-true (the value-slot C half of [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 landed in feat(spec)!: flow value slots refuse the {…} template dialect, naming the CEL spelling of each token (#19939, C half) #22259 — fields.* / assignment values refuse {…} with the CEL spelling, date macros and {$User.*} kept; the skill's item 1 already said so); Remove explicit pnpm version from workflows to fix version conflict #7 — no line ratchet covers skills/** by design, the token ratchet does, and 88 bytes of SKILL.md headroom bounded the rewrite. Both recorded on the PR.
  • "The same reference outside a filter (a {{ }} hole, http url) only renders an empty string — a warning": the sentence is inherited from the single-brace era and kept for holes; the blank render was measured for {{ $User.Id }} (ok: true), the unknown-field warning at validate for a hole was read, not measured — for the maintainer's eye, not blocking.
  • The skill eval run is NOT MEASURED: no runner in this repository reads must_contain / must_not_contain (the tree holds only the eval JSONs and two READMEs); the assertions are judged by reading, and {{ is a substring the right answer contains.
  • CI on the head at this write: Test Core (1/6), Test Core (2/6), Lint & Repo Gates, Type Check · workspace still in_progress, nothing red; the dev's 24-family union ran exit 0 on the head (--ran 24/24).
  • Git identity: one commit, authored and committed by objectstack-fleet[bot] with the fleet e-mail, model-free trailer pair — no deviation.
  • open_questions: none. Out-of-scope findings: {{ $User.Id }} in a text slot passes objectstack validate and renders blank with ok: true (measured: the schema accepts, the engine evaluates to By ) — a silent-drop trap the new paragraph warns about; the door should refuse a $-root no flow variable can resolve the way it refuses {$User.Id} — the seat files it for the spec lane (link on the card); SKILL.md:154 ({NODE_ID.error} / {$error}) names no slot — noted; the eval's token ceiling is lowerable to 1,253 — polish, outside this surface.

Implemented-by: claude/issue-22454-automation-skill-text-slot-delimiter
Reviewed-by: session_01JmWtcHfGbC4ncw4GFKWuRA

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 — PR #22475(#22454)— skills 席 1,2026-10-09T12:02Z

  • 改了什么: 已发布技能 objectstack-automation 的两个文件。SKILL.md:notify 示例改成 title: 'Done: {{ record.title }}' / message: 'Closed {{ record.closed_at | date:long }}';"文本槽"那段改成三句、各对一道门(notify/screen/end 的五个文本槽读 {{ }} hole,validate / registerFlow 拒单括号并给出 {{ }} 拼写;{$User.Id} / {NOW()} 没有 hole、{{ $User.Id }} 渲染空白,先赋变量;recipients / inputs / http / filter 仍是单括号),删掉"{{x}} 是 template-field 方言"的假句;§ Filter tokens 两处把 title / message 从单括号位置里拿掉,删两行导语付 token 棘轮(实质在 161–162 行与同节第二条)。eval 2 的期望改为 title: 'Won: {{ record.name }}',{{ 从 must_not_contain 挪到 must_contain。净 −1 行、+77 字节,token 棘轮 5,783/5,785 与 1,253/1,255。
  • 为什么改: PR feat(spec)!: flow text slots read the {{ }} delimiter, refusing a single-brace token with its hole spelling (#22110) #22315 落地后 main 在构建门与保存门拒绝文本槽里的单括号 token;这份技能经 npx skills add 装进客户项目,AI 作者先读它、再被门拒。旧示例两行在构建产物上实测被拒(门自己给出 {{ }} 拼写),新示例实测被接受并渲染。
  • 风险与代价(含回滚): 纯文本,不碰代码、不发包、无 changeset;值槽指引一字未扩(feat(spec)!: flow value slots refuse the {…} template dialect, naming the CEL spelling of each token (#19939, C half) #22259 已落的那半与技能原句一致)。回滚 = 还原两个文件的一次提交。一处边界给你过目:{{ $User.Id }} 写在文本槽里能过 validate、渲染成空白(ok: true)——技能新段已警告,本席另立 finding 给 spec 车道让门响亮拒绝。
  • 席位意见: 建议批准。三处改法逐一对应 main 上 FLOW_NODE_TEXT_SLOTS 的五个位置与门的拒绝句,不新增任何声明;删导语有家、棘轮只减不增。席内契约复核 PASS 在案(6080468777)。
  • 你要做的: 看一眼 82 行 diff,认可就给一个 APPROVED;批准后席位落地(你顺手 ready + auto-merge 也可)。

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

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants