Add a BXL card-authoring skill - #116
Conversation
Card authors get no guard rails from the BXL engine: it tolerates
missing values, catches spreadsheet error values rather than throwing,
and compiles a readable spreadsheet dialect down to jq, so several ways
of getting an expression wrong land a plausible value in the field
instead of an error.
The skill is the decision-and-trap layer for that surface — tag choice,
what the derive profile refuses when the field is defined, aggregation
over linked and query-backed collections, cyclic graphs, dates,
memoization, and { as: FieldDef } materialization. Every claim in it was
verified against the engine, and the boxel monorepo carries a suite that
pins each one and fails when the copied skill and the engine disagree.
The glossary's bxl and computeVia: expression(...) entries named
library-bxl, extension-libs/bxl/ and bxl-computevia-fields, none of
which exist as their own targets; both now point here, and the bxl entry
records that card code imports the platform module.
Skill/bxl-authoring.json points its instructionsSource at the skills/
tree rather than duplicating the body, so one file serves the plugin and
the assistant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a new “BXL authoring” skill to provide guardrails and diagnostics guidance for writing expression()/fx/jq BXL in Boxel cards, and updates repository navigation/docs to reference it.
Changes:
- Added
skills/bxl-authoring/SKILL.mdwith decision guidance, common silent-failure traps, and a review checklist. - Added an in-app skill card (
Skill/bxl-authoring.json) that reuses the same markdown viainstructionsSource. - Updated
index.mdandskills/glossary.mdreferences to point to the new skill.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/glossary.md | Updates glossary entries for computeVia: expression(...) and bxl to point at the new authoring skill and clarify import contexts. |
| skills/bxl-authoring/SKILL.md | Introduces the new skill documentation covering BXL call-site choices, derive-profile constraints, traps, and checklist. |
| index.md | Lists the new bxl-authoring skill under Foundation. |
| Skill/bxl-authoring.json | Adds in-app assistant card that sources instructions from the markdown skill file. |
| README.md | Documents the instructionsSource option to avoid duplicating skill markdown under Skill/. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The derive profile's volatile set is six calls, not four: isAfter and isBefore default their second argument to the current time, so the profile classes them with NOW. They sat among the validator helpers the skill blesses, and the date section never mentioned them, so isBefore(StartDate, EndDate) — the natural thing to reach for — threw at module load with nothing on the page to warn about it. Both are now named in the refusal list and in the dates section, with the operator form that is deterministic. The glossary's skill catalog gains bxl-authoring, which its own maintenance rule requires. Its bxl entry now says outright that the platform module is the primary import and that the entry sits under realm-bundled libraries only for the legacy relative-bundle case, rather than describing both models as equals under a header that contradicts one of them. The same correction reaches the two boxel-patterns references index.md names as the capability and import catalogues: libraries.md gains an @cardstack/bxl entry and a lookup-index row, and integration-surfaces.md stops filing bxl under realm-bundled libraries. The README's claim that the pointer form cannot drift covered only the body; the chooser-facing metadata is still hand-authored in three places, and two of them already disagreed. Said so, and aligned them. Also quotes the boxel-skill-authoring description, which contained a `: ` inside an unquoted YAML scalar and therefore did not parse: the skill documenting the boxel.kind contract was the one skill in the tree that could not be indexed as a skill, which is exactly the silent trap its own warning section describes. All 17 now parse with the marker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging Workspace Sync SuccessfulSuccessfully synced changes to staging workspace Sync Details |
Card authors — human and agent — get no guard rails from the BXL engine. It tolerates missing values, catches spreadsheet error values rather than throwing, and compiles a readable spreadsheet dialect down to jq, so several of the ways to get an expression wrong land a plausible value in the field instead of an error. This adds the authoring skill for that surface.
skills/bxl-authoring/SKILL.mdis the decision-and-trap layer, not a syntax reference — the syntax surface stays in the engine's own docs and on bxl.boxel.site. It covers which of the three call-site forms to reach for; what thederiveprofile refuses when the field is defined, and the diagnostic it throws; aggregation over linked and query-backed collections; blank-input propagation; error values; cyclic graphs; dates; memoization;{ as: FieldDef }materialization; and a review checklist.Four of the traps produce a wrong value with nothing to signal it:
SUM(Claims[].Paid)callsSUMonce per claim and the field receives[10, 5]rather than15. The rule stops at the argument that iterates: a comma list is collected by the compiler, an array-valued field is already one value, and wrapping a scalar parameter is its own bug —ROUND([1.234], 2)andNPV([0.1], CashFlows)both blank the field.\(…)in a plain string is inert. JS eats the backslash, the field renders(.bpSystolic)/(.bpDiastolic), and no lint pass in a realm flags it. Either tag preserves it, and inside the interpolation you are in jq, where a PascalCase label compiles as a function call and throws on first read.&renders a blank operand as the textnull.Name & " (" & Tier & ")"on a card with no tier yieldsAcme (null).CONCATandTEXTJOINdrop blanks; the skill teaches the// ""guard.ROUNDabsorbs a null operand, but a spreadsheet error value propagates out through it —ROUND(NA()) + 5is null, not 5. And only those values are caught: a misspelled function name compiles happily and throws'SUMM/1' is not definedon first read, so a formula typo indexes as an error rather than a blank field.Dates get their own section because a computed is evaluated server-side for the index and in the viewer's zone in the browser. Every date function answers identically across host zones, and
TODAY/NOWare refused in a computed outright — so the guidance is to compute the fact (a serial, a span) and let the component phrase it.Verified, and pinned against drift
Every behavioral claim was checked against the engine rather than read off its docs. Two examples in the engine's own
syntax-modes.mdhanded an iterating path straight toSUM, and are corrected in the companion monorepo PR.Because a skill ships to authors who cannot run the engine to check it, the monorepo carries a suite that pins each claim twice — the snippet still appears in this file, and the behavior it describes still holds — plus a check that every repo path the skill cites resolves. It reads the copy that
pnpm build:skillsbrings in from a pinned tag here, so a version bump fails there if this file and the engine ever disagree. It was mutation-tested: moving an entry between the refused and allowed lists, swapping the WRONG/RIGHT aggregate examples, renaming a cited doc, falsifying a reason cell, or listing an error value the engine never raises each fail a case.One case is anchored to the engine rather than to the suite: it walks the profile's own denied-call table and requires every ban to be either named on this page or waived in the suite under the category this page describes it by. Comparing the page against the suite's cases alone is a closed loop — which is how
isAfter/isBeforeinitially stayed missing from a refusal list that blessed the validator helpers they sit among. A ban the engine gains now fails until someone decides which it is.Both harnesses, one file
Skill/bxl-authoring.jsonpoints itsinstructionsSourceat../skills/bxl-authoring/SKILL.mdrather than carrying a sibling.md, the waySkill/source-code-editing.jsonalready does. One file serves the plugin and the in-app assistant, so the double-authoring invariant is satisfied without a second copy of the body.README.mdrecords the option next to the invariant — and is explicit that it covers the body only: the chooser-facing metadata is still hand-authored in three places (the card'scardInfo, the SKILL.md frontmatter, and theindex.mdcatalog line), which is where drift actually shows up. Those three are aligned here.Glossary
The
computeVia: expression(...)entry pointed atlibrary-bxlandbxl-computevia-fields, and the bxl entry atlibrary-bxlandextension-libs/bxl/. None of those exist as their own targets; both now point atbxl-authoring.The bxl entry sits under Realm-bundled libraries, a section whose preamble is "libraries shipped inside a realm filesystem rather than via npm" — which the platform module no longer is. Rather than move someone else's section, the entry now says outright that
@cardstack/bxlis the primary import and that it appears there only for the legacy relative-bundle case.skills/glossary.md's own skill catalog (§22) gains abxl-authoringline — the list the repo's maintenance rule names by function — andindex.mdlists the skill under Foundation.Ordering across the two repos
Merging this reaches the in-app assistant — staging realm on merge, production on release — but not the boxel-cli / Claude Code harness, which reads a copy pinned by
BOXEL_SKILLS_VERSIONin the boxel monorepo (currentlyv0.1.0). Full sequence:BOXEL_SKILLS_VERSIONto that tag and re-runspnpm build:skills, committing the copied tree → the plugin ships it, and the drift guard in that PR gets a file to read.Until step 3,
packages/boxel-cli/plugin/skills/bxl-authoring/does not exist in the monorepo, so that PR's guard fails deliberately rather than passing against a missing file.Also in this PR
Two things the skill's own subject matter turned up in the surrounding tree:
skills/boxel-patterns/references/libraries.mdhad no@cardstack/bxlentry at all, andintegration-surfaces.mdfiled bxl under realm-bundled libraries pointing at alibrary-<name>extension that does not exist here. Both corrected, so an author who looks up whereexpressioncomes from finds it.skills/boxel-skill-authoring/SKILL.md's frontmatter did not parse: its unquoteddescriptioncontainsboxel.kind: skill, and a:inside a plain YAML scalar is invalid, so the file indexed without frontmatter andkindwas never set. The skill that documents theboxel.kind: skillcontract was the one skill in the tree that could not appear in the assistant's chooser — exactly the silent trap its own warning section describes. Quoting the description fixes it; all 17 skills now parse with the marker.