Skills

12. Bundled scripts (level 3)

View this stage's code on GitHub →

A skill folder can hold more than SKILL.md — scripts/, references/, assets/ — and none of it costs any context until the body explicitly points at it. That's level 3: a two-line body can reference a two-hundred-line script, and neither the script nor its output enters the conversation until the model actually runs it. The one wrinkle is that the body names its files with paths relative to its own folder (scripts/sha256.sh), while the agent runs from the project root. Telling the model which folder that is turns out not to be enough — models often run the relative path as-is and get "file not found" — so before a body reaches the model, its bundled paths are rewritten to paths from the project root (.petite/skills/badger/scripts/sha256.sh).

Your task

  1. Let a skill folder hold scripts/, references/, or assets/ alongside SKILL.md — none of it loads into context unless the body names it.
  2. Before a resolved body reaches the model, rewrite its bundled paths (scripts/, references/, assets/) to paths from the project root, and prefix a header naming the skill's folder (Skill: name (located at path)).

The solution

Try the task above first. When you want to compare, this is exactly what changed since stage 11 (just diff 11 12 shows the same).

Show the solutionHide the solution +44 −13 lines
.petite/skills/badger/SKILL.md+8 −0
⋯ ---name: badgerdescription: Reports the checksum of the project's data file, demonstrating a bundled script.--- Run `scripts/sha256.sh` using the Bash tool. Respond with only the value it prints, and nothing else.
.petite/skills/badger/data.txt+1 −0
⋯ sett location: forest edge, 47 tunnels deep
.petite/skills/badger/scripts/sha256.sh+2 −0
⋯ #!/usr/bin/env bashcd "$(dirname "$0")/.." && sha256sum data.txt | cut -c1-8
skills.py+33 −13
⋯ """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"Stage 12: level 3 — bundled scripts A skill folder can hold more than SKILL.md: scripts/, references/,assets/ — level 3 of progressive disclosure. These never enter contexton their own; the body has to point at them, and only then does themodel load/run them (here, scripts/, via the Bash tool). A body says "Run `scripts/sha256.sh`" using a path relative to its OWNskill folder — but the agent runs from the project root. Only tellingthe model which folder that is isn't enough: models often run therelative path as-is and get "file not found". So before the bodyreaches the model, its bundled paths (scripts/, references/, assets/)are rewritten to project-root paths, e.g. "scripts/sha256.sh" becomes".petite/skills/badger/scripts/sha256.sh".""" import os⋯ def resolve_slash_command(prompt, skills_dir=SKILLS_DIR):     args = tokens[i:]     return [substitute_arguments(load_skill_body(name, skills_dir), args) for name in expanded_names]    return [        _with_folder_header(name, substitute_arguments(load_skill_body(name, skills_dir), args), skills_dir)        for name in expanded_names    ]  def _with_folder_header(name, body, skills_dir=SKILLS_DIR):    """Prefix a skill's (already-substituted) body with a header naming    its own folder, and rewrite the body's bundled paths (scripts/,    references/, assets/) to paths from the project root, where the    agent, and so every Bash/Read call, actually runs.    """    folder = os.path.join(skills_dir, name)    body = re.sub(        r"(?<![\w./-])((?:scripts|references|assets)/[\w./-]+)",        lambda m: os.path.join(folder, m.group(1)),        body,    )    return f"Skill: {name} (located at {folder})\n\n{body}"  def substitute_arguments(body, args):

Try it

Run this stage's own code:

just stage 12
cd .stages/12
# bundled skill "badger", body: "Run `scripts/sha256.sh` using the Bash tool. ..."
just run "/badger"

Expect: 4158bc93 — the value the bundled script prints; check it yourself with bash .petite/skills/badger/scripts/sha256.sh

.stages/12 is a git worktree: your checkout stays on main, so just keeps working there. Compare with the previous stage with just diff 11 12, 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.

References

← 11. Stack multiple skills 13. Model-invoked skills →