Skills
11. Stack multiple skills
View this stage's code on GitHub →Claude Code lets a prompt invoke several skills at once — /rabbit /fox 4127 — so this project does too. The expansion rule is simple to say and easy to get subtly wrong: walk the leading /name tokens, keep expanding while each one resolves to a real skill, and stop at the first token that doesn't — that token and everything after becomes the shared argument text for every skill that was expanded. Getting this right means building a small table of cases (/a /b 4127 vs /a 4127 /b vs /a /nonexistent 4127) and checking each one explicitly, because the three look almost identical.
Your task
- Walk the leading
/nametokens in the prompt, expanding every one that resolves to a real skill. - Stop at the first token that doesn't resolve — that token and everything after it becomes the shared argument text for every skill that was expanded.
- Build one user message per expanded skill body instead of overwriting a single message.
The solution
Try the task above first. When you want to compare, this is exactly what changed since stage 10 (just diff 10 11 shows the same).
Show the solutionHide the solution +59 −40 lines
.petite/skills/fox/SKILL.md+8 −0
⋯ ---name: foxdescription: Reports the den location for the project's fox, demonstrating skill stacking.--- Include the word "den" in your answer, on its own line. Do not call any tools.
.petite/skills/walk/SKILL.md+8 −0
⋯ ---name: walkdescription: Takes an animal for a walk, demonstrating shared arguments in a skill stack.--- Include this sentence in your answer, on its own line: Take the $0 for a walk. Do not call any tools.
main.py+9 −9
⋯ """Stage 9: slash commands, level 2 (invoke)Stage 11: stacking multiple skills in one prompt A "/name" prompt loads that skill's SKILL.md body instead of the rawprompt — see skills.py for how resolution and progressive disclosure work."/fox /walk 4127" now expands into one user message per skill — seeskills.py for how the expansion and argument-sharing rules work.""" import argparse⋯ def main(): client = OpenAI(api_key=API_KEY, base_url=BASE_URL) messages = [{"role": "user", "content": args.prompt}] skill_body = resolve_slash_command(args.prompt) if skill_body is not None: print(f"[agent] resolved slash command to skill body ({len(skill_body)} chars)", file=sys.stderr) messages[0]["content"] = skill_body skill_bodies = resolve_slash_command(args.prompt) if skill_bodies is not None: print(f"[agent] resolved {len(skill_bodies)} skill invocation(s)", file=sys.stderr) messages = [{"role": "user", "content": body} for body in skill_bodies] else: messages = [{"role": "user", "content": args.prompt}] skills = discover_skills() system_prompt = build_skills_system_prompt(skills)
skills.py+34 −31
⋯ """Stage 10: skill arguments ($ARGUMENTS, $0, $1, ...) A slash command can pass arguments after the skill name:"/feed rabbit carrots" invokes "feed" with args ["rabbit", "carrots"].The body can reference them with placeholders, substituted before thebody is sent to the model: $ARGUMENTS everything after the skill name, joined by spaces $ARGUMENTS[0], [1] a single argument, by position $0, $1, ... shorthand for the same thing Order matters when substituting: $ARGUMENTS[0] must be handled before thebare $ARGUMENTS, or a naive string replace would mangle it into"rabbit carrots[0]".Stage 11: stacking multiple skills in one prompt A prompt can invoke more than one skill at once: "/rabbit /fox 4127"loads both bodies, and the trailing "4127" reaches each of them as$ARGUMENTS. Expansion runs left to right from the start of the prompt —every token that names a real skill gets expanded; the first token thatdoesn't name a skill ends the run, and it (plus everything after it)becomes the shared argument text for every skill that WAS expanded. "/rabbit /fox 4127" -> expands [rabbit, fox], args = "4127" "/rabbit 4127 /fox" -> expands [rabbit], args = "4127 /fox" "/rabbit /owl 4127" (no owl skill) -> expands [rabbit], args = "/owl 4127"""" import os⋯ def load_skill_body(name, skills_dir=SKILLS_DIR): def resolve_slash_command(prompt, skills_dir=SKILLS_DIR): """If prompt starts with '/', resolve the first word as a skill name, substitute any argument placeholders in its body, and return that as the user message content. """If prompt starts with one or more "/skill-name" tokens, expand them all and return a list of substituted bodies, one per skill — in the order they appeared, each with the same trailing argument text. Returns None if the prompt isn't a slash command or the named skill can't be found, so the caller falls back to the raw prompt. """ if not prompt.startswith("/"): return None Expansion stops at the first token that isn't a real skill name; that token and everything after it becomes the shared $ARGUMENTS text. rest = prompt[1:].strip() if not rest: Returns None if the prompt doesn't start with a recognized skill invocation at all, so the caller falls back to the raw prompt. """ tokens = prompt.split() expanded_names = [] i = 0 while i < len(tokens) and tokens[i].startswith("/"): candidate = tokens[i][1:] if load_skill_body(candidate, skills_dir) is None: break expanded_names.append(candidate) i += 1 if not expanded_names: return None parts = rest.split() skill_name, args = parts[0], parts[1:] body = load_skill_body(skill_name, skills_dir) if body is None: return None args = tokens[i:] return substitute_arguments(body, args) return [substitute_arguments(load_skill_body(name, skills_dir), args) for name in expanded_names] def substitute_arguments(body, args):
Try it
Run this stage's own code:
just stage 11
cd .stages/11
# bundled skills: "fox" (adds the word den) and "walk" (adds: Take the $0 for a walk.)
just run "/fox /walk dog"
Expect: Both skills in one answer — den, and Take the dog for a walk. — with dog as their shared $ARGUMENTS
.stages/11 is a git worktree: your checkout stays on main, so
just keeps working there. Compare with the previous stage with just diff 10 11,
or browse it on GitHub;
just clean-stages removes the worktrees.
Where this fits
The whole agent; this stage builds the highlighted part. Click any part to jump to its stage.