feat(web): a coding agent is offered its repository's agent skills, and a `/name` loads one
Merged
DA
7d ago+949+949+012345678901234567890123456789-3-3-0123456789
What
PR A of the agent-skills rollout (Dean: "support agent skills in the composer… auto-detect skills from the repo… / shows a list"). This PR is the agent side. The desktop's / picker comes in follow-up PRs.
- Where skills are found:
server/hosted/repository-skills.tsreads a repository's skill folders from GitHub on the default branch, through the existing read path (readRepositoryFolder+readRepositoryFile, on a read token for that one repository). The two roots are.agents/skills/<name>/SKILL.md(Codex and the cross-harness standard; this repo's own skills live there) and.claude/skills/<name>/SKILL.md(Claude Code).- Only folders count. GitHub lists a symlinked
.claude/skills/x → .agents/skills/xas a blob, so it dedupes naturally. When two roots have the same name, the first root wins. - The name comes from the folder (the same way eve names one from its path) and must match the spec's
[a-z0-9-], ≤64. A skill with nodescriptionis skipped. Claude Code'suser-invocable: falseis read, for the picker to honour later. - Bounds: 64 skills,
SKILL.md≤ 64 KB, description ≤ 1024 chars, 8 reads in flight. - Frontmatter is read by a small reader (plain, quoted, and
|/>block scalars) instead of adding a YAML dependency.
- Only folders count. GitHub lists a symlinked
- How a skill reaches the agent (eve-native): eve 0.74 has dynamic skills and a
load_skilltool. It lists the skills to the model and loads one's body when the user names it, but it never scans the checkout.coder/skills/repository.ts(session.started) hands eve the repository's skills viahost.skills(coder-host/skills.ts).- Each body is prefixed with its folder in
/workspace/repository, soreferences/…resolve in the checkout. It is passed as markdown only, so eve writes nothing to the sandbox. coder/tools/load_skill.tsre-exports eve's tool, because the coder runsdefaultTools: false.- The instructions add one line: a
/namein the developer's message means callload_skillfirst. - The composer's token will literally be
/namein the message text, so no wire change is needed. It works for a start task and for a steering message alike, because skills are resolved per session rather than per turn. - Side effect: like Claude Code and Codex, the agent sees the repo's skill list and may load one unprompted.
- A GitHub failure at session start is logged and means no skills, never a failed start.
- Relay: eve reports a skill load as its own
load-skillaction /load-skill-result, not atool-call. The shared relay now records it as aload_skillcall, which was otherwise refused (no_call).load_skillis registered inCODER_TOOL/ the writer's tool set and in the context estimate's authored tools.
Tests
tests/repository-skills.test.ts, over a scripted GitHub (tests/support/skills-repository.ts):- both roots, frontmatter forms, first-root-wins, a link, a bad name, oversize, missing, undescribed
- the 64-skill bound
- the description bound
- GitHub failing
tests/coder-host.test.ts:host.skillsoffers the repo's skills with the checkout-path preamble, and none when the repo is unreached or GitHub can't be read- a
load-skillaction and its result land as aload_skillcall (completed and failed). This fails onmain's relay.
tests/coder-agent.test.ts: the instructions name/name→load_skill. The authored tools still equal the writer's set,load_skillincluded.coder/evals/coder.eval.ts(scripted model, real Postgres): a session opened with/release-notes for this week- The scripted model calls
load_skillonly if eve's announcement lists the skill. - The answer holds the
load_skillpart with the skill's body. - The reply follows it.
- I ran it locally against Postgres 16: passes.
- The scripted model calls
Evidence
./scripts/check.sh: exit 0.pnpm --filter @luke/coder test:agent(withDATABASE_URL): passes.- Server-only, no Mac UI:
./scripts/verify.shwas not run (Linux VM), and CI builds nothing for the Mac.
Notes
- Follow-ups: B, the desktop's
planning.skillsread (same reader behindGET /api/plans/{id}/skills) and an agent-tab row for aload_skillcall (it renders as a generic call until then). C, the composer's/picker and token.
🤖 Generated with Claude Code