From c9e57c28c29d74c88d939d0f68b69d8da968e373 Mon Sep 17 00:00:00 2001 From: os-warren Date: Tue, 27 Oct 2026 03:51:01 +0000 Subject: [PATCH] docs(skills): objectstack-automation teaches the read-only `record_link` screen field, with an eval prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `screen` node row of SKILL.md teaches `type: 'record_link'`: the field shows the `reference` record whose id `defaultValue` evaluates to, as a link to its record page, and takes no input — both keys required, the five input keys refused, a resume value under its name refused — with the one result-screen example over a `create_record` `outputVariable`, and the two workarounds an author otherwise reaches for (a completed `end` `message`, a markdown link in a screen `description`). One eval prompt (id 9) covers the result screen. The Filter operands bullet's restatement of item 1's refusal detail is cut; the two touched token-ratchet rows move by the measured remainder under the maintainer's ruling A. Claude-Session: https://claude.ai/code/session_01JQwxbYLKc8xxih9Em65LNW Co-authored-by: Claude --- scripts/check-skills-token-ratchet.mjs | 29 +++++++++++++++++-- skills/objectstack-automation/SKILL.md | 6 ++-- .../evals/flows-triggers-approvals.json | 10 +++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 9bd4258cd2f..0bf81feee7a 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -391,7 +391,23 @@ export const CEILINGS = new Map([ // 6004: +233 net, 9 absorbed by headroom, ceiling +224. Maintainer ruling // 2026-10-27 on the skills-lane decision card 「skills 修复卡的教学和 eval 都撞 // token 天花板,怎么付?」, verbatim and untranslated: 「A:先削重复,余额按实测提高」. - ['skills/objectstack-automation/SKILL.md', 6004], + // 6004 -> 6164 (card #26347, the skills half of the `record_link` screen + // field): the `screen` node row teaches the read-only `type: 'record_link'` + // field — it shows the `reference` record whose id `defaultValue` evaluates + // to, both keys required, the five input keys refused, the resume refuses a + // value under its name — with the one result-screen example over a + // `create_record` `outputVariable`, and the two workarounds an author + // otherwise reaches for (a completed `end` `message`, a markdown link in a + // screen `description`). Paid down first: the Filter operands bullet + // restated Valid-but-silently-wrong item 1's refusal detail + // (`expression-invalid`, "with its envelope") and was cut to the bare + // refusal (-41 bytes). Arithmetic in priced bytes (stamp line excluded): + // 24014 -> 24654 (+681 added, -41 cut, +640 net) -> ceil(24654 / 4) = 6164; + // headroom was 2 bytes, 0 tokens, so the ceiling moves +160, pinned AT the + // measurement. Maintainer ruling 2026-10-27 on the seat's decision card + // 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and + // untranslated: 「A:先削重复,余额按实测提高」. + ['skills/objectstack-automation/SKILL.md', 6164], // 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above). // The invariant-vs-transition-gate choice, placed in the field-conditional-rule // bullet list where the tool is actually picked. Both wrong picks are silent: @@ -737,7 +753,16 @@ export const CEILINGS = new Map([ // +568 tokens, nothing in the file to delete, zero headroom, ceiling +568. // Same maintainer ruling as the SKILL.md row above (2026-10-27, verbatim and // untranslated): 「A:先削重复,余额按实测提高」. - ['skills/objectstack-automation/evals/flows-triggers-approvals.json', 1823], + // 1823 -> 2141 (card #26347): one eval prompt for the `record_link` result + // screen — a `convert_lead` screen flow that creates an account and a contact + // and ends on a screen linking both, the ideal answer written so it never + // spells the negated `type: 'record'`. Paid down first: the file's eight + // fixtures share no sentence, nothing to cut (0 bytes). 7289 -> 8561 bytes + // (+1272) -> ceil(8561 / 4) = 2141; headroom was 3 bytes, 0 tokens, so the + // ceiling moves +318, pinned AT the measurement. Same maintainer ruling as + // the SKILL.md row above (2026-10-27, verbatim and untranslated): + // 「A:先削重复,余额按实测提高」. + ['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2141], // 894 -> 1095 (#26179): one regression case for the field permission `when` // (the contract prompt: lock the legal terms for finance from approval on, // legal keeps the edit) — expected: `when` on the finance set's entries with diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 4e8d5bcc7b8..8fba4a2fbee 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -88,7 +88,7 @@ Flows are built from **20 built-in node types** (plugins register more via | `notify` | Send a notification through the messaging service (inbox channel by default) | | `connector_action` | Invoke a pre-built integration connector | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | -| `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 | +| `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 | #### Human Decision @@ -206,8 +206,8 @@ variables: [ evaluated before the query (`filter: { id: { dialect: 'cel', source: 'record.id' } }`). A whole-string **query placeholder** (`{current_user_id}`, `{today}`, `{current_year_start}`) passes through verbatim for the query - engine to expand; every other `{…}` token is refused (`expression-invalid`) - with its envelope, and a flow variable never shadows a placeholder. Only + engine to expand; every other `{…}` token is refused, and a flow variable + never shadows a placeholder. Only `filter` gets this hand-off — in a text slot `{{ current_year_start }}` is a nonsense reference. - **An operand that evaluates to `null` refuses the node** (not diff --git a/skills/objectstack-automation/evals/flows-triggers-approvals.json b/skills/objectstack-automation/evals/flows-triggers-approvals.json index 8eda3494f14..8ad05dc2ef7 100644 --- a/skills/objectstack-automation/evals/flows-triggers-approvals.json +++ b/skills/objectstack-automation/evals/flows-triggers-approvals.json @@ -80,6 +80,16 @@ "must_contain": ["type: 'screen'", "type: 'file'", "create_record", "signed_copy"], "must_not_contain": ["type: 'upload'", "type: 'attachment'"] } + }, + { + "id": 9, + "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.", + "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.", + "files": [], + "assertions": { + "must_contain": ["record_link", "reference", "defaultValue", "outputVariable", "dialect: 'cel'"], + "must_not_contain": ["type: 'record'", "]("] + } } ] }