Skills that load only when you need them.
Loomcycle has had skills for a long time. A skill is a Markdown file (frontmatter plus body) that teaches an agent how to do a specific thing: draft a LinkedIn post, review a diff, extract structured data from a transcript. The frontmatter names the tools the skill needs; the body is the instructions.
The old wiring was simple. At server boot, every SkillDef listed in an agent's skills: field was concatenated onto the system prompt. If the agent had five skills wired up, all five bodies rode in every request to the model, whether the agent invoked them or not. Cheap to implement. Expensive to run.
RFC BA is the fix. It ships across v1.14.0 (the primitive change) and v1.14.1 (a bundle-side cleanup for the document-agent). Bodies stop riding in the system prompt at boot; the runtime auto-wires a new Skill tool that lists what's available and invokes on demand; the agent's skills: field becomes a pattern allowlist. The skill_def_scopes field, which used to gate authoring, is gone.
The short version. Every agent that can use skills gets a Skill tool with two ops: list enumerates the agent-visible catalog (static bundles + inline defs + the caller's tenant SkillDefs) filtered by an allowlist and an optional /-glob pattern; invoke loads and runs a permitted skill. The agent's skills: field carries the allowlist patterns (globs like doc/* or writing/*). Empty means allow-all with the Skill tool auto-added; -* means deny (no Skill tool wired). Removed: the skill_def_scopes field, which used to gate SkillDef authoring separately; authoring is now gated by skills: itself. Bundled document-agent skills group under doc/* as of v1.14.1. Local-first routing (ollama-local → deepseek → anthropic) becomes the default for the standalone document-agent and chat bundles.
The context-budget problem
An agent I've been running for two months is a document-editing supervisor. It has skills for chunk-authoring, chunk-review, style-checking, section-summarizing, index-updating, publish-status flipping, and a dozen more. Every one of those skill bodies is a couple of hundred lines of Markdown with worked examples and edge-case guidance. The old wiring loaded all of them into the system prompt at every request.
On a warm cache that was fine, more or less. On the first request of a session, every skill body counted against the context budget before the agent had done any work. Auto-compaction started firing earlier than it should. The skills the agent actually invoked in a given session were often a small subset of what was loaded. The rest was dead weight.
Two ways out. One: split the supervisor into smaller agents, each with a narrower skill set. That works but scatters the operational surface; six agents means six audit trails, six tenant bindings, six invoice lines. Two: make the substrate load skills only when they're invoked. RFC BA is option two, done at the substrate layer so no agent's YAML has to change to opt in.
The Skill tool
Every agent that can use skills now gets a Skill tool auto-added to its allowed-tools list at config load. Two ops:
// list what's available (name + description only) {"tool":"skill", "input":{"op":"list", "pattern":"doc/*"}} // optional glob // invoke the skill body (loads the Markdown, runs the prompt) {"tool":"skill", "input":{"op":"invoke", "name":"doc/chunk-review"}}
list returns a filtered catalog: static bundles + inline defs + the caller's tenant SkillDefs, narrowed by the agent's allowlist and (optionally) a /-glob pattern. Only name and description come back. Bodies stay in the substrate. The agent decides what to invoke based on the descriptions, then calls invoke and the body streams into that turn as the tool result.
invoke takes a bare name (doc/chunk-review). The runtime checks the name against the allowlist via skillmatch, an ordered pattern-allowlist evaluator with /-grouped grammar. A name that isn't permitted returns a typed error the agent can read; the invocation never happens. On success the skill body loads, the prompt runs, and the result comes back in the tool_result envelope like any other tool call.
Existing skill bodies are unchanged. The frontmatter shape is unchanged. The Markdown-body-with-examples convention is unchanged. What changed is when the body enters the model's context: on invoke, not at boot.
The allowlist
The agent's skills: field used to be an exact-name bundle list: a list of skill names to concatenate at boot. In RFC BA it's a pattern allowlist. Globs group skills by domain:
// AgentDef fragment skills: - doc/* - writing/* - -writing/adversarial-* // negative pattern; denies specific matches
The evaluator (skillmatch) walks the patterns in order. First match wins. Negative patterns start with - and deny; positive patterns allow. Empty skills: is a special case: allow all (the Skill tool is still auto-added; the agent has access to every skill in its scope). [-*] is deny-all: no Skill tool gets wired and no invocation is possible from that agent's runs.
The grammar is deliberately close to how filesystems treat glob patterns. Skills are named with /-grouped segments (doc/chunk-review, writing/linkedin-post, marketing/analyze-competitors). The /-glob pattern lets an agent grant a whole domain in one line without listing every leaf. Adding a new skill under an existing domain doesn't need an AgentDef edit.
A subtle detail: the allowlist patterns are excluded from the AgentDef's content_sha256. That's an authority-only field, like scope grants; changing the allowlist doesn't fork the agent's content-addressed identity. Compare to providers: or search_providers:, which do participate in the hash because they change the agent's behavior. A skills allowlist tightens or loosens what the agent can access; it doesn't change what the agent is.
The removed field: skill_def_scopes
Before RFC BA, SkillDef authoring (create, fork, promote, retire) was gated by a separate skill_def_scopes field on the agent. That was two fields for one concern: skills: said what the agent could use; skill_def_scopes said what the agent could author. Two fields that had to stay coherent, and did not.
RFC BA unifies both under skills:. The allowlist gates access to a skill: listing it, invoking it, creating a new SkillDef under a name that matches the allowlist, forking one, promoting or retiring it. Authoring a SkillDef with a name outside the agent's allowlist returns the same typed error as trying to invoke a name outside. One field, one truth.
This is a breaking change. Any AgentDef YAML that used skill_def_scopes won't load on v1.14.0. Two paths: either promote skill_def_scopes patterns into skills: (in most deployments they were identical anyway; the split was theoretical), or delete skill_def_scopes and accept the default (empty skills: = allow-all). No adapter change is needed; both @loomcycle/client and the Python adapter model skills as a string array and pass it through opaquely.
How list and invoke agree with the substrate
The Skill tool composes with two substrate concerns: which skills exist, and which ones this agent may reach.
Which skills exist: a merge of three sources. Static bundles (skill files shipped with the loomcycle binary, like the document-agent family under doc/*), inline defs (skills declared inline inside the operator's bundles: config in loomcycle.yaml), and tenant SkillDefs (skills created at runtime and stored in the tenant's substrate). The Skill tool draws from all three transparently. To the agent, a bundled skill and a tenant-authored one look the same.
Which the agent may reach: the allowlist. The skillmatch evaluator runs the same way against all three sources; a tenant SkillDef named doc/my-custom-review and a bundled skill named doc/chunk-review are both grantable by a doc/* line. The substrate resolves precedence: an inline def or a tenant def with the same name as a bundled one shadows the bundle. That's the same shape as AgentDef overlay, and by design.
One implementation detail worth naming: the ephemeral per-run "skills available" note. When an agent has a whitelist (positive patterns), the runtime injects a short list of matching skill names + descriptions as an ephemeral system message at the start of that specific run. The agent doesn't have to call Skill op=list to discover them; they're pre-listed. For deny-all or empty-allow-all agents, no note is injected. This gives the agent a running-start view without paying the full-body cost. If the agent wants richer detail, it calls list.
The document-agent bundle, regrouped
The document-agent is the bundled agent that manages chunked-graph Documents (RFC AK). It's the workhorse behind the launch publishing plan document I've been editing across the last three months. It has a lot of skills.
v1.14.1 collapses the whole family under doc/*. Standalone SKILL.md directories plus embedded inline keys all move into the doc/ domain: doc/chunk-add, doc/chunk-update, doc/chunk-move, doc/chunk-delete, doc/chunk-review, doc/document-export, doc/document-import, doc/document-summarize, doc/query, and so on. The doc-manager agent's allowlist collapses to skills: [doc/*]. Every doc-agent skill grants in one line.
Same release: the standalone document-agent and chat bundles switch to local-first routing. The provider cascade becomes ollama-local → deepseek → anthropic; the model names become tier aliases (chat-local, chat, whatever the operator's alias map resolves to); autocompact_at_pct drops from 80 to 60. A homelab operator with a working ollama-local install (see the TrueNAS field report) runs these bundles against their own hardware by default. Cloud providers fall over automatically if local-inference errors, but nothing routes to Anthropic unless everything else has failed.
Why this doesn't fragment the substrate
When I first sketched RFC BA I worried about two failure modes. The first: agents forgetting to call Skill op=list because the ephemeral note didn't remind them well enough. The second: the substrate resolving allowlist collisions in ways an operator can't reason about.
For the first, the fix is a system-prompt convention. Every agent that has a whitelisted allowlist gets the ephemeral note appended describing the tools it has and which skills are grantable. Empirically the agents I've tested with this pattern (Anthropic, OpenAI o-series, DeepSeek, Ollama qwen3) all call list or invoke directly by name from the note, so the two paths work interchangeably. The bodies never enter unless invoked.
For the second, the shape is deliberately conservative. Precedence is tenant > inline > bundle. Ordered patterns evaluate first-match-wins. Positive-then-negative is legal and behaves like the reader expects. Every resolve decision is logged at debug level so an operator digging into "why did the agent invoke my custom doc/chunk-review instead of the bundled one" can trace it in the audit log.
What this five-line arc unlocks
For a homelab agent with two skills, RFC BA doesn't change the context budget meaningfully. For a supervisor with a dozen skills wired up, it moves the skill bodies out of the boot-time system prompt and into on-demand invocations. That's a real budget win, and it composes with the auto-compaction dropping to 60% and the local-first routing bundle switch: three changes together shrink the standalone document-agent's per-request cost on the reference workload by more than half.
For the substrate itself, the win is different. The Skill tool's list/invoke pair is the same shape as MCP's tools/list/tools/call. Skills are now tools-of-tools: an operator adds a tenant SkillDef and it appears in the agent's Skill op=list output without a redeploy, without a fork, without an operator-side lookup table. The substrate is one primitive smaller, and one primitive more MCP-shaped.
Next in this sequence: RFC BB (first-class web-search providers) which cleans up the WebSearch tool's Brave-only shape into a proper multi-provider fallback circuit, and RFC BC (client-executed tools) which inverts the tool-direction so a browser or IDE can register its own tools that the agent calls as if they were built in. Both land in the same three-day arc.
Companion reading: the previous release digest (encrypted credentials + cost ledger + token budgets), MCP tools as data (how loomcycle import claude-code lifts .claude/skills/*/SKILL.md into loomcycle's substrate), and the skills help topic for the operator-side reference.