Skip to content

Migrate agent skills into the Runner repository - #106

Merged
leoncheng57 merged 39 commits into
mainfrom
feat/migrate-agent-skills
Aug 26, 2026
Merged

Migrate agent skills into the Runner repository#106
leoncheng57 merged 39 commits into
mainfrom
feat/migrate-agent-skills

Conversation

@leoncheng57

Copy link
Copy Markdown
Owner

Summary

  • import leoncheng57/agent-skills as a non-squashed agent-skills/ subtree, preserving its source history
  • keep portable skills, commands, simulations, and the catalog site separate from runtime reminders and the installed-skill API catalog
  • make the catalog an npm workspace covered by the Runner's root lockfile, typecheck, tests, and production build
  • update skill/command source links and every installation method for the new nested repository location
  • remove the old standalone lockfile and inactive nested CI/Pages workflows now superseded by root CI

Verification

  • npm ci --ignore-scripts
  • npm run typecheck
  • npm test (449 Runner tests + 405 catalog tests)
  • npm run build
  • npm run test:e2e (250 passed, 1 expected screenshot-runner skip)
  • npx --yes skills add ./agent-skills --list (13 skills discovered)

Closes #104.

Source-repository companion: leoncheng57/agent-skills#37

Research independent tasks with parallel read-only subagents, compile the
findings into decision-closed handoff prompts, and fire them off as parallel
agent sessions in fresh git worktrees.
Four skills, all using only the portable `name` + `description` frontmatter
keys that OpenCode recognises.

- ascii-diagrams: conventions for three diagram types (annotated file tree
  with NEW/MOD status column, data flow with inline failure-mode annotations,
  UI mockup with change markers). Plus EXAMPLES.md with three worked examples.
- deep-research-subagents: escalating ONE large question to parallel
  read-only subagents, and synthesising the reports.
- background-subagent: firing the current prompt at a background subagent
  and returning immediately.
- docs-and-diagram-tooling: which visual medium to pick, and the render and
  preview tooling actually installed on this machine.

The subagent skills are written against verified behaviour of opencode
1.18.19: the task tool's parameter set, the enforced read-only permission
list on the `explore` subagent, the default `subagent_depth` of 1, and the
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS gate on `background: true`.

