Skip to content

Commit d1a31d0

Browse files
objectstack-fleet[bot]os-warrenclaude
authored
docs(skills): objectstack-automation teaches the read-only record_link screen field, with an eval prompt (#26446)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8W2xMZ3uXnQ8MwyyurDKART)_ Fixes #26347 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`skills/objectstack-automation/SKILL.md` 的 `screen` 节点行(L91)教只读的屏幕字段 `type: 'record_link'`:它显示 `reference` 对象里 id 等于 `defaultValue` 求值结果的那条记录(按显示名,链接到记录页),不收输入;`reference` 与 `defaultValue` 必填,`required` / `options` / `placeholder` / `min` / `max` 拒收,恢复时在它名下带值拒收(`400 INVALID_SCREEN_INPUT`);附一个结果屏示例(`defaultValue: { dialect: 'cel', source: 'account.id' }` 读 `create_record` 的 `outputVariable`),并点明两种会被拒或原样显示的替代写法。eval 文件加第 9 题(卡上原题)。棘轮脚本两行上限按实测调整,裁决原文写在行旁;「Filter operands」小节删去一句重复 item 1 的话。 - **为什么改**:#26355 已把 `record_link` 落进 `@objectstack/spec`,技能没教。AI 作者写「创建完成后显示新记录链接」的结果屏时,会去写 completed `end` 的 `message`(被 spec 拒收)或在屏幕 `description` 里写 markdown 链接(runner 原样显示成文字)。 - **风险与回滚**:只改已发布技能包的文字、一个 eval 夹具和棘轮脚本的两行上限,不改运行时;回滚即 revert 本 PR(上限行随之回落)。 - **先看**:① `skills/objectstack-automation/SKILL.md` L91 `screen` 行的新增子句;② `scripts/check-skills-token-ratchet.mjs` 两行上限及注释;③ `evals/flows-triggers-approvals.json` 第 9 题。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` → 看到 `skills/objectstack-automation/SKILL.md is 6164 tokens (ceiling 6164; headroom 0)` 与 `flows-triggers-approvals.json is 2141 tokens (ceiling 2141; headroom 0)`,末行 `authored bundle file(s) within their ceilings`。 - **需要你做的**:Tier H(`skills/**`),请 approve 或直接 Merge when ready。 ## What changes Card #26347's 「What is owed」 section, and nothing wider: 1. **`SKILL.md` L91, the `screen` node row** (the one home — the row already enumerates `config.fields[]` and teaches the `file` member, so an author writing a screen flow's result screen reads it): `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page; the one example `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`; both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`); a completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Every clause was read from the merged code, not the card: `packages/spec/src/automation/builtin-node-config.zod.ts` (`SCREEN_FIELD_RECORD_LINK_TYPE`, `…_REFERENCE_REQUIRED`, `…_DEFAULT_VALUE_REQUIRED`, `SCREEN_FIELD_RECORD_LINK_REFUSED_KEYS`, the `superRefine` at the field, and `EndConfigSchema`'s refusal of `message` on a completed end), `screen-field-record-link.test.ts`, `packages/services/service-automation/src/builtin/screen-record-link-resume.test.ts` (the `account.id` spelling over `outputVariable: 'account'`, `INVALID_SCREEN_INPUT`), `screen-input-contract.ts`, the error-code ledger (400), and `ui/packages/app-shell/src/views/FlowRunner.tsx` (the `description` is a plain text child of the dialog description). 2. **`evals/flows-triggers-approvals.json`**: entry 9, the card's prompt verbatim; the ideal answer names the `screen` flow, two `create_record` nodes with `outputVariable`, the last `screen` of two `type: 'record_link'` fields with `reference` and `defaultValue: { dialect: 'cel', source: 'account.id' }` / `'contact.id'`, no `required` / `placeholder` / `options`, no completed-`end` `message`, no markdown link in the `description`; assertions as the card gives them. The expected output is written so that it never spells either `must_not_contain` string (it says "never the object field type" instead of the negated spelling). 3. **`SKILL.md` L209–210, Filter operands** — the one genuine duplicate cut: the bullet restated Valid-but-silently-wrong item 1's refusal detail (`expression-invalid`, "with its envelope"); the surviving home is item 1 at L233–235, which names the gate, the error id and the CEL spelling the refusal carries. −41 bytes. 4. **`scripts/check-skills-token-ratchet.mjs`**, the two objectstack-automation rows only, under the maintainer's ruling on the seat's decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap **A** (2026-10-27 01:21Z), verbatim and untranslated: 「A:先削重复,余额按实测提高」. Step one, duplicates cut first: the one above in `SKILL.md`; the eval file's eight fixtures share no sentence (0 bytes to cut). Step two, each row follows its measurement (`ceil(utf8 bytes / 4)`, `SKILL.md` minus its 20-byte stamp line): `SKILL.md` priced bytes 24014 → 24654 (+681 added, −41 cut, +640 net), 6004 → **6164** (headroom was 2 bytes, 0 tokens; +160); `evals/flows-triggers-approvals.json` 7289 → 8561 bytes (+1272), 1823 → **2141** (headroom was 3 bytes, 0 tokens; +318). Both pinned AT the measurement, each comment quoting the tap. Rows measured against `origin/main` `75afeffd` (#26420 landed), the branch's base; no other package's row is touched. `references/_index.md` is generator-owned (`gen:skill-refs`) and unchanged (no schema moved). `references/examples-flows.md` is untouched (the row held the teaching). `SKILL.md` line 1 is untouched (#25640 owns the stamp). #26362 (the approval-lock read) is the next card on this package and is not taught here. **Two readings** (lines, and tokens as the ratchet counts them): | File | Lines before → after | Tokens before → after | |:--|:--|:--| | `SKILL.md` | 441 → 441 (0) | 6004 → 6164 (+160) | | `evals/flows-triggers-approvals.json` (not .md, reported for the raise) | 85 → 95 (+10) | 1823 → 2141 (+318) | | **Whole package** (`SKILL.md` + `references/**/*.md`, 4 files; the package has no `rules/`) | **1001 → 1001 (0)** | **13931 → 14091 (+160)** | Removed: the Filter operands restatement only. `skip-changeset`: the published skills packages carry no `package.json`; nothing is released from any package. ## Why The key landed in `@objectstack/spec` with PR #26355 (`4548ba34e6`) and the skill did not teach it, so an AI author asked for a result screen reached for a completed `end` `message` (refused at parse) or a markdown link in the screen `description` (shown as literal text). `Metadata-protocol: none`: the protocol already carries the key; the gap was the teaching. ## Tests - `node scripts/check-skills-token-ratchet.mjs` — exit 0: both touched rows at headroom 0, `81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted`. - `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0, `79 cases pass`. - `pnpm --filter @objectstack/spec run check:skill-docs`, `check:skill-refs`, `check:skill-examples` — see Verification. The new teaching is a table cell (inline code), so no `os:check` block was added; the example count stays as on `main`. - Eval 9 is a fixture (the repo has no eval runner): a script checked every `must_contain` string occurs in its expected output and neither `must_not_contain` string occurs in the prompt or expected output. ## Acceptance notes - The dispatch order's "inside the package's ceilings" cannot hold at headroom 0 on both touched files; the seat's claim widened the surface to the touched ratchet rows under the ruling above. - `evals/README.md`, if it enumerates the eval topics, was not updated; not read, not filed: observation only. - The PR body's first two lines are the harness-required attribution block (the os-dev exception); `Fixes #26347` follows immediately. ## Verification Gate list derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final tree (base `75afeffd`, head `c9e57c28`), each run locally with the exit code captured before any pipe; `--ran` reconciliation below. No commit followed the gate runs, so every reading is of `c9e57c28`. **Reconciliation** (`node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack`): exit 0 — `53 derived famil(ies) accounted for — 52 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`, 0 UNRUN. The one NOT MEASURED is `pnpm check:dual-build-cjs-loads` (exit 3: its prerequisite is a whole-repo build; this diff touches no package source, and CI's `Lint & Repo Gates` runs it — a declared narrowing). The closures for `check:skill-examples` and `check:doc-formula-expressions` (`@objectstack/client...`, `@objectstack/client-react...`, `@objectstack/lint...`, `@objectstack/formula...`) were built under `scripts/pm/os-verify-lock.sh` (slot `issue-26347`, `VERDICT command-exit 0`, 276 s held, 0 s waited); `check:dts-closure` and `check:lean-entry-closure` ran after that build (`38 built package(s) swept - 160/160 declared declaration file(s) present`). `node scripts/check-comment-mask-corpus.mjs` ran in the serial cheap runner, never beside `check:skill-examples`. `check:pm-dispatch-gates` ran detached on the final head: fast tier, `1901 cases pass`, exit 0. Two runs are outside the derivation and recorded anyway: `check:skill-refs` and `check:skill-examples`, the card's acceptance gates (the eval file and the row are the only skill edits; the example count stays at 264). <details><summary>Every command with its exit code on c9e57c2 (55 lines: the 53 derived, plus `check:skill-refs` and `check:skill-examples`)</summary> - `node scripts/check-ci-filter-parity.mjs` — exit 0 - `node scripts/check-closing-keyword-parity.mjs` — exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` — exit 0 - `node scripts/check-comment-mask-corpus.mjs` — exit 0 - `node scripts/check-declaration-mirrors.mjs` — exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` — exit 0 - `node scripts/check-doc-route-spelling.mjs --advisory` — exit 0 - `node scripts/check-doc-route-spelling.mjs --self-test` — exit 0 - `node scripts/check-dts-emitted.mjs --self-test` — exit 0 - `node scripts/check-scripts-symbol-anchors.mjs` — exit 0 - `node scripts/check-scripts-symbol-anchors.mjs --self-test` — exit 0 - `node scripts/check-self-test-wired.mjs` — exit 0 - `node scripts/check-self-test-wired.mjs --self-test` — exit 0 - `node scripts/check-self-test-workflow-commands.mjs` — exit 0 - `node scripts/check-self-test-workflow-commands.mjs --self-test` — exit 0 - `node scripts/check-skills-token-ratchet.mjs` — exit 0 - `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0 - `node scripts/check-whole-set-label-write.mjs` — exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` — exit 0 - `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` — exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` — exit 0 - `node scripts/ci/prune-turbo-cache.mjs --self-test` — exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` — exit 0 - `pnpm check:agent-test-spelling` — exit 0 - `pnpm check:bash32-floor` — exit 0 - `pnpm check:cli-command-ids` — exit 0 - `pnpm check:corpus-claim-drift` — exit 0 - `pnpm check:cross-package-test-inputs` — exit 0 - `pnpm check:doc-authoring` — exit 0 - `pnpm check:driver-memory-census` — exit 0 - `pnpm check:entry-guard` — exit 0 - `pnpm check:gitlink-declared` — exit 0 - `pnpm check:merge-driver` — exit 0 - `pnpm check:nul-bytes` — exit 0 - `pnpm check:parse-guard` — exit 0 - `pnpm check:pm-governed-merges` — exit 0 - `pnpm check:pm-skill-id-lint` — exit 0 - `pnpm check:pnpm-filter-targets` — exit 0 - `pnpm check:pnpm-flag-spellings` — exit 0 - `pnpm check:ratchet-remedy-authority` — exit 0 - `pnpm check:refd-timer-probe` — exit 0 - `pnpm check:role-word` — exit 0 - `pnpm check:skill-compatibility` — exit 0 - `pnpm check:skill-frame-sync` — exit 0 - `pnpm check:skill-identifier-liveness` — exit 0 - `pnpm check:sourcemap-no-sources-content` — exit 0 - `pnpm check:watch-hint-literal` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-docs` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-refs` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-examples` — exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` — exit 0 - `pnpm check:dts-closure` — exit 0 - `pnpm check:lean-entry-closure` — exit 0 - `pnpm check:dual-build-cjs-loads` — NOT MEASURED (exit 3, PREREQUISITE NOT MET: whole-repo build; CI's Lint & Repo Gates runs it) - `pnpm check:pm-dispatch-gates` — exit 0 </details> <details><summary>Token ratchet on c9e57c2</summary> - `skills/objectstack-automation/SKILL.md is 6164 tokens (ceiling 6164; headroom 0)` - `skills/objectstack-automation/evals/flows-triggers-approvals.json is 2141 tokens (ceiling 2141; headroom 0)` - `81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.` - `--self-test`: `79 cases pass.` </details> <details><summary>check:skill-examples on c9e57c2</summary> `264 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them` (unchanged from `main`: the new teaching is a table cell, not an `os:check` block). </details> ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum). - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's run (repo-wide scan). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes (`skip-changeset`); both declaration lines are filled in above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block and the quoted ruling are the required exceptions). --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbYLKc8xxih9Em65LNW)_ Co-authored-by: os-warren <warren@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0852dd commit d1a31d0

3 files changed

Lines changed: 40 additions & 5 deletions

File tree

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,23 @@ export const CEILINGS = new Map([
391391
// 6004: +233 net, 9 absorbed by headroom, ceiling +224. Maintainer ruling
392392
// 2026-10-27 on the skills-lane decision card 「skills 修复卡的教学和 eval 都撞
393393
// token 天花板,怎么付?」, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
394-
['skills/objectstack-automation/SKILL.md', 6004],
394+
// 6004 -> 6164 (card #26347, the skills half of the `record_link` screen
395+
// field): the `screen` node row teaches the read-only `type: 'record_link'`
396+
// field — it shows the `reference` record whose id `defaultValue` evaluates
397+
// to, both keys required, the five input keys refused, the resume refuses a
398+
// value under its name — with the one result-screen example over a
399+
// `create_record` `outputVariable`, and the two workarounds an author
400+
// otherwise reaches for (a completed `end` `message`, a markdown link in a
401+
// screen `description`). Paid down first: the Filter operands bullet
402+
// restated Valid-but-silently-wrong item 1's refusal detail
403+
// (`expression-invalid`, "with its envelope") and was cut to the bare
404+
// refusal (-41 bytes). Arithmetic in priced bytes (stamp line excluded):
405+
// 24014 -> 24654 (+681 added, -41 cut, +640 net) -> ceil(24654 / 4) = 6164;
406+
// headroom was 2 bytes, 0 tokens, so the ceiling moves +160, pinned AT the
407+
// measurement. Maintainer ruling 2026-10-27 on the seat's decision card
408+
// 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and
409+
// untranslated: 「A:先削重复,余额按实测提高」.
410+
['skills/objectstack-automation/SKILL.md', 6164],
395411
// 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above).
396412
// The invariant-vs-transition-gate choice, placed in the field-conditional-rule
397413
// bullet list where the tool is actually picked. Both wrong picks are silent:
@@ -765,7 +781,16 @@ export const CEILINGS = new Map([
765781
// +568 tokens, nothing in the file to delete, zero headroom, ceiling +568.
766782
// Same maintainer ruling as the SKILL.md row above (2026-10-27, verbatim and
767783
// untranslated): 「A:先削重复,余额按实测提高」.
768-
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 1823],
784+
// 1823 -> 2141 (card #26347): one eval prompt for the `record_link` result
785+
// screen — a `convert_lead` screen flow that creates an account and a contact
786+
// and ends on a screen linking both, the ideal answer written so it never
787+
// spells the negated `type: 'record'`. Paid down first: the file's eight
788+
// fixtures share no sentence, nothing to cut (0 bytes). 7289 -> 8561 bytes
789+
// (+1272) -> ceil(8561 / 4) = 2141; headroom was 3 bytes, 0 tokens, so the
790+
// ceiling moves +318, pinned AT the measurement. Same maintainer ruling as
791+
// the SKILL.md row above (2026-10-27, verbatim and untranslated):
792+
// 「A:先削重复,余额按实测提高」.
793+
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2141],
769794
// 894 -> 1095 (#26179): one regression case for the field permission `when`
770795
// (the contract prompt: lock the legal terms for finance from approval on,
771796
// legal keeps the edit) — expected: `when` on the finance set's entries with

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Flows are built from **20 built-in node types** (plugins register more via
8888
| `notify` | Send a notification through the messaging service (inbox channel by default) |
8989
| `connector_action` | Invoke a pre-built integration connector |
9090
| `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) |
91-
| `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen |
91+
| `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally |
9292

9393
#### Human Decision
9494

@@ -206,8 +206,8 @@ variables: [
206206
evaluated before the query (`filter: { id: { dialect: 'cel', source:
207207
'record.id' } }`). A whole-string **query placeholder** (`{current_user_id}`,
208208
`{today}`, `{current_year_start}`) passes through verbatim for the query
209-
engine to expand; every other `{…}` token is refused (`expression-invalid`)
210-
with its envelope, and a flow variable never shadows a placeholder. Only
209+
engine to expand; every other `{…}` token is refused, and a flow variable
210+
never shadows a placeholder. Only
211211
`filter` gets this hand-off — in a text slot `{{ current_year_start }}` is a
212212
nonsense reference.
213213
- **An operand that evaluates to `null` refuses the node** (not

‎skills/objectstack-automation/evals/flows-triggers-approvals.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@
8080
"must_contain": ["type: 'screen'", "type: 'file'", "create_record", "signed_copy"],
8181
"must_not_contain": ["type: 'upload'", "type: 'attachment'"]
8282
}
83+
},
84+
{
85+
"id": 9,
86+
"prompt": "Build a screen flow `convert_lead` that asks for a company name and a contact name, creates a `crm_account` and a `crm_contact` linked to it, and ends on a screen that shows links to both new records.",
87+
"expected_output": "A `screen` flow (`type: 'screen'`): a first `screen` node collecting `company_name` and `contact_name`; two `create_record` nodes with `outputVariable` (`account`, `contact`), the contact's `fields` linking it with `account: { dialect: 'cel', source: 'account.id' }`; then a last `screen` node whose fields are two `type: 'record_link'` fields, each with `reference` naming its object (`crm_account`, `crm_contact`) and `defaultValue: { dialect: 'cel', source: 'account.id' }` / `{ dialect: 'cel', source: 'contact.id' }`, carrying no `required`, `placeholder` or `options`. The `end` node carries no `message` (refused on a completed end) and the screen `description` holds no markdown link — the links are the `record_link` fields, spelled `record_link`, never the object field type.",
88+
"files": [],
89+
"assertions": {
90+
"must_contain": ["record_link", "reference", "defaultValue", "outputVariable", "dialect: 'cel'"],
91+
"must_not_contain": ["type: 'record'", "]("]
92+
}
8393
}
8494
]
8595
}

0 commit comments

Comments
 (0)