| name | author-skill |
| description | Author a new skill for this repo end to end — take a topic, check for existing coverage, drill the official docs behind it, write a filled handoff brief to docs/handoffs/, then draft the SKILL.md and run the post-draft checks. Use when asked to write, author, create, or scaffold a new skill, or when a drift-audit new-skill candidate has been accepted. Encodes this repo's own conventions rather than generic skill advice — verb naming for behavioral skills and fabric-/pbir-/pbid- prefixes for platform ones, the description as the entire trigger mechanism, long detail split into references/, lint-frontmatter.py, and which tree a new skill belongs in and its deploy step. Drills before it writes and never encodes an unverified claim. Ends at a linted draft plus a fresh-session test plan; writes no test fixtures and does not commit — fixtures and validation are test-skill's, which reads the brief back off disk. To fold a session learning into guidance that already exists, use learn instead. |
| argument-hint | [topic] |
| allowed-tools | Read Write Edit Glob Grep Bash WebFetch |
| model | fable |
| effort | max |
| disable-model-invocation | false |
| context | inline |
Author a skill
Take a topic and end at a linted SKILL.md draft plus a runnable
fresh-session test plan. The input is a subject, not a specification —
deciding the name, the boundaries, and what the skill deliberately will
not cover is the work, not a precondition for it.
This skill is house-style. It encodes how this repo builds skills; the
generic mechanics of the Agent Skills format are somebody else's job
(see step 1).
Paths below are relative to the repo root. This skill is project scope and
fires only in sessions here, but a skill it authors in skills/ deploys
through ~/.claude/skills — a real directory holding one junction per
skill in that tree.
An edit is immediate; a new skill needs the linker once. The
asymmetry follows from those per-skill junctions. Editing an
already-junctioned skill is live the moment it hits disk, for every
session on this machine, committed or not. But a directory that did not
exist at the last scripts/link-claude.ps1 run has no junction, so
a newly authored skill is invisible everywhere — absent from the
listing, and /<name> answers Unknown command. Measured 2026-09-02:
land was missing from a listing of 8 junctions immediately after its
SKILL.md was written, and appeared only once the linker ran. On this
machine the form is ./scripts/link-claude.ps1 -SkillGroups workflow,social,
never bare — see root CLAUDE.md.
A platform skill is the exception that proves the rule: fabric and
powerbi are pruned from user scope here on purpose, so a new one stays
unlinked by design and enters no session's payload. Nothing is broken
when that happens, and no linker run changes it.
A project-scope skill needs the linker not at all, which is the
third case and the easiest to get wrong in the other direction. Nothing
junctions .claude/skills/ — Claude Code reads it in place — so a new
skill there is live on save for sessions in this repo, with no deploy
step to forget. Do not run the linker "to pick it up"; it selects out of
skills/ and will not see it. The trade is that it is live only here,
which is the whole reason it is there.
Once linked, a half-drafted skill is a live half-drafted skill; finish
the frontmatter before walking away.
1. Check this is the right skill
Two routes out, both cheap to check first.
Guidance that already exists → /learn. The dividing line is whether
the destination exists. /learn folds a session learning into a skill,
rule, or CLAUDE.md that should already have covered it. This skill
creates guidance that has no home yet. A learning that arrives as "the
fabric-cicd skill was wrong about X" is /learn work even if the fix
is large.
Generic skill-authoring help → skill-creator or
plugin-dev:skill-development. Both are loaded and both are good.
They are not wrong, only generic: they know the Agent Skills format,
progressive disclosure, and description tuning in the abstract. They do
not know this repo's naming rules, its lint command, its references/
split, its junction deployment, or the discipline of writing a handoff
brief first. That gap is the entire reason this skill exists alongside
them. Say so plainly when the overlap comes up, so the duplication reads
as deliberate rather than as something nobody noticed.
If the user wants a subagent rather than a skill, this is the wrong
skill: the artifact lives in claude/agents/ and uses
templates/subagent-handoff.md. Stop and say so.
2. Check for existing coverage
Before naming anything, find out whether the topic is already covered.
grep -li "<topic term>" skills/*/*/SKILL.md .claude/skills/*/SKILL.md
grep -n -i "<topic term>" skills/fabric/fabric-gotchas/SKILL.md
Check fabric-gotchas explicitly. /learn treats it as the default
home for cross-product troubleshooting content, so a topic can be
half-covered there without any dedicated skill existing.
An incidental grep hit is not coverage. The worked failure is a
Mirroring search matching "Spark History Server mirror" in
fabric-spark-monitoring — same substring, unrelated subject. Read the
surrounding heading before counting a hit. Conversely, a topic can be
genuinely covered under vocabulary you did not grep for; skim the
skills/README.md section list for the relevant domain as a second
pass.
A grep finds a word; the script finds a competitor. The second
outcome below — two descriptions that both half-match one request — is
invisible to a grep, because neither description need contain the other's
vocabulary. Run it against the nearest existing skill the topic would sit
beside:
uv run --with pyyaml scripts/skill-overlap.py overlap --skill <nearest-skill>
It ranks that skill's pairs by shared distinctive tokens, so what already
clusters around your topic shows up before you have written a line. There
is no way to score a description that does not exist yet, which is why
this runs against the neighbour rather than the candidate. Read its output
into the three outcomes; it changes what you find, not what you do about
it.
Before authoring a platform skill, check upstream first. Microsoft
ships a catalog at microsoft/skills-for-fabric, and vendoring one of
its skills is sometimes the better answer than authoring a parallel one.
drift-audit's skills-for-fabric registry entry carries the
counterpart table as a starting map. Decide vendor-versus-author
deliberately; the failure this prevents is a local skill and an upstream
one competing for the same request, with no record of why both exist.
Three outcomes:
- Covered correctly — stop. Say where, and that nothing is needed.
- Covered partially, in a skill that owns the domain — stop and
propose extending that skill instead. A second skill splitting one
domain makes both harder to trigger, because the model is choosing
between two descriptions that both half-match.
- Not covered — continue.
The stop cases are real stops. Do not proceed to drilling because the
topic is interesting.
3. Decide the name and namespace
Put the proposal to the user before drilling. Naming is cheap to change
now and expensive later — the name is in the directory, the slash
command, and every cross-reference.
-
Behavioral, cross-domain skills take the verb you invoke —
commit, learn, code-review, drift-audit. Read the name as the
user typing it.
-
Platform skills take a namespace prefix — fabric-, pbir-, or
pbid-.
-
powerbi-* is reserved. Those are vendored from
microsoft/skills-for-fabric and keep upstream naming so re-sync
diffs stay clean. Never take that prefix for a local skill.
-
Pick the TREE before the group, and pick it by asking what the
skill acts on — not from the namespace, which cannot answer this.
Does it act on this repo, or on the user's own work?
- Acts on
agent-config itself — its groups, its linter, its handoff
queue, its audit ledger: .claude/skills/<name>/SKILL.md, project
scope. No group directory. It deploys nowhere and no script reaches
it, which is the point: outside this working tree it has nothing to
act on, so shipping it to user scope would put it in the startup
listing of every client-repo session for no possible benefit.
- Acts on the user's work — code, Fabric items, reports, any repo:
skills/<group>/<name>/SKILL.md, deployable payload.
Getting this wrong is silent and costs listing budget everywhere. The
seven skills at project scope today (author-skill, test-skill,
learn, drift-*, land) were all at user scope until 2026-09-09
for exactly this reason.
-
Within skills/, the namespace picks the group directory, and the
group directory is load-bearing. skills/fabric/ for fabric-*,
skills/powerbi/ for pbir-, pbid-, pbip- and the vendored
powerbi-*, skills/workflow/ for the behavioral ones — which now
means the repo-general verbs only, code-review and commit — and
skills/social/ for personal-profile writing (linkedin-highlights),
split out so a workplace Copilot deployment of workflow never
carries it. A new group needs adding to the machine's
-SkillGroups default everywhere that default is written, or the
next documented linker run prunes it.
-
Depth is pinned per tree and a misplacement fails twice silently.
The pre-commit hook matches
^(skills/[^/]+|\.claude/skills)/[^/]+/SKILL\.md$ — two directories
under skills/, one under .claude/skills/. A skill placed flat at
skills/<name>/SKILL.md, or nested a level deeper in either tree, is
invisible to the linter and to Claude Code, which discovers skills
exactly one level under a skills root.
-
Name the job, not the target, where they differ. drift-audit is
named that way because it audits rules, CLAUDE.md, and the MCP
templates too — skill-audit would have named a quarter of its scope
and collided with a plausible future skill.
-
The linter enforces the mechanics: lowercase letters, digits and
hyphens only, ≤ 64 chars, and no anthropic or claude anywhere in
the name.
Check the name against the loaded plugin skills, not just this repo.
A local skill competing with skill-creator or a plugin-dev:* skill
for the same trigger surface is a real collision even though the
directories never touch.
Also settle the scope question at this point: is this one skill or
two? A topic that splits cleanly into a reference half and a workflow
half is often two skills (pbir-cli and pbir-report-workflow are the
in-repo example). Decide with the user now; discovering it during
drafting means rewriting the brief.
4. Drill the sources
Do not write from training data. The point of this step is that every
claim in the finished skill traces to something read during this run.
- Microsoft Learn —
microsoft_docs_search to find the pages, then
microsoft_docs_fetch for the ones that matter. Search returns
500-token excerpts, which are enough to locate a page and never enough
to encode a constraint from.
- Exact repo bytes, changelogs, release notes —
github-mcp.
- Anything else —
WebFetch.
MCP tools are not in this skill's allowed-tools, which only
pre-approves and never restricts (Claude Code skills docs, verified
2026-09-11), so leaving them out costs at most a permission prompt.
Prefer them when present; fall back without ceremony when not.
Record what was drilled and what was not. The undrilled set is what
bounds the draft, and it is the part that gets lost if it is not written
down at the time. A brief that says "the REST surface was not drilled;
nothing in this skill describes it" is what stops the next reader
assuming the omission was an oversight. The template's Sources
drilled section is where both halves go — not Notes, and not a
sentence buried in Scope.
Evidence from a client's estate is cited by kind, never by name.
Observed platform behaviour often comes from client work, and the
behaviour is generic where the names are not. Write "a client sandbox
workspace" and "a directory of upload files", not the workspace, item,
table, column or repo names you saw — and never write a list of names
"to genericize at draft time", which is the leak itself. This repo is
public, a pushed name cannot be fixed forward, and the identity guard
matches only names already on the denylist. On 2026-09-10 a brief
reached public main carrying a client's workspace and table names in
exactly such a list.
Delegating the drill is fine; delegating the verification is not. A
large surface splits cleanly across subagents by source family, and each
one must return a verbatim quote per claim rather than a summary. Then
re-read the quotes behind anything load-bearing — a permission, a limit,
a path, a gate — yourself, before it reaches the draft. Observed
2026-09-11: a subagent marked a claim CONFIRMED against a quote that did
not support it, and the firsthand re-reads also caught a wrong endpoint
path that two shipped skills had been carrying.
Stop drilling when new pages stop changing the outline, not when the
source list is exhausted.
5. Write the handoff brief
The target is docs/handoffs/execute/<name>.md, built from
docs/handoffs/templates/skill-handoff.md.
Check whether that path is already occupied before writing a byte.
A /drift-update escalation leaves its scoping input at exactly this
name, so the file that authorizes the work and the file this step
produces collide by default. Such a file is normally untracked, which
makes an overwrite unrecoverable — git has nothing to restore.
If something is there, read it in full and treat it as source rather
than as an obstacle. Carry across everything that outlives it — the
authorization, the audit evidence, the undrilled set, and anything
citing a gitignored path that will not survive on its own — then
confirm with the user that the file may be replaced. Never overwrite
a brief path unread.
Every heading in the template survives into the filled brief.
Sections that do not apply get N/A — <brief reason> under the heading,
never deletion.
Strip the per-section > Guidance: notes. They are instructions for
filling the template, not content, and a brief that keeps them reads as
half-finished. Exactly two blocks are reproduced verbatim: the guidance
note directly under the title, and Claude Code's post-draft checklist.
docs/handoffs/examples/author-skill.example.md is the reference
— two guidance blocks in the finished brief, not one per heading. The
other two examples predate this convention and strip both; follow the
author-skill one.
Fill Last verified with today's date.
The brief is not ceremony and not a handoff to another surface. It is
the record of what was decided and what was deliberately left out, and
the excluded set is the half that cannot be reconstructed from the
finished skill. Write it before drafting even though the same session
does both — the ordering is what makes the scope decisions explicit
instead of emergent.
If an existing brief in docs/handoffs/examples/ matches the new
skill's shape, lift it as design source and record that lineage in
Changes from source proposal using the wording that
examples/README.md specifies, rather than re-narrating the design.
Show the brief to the user before drafting from it.
6. Draft the SKILL.md
Write the SKILL.md from the brief, at the path the tree decision in
step 3 picked — skills/<group>/<name>/SKILL.md for payload,
.claude/skills/<name>/SKILL.md for a skill that maintains this repo.
The description is the entire model-invoked trigger mechanism.
Write it to fire on the queries the skill should answer — the user's
vocabulary, error strings, tool and command names — not to summarize the
body. A description that reads as an accurate abstract and never
triggers has failed at its only job. Where the skill neighbours another,
spend a clause on the disambiguation.
A disambiguation pointer must be reachable from where it is read. A
paths:-scoped skill is in the listing only once one of its own globs
has matched, so "use <other-skill>" is dead whenever the two share no
trigger file. The failure is worse than a no-op: the model calls the
Skill tool, gets Unknown skill, and reports that skill as not
installed — a false diagnosis layered on a dead pointer, in a session
where it was deployed and merely not activated. So point at a co-active
neighbour by name, and point off the shared surface by file —
opening the file is what activates the skill there, which makes the file
the only pointer that works cold. Measured 2026-09-04: pbir-filters
said "for filter-pane styling use pbir-themes", and their globs are
disjoint.
Long detail goes to the skill's own references/, not the body. Root CLAUDE.md is explicit about this. Command flag tables,
per-item-type matrices, and long worked examples belong in a reference
file the body points at. The linter caps the body at 500 lines, but that
is a backstop, not a target.
If you are relocating content out of an existing body rather than
writing new, verify nothing was lost before committing. Moving prose
between files is where silent content loss happens — it happened twice
during the 2026-08-31 body-slimming pass, once from a substring heading
match that dropped a whole section. Match headings exactly, with an
assertion, never by substring, and diff the result against HEAD:
n = lambda s: re.sub(r'\s+', ' ', s).strip()
u = lambda s: set(re.findall(r'https?://[^\s\)\|]+', s))
print("URLs lost:", u(orig) - u(new_skill_plus_all_references))
missing = [l for l in orig.split('\n') if len(n(l)) > 45 and n(l) not in n(new)]
Every surviving entry in missing must be explainable as an intentional
rewrite. An empty missing and an empty URL set is the pass condition.
Match the house voice: numbered steps, bold lead-ins for the rule being
stated, an explicit constraints section at the end, and reasons attached
to rules that would otherwise look arbitrary.
Nothing goes in that drilling did not establish. A plausible claim
with no source behind it is the failure mode this whole procedure exists
to prevent. If something is believed but unverified, mark it as such
inline with the date and version, the way /learn does, so a later
/drift-audit can confirm or remove it.
7. Post-draft checks
Run all four. Each catches something the others do not.
uv run --with pyyaml scripts/lint-frontmatter.py <the SKILL.md you wrote>
Re-count the description. The linter gates description at 1,024
and when_to_use at 512 — the two halves of the 1,536 listing
truncation point — but it reports overflow only after the fact and
never warns on a near miss:
uv run --with pyyaml python -c "import sys,yaml; print(len(yaml.safe_load(open(sys.argv[1],encoding='utf-8').read().split('---')[1])['description']))" <the SKILL.md you wrote>
1,024 is the Agent Skills spec cap, and description is one of the six
fields the claude.ai upload path accepts — so that is the field that has
to stay portable. Re-count after any wording change, not once at the
end. If the skill sets when_to_use, count it too against its own 512:
it is a Claude Code extension the spec does not carry, so spending the
remainder there costs no portability the skill still had.
cat the whole file after any edit. YAML frontmatter is a single
malformed line away from the skill silently not loading, and the Edit
tool on Windows is where that line comes from.
pre-commit run --all-files
8. Register it in skills/README.md
Add the entry to the section the namespace implies — Behavioral,
Microsoft Fabric platform, or Power BI Desktop / Reports. Update the
count in the section heading where one is present.
Match the house style there: what the skill covers, and — where the name
is not self-evident — why it is named that. The drift-audit and
drift-handoff entries are the model for the second half. An entry that
only restates the description earns nothing the description does not
already do.
This is the one place this skill edits a file it did not create. Keep
the diff to the single added entry and the count.
9. Report, sweep, hand off
Report:
- What was drilled and what was not. The undrilled set, verbatim
from the brief.
- The scope decisions — what the skill deliberately does not cover,
and why.
- Check results — lint, description count,
pre-commit.
- That the skill is not behaviourally tested. Say it plainly —
but for the right reason. It is not that the file cannot reload:
skills hot-reload in-session, fixed upstream in 2.1.216 and verified
here 2026-08-31 and again 2026-09-02 on an in-place
description
edit. It is that the session which wrote the skill is the worst
place to test it — accumulated context can mask a co-load failure,
and you already know the answers you are checking for. A cold run
against a --safe-mode baseline is the test.
Name the specific queries it should fire on, so the fresh-session
test is runnable rather than aspirational — those queries are what
/test-skill reads back out of the brief, so a vague one here
becomes a vague test there.
Then sweep docs/handoffs/execute/ for briefs whose work has
landed. A queued brief is deleted once its change lands, or promoted
into examples/ in the one case where something cites it. That
convention has already failed once unattended, which is why the sweep is
a step here instead of a habit.
- The brief written at step 5 stays queued. Nothing is validated
until the fresh-session test has run.
- For any other brief whose skill or subagent now exists, propose
deletion or promotion and name which. Propose rather than act: you
can see that an artifact exists, but not whether the user considers
the work landed. Deletion itself is safe — briefs are committed when
written, so the deleting commit and its content stay recoverable.
Hand off to /test-skill, then /commit. Do not commit here, and
do not test here either. /test-skill reads the brief from disk, so it
can run now or in a fresh session next week — the two do not have to be
back to back, and a cold run is the better one.
10. Constraints
- No unverified claims. If step 4 did not establish it, it does not
go in the skill. Marked-unverified is the only exception, and it
carries a date.
- The stop cases in steps 1 and 2 are stops. Route to
/learn,
propose an extension, or hand off to a generic authoring skill — do
not author anyway.
- Name before drilling. Naming and the one-skill-or-two call go to
the user first; both are cheap now and expensive after a draft exists.
- Brief before draft, even in one session.