Repository navigation
Commit 8b713fa
docs(skills): objectstack-automation teaches the {{ }} delimiter in flow text slots (#22475)
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
- `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.
- The eval's ratchet ceiling (1,255 → 1,253) is lowerable in
`scripts/check-skills-token-ratchet.mjs`, outside this card's file
surface. carrier: 承接者:无 — noted, not filed.
- `{{ $User.Id }}` renders blank with `ok: true` and passes `objectstack
validate` (`validate-flow-template-paths.ts` skips an unresolvable root;
`lint-flow-patterns.ts` reads 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.
- The card reads #19939 as "the value-slot dialect", not landed: its C
half (`fields.*` / `assignments.*` refusal) landed in #22259; the card
stays `pm:queue` for 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](https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA)_
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>1 parent 97610a5 commit 8b713fa
2 files changed
Lines changed: 17 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
109 | | - | |
| 108 | + | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | 199 | | |
203 | 200 | | |
204 | 201 | | |
205 | 202 | | |
206 | 203 | | |
207 | | - | |
208 | | - | |
| 204 | + | |
| 205 | + | |
209 | 206 | | |
210 | 207 | | |
211 | 208 | | |
212 | 209 | | |
213 | | - | |
| 210 | + | |
214 | 211 | | |
215 | 212 | | |
216 | 213 | | |
| |||
235 | 232 | | |
236 | 233 | | |
237 | 234 | | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
245 | 244 | | |
246 | 245 | | |
247 | 246 | | |
| |||
Lines changed: 3 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | | - | |
| 20 | + | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| |||
0 commit comments