Assistant Skills¶
A skill is a folder of instructions you give the ubAI assistant. Drop it into the deployment’s skills directory and the assistant picks it up on its next turn — no rebuild, no restart. (Nothing re-reads the directory on its own: the next question somebody asks the assistant, once the re-scan TTL has expired, is what performs the scan. See Checking that a skill loaded below.)
Use a skill to teach the assistant something the product cannot know: your release checklist, how your team names requirements, which report answers which question, the phrasing your auditors expect.
Warning
Anything written to the skills directory changes what the assistant does. Treat write access to it exactly as you treat write access to the deployment’s configuration.
Where skills come from¶
The assistant reads two directories, in this order:
Directory |
What it holds |
|---|---|
|
Your own skills. This is the one you add folders to. |
|
Skills that ship with ubTrace. Part of the product, read-only, nothing to install. |
The first directory wins a name collision, so your own skills take precedence. Give a folder the same name as a shipped skill and yours is the one that loads, which is how you replace a shipped skill whose wording does not suit your project — you cannot edit the shipped copy, but you can override it. The copy that lost is reported as skipped on the health endpoint (see Checking that a skill loaded), with the reason naming the directory that won, so an accidental collision is visible rather than silent.
Both directories are mounted read-only into the assistant’s container: it reads skills and never writes them.
Folder layout¶
One folder per skill, named exactly as the skill is named, containing a
SKILL.md:
skills/
release-checklist/
SKILL.md
references/
rollback.md
SKILL.md carries the whole skill. The other files are optional: the
assistant reads them only when the skill points it at one, which keeps
SKILL.md short while the detail stays available.
Vocabulary in a skill¶
Whatever a skill’s text says reaches the assistant on whatever project the
deployment serves — the same wording is in front of the assistant no matter
which project a question is about. A skill that ships with ubTrace
therefore names no concrete need or link type: it binds roles to the live
schema of the project being asked about instead, and the bundled
traceability skill is the pattern to copy. A skill you write in your
own skills/ folder is different: it may name your project’s types freely —
that is exactly what it is for. If a shipped skill’s wording does not suit
your project, give your own folder the same name and yours is the one that
loads (see Where skills come from above).
A complete example¶
skills/release-checklist/SKILL.md:
---
name: release-checklist
description: The checks a release must pass before it ships
when_to_use: when the user asks about releasing, shipping, or a release gate
agents: all
license: proprietary
---
Before calling a release ready, confirm all four:
1. Every requirement in the milestone has a passing test.
2. No requirement is left in state `draft`.
3. The safety chapter's requirements are covered end to end.
4. The changelog names the release.
Report the four as a table, one row per check.
For the rollback procedure, read `references/rollback.md`.
Header fields¶
The header is YAML between two --- lines at the very top of the file. Keys
this harness does not recognise are ignored, so a skill written for another tool
still loads.
Field |
Required |
Meaning |
|---|---|---|
|
yes |
The skill’s name. Must equal the folder name, and may contain only
lowercase letters, digits and single dashes ( |
|
yes |
One line saying what the skill does. The assistant reads this to decide whether to open the skill, so make it concrete. |
|
no |
When the skill applies, in your own words. Set it to the single word
|
|
no |
Which agents may see the skill. One value or a list. Defaults to |
|
no |
|
|
no |
|
|
no |
Free text recording the skill’s licence. Recorded, never interpreted. |
when_to_use: always¶
when_to_use is normally prose that helps the assistant decide. The single
value always (any capitalisation) means something different: the skill’s full
text is placed in front of every agent it is scoped to, on every turn, and it is
not offered as something to open — it is already there.
Use always for house rules. Use prose for anything situational: a skill the
assistant opens only when it applies costs nothing on the turns where it does
not.
Running a skill by hand¶
Start a message with /<name> — for example /release-checklist is 2.1
ready? — and that skill is loaded for the turn as if it were always, for
every agent it is scoped to. A / that names no skill is left alone and
answered as an ordinary question, so /usr/local/bin and /2 still work.
Which agents can see a skill¶
A turn is answered by several agents. agents: decides which of them a skill
reaches.
An id names an agent by its role — its position in the turn — and, where a role runs once per subject area, by its domain.
Role |
Domains |
What it does |
|---|---|---|
|
— |
Decides what the turn needs and which agents to send. |
|
|
Queries the traceability graph and reads documentation. |
|
|
Reshapes results for display. Reserved — see the note below. |
|
— |
Writes the answer the user reads. Reserved — see the note below. |
Write one of these five forms:
Write |
Meaning |
|---|---|
|
Every agent that has skills enabled. The default. |
|
No agent. Turns the skill off without deleting it — and it wins over every other value in the same list. |
|
A role, whatever domain it runs under. |
|
A domain, whatever role. Reaches every agent working on needs. |
|
Exactly one agent. |
As a list:
agents:
- planner
- research:needs
An unrecognised value is an error and the skill is skipped, with the reason reported on the health endpoint — a silently ignored selector would produce a skill that loads and is never seen.
Note
display-composition, render and the reports domain are accepted
ids, but no agent behind them currently reads skills — the reports agent runs
no model at all. A skill scoped only to those loads and does nothing. Scope
skills to planner, research, needs or docs_qa.
Installing a skill¶
Docker Compose¶
Put the folder in skills/ beside your compose file (docker-compose.yml,
or docker-compose-prod.yml in an offline bundle, which ships the directory
with a README.md in it):
skills/release-checklist/SKILL.md
The directory is bind-mounted read-only into the ubtrace-copilot container at
/app/skills. No restart is needed; the next assistant turn after the
re-scan TTL re-reads it. The skills that ship with ubTrace are
already inside the container at /app/skills-bundled — you never install
those, and you cannot edit them.
Both paths are named by COPILOT_SKILLS_PATH, a :-separated list in which
the earlier directory wins a name collision — your own directory is listed first,
so a skill you write overrides a shipped skill of the same name. Adding a further
directory means adding a mount and naming it in that list; a single path is still
perfectly valid.
Kubernetes¶
The chart provisions a PersistentVolumeClaim (<release>-skills) and
mounts it read-only at /app/skills in the copilot pod. The shipped skills
need no volume — they come with the image at /app/skills-bundled.
Because the copilot’s mount is read-only, kubectl cp into the copilot pod is
refused (Read-only file system). Write into the claim from a short-lived
helper pod instead, the same pattern Kubernetes Deployment uses for the artifact
claims. The helper is pinned to the copilot’s node: the default claim is
ReadWriteOnce, which only pods on one node can attach.
NAMESPACE=ubtrace
CLAIM=$(kubectl get pvc -n "$NAMESPACE" -o name | grep -- '-skills$' | cut -d/ -f2)
echo "$CLAIM" # e.g. ubtrace-skills
kubectl run ubtrace-skills-import --image=busybox --restart=Never -n "$NAMESPACE" \
--overrides="{
\"spec\": {
\"affinity\": {\"podAffinity\": {\"requiredDuringSchedulingIgnoredDuringExecution\": [{
\"labelSelector\": {\"matchLabels\": {\"app.kubernetes.io/component\": \"copilot\"}},
\"topologyKey\": \"kubernetes.io/hostname\"
}]}},
\"containers\": [{
\"name\": \"import\", \"image\": \"busybox\", \"command\": [\"sleep\", \"3600\"],
\"volumeMounts\": [{\"name\": \"skills\", \"mountPath\": \"/skills\"}]
}],
\"volumes\": [{\"name\": \"skills\", \"persistentVolumeClaim\": {\"claimName\": \"$CLAIM\"}}]
}
}"
kubectl wait --for=condition=Ready pod/ubtrace-skills-import -n "$NAMESPACE" --timeout=60s
kubectl cp ./release-checklist "$NAMESPACE/ubtrace-skills-import:/skills/release-checklist"
kubectl exec ubtrace-skills-import -n "$NAMESPACE" -- ls /skills # release-checklist
kubectl delete pod ubtrace-skills-import -n "$NAMESPACE"
The copilot picks the folder up on its next scan — the first assistant turn
after the re-scan TTL has expired; no restart is needed. Point
persistence.skills.existingClaim at your own claim to populate the volume
some other way — for instance a ReadWriteMany claim your CI writes to.
Checking that a skill loaded¶
GET /api/v1/copilot/health reports every skill the assistant loaded and
every folder it skipped. Send an access token with the request: the skills
report names the directory on disk and the reason each folder was skipped, so it
is returned to an authenticated caller only. Called without credentials the
endpoint answers 200 with the bare liveness status and nothing else — it is
also the container’s health probe, and it must stay reachable without a login
(and without a licence) so a healthy service is never restarted for want of one:
{ "status": "ok" }
Note
The endpoint reports the catalogue as of the last scan, and it never
reads the directory itself — it is also the container’s liveness probe, and a
probe that walked a network mount would take the service down with the
mount. A scan happens at startup and then only when an assistant turn asks
for the catalogue and the last scan is older than
COPILOT_SKILLS_RESCAN_TTL_MS (default 30 s). That TTL is an expiry
consulted on the next use, not a timer: on an idle deployment nothing
re-reads the directory at all. So after adding a skill, ask the assistant
anything, then read the endpoint.
A folder you cannot find in loaded is one of two different things, and
the payload tells you which: a folder in rejected was scanned and
refused (the reason says why); a folder in neither list has not been
scanned yet — ask the assistant a question and read the endpoint again. If
it is in neither list after a turn, the copilot cannot see the directory:
check the mount and COPILOT_SKILLS_PATH. The copilot log carries the
same verdict (Skill catalogue loaded: N skill(s), M rejected., plus one
line per rejected folder) for every scan that found a change.
With a valid access token the full report is returned:
{
"status": "ok",
"skills": {
"rootPath": "/app/skills",
"rootPaths": ["/app/skills", "/app/skills-bundled"],
"loaded": [
{ "name": "release-checklist", "agents": ["all"], "alwaysLoad": false }
],
"rejected": [
{
"directoryName": "house-style",
"reason": "frontmatter \"name\" (style) must equal the directory name \"house-style\"."
}
]
}
}
rootPaths lists every skills directory in the order they are read — the
first entry wins a name collision, and your own directory is first. rootPath repeats the first of them and is
kept so a reader written for a single directory still works.
A skill with a problem is skipped and named, with the reason; the rest keep working and the service stays healthy. Common reasons:
the
namein the header does not match the folder name;nameuses characters outsidea-z,0-9and-;descriptionis missing;the header is not valid YAML;
agentsnames something that is not an id;the folder, or its
SKILL.md, resolves outside the skills directory — a symbolic link pointing out of the mount is refused, not followed;a file is a hard link, or is larger than 4 MB (see Limits);
the skill can never be reached — not
always, not model-invocable and not user-invocable;a skill of the same name is provided by an earlier directory, which wins — the reason names that directory.
Limits¶
A skill’s text is used up to about 20 000 characters; longer text is cut and marked.
Always-loaded skills share a budget of about 40 000 characters per agent. Past it, whole skills are left out rather than half-included, and the ones left out are named in the prompt.
Everything a skill is made of must live inside its own skills directory: its folder, its
SKILL.md, and every file it references. Anything that resolves outside — including through a symbolic link, and including into the other skills directory — is refused rather than read. A skill’s referenced files are always read from the directory that skill came from. A symbolic link that stays inside the directory is fine; a hard link is always refused, because its content is shared with a file the directory does not contain. Copy the file in instead.No single file may exceed 4 MB. A larger one is refused by name and the rest of the skill still loads.