Add migrate-skill host command to convert legacy Skill cards to SKILL.md#5315
Conversation
Adds a `migrate-skill` host command that scans a realm for legacy `Skill` cards (and subclasses) and writes each one out as a `skills/<name>/SKILL.md` file with `boxel.kind: skill` frontmatter — the markdown-first skill representation the platform now indexes via `MarkdownDef.frontmatter` / `SkillFrontmatterField`. For each skill the command emits the shared top-level `name`/`description` keys plus a `boxel:` namespace carrying `kind: skill` and the skill's `commands` (codeRef + requiresApproval) in the same shape `SkillFrontmatterField.commands` parses back out. Instructions become the markdown body. Targets that already exist are skipped unless `overwrite` is set, and slug collisions are de-duplicated within a run. CS-11549 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 2h 1m 8s ⏱️ +51s Results for commit 5b208f6. ± Comparison against earlier commit 8d797a0. Realm Server Test Results 1 files ±0 1 suites ±0 10m 42s ⏱️ +3s Results for commit 5b208f6. ± Comparison against earlier commit 8d797a0. |
The `service:realm` stub was registered after `setupBaseRealm` / `setupMockMatrix`, whose beforeEach hooks look up `service:realm` first and instantiate the real singleton — so the later `register` never took effect and `realmOf` ran unstubbed. Resolution then depended on whether the realm happened to be known to the real service, which held only for the first test (cached setup) and failed after teardown. Move the registration ahead of those helpers, matching update-room-skills-test. CS-11549 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two robustness fixes from a review pass: - Sort skills by id before assigning slugs, so collision suffixes (-2/-3) are stable across runs and the skip-if-exists check stays idempotent rather than minting duplicate files when search order shifts. - Skip and report (new `emptySkillIds`) skills with no instructions instead of writing an empty SKILL.md. This guards markdown-backed subclasses (e.g. SkillPlusMarkdown), whose `instructions` is computed from a linked file that may not resolve in a search result — avoiding silent data loss. CS-11549 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| let entry: FrontmatterCommand = { codeRef: { module, name } }; | ||
| if (command.requiresApproval) { | ||
| entry.requiresApproval = true; | ||
| } |
There was a problem hiding this comment.
[Claude Code 🤖]
Fixed in 8d797a0. requiresApproval is now emitted explicitly for every migrated command — command.requiresApproval ?? true — so an explicit false is preserved and a missing value defaults to true, matching command-auto-execute.ts (=== false) and message-builder.ts (?? true). The integration test now seeds both an approval-required and an auto-execute command and asserts the false survives migration.
(Written by Claude on Matic's behalf.)
| new CommandField({ | ||
| codeRef: { module: COMMAND_MODULE, name: 'DoThing' }, | ||
| requiresApproval: true, | ||
| }), |
| { | ||
| codeRef: { module: COMMAND_MODULE, name: 'DoThing' }, | ||
| requiresApproval: true, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c120d633bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (command.requiresApproval) { | ||
| entry.requiresApproval = true; | ||
| } |
There was a problem hiding this comment.
Preserve false requiresApproval flags
When a legacy skill command explicitly has requiresApproval: false, this migration drops the field because it only writes the flag for truthy values. The migrated markdown then rehydrates the command with requiresApproval unset, and the host's auto-execute predicate only treats commands as approval-free when the value is strictly false (command-auto-execute.ts), so existing no-approval skill commands start requiring manual approval after migration.
Useful? React with 👍 / 👎.
The frontmatter only carried requiresApproval when truthy, so a command with an explicit `requiresApproval: false` lost it. Downstream the host auto-executes a command only when `requiresApproval === false` (command-auto-execute.ts) and otherwise defaults a missing value to `true` (message-builder.ts) — so the dropped `false` silently flipped an auto-executing command back to approval-required. Emit `requiresApproval` explicitly for every command, defaulting a missing source value to `true` to match the downstream default. Test now covers both an approval-required and an auto-execute command and asserts the explicit `false` survives migration. CS-11549 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l-command-legacy-skill-cards-skillsnameskillmd
What
Adds a
migrate-skillhost command (@cardstack/boxel-host/commands/migrate-skill) that scans a realm for legacySkillcards and writes each one out as askills/<name>/SKILL.mddirectory — the markdown-first skill representation the platform now indexes viaMarkdownDef.frontmatter/SkillFrontmatterField.This is Phase B / §8 Phase 2 of the Unified Skills Source spec: "a
migrate-skillcommand converts a user's legacy Skill cards intoskills/<name>/SKILL.mddirectories in their realm."How
For each skill found in the realm (the
typefilter matchesSkilland its subclasses —SkillPlus,SkillPlusMarkdown), the command maps the card fields onto frontmatter + a markdown body:cardTitle→ top-levelnamecardDescription→ top-leveldescriptioncommands(codeRef+requiresApproval) →boxel.commands, the same shapeSkillFrontmatterField.commandsparses back out, so the host command-definition upload flow reads them identicallyinstructions→ the markdown bodyboxel.kind: skillis always added so the file is indexed as a skillThe top-level
name/descriptionare shared with Claude Code (which reads the same file byte-for-byte); everything Boxel-specific is namespaced underboxel:.Example transformation
1. A skill with commands —
Skill/data-management.json:{ "data": { "type": "card", "attributes": { "cardTitle": "Data Management", "cardDescription": "Query and mutate cards in a realm", "instructions": "# Data Management\n\nUse `SearchCardsByQuery` to find cards before editing them.", "commands": [ { "codeRef": { "module": "@cardstack/boxel-host/commands/search-cards", "name": "SearchCardsByQueryCommand" }, "requiresApproval": false }, { "codeRef": { "module": "@cardstack/boxel-host/commands/patch-fields", "name": "PatchFieldsCommand" }, "requiresApproval": true } ] }, "meta": { "adoptsFrom": { "module": "https://cardstack.com/base/skill", "name": "Skill" } } } }→
skills/data-management/SKILL.md:2. A skill with no commands —
Skill/tone-of-voice.json:{ "data": { "type": "card", "attributes": { "cardTitle": "Tone of Voice", "cardDescription": "How the assistant should phrase replies", "instructions": "Be concise. Prefer plain language over jargon.", "commands": [] }, "meta": { "adoptsFrom": { "module": "https://cardstack.com/base/skill", "name": "Skill" } } } }→
skills/tone-of-voice/SKILL.md(theboxel.commandskey is omitted entirely when there are none):Behavior notes
skippedSkillIdsunlessoverwrite: trueis passed.Data Management→data-management), de-duplicated within a run, and fall back to the source filename when a skill has no usable name. Skills are processed in a stable id order so the de-dup suffixes (-2,-3) are deterministic — re-running is idempotent (existing targets are recognized and skipped, not duplicated).emptySkillIdsrather than written out as an emptySKILL.md. This guards markdown-backed subclasses (e.g.SkillPlusMarkdown), whoseinstructionsis computed from a linked file that may not resolve in a search result — so nothing is ever silently dropped into an empty file.Skillcards. Both representations coexist during the spec's dual-path migration window; removing the push path /Skillclass is later-phase cleanup. Existing references (Matrix roomenabledSkillCards,linksTo(Skill)fields) keep resolving to the legacy cards and are not rewired here. If a write fails partway, the run aborts but already-written files are skipped on re-run, so re-running resumes safely.Inputs / outputs
MigrateSkillInput):realm(required),overwrite(optional).MigrateSkillResult):migratedFiles(URLs written),skippedSkillIds(target already existed),emptySkillIds(no instructions to write).Testing
packages/host/tests/integration/commands/migrate-skill-test.gtscovering: a skill with commands, a skill without commands (noboxel.commandskey), and the skip / overwrite behavior. Built on the same realm-seeding pattern asupdate-room-skills-test.ember-tsc --noEmitis clean for host; eslint clean on the new host files.CS-11549