Every diagram is character-count aligned and under 100 columns.
Adds a standalone site at the repo root that renders every skills/*/SKILL.md
automatically — there is no hand-maintained list anywhere, so a new skill
directory is the only step needed to get a card on the site. The app lives at
the root precisely so the glob can reach skills/ without moving it: the
vercel-labs/skills CLI treats that directory as a priority search path.

- src/lib/frontmatter.ts is based on the parser on leoncheng.dev with four
  bugs fixed: folded/literal block scalars (a `description: >-` used to become
  the literal string ">-"), values that merely start and end with a quote,
  CRLF documents (previously read as having no frontmatter at all), and the
  spec's nested `metadata:` map.
- Install commands are templated from one place; the codeload tarball's
  `<repo>-<branch>` root is derived from DEFAULT_BRANCH rather than hardcoded,
  because renaming the default branch would otherwise break the curl command
  silently.
- Design mirrors leoncheng.dev: cream paper with the dot grid, navy ink, hard
  offset shadows, and the terminal-window chrome from its guide cards.
- Deploys via the current Pages flow (upload-pages-artifact + deploy-pages),
  serving ./docs at https://leoncheng.dev/agent-skills/.

66 unit tests cover the parser, skill derivation, and the install templates.
The smallest useful skill in the repo, built as a controlled experiment
against a plugin that does the same job deterministically.

Findings recorded in the skill's Reliability section:

- The undocumented `experimental.text.complete` plugin hook DOES fire.
  Verified end-to-end, not inferred from types: the 1.18.19 binary carries
  the call site in SessionProcessor's `text-end` case, and a probe plugin
  in an isolated project made a model's one-word reply come back wrapped
  in emoji it never generated. `output.text` is mutable and persisted.
- It fires once per text PART, not per response, so a one-pair-per-response
  guarantee is not achievable from this hook alone.
- A plugin cannot enforce "never mention the ducks" — the model saw them in
  its own prior turns and volunteered an explanation for them.
- The skill approach did NOT decay within 6 tool-heavy turns, which is
  better than expected. Post-compaction behaviour remains unverified.

Lesson: skills are for on-demand procedures; always-on behaviour belongs in
a plugin, a hook, or AGENTS.md.

The companion plugin is user configuration, not a repo artifact. It lives at
~/.config/opencode/plugins/duck-mode.ts and is off unless DUCK_MODE=1.
pages.yml runs `npm run build` before upload-pages-artifact, so the
committed output is never deployed. Keeping it only guarantees a stale
artifact and a diff on every skill added.
duck-mode: skill vs plugin, and which one actually works
Site: catalog for agent-skills, deployed to GitHub Pages
feat: ASCII diagramming and subagent delegation skills
New skills:
- skill-smith: authoring/reviewing/installing skills. Gates on the no-op
  test, triages between AGENTS.md / hook / skill, emits portable frontmatter
  only, and covers the relative-symlink + restart-OpenCode install.
- grill-me: rounds-and-frontier interview model, adapted from upstream
  `grilling`. Replaces the stale one-question-at-a-time version.
- red-team-this: adversarial review of the agent's own output. Six fixed
  objection classes, mandatory file:line evidence, expected-cost ranking,
  and a proceed / proceed-with-change / stop verdict.
- worktree-up: the git worktree setup procedure (the rule itself lives in
  AGENTS.md). Sibling <repo>.worktrees/<topic>, branch off origin's default,
  deps, and fixed-port collision checks.

Vendored from mattpocock/skills (MIT) @ 0ab1b63:
- tdd, diagnosing-bugs, code-review as flat skills
- writing-for-agents (+ mechanics) as reference docs under skill-smith

Frontmatter on every vendored skill normalised to name + description, names
matched to their new flat directories, and cross-references to unvendored
skills resolved by inlining (tdd -> codebase-design glossary) or rewriting
(code-review -> self-contained gh/glab issue fetching).

Attribution, upstream commit SHAs, per-file change notes and the MIT licence
text in CREDITS.md, referenced from the README.
These four were not carrying their weight. Three of them (code-review,
diagnosing-bugs, tdd) plus skill-smith's two writing-for-agents reference
docs were the repository's only vendored third-party content, so nothing
vendored remains after this. Nine skills are left.

CREDITS.md is trimmed rather than deleted: skills/grill-me/SKILL.md is
adapted from mattpocock/skills and still requires attribution. The
"What was vendored" table and the "Updating vendored content" section go
(both described only files that no longer exist); the "Adapted, not
vendored" section and the MIT licence text stay. Surrounding prose in
CREDITS.md and README.md no longer claims vendored content exists.

Closes leoncheng57/leoncheng57.github.io#249
Replace the single unstyled footer line with the shared design-system
footer (leoncheng.dev #198): an optional extra row above a main row of
`← leoncheng.dev` / feedback trigger / copyright.

SiteFooter, FeedbackTrigger and both CSS modules are ported from
leoncheng57/leoncheng57.github.io. The CSS is byte-identical apart from
its header comment so future re-syncs stay a clean diff.

Two adaptations the port forces:

- The home link is a plain `<a href="https://leoncheng.dev">`, not the
  upstream react-router `<Link to="/">`. This site is deployed at
  leoncheng.dev/agent-skills/ and owns `/` for its own catalogue, so a
  relative link would land on the catalogue rather than the personal
  site.
- The MIT/source line moves into the footer's extraRow slot, so nothing
  is lost.

Colours are wired up in tokens.css by pointing the `--sf-*` / `--fb-*`
custom properties at this repo's palette. They are var() references, not
literals, so the existing dark-mode block flips the footer too. Two
mappings are not one-to-one: `--sf-line`/`--fb-line` use --hr-color
rather than --text-primary (same value in light, a muted slate instead
of near-white for the 3px rule in dark), and `--fb-focus`/`--fb-accent`
use --link-color because upstream's --blue-emphasis is not defined here
(it is the same #087da8).

FeedbackTrigger reads the page path through a new `currentPagePath()`
helper instead of `window.location.pathname` inline, so the module is
importable without a DOM. The path keeps the /agent-skills/ base prefix,
which is what distinguishes this site from leoncheng.dev in the shared
form's triage.

Tests are adapted to this repo's `node` Vitest environment rather than
adding jsdom and a DOM testing library: SiteFooter is asserted through
react-dom/server, and feedbackConfig keeps its pure unit tests. Dialog
interaction is covered by manual browser checks instead.
WebKit renders input[type="search"] with native searchfield chrome — a
light rounded pill with a magnifier glyph that collides with the `filter`
label — and nothing in src/ ever reset the appearance.

Current WebKit does not paint that pill here, because it drops the native
look once an author sets both `border` and `background`, which
.filterInput does. That degradation is an undocumented heuristic rather
than a guarantee, so the flat rendering was incidental; `appearance: none`
makes it explicit.

The part that does reproduce on current WebKit is the shadow-tree cancel
button, which appears as a grey circle-X as soon as the field is
non-empty and is not covered by resetting the input's own appearance.
It is reset too, and replaced with a styled clear button: Escape does not
clear this field (measured both with and without the native button), so
dropping it outright would have cost the only one-click clear.

The clear button forces the wrapper from <label> to <div> with an
explicit <label for> — a label may not contain interactive content other
than its own control. It stays mounted while empty (visibility: hidden,
aria-hidden, tabIndex -1) so the container does not widen on the first
keystroke.

.filter:focus-within is untouched.

Refs leoncheng57/leoncheng57.github.io#250
No SKILL.md declared a `metadata:` block, so `skill.tags` was `[]`
everywhere and the tag rows on both the card and the detail page were
dead code. Add `metadata.tags` to all nine skills and turn the card's
chips into buttons that drive the existing filter.

The vocabulary is deliberately seven tags shared across nine skills,
rather than a bespoke tag per skill: a tag carried by exactly one skill
groups nothing and filters nothing.

  critique      grill-me, red-team-this
  diagrams      ascii-diagrams, docs-and-diagram-tooling
  docs          ascii-diagrams, docs-and-diagram-tooling
  output-style  ascii-diagrams, duck-mode
  research      deep-research-subagents, parallel-research-handoff
  subagents     background-subagent, deep-research-subagents,
                parallel-research-handoff
  worktrees     parallel-research-handoff, worktree-up

`metadata` is a free-form string->string map in the Agent Skills spec
and OpenCode ignores it, so portability is unaffected.

Clicking a chip sets the catalogue query, because `filterSkills` already
matches tag text — no second filtering path. The chips are real
`<button>`s so they are keyboard-reachable, and focus moves to the
filter input afterwards so the query that caused the grid to change is
visible (and scrolled into view) rather than implied.

`skills.test.ts` now asserts against the real skill set: 1-3 tags each,
all drawn from the vocabulary, and every vocabulary tag used by at least
two skills. That last assertion is what stops the vocabulary rotting one
bespoke tag at a time.

Closes leoncheng57/leoncheng57.github.io#251
…t-local install method (#9)

* feat(install): add a project-local install method

All four existing methods install globally into ~/.agents/skills/<skill>.
Add a fifth that copies the skill into .agents/skills/<skill> relative to a
repository root, so it travels with the codebase and loads only there —
.agents/ is the highest-reach project path too (OpenCode, Cursor, Codex,
Copilot, Gemini CLI, Amp, Roo, Zed), with .claude/skills/ the Claude Code
equivalent.

InstallMethod grows a `scope` field so the two groups can be told apart in
the UI and asserted in tests, and INSTALL_SCOPES gains the project-scoped
.agents/skills row that the new command actually writes to.

The tests pin the exact command, the global-first ordering the lede depends
on, and the one thing that would silently break it: a `~/` creeping into the
project destination, or a relative path creeping into a global one.

Closes leoncheng57/leoncheng57.github.io#253

* feat(skill-page): put Instructions above Install, collapse both

The four install blocks filled a screen and pushed the skill's actual
content below the fold. Instructions now comes first and both sections are
native <details>, collapsed on load: keyboard- and screen-reader-operable
with no JS, and working without it.

The <h2> moves inside the <summary> rather than being replaced by one, so
heading navigation still reaches it while collapsed and the existing
aria-labelledby="instructions" keeps pointing at a real element; the install
disclosure is labelled the same way. Summaries carry the reading time and
the method count so a shut section still says something.

rehype-slug gives every body heading an id, so a deep link like
#hard-rules used to target a node the browser will not scroll to once its
<details> is shut — SkillRoute now opens the ancestors first and scrolls on
the next frame, on load and on hash change alike.

Install gets --surface-raised plus the site's border and offset shadow so it
reads as a utility panel rather than document body. That framing exposed a
pre-existing overflow (present on main too): .method is a grid item, so its
min-width: auto let the longest git clone command size the track and push
the card out of its container instead of scrolling inside .command.

The disclosure marker is a rotated glyph, not a replacement for the focus
ring: :focus-visible still resolves to the global outline on both summaries.

Closes leoncheng57/leoncheng57.github.io#252
upload-pages-artifact@v3 bundles upload-artifact@v4, which still targets the deprecated Node 20 action runtime. v5 bundles upload-artifact v7 and keeps the existing path input compatible.\n\nCloses leoncheng57/leoncheng57.github.io#266
Adds an optional `skills/<name>/SIMULATION.md` — one short, content-authored
transcript of the skill firing — rendered on the skill page as a collapsed
"Simulation Example" section above Full Instructions.

The transcript lives beside SKILL.md rather than inside it because SKILL.md is
agent context: every line is injected into the model when the skill loads. A
worked example there is pure token cost, and models imitate an example's
literal content instead of following the procedure. Same reasoning that
already produced skills/ascii-diagrams/EXAMPLES.md.

Discovery is a second `import.meta.glob`, joined to skills by directory name,
so dropping the file in is the only step — no registry, no per-skill React.
One generic component walks whatever the parser produced.

- Turns are `## user` / `## assistant` / `## tool` / `## note`, optionally
  labelled after an em dash. The splitter is fence-aware: transcripts are
  mostly code fences and one will eventually contain a line starting `## user`.
- `title`, `trigger` and `caveat` are all required. `trigger` is checked
  against both the first user turn and the skill description, so renaming a
  trigger fails the build rather than leaving a stale example behind.
  `caveat` names what the transcript compresses and renders in the panel.
- Malformed files parse to null and are skipped: the glob is eager, so
  throwing would take the site down over one typo. CI is strict instead.
- Skills without one render nothing. A reviewed WITHOUT_SIMULATION list in the
  tests keeps that a decision rather than drift, mirroring TAG_VOCABULARY.
- SkillMarkdown gains a className passthrough so the panel can re-point prose
  at the terminal palette and strip the nested per-<pre> chrome, keeping one
  react-markdown pipeline.

skills/grill-me ships the reference implementation; README documents the
convention and the authoring rules.
Wave 1 of the SIMULATION.md rollout, chosen to prove the convention at both
extremes before the remaining skills are written against it.

duck-mode is the smallest possible case: two-line replies, no reasoning to
show. Its transcript carries the two clauses that are easy to miss — the file
written to disk contains no ducks, and the assistant never mentions them —
and its note says plainly what a static transcript cannot show, that nothing
enforces any of it.

worktree-up is the opposite: four consecutive tool turns and almost no prose
until the report. It ends on the port collision rather than the happy path,
because `lsof` naming a PID and `ps -p` proving whose it is are the two
commands the skill exists to make you run.

Neither needed a role outside user/assistant/tool/note, so the four-word
vocabulary holds. Both are removed from WITHOUT_SIMULATION.
Wave 2 of the SIMULATION.md rollout: red-team-this, ascii-diagrams,
background-subagent and deep-research-subagents.

Each transcript reaches the guard the skill exists to enforce rather than
stopping at the happy path:

- red-team-this opens on the side-switch, grounds every ranked objection in a
  file:line or a command that ran, keeps the one ungrounded objection in its
  own bucket, and reports two objection classes as empty rather than skipping
  them silently.
- background-subagent splices both branches of the precondition check — flag
  unset and refused, then set and launched — and ends the turn at the report,
  because not continuing is the entire value of the mechanism.
- deep-research-subagents fans out four concurrent read-only agents with
  explicit do-not-read boundaries, then synthesises: two reports disagreed and
  the conflict is resolved by reading the cited lines, not by picking a side.
- ascii-diagrams produces one annotated tree with two invisible-in-the-diff
  traps marked at the line where each bites, then runs the width check.

Two things this wave changed beyond content.

red-team-this emits a literal `## Speculative` section. A heading inside a turn
body mints a rehype-slug id that collides with the instruction body rendered
further down the same page, so the transcript uses bold — the no-headings rule
holds and the test caught it.

Ranked tables and file trees were being wrapped into unreadable fragments by
the 74ch prose measure. The panel now caps only paragraphs, lists and quotes,
letting tables, trees and command output use the full panel width.
)

Wave 3, the two skills whose real runs are far too long to show. Both
transcripts therefore stop somewhere deliberate and say so in the caveat.

parallel-research-handoff ends at the second user checkpoint rather than at
the launch. The skill mandates showing the prompts before firing and asking
plan-mode and PR-versus-commit; running straight through them would advertise
an autonomy the skill withholds. The handoff prompt shown carries the negative
finding and the SHARED-RESOURCE rule, which are the parts agents most often
get wrong.

build-waves shows exactly one wave boundary out of six: the next wave's
read-only research overlapping the current wave's writes, the plan file and
todowrite updated together, and the run continuing without asking permission.
Its note states plainly what one boundary cannot demonstrate — that the queue
survives compaction and a model change, which is the whole point of putting it
on disk.

Also fixes a real defect in the trigger-drift test. Trigger phrases are matched
against hard-wrapped markdown, where a phrase routinely straddles a line break,
so "do not stop until it is done" failed to match itself. Whitespace is now
flattened before comparing; line wrapping is not semantic.
Completes the SIMULATION.md rollout. Every shipped skill now has one and
WITHOUT_SIMULATION is empty.

This skill was pencilled in as the likely permanent exception — it is mostly a
machine inventory and a decision table, which is not obviously transcript
shaped. That turned out to be wrong. Its named failure is precisely a
transcript: an agent hand-writes a Mermaid block into a file and declares it
done, and nobody discovers it does not parse until a reader opens the page.
The example therefore turns on the render call, with the medium chosen from
where the reader opens the doc and the house style read from an existing file
before anything is written.

Also fixes the absence test, which could no longer be reached through real
data once coverage was complete. It now layers one synthetic skill over the
real catalog, so the "renders nothing" branch stays covered rather than
depending on a gap in the catalog staying open.
Adds OpenCode custom commands alongside the skills, on their own /commands
route, with six commands chosen to demonstrate the three legitimate shapes and
every capability a command has that a skill does not.

Why both exist, which is the thing the catalogue has to teach: a skill is
model-invoked and its description is resident in the agent's context on every
turn — twelve skills currently cost ~5,700 characters, permanently, growing
linearly. A command is human-invoked and costs nothing until typed. That makes
a large command catalogue free where a large skill catalogue is not, and makes
a command the right tool for re-asserting exact instructions at turn 40, after
the skill body injected at turn 1 has been compacted away.

The six:

  /worktree-up  thin trigger over a skill, $ARGUMENTS
  /grill-me     thin trigger, agent: plan so it cannot start implementing
  /red-team     thin trigger over red-team-this
  /verify       !`npm test` output injected before the model sees the prompt
  /handoff      subtask: true, compiles off-context, composite of two skills
  /standup      no skill behind it, deliberately

House rule, enforced by a test: a command carries the happy path and its skill
carries the failure modes. No command restates a failure-mode table, because
two copies drift.

Three decisions worth recording.

Relations are DERIVED from the template body, not declared in frontmatter.
Name equality would not work — /red-team defers to red-team-this and /verify to
human-verification-steps, because nobody types a skill's full name as a slash
command — and this repo should not add speculative keys to a frontmatter
namespace OpenCode owns. Reading back the deferral cannot drift from the truth.

Command worked examples live in a separate command-simulations/ directory, the
one place this diverges from skills. OpenCode registers every .md in commands/
as invocable, so a sibling verify.SIMULATION.md would put a bogus
/verify.SIMULATION in the user's autocomplete. The format is identical and the
skill parser is reused verbatim.

Commands ship OpenCode-only. Claude Code reads .claude/commands/ with a
different dialect (argument-hint, allowed-tools), so unlike SKILL.md these are
not portable. The install table says so rather than implying otherwise.

SimulationPanel gains optional sourcePath/sourceUrl props so a command page can
point the terminal chrome at its own transcript; skillName still works
unchanged.
@leoncheng57

Copy link
Copy Markdown
Owner Author

Post-merge archive follow-ups

Current hosting topology: the catalog is deployed by the old repository at https://leoncheng.dev/agent-skills/. This repository has Pages configured at https://leoncheng.dev/custom-dca-opencode/, but its root currently returns 404 and gh-pages is used for PR screenshot artifacts; the Runner application itself is local/Tailscale-hosted rather than a public static website.

@leoncheng57

leoncheng57 commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Stacked follow-up opened: #113 adds the public React website and moves the catalog under /custom-dca-opencode/agent-skills/. Merge this PR first, then retarget #113 to main.

@github-actions
github-actions Bot temporarily deployed to pr-preview-106 August 26, 2026 07:11 Destroyed
@leoncheng57
leoncheng57 merged commit ec4c969 into main Aug 26, 2026
6 checks passed
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:high Highest-priority work to do now

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate agent-skills (skills/commands catalog) into this repository

1 participant