Skip to content

Add migrate-skill host command to convert legacy Skill cards to SKILL.md#5315

Merged
jurgenwerk merged 5 commits into
mainfrom
cs-11549-migrate-skill-command-legacy-skill-cards-skillsnameskillmd
Jun 24, 2026
Merged

Add migrate-skill host command to convert legacy Skill cards to SKILL.md#5315
jurgenwerk merged 5 commits into
mainfrom
cs-11549-migrate-skill-command-legacy-skill-cards-skillsnameskillmd

Conversation

@jurgenwerk

@jurgenwerk jurgenwerk commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Adds a migrate-skill host command (@cardstack/boxel-host/commands/migrate-skill) that scans a realm for legacy Skill cards and writes each one out as a skills/<name>/SKILL.md directory — the markdown-first skill representation the platform now indexes via MarkdownDef.frontmatter / SkillFrontmatterField.

This is Phase B / §8 Phase 2 of the Unified Skills Source spec: "a migrate-skill command converts a user's legacy Skill cards into skills/<name>/SKILL.md directories in their realm."

How

For each skill found in the realm (the type filter matches Skill and its subclasses — SkillPlus, SkillPlusMarkdown), the command maps the card fields onto frontmatter + a markdown body:

  • cardTitle → top-level name
  • cardDescription → top-level description
  • commands (codeRef + requiresApproval) → boxel.commands, the same shape SkillFrontmatterField.commands parses back out, so the host command-definition upload flow reads them identically
  • instructions → the markdown body
  • boxel.kind: skill is always added so the file is indexed as a skill

The top-level name / description are shared with Claude Code (which reads the same file byte-for-byte); everything Boxel-specific is namespaced under boxel:.

Example transformation

1. A skill with commandsSkill/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:

---
name: Data Management
description: Query and mutate cards in a realm
boxel:
  kind: skill
  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
---

# Data Management

Use `SearchCardsByQuery` to find cards before editing them.

2. A skill with no commandsSkill/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 (the boxel.commands key is omitted entirely when there are none):

---
name: Tone of Voice
description: How the assistant should phrase replies
boxel:
  kind: skill
---

Be concise. Prefer plain language over jargon.

Behavior notes

  • Targets that already exist are skipped and reported in skippedSkillIds unless overwrite: true is passed.
  • Directory slugs are derived from the skill name (Data Managementdata-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).
  • Skills with no instructions to transcribe are skipped and reported in emptySkillIds rather than written out as 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 — so nothing is ever silently dropped into an empty file.
  • The command is non-destructive: it never deletes or repoints the legacy Skill cards. Both representations coexist during the spec's dual-path migration window; removing the push path / Skill class is later-phase cleanup. Existing references (Matrix room enabledSkillCards, 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

  • Input (MigrateSkillInput): realm (required), overwrite (optional).
  • Result (MigrateSkillResult): migratedFiles (URLs written), skippedSkillIds (target already existed), emptySkillIds (no instructions to write).

Testing

  • Integration test added at packages/host/tests/integration/commands/migrate-skill-test.gts covering: a skill with commands, a skill without commands (no boxel.commands key), and the skip / overwrite behavior. Built on the same realm-seeding pattern as update-room-skills-test.
  • ember-tsc --noEmit is clean for host; eslint clean on the new host files.

CS-11549

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>
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ± 0      1 suites  ±0   2h 1m 8s ⏱️ +51s
3 200 tests +18  3 185 ✅ +18  15 💤 ±0  0 ❌ ±0 
3 219 runs  +18  3 204 ✅ +18  15 💤 ±0  0 ❌ ±0 

Results for commit 5b208f6. ± Comparison against earlier commit 8d797a0.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   10m 42s ⏱️ +3s
1 733 tests ±0  1 733 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 826 runs  ±0  1 826 ✅ ±0  0 💤 ±0  0 ❌ ±0 

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>
@jurgenwerk jurgenwerk requested a review from Copilot June 23, 2026 09:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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>
@jurgenwerk jurgenwerk requested review from a team and lukemelia June 23, 2026 11:51
@habdelra habdelra requested a review from Copilot June 23, 2026 13:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +154 to +157
let entry: FrontmatterCommand = { codeRef: { module, name } };
if (command.requiresApproval) {
entry.requiresApproval = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems true to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.)

Comment on lines +110 to +113
new CommandField({
codeRef: { module: COMMAND_MODULE, name: 'DoThing' },
requiresApproval: true,
}),
Comment on lines +157 to +161
{
codeRef: { module: COMMAND_MODULE, name: 'DoThing' },
requiresApproval: true,
},
],
@jurgenwerk jurgenwerk marked this pull request as ready for review June 24, 2026 09:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +155 to +157
if (command.requiresApproval) {
entry.requiresApproval = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

jurgenwerk and others added 2 commits June 24, 2026 12:02
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
@jurgenwerk jurgenwerk merged commit a55e4fb into main Jun 24, 2026
126 of 129 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants