chore(plugins): sync generated content mirrors

Regenerate plugins/*/skills/ from plugins/*/.apm/ after the previous
four commits, via scripts/sync-plugin-content.sh --all. The mirror is
generated output (ADR-0017) that check-plugin-content-sync's pre-push
hook diffs against .apm/; nothing here is hand-edited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
This commit is contained in:
2026-09-07 20:43:51 +00:00
parent 09eea5e7ab
commit af8b46cd57
45 changed files with 467 additions and 201 deletions

View File

@@ -4,6 +4,8 @@ disable-model-invocation: true
description: >
Ultra-compressed output mode that drops articles, filler and pleasantries while
keeping technical substance exact, cutting token usage by roughly 75%.
metadata:
version: "1.0.0"
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.

View File

@@ -4,6 +4,8 @@ description: >
Use when the user says "diagnose this" or "debug this", reports something
broken, throwing, or failing, or says something got slow. Not filing or
triaging a reported bug -> `triage`. Not test-first feature work -> `tdd`.
metadata:
version: "1.0.0"
---
# Diagnose

View File

@@ -5,6 +5,8 @@ description: >
relentless interview — one question at a time, down each branch of the
decision tree. Not a plan to challenge against `CONTEXT.md` and ADRs ->
`grill-with-docs`.
metadata:
version: "1.0.0"
---
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

View File

@@ -4,6 +4,8 @@ description: >
Use when a plan should be stress-tested against the project's domain model —
the interview challenges terms against `CONTEXT.md` and writes decisions into
it and into ADRs as they land. Not a plain interview -> `grill-me`.
metadata:
version: "1.0.0"
---
<what-to-do>

View File

@@ -6,6 +6,8 @@ description: >
testable and AI-navigable — deepening opportunities that turn shallow modules
into deep ones, informed by `CONTEXT.md` and `docs/adr/`. Not debugging a
failure -> `diagnose`.
metadata:
version: "1.0.0"
---
# Improve Codebase Architecture

View File

@@ -5,6 +5,8 @@ description: >
a data model, state machine or business logic, or to mock up a UI in several
variations. Not production code -> `tdd`. Not talking a design through ->
`grill-me`.
metadata:
version: "1.0.0"
---
# Prototype

View File

@@ -6,6 +6,7 @@ description: >-
documentation written from existing code or specs -> `write-docs`. Not a bug
or incident -> `diagnose`.
metadata:
version: "1.0.0"
category: research
allowed-tools:
- Grep

View File

@@ -4,6 +4,8 @@ description: >
Use when the user wants a feature built or a bug fixed test-first, in a strict
red-green-refactor loop, one behaviour at a time. Not diagnosing an existing
bug -> `diagnose`. Not throwaway exploratory code -> `prototype`.
metadata:
version: "1.0.0"
---
# Test-Driven Development

View File

@@ -4,6 +4,8 @@ description: >
Use when the user wants an issue created, triaged, or moved through the
tracker's triage states, or an issue prepared for an AFK agent. Not debugging
the bug itself -> `diagnose`. Not fleshing out a design -> `grill-with-docs`.
metadata:
version: "1.0.0"
---
# Triage

View File

@@ -6,10 +6,10 @@ description: >
module", "create docs for this feature", "write a README for this". Not an ADR
or other decision record -> `grill-with-docs`. Not an external tool researched
from its docs -> `research`.
version: "1.0"
updated: 2026-05-17
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
metadata:
version: "1.0"
category: implement
source:
- repo: anthropics/skills

View File

@@ -2,6 +2,8 @@
name: zoom-out
description: Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.
disable-model-invocation: true
metadata:
version: "1.0.0"
---
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary.

View File

@@ -9,6 +9,7 @@ description: >
Not a Gitea remote's branches -> `gitea-branches`.
metadata:
version: "1.0.0"
category: git
source_keys:
- context7-git-htmldocs
@@ -20,7 +21,7 @@ metadata:
## Gotchas
- **Uncommitted changes abort a switch.** `git switch` refuses rather than clobbering conflicting local edits. Offer to stash and retry — forcing the checkout past it is how work disappears.
- **A branch and a tag can carry the same name.** Detect it before acting — `git branch --list <name>` and `git tag --list <name>`; output from both means the name is ambiguous. Prefer `git switch` over `git checkout`, and where a command accepts either ref, disambiguate with `refs/heads/<name>` or `refs/tags/<name>`.
- **A branch and a tag can carry the same name.** Detect it before acting — `rtk git branch --list <name>` and `rtk git tag --list <name>`; output from both means the name is ambiguous. Prefer `git switch` over `git checkout`, and where a command accepts either ref, disambiguate with `refs/heads/<name>` or `refs/tags/<name>`.
- **`main` and `master` are a refusal, not a gate.** Force-pushing, force-deleting, or renaming them is rejected even when the caller passes `confirm: true` — no flag makes the remote's history recoverable. Offer a new branch instead.
## Step 1 — Determine the branching pattern
@@ -51,7 +52,7 @@ These gates are passable. The `main`/`master` refusal in Gotchas is not.
## Step 4 — Set tracking
A new branch's first push must be `git push -u origin <branch>`. The push itself is `git-remotes`' — every remote-side gate lives there, which is why Step 2's remote-delete row hands off the same way — but the upstream requirement originates here, so carry it in the handoff. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened.
A new branch's first push must be `rtk git push -u origin <branch>`. The push itself is `git-remotes`' — every remote-side gate lives there, which is why Step 2's remote-delete row hands off the same way — but the upstream requirement originates here, so carry it in the handoff. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened.
## Step 5 — Return a structured result

View File

@@ -8,21 +8,21 @@ source_keys:
One command per action. Where two forms exist, the first is the default and the second the escape
hatch.
- **create** — `git switch -c <branch> <base>`. Base comes from the config's `base_branch`
- **create** — `rtk git switch -c <branch> <base>`. Base comes from the config's `base_branch`
(`main` under GitHub Flow, usually `develop` under Gitflow).
- **switch** — `git switch <branch>` moves to an existing local branch; it aborts rather than
clobbering conflicting local changes. `git switch -` returns to the previous branch.
- **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits,
which is why it is the default. `git branch -D <branch>` forces the deletion and discards that
- **switch** — `rtk git switch <branch>` moves to an existing local branch; it aborts rather than
clobbering conflicting local changes. `rtk git switch -` returns to the previous branch.
- **delete (local)** — `rtk git branch -d <branch>` refuses when the branch holds unmerged commits,
which is why it is the default. `rtk git branch -D <branch>` forces the deletion and discards that
work — only after the destructive-operation gates pass and `confirm: true` is set.
- **delete (remote)** — not this skill's. Deleting a remote branch is a push, and every remote-side
gate lives in `git-remotes`; hand it there rather than running the push from here. Its
`references/push.md` carries the command and the refspec form.
- **rename** — `git branch -m <old> <new>`.
- **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
- **rename** — `rtk git branch -m <old> <new>`.
- **list** — `rtk git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
`--merged` / `--no-merged` (filter by merge status into the current branch).
- **track** — `git branch --set-upstream-to=origin/<branch>` sets an upstream without pushing.
`git branch -vv` shows the tracking state of every local branch.
- **track** — `rtk git branch --set-upstream-to=origin/<branch>` sets an upstream without pushing.
`rtk git branch -vv` shows the tracking state of every local branch.
## get-intent
@@ -40,17 +40,17 @@ intent is worse than one built on none.
A switch aborts rather than clobbering conflicting local changes (see Gotchas). Stash is the way
past it: it shelves the working tree and index so the branch pointer can move.
- **save** — `git stash push -m "<message>"`. Add `-u` to include untracked files; verified on Git
- **save** — `rtk git stash push -m "<message>"`. Add `-u` to include untracked files; verified on Git
2.39.5, a plain `push` leaves them in place, and a plain `push` with *only* untracked changes
reports `No local changes to save` and stashes nothing. Bare `git stash` is `push` with no message.
- **restore** — `git stash pop` applies the newest entry and deletes it. `git stash apply stash@{n}`
- **restore** — `rtk git stash pop` applies the newest entry and deletes it. `rtk git stash apply stash@{n}`
applies without deleting, for replaying one shelf onto more than one branch.
- **list** — `git stash list`; `git stash show -p stash@{n}` prints that entry's diff.
- **drop** — `git stash drop stash@{n}` deletes one entry. `git stash clear` deletes all of them
- **list** — `rtk git stash list`; `rtk git stash show -p stash@{n}` prints that entry's diff.
- **drop** — `rtk git stash drop stash@{n}` deletes one entry. `rtk git stash clear` deletes all of them
and nothing recovers them — confirm before running it.
- **branch from a stash** — `git stash branch <branch> stash@{n}` creates a branch at the commit the
- **branch from a stash** — `rtk git stash branch <branch> stash@{n}` creates a branch at the commit the
stash was taken from and pops it there. Use it when the stash no longer applies to the current tip.
**A conflicting `pop` keeps the entry.** Verified on 2.39.5: it exits 1, writes conflict markers,
prints "The stash entry is kept in case you need it again", and `git stash list` still shows it.
Resolve, `git add`, then `git stash drop` the entry by hand — otherwise it silently accumulates.
Resolve, `rtk git add`, then `rtk git stash drop` the entry by hand — otherwise it silently accumulates.

View File

@@ -8,9 +8,9 @@ source_keys:
The two-dot and three-dot forms mean different things and are easy to swap by accident — check the
direction before reporting a result.
- `git log main..feature` — commits on `feature` that are not on `main`.
- `git log feature..main` — the reverse direction: commits on `main` not on `feature`.
- `git log --left-right main...feature` — both diverging sets at once (symmetric difference).
- `git diff main...feature` — the diff from the common ancestor to `feature`'s tip, which is what
- `rtk git log main..feature` — commits on `feature` that are not on `main`.
- `rtk git log feature..main` — the reverse direction: commits on `main` not on `feature`.
- `rtk git log --left-right main...feature` — both diverging sets at once (symmetric difference).
- `rtk git diff main...feature` — the diff from the common ancestor to `feature`'s tip, which is what
a reviewer sees, rather than the diff between the two tips.
- `git merge-base main feature` — print the common ancestor commit.
- `rtk git merge-base main feature` — print the common ancestor commit.

View File

@@ -9,22 +9,22 @@ source_keys:
Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase and cherry-pick
belong to `git-commits`; revert to `git-history`.
- **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the
- **Fast-forward** — `rtk git merge <branch>` advances the pointer with no merge commit when the
target has not diverged.
- **True merge** — `git merge --no-ff <branch>` forces a merge commit even when a fast-forward is
- **True merge** — `rtk git merge --no-ff <branch>` forces a merge commit even when a fast-forward is
possible. Gitflow requires it on every supporting-branch merge.
- **Squash merge** — `git merge --squash <branch>` stages the combined diff without committing.
Follow it with a `git commit`.
- **Octopus merge** — `git merge branch-a branch-b branch-c` merges more than two branches at
- **Squash merge** — `rtk git merge --squash <branch>` stages the combined diff without committing.
Follow it with a `rtk git commit`.
- **Octopus merge** — `rtk git merge branch-a branch-b branch-c` merges more than two branches at
once, but fails outright on any conflict. Use sequential two-way merges when conflicts are
likely.
## Conflict resolution
When Git cannot auto-merge it writes conflict markers and stops mid-merge. Run `git status` to
list the conflicted files, edit each to resolve its markers, then `git add <file>` and
`git merge --continue`.
When Git cannot auto-merge it writes conflict markers and stops mid-merge. Run `rtk git status` to
list the conflicted files, edit each to resolve its markers, then `rtk git add <file>` and
`rtk git merge --continue`.
- `git merge --abort` restores the pre-merge state.
- `git mergetool` opens the configured merge tool.
- `git diff --diff-filter=U` shows only the still-conflicted files.
- `rtk git merge --abort` restores the pre-merge state.
- `rtk git mergetool` opens the configured merge tool.
- `rtk git diff --diff-filter=U` shows only the still-conflicted files.

View File

@@ -8,6 +8,7 @@ description: >
`git-commits`. Not a Gitea server's history -> `gitea-branches`.
metadata:
version: "1.0.0"
category: git
source_keys:
- git-scm-bisect-docs
@@ -34,13 +35,13 @@ allowed-tools: Bash
## Step 2 — Query the log
Default to `git log --oneline`, then narrow by whatever is known:
Default to `rtk git log --oneline`, then narrow by whatever is known:
- **Content**: `git log -S"string"`, or `-G"regex"` to match any diff line. `--pickaxe-regex` makes the `-S` argument a POSIX ERE; `--pickaxe-all` shows every file in a matching changeset.
- **A line or function**: `git log -L <start>,<end>:<file>` or `git log -L :<function>:<file>`. Confirm the range resolves before reporting on it — an off-by-one silently omits the target.
- **A file across renames**: `git log --follow -- <file>`. Without `--follow` the history stops at the rename boundary.
- **Content**: `rtk git log -S"string"`, or `-G"regex"` to match any diff line. `--pickaxe-regex` makes the `-S` argument a POSIX ERE; `--pickaxe-all` shows every file in a matching changeset.
- **A line or function**: `rtk git log -L <start>,<end>:<file>` or `rtk git log -L :<function>:<file>`. Confirm the range resolves before reporting on it — an off-by-one silently omits the target.
- **A file across renames**: `rtk git log --follow -- <file>`. Without `--follow` the history stops at the rename boundary.
- **Mainline only**: `--first-parent` follows the integration branch and skips commits merged in from side branches.
- **Structured output**: `git log --format="%h | %s | %an (%ar)"`.
- **Structured output**: `rtk git log --format="%h | %s | %an (%ar)"`.
If you need the placeholder catalogue, format presets, `--diff-filter` letters, full `-L` syntax, ancestry filters, pickaxe binary-file behaviour, or `git diff` output-control flags such as `--stat`, `--word-diff` and the whitespace options, read `references/git-log-format.md`.
@@ -49,8 +50,8 @@ If you need the placeholder catalogue, format presets, `--diff-filter` letters,
Offer the operation and its consequence; run it only once the user has chosen.
- Backporting the commit to another branch is a cherry-pick, and cherry-pick is `git-commits`' — it owns the destination-branch check, the `rtk git` wrapper and the `--abort` path. Hand it the SHA; do not run `git cherry-pick` from here.
- `git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history.
- `git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line.
- `rtk git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history.
- `rtk git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line.
For diff output control on the located commit, read `references/git-log-format.md`.

View File

@@ -12,43 +12,43 @@ or line range to search the log for. Binary search reduces the trials from O(N)
## Manual flow
```bash
git bisect start
git bisect bad [HEAD] # mark current (or specified) as broken
git bisect good <commit> # mark known-good baseline
rtk git bisect start
rtk git bisect bad [HEAD] # mark current (or specified) as broken
rtk git bisect good <commit> # mark known-good baseline
# Git checks out the midpoint; test it
git bisect good # test passes
git bisect bad # test fails
rtk git bisect good # test passes
rtk git bisect bad # test fails
# Repeat until git reports "X is the first bad commit"
git bisect reset # return to the original HEAD
rtk git bisect reset # return to the original HEAD
```
## Automated
With a test command available, use `git bisect run <cmd>`. Git reads the exit code: `0` good,
With a test command available, use `rtk git bisect run <cmd>`. Git reads the exit code: `0` good,
`1`–`124` bad, `125` skip (build broken), `126`–`127` POSIX shell errors, treated as bad, and
`128` or above aborts the session outright rather than marking the commit bad.
## Untestable commits
`git bisect skip` excludes a commit that cannot be built or tested without deciding good or bad
`rtk git bisect skip` excludes a commit that cannot be built or tested without deciding good or bad
for it. When the first bad commit is adjacent to a skipped range, bisect reports that it cannot
pinpoint the culprit and lists the candidates — that is the precise answer the skip range allows,
not a failure.
## Undoing a wrong good/bad call
`git bisect log` prints the session's decision history. Save it, edit out the mistaken entry, and
`rtk git bisect log` prints the session's decision history. Save it, edit out the mistaken entry, and
resume from the corrected log rather than restarting the search:
```bash
git bisect log > bisect.log
rtk git bisect log > bisect.log
# edit bisect.log, removing the wrong decision
git bisect reset && git bisect replay bisect.log
rtk git bisect reset && rtk git bisect replay bisect.log
```
## Narrowing and speeding up
- `git bisect start HEAD v1.2 -- src/` restricts bisection to a path, cutting the trial count.
- `rtk git bisect start HEAD v1.2 -- src/` restricts bisection to a path, cutting the trial count.
- `--no-checkout` updates the `BISECT_HEAD` ref instead of checking out a working tree — useful
for tests that do not need one, and automatic in bare repos.
- `--first-parent` follows only first parents at merges, finding the integration commit that
@@ -56,12 +56,12 @@ git bisect reset && git bisect replay bisect.log
## Inspecting the remaining candidates
`git bisect visualize` (alias `view`) opens the suspects in gitk, falling back to `git log` when
`rtk git bisect visualize` (alias `view`) opens the suspects in gitk, falling back to `git log` when
no graphical display is detected. Add `--stat` or `-p` for a diffstat or full patches.
## Hunting a non-bug property change
`git bisect start --term-new <new> --term-old <old>` searches for any property change — a
`rtk git bisect start --term-new <new> --term-old <old>` searches for any property change — a
performance regression, say — instead of a bug. Use the custom terms in place of `good` and `bad`
for the rest of the session.

View File

@@ -116,15 +116,15 @@ source_keys:
**`-S<string>`** — finds commits where the **count** of `<string>` changed (i.e. the string was added or removed net). Does not match commits where the string merely appears in a diff hunk without a count change.
```bash
git log -S"my_function"
git log -S"my_function" --pickaxe-regex # treat as POSIX ERE
git log -S"my_function" --pickaxe-all # show all files in matching changesets
rtk git log -S"my_function"
rtk git log -S"my_function" --pickaxe-regex # treat as POSIX ERE
rtk git log -S"my_function" --pickaxe-all # show all files in matching changesets
```
**`-G<regex>`** — finds commits where any added or removed **line** in the patch matches `<regex>`. Broader than `-S`: matches whenever the pattern appears in diff text regardless of count.
```bash
git log -G"frotz\(nitfol"
rtk git log -G"frotz\(nitfol"
```
**Critical distinction:** given a diff that removes one occurrence of `foo` and adds one occurrence of `foo` (net change = 0):
@@ -151,8 +151,8 @@ Selects commits (in `git log`) or files (in `git diff`) by change type:
Lowercase letters **exclude** that type:
```bash
git log --diff-filter=ad # exclude added and deleted files
git log --diff-filter=M # only show commits with modified files
rtk git log --diff-filter=ad # exclude added and deleted files
rtk git log --diff-filter=M # only show commits with modified files
```
`C` and `R` only appear when copy/rename detection is enabled (`-C`, `-M` flags or `diff.renames` config).
@@ -162,10 +162,10 @@ git log --diff-filter=M # only show commits with modified files
Traces the evolution of a specific range of lines or a named function through commits. Implies `--patch`.
```bash
git log -L 10,20:file.txt
git log -L /start_pattern/,/end_pattern/:file.txt
git log -L :myfunction:src/app.c
git log -L /init/,+15:config.py # 15 lines after first match of /init/
rtk git log -L 10,20:file.txt
rtk git log -L /start_pattern/,/end_pattern/:file.txt
rtk git log -L :myfunction:src/app.c
rtk git log -L /init/,+15:config.py # 15 lines after first match of /init/
```
Range formats:
@@ -182,12 +182,12 @@ Limitations: incompatible with `--raw`, `--numstat`, `--shortstat`, `--name-only
## Graph and Ancestry Filters
```bash
git log --first-parent # at merges, follow only first parent (mainline evolution)
git log --merges # only merge commits (≥2 parents); equivalent to --min-parents=2
git log --no-merges # only non-merge commits; equivalent to --max-parents=1
git log --ancestry-path D..M # only commits actually on the path from D to M
git log --min-parents=<n> # include only commits with ≥ n parents
git log --max-parents=<n> # include only commits with ≤ n parents
rtk git log --first-parent # at merges, follow only first parent (mainline evolution)
rtk git log --merges # only merge commits (≥2 parents); equivalent to --min-parents=2
rtk git log --no-merges # only non-merge commits; equivalent to --max-parents=1
rtk git log --ancestry-path D..M # only commits actually on the path from D to M
rtk git log --min-parents=<n> # include only commits with ≥ n parents
rtk git log --max-parents=<n> # include only commits with ≤ n parents
```
`--ancestry-path` is significant: without it, `D..M` includes all commits reachable from M but not D — including side branches that merged into the path. With it, only commits directly between D and M are shown.
@@ -197,17 +197,17 @@ git log --max-parents=<n> # include only commits with ≤ n parents
### --stat
```bash
git diff --stat # diffstat: file names + ± bar
git diff --stat=<width>,<name-width>,<count>
git diff --compact-summary # alongside --stat: shows new/gone, +x/-x (executable), +l (symlink)
git diff --numstat # machine-readable: <added>\t<deleted>\t<path>; - for binary
rtk git diff --stat # diffstat: file names + ± bar
rtk git diff --stat=<width>,<name-width>,<count>
rtk git diff --compact-summary # alongside --stat: shows new/gone, +x/-x (executable), +l (symlink)
rtk git diff --numstat # machine-readable: <added>\t<deleted>\t<path>; - for binary
```
### --name-only / --name-status
```bash
git diff --name-only # only filenames, one per line
git diff --name-status # status letter + filename per line
rtk git diff --name-only # only filenames, one per line
rtk git diff --name-status # status letter + filename per line
```
`--name-status` uses the same status letters as `--diff-filter`.
@@ -215,10 +215,10 @@ git diff --name-status # status letter + filename per line
### --word-diff
```bash
git diff --word-diff # inline word-level diff with [-removed-] {+added+} markers
git diff --word-diff=color # color only, no markers
git diff --word-diff=porcelain # machine-readable: +/- prefixed lines, ~ for newlines
git diff --word-diff-regex=<re> # define what counts as a "word"
rtk git diff --word-diff # inline word-level diff with [-removed-] {+added+} markers
rtk git diff --word-diff=color # color only, no markers
rtk git diff --word-diff=porcelain # machine-readable: +/- prefixed lines, ~ for newlines
rtk git diff --word-diff-regex=<re> # define what counts as a "word"
```
### Whitespace Flags

View File

@@ -10,6 +10,7 @@ description: >
Not submodule pointers -> `git-submodules`.
metadata:
version: "1.0.0"
category: git
source_keys:
- git-scm-remote-docs
@@ -27,7 +28,7 @@ metadata:
## Step 1 — Clear the force-push gate
`main` and `master` are a hard refusal: decline a force-push targeting either, whatever confirmation accompanies it, because no local approval can restore what the remote loses. On any other branch, `git push --force` and `-f` run only after the caller passes `confirm: true` for that specific push — for a human caller, prompt instead of failing.
`main` and `master` are a hard refusal: decline a force-push targeting either, whatever confirmation accompanies it, because no local approval can restore what the remote loses. On any other branch, `rtk git push --force` and `-f` run only after the caller passes `confirm: true` for that specific push — for a human caller, prompt instead of failing.
## Step 2 — Dispatch

View File

@@ -11,19 +11,19 @@ Fetch **with no refspec** updates remote-tracking branches (`refs/remotes/<name>
That safety comes from the default refspec, not from `fetch` itself. Give it an explicit one and it writes to local branches: verified on Git 2.39.5, `git fetch origin main:probe` fast-forwarded the local `probe` branch, and a `+` prefix force-updates the destination, discarding whatever commits it held. Treat any `fetch` carrying a `<src>:<dst>` refspec as a branch update, not a read.
- **One remote**: `git fetch <remote>` — all branches
- **One branch**: `git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref
- **All remotes**: `git fetch --all`
- **Prune properly**: `git fetch --all --prune --prune-tags` cleans stale branches *and* tags
- **Auto-prune**: `git config --global fetch.prune true` (or `remote.<name>.prune` to scope it to one remote), and `fetch.pruneTags true` for tags
- **One remote**: `rtk git fetch <remote>` — all branches
- **One branch**: `rtk git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref
- **All remotes**: `rtk git fetch --all`
- **Prune properly**: `rtk git fetch --all --prune --prune-tags` cleans stale branches *and* tags
- **Auto-prune**: `rtk git config --global fetch.prune true` (or `remote.<name>.prune` to scope it to one remote), and `fetch.pruneTags true` for tags
## Shallow and partial fetch
```bash
git fetch --depth=<n> # deepen history, or create a shallow clone
git fetch --unshallow # convert a shallow clone to full history
git fetch --update-shallow # allow the fetch to update the shallow boundary
git fetch --refmap='' <remote> <branch> # fetch without updating any tracking ref (FETCH_HEAD only)
rtk git fetch --depth=<n> # deepen history, or create a shallow clone
rtk git fetch --unshallow # convert a shallow clone to full history
rtk git fetch --update-shallow # allow the fetch to update the shallow boundary
rtk git fetch --refmap='' <remote> <branch> # fetch without updating any tracking ref (FETCH_HEAD only)
```
## Default fetch refspec

View File

@@ -9,11 +9,11 @@ source_keys:
Default strategy: `--ff-only`. It fails on divergence, which forces a conscious choice instead of an accidental merge commit.
- **Fast-forward only**: `git pull --ff-only` — the recommended default
- **Rebase**: `git pull --rebase` replays your commits on top for linear history, but rewrites SHAs. Verify nothing being replayed has been pushed: rebasing published commits breaks everyone downstream.
- **Merge**: `git pull --no-rebase` — three-way merge commit, preserves original commits, non-linear
- **Rebase preserving merges**: `git pull --rebase=merges` keeps intentional local merge commits during the replay
- **Stage without committing**: `git pull --squash` collapses incoming commits into staged changes; you write the message
- **Fast-forward only**: `rtk git pull --ff-only` — the recommended default
- **Rebase**: `rtk git pull --rebase` replays your commits on top for linear history, but rewrites SHAs. Verify nothing being replayed has been pushed: rebasing published commits breaks everyone downstream.
- **Merge**: `rtk git pull --no-rebase` — three-way merge commit, preserves original commits, non-linear
- **Rebase preserving merges**: `rtk git pull --rebase=merges` keeps intentional local merge commits during the replay
- **Stage without committing**: `rtk git pull --squash` collapses incoming commits into staged changes; you write the message
- **Merge strategy**: Git 2.34+ defaults to `ort` (`recursive` is now an alias for it). Strategy options such as `-X ours`, `-X theirs`, `-X ignore-space-change` pass through unchanged.
- **Submodules**: `--recurse-submodules` only fetches submodules already checked out. Newly added ones are not initialized — use the `git-submodules` skill for those.
@@ -37,7 +37,7 @@ Highest wins:
4. `branch.autoSetupRebase` (set automatically when the tracking branch was created)
```bash
git config pull.ff only # deterministic default across Git versions
git config --global pull.rebase true
git config branch.develop.rebase false # develop always merges, regardless of the global default
rtk git config pull.ff only # deterministic default across Git versions
rtk git config --global pull.rebase true
rtk git config branch.develop.rebase false # develop always merges, regardless of the global default
```

View File

@@ -9,14 +9,14 @@ source_keys:
Default: safe push to the same-named branch on the remote.
- **Force-push**: never bare `--force`. Use `git push --force-with-lease --force-if-includes <remote> <branch>`, after the SKILL.md Step 1 gate.
- **Basic**: `git push <remote> <branch>`
- **Set upstream**: `git push -u <remote> <branch>` — push and configure tracking
- **Multi-remote**: push sequentially (`git push origin develop`, `git push staging develop`), or add a second push URL with `git remote set-url --add <name> <url>` to reach both in one command
- **Delete a remote branch**: `git push <remote> --delete <branch>` — clearer than the `:<branch>` form
- **Bulk**: `git push --all` (all local branches), `git push --tags` (all tags), `git push origin <tag>` (one tag)
- **Delete remote branches with no local counterpart**: `git push --prune origin 'refs/heads/*:refs/heads/*'`
- **Force only part of a multi-ref push**: prefix the one refspec that needs it with `+` — `git push origin +release develop` forces `release` while safe-pushing `develop`. A `+` prefix is a force-push and passes the SKILL.md Step 1 gate like any other.
- **Force-push**: never bare `--force`. Use `rtk git push --force-with-lease --force-if-includes <remote> <branch>`, after the SKILL.md Step 1 gate.
- **Basic**: `rtk git push <remote> <branch>`
- **Set upstream**: `rtk git push -u <remote> <branch>` — push and configure tracking
- **Multi-remote**: push sequentially (`rtk git push origin develop`, `rtk git push staging develop`), or add a second push URL with `rtk git remote set-url --add <name> <url>` to reach both in one command
- **Delete a remote branch**: `rtk git push <remote> --delete <branch>` — clearer than the `:<branch>` form
- **Bulk**: `rtk git push --all` (all local branches), `rtk git push --tags` (all tags), `rtk git push origin <tag>` (one tag)
- **Delete remote branches with no local counterpart**: `rtk git push --prune origin 'refs/heads/*:refs/heads/*'`
- **Force only part of a multi-ref push**: prefix the one refspec that needs it with `+` — `rtk git push origin +release develop` forces `release` while safe-pushing `develop`. A `+` prefix is a force-push and passes the SKILL.md Step 1 gate like any other.
## Refspec syntax — `[+]<src>[:<dst>]`
@@ -48,19 +48,19 @@ Two mitigations:
# Option 1 — dedicated push-only remote: background tools fetch `origin`, you push
# through a separate remote that nothing else touches, so its tracking ref can't be
# poisoned by an unrelated fetch.
git remote add origin-push $(git config remote.origin.url)
git push --force-with-lease origin-push
rtk git remote add origin-push $(rtk git config remote.origin.url)
rtk git push --force-with-lease origin-push
# Option 2 — explicit SHA via a local tag, unaffected by tracking-branch state
git fetch
git tag base master
git rebase -i master
git push --force-with-lease=master:base master:master
rtk git fetch
rtk git tag base master
rtk git rebase -i master
rtk git push --force-with-lease=master:base master:master
```
`--force-if-includes` adds a second check on top of bare `--force-with-lease`: it verifies the remote-tracking tip actually appears in your local branch's reflog, i.e. you genuinely integrated it before rewriting. It is a no-op without `--force-with-lease`, and has no effect with the `--force-with-lease=<ref>:<sha>` form, which already pins an exact SHA.
Safest combination: `git push --force-with-lease --force-if-includes origin`.
Safest combination: `rtk git push --force-with-lease --force-if-includes origin`.
## Server-side policy

View File

@@ -9,31 +9,31 @@ source_keys:
Which remotes exist, where they point, and what they track.
`git remote show <name>` needs network access — use `-n` for cached data offline, or `git remote -v`, which lists URLs without querying.
`rtk git remote show <name>` needs network access — use `-n` for cached data offline, or `rtk git remote -v`, which lists URLs without querying.
## Add, remove, rename, inspect
- **Add**: `git remote add <name> <url>`, or `-f` to fetch immediately
- **Remove**: `git remote remove <name>` — deletes the remote, all its tracking refs, and its config
- **Rename**: `git remote rename <old> <new>`
- **Inspect**: `git remote -v` (URLs, offline) or `git remote show <name>` (live tracking status)
- **Effective URLs**: `git remote get-url <name>` shows the URL after `insteadOf` rewrites; `git remote get-url --push --all <name>` lists every push URL
- **Add**: `rtk git remote add <name> <url>`, or `-f` to fetch immediately
- **Remove**: `rtk git remote remove <name>` — deletes the remote, all its tracking refs, and its config
- **Rename**: `rtk git remote rename <old> <new>`
- **Inspect**: `rtk git remote -v` (URLs, offline) or `rtk git remote show <name>` (live tracking status)
- **Effective URLs**: `rtk git remote get-url <name>` shows the URL after `insteadOf` rewrites; `rtk git remote get-url --push --all <name>` lists every push URL
## Tracking, mirroring, housekeeping
- **Track one branch**: `git remote add -t <branch> <name> <url>` (repeatable); `--no-tags` suppresses tag import entirely
- **Track one branch**: `rtk git remote add -t <branch> <name> <url>` (repeatable); `--no-tags` suppresses tag import entirely
- **Mirror**: `--mirror=fetch` mirrors all refs locally (bare repos only); `--mirror=push` makes every push behave like `--mirror`
- **Prune stale tracking refs without fetching**: `git remote prune <name>`, with `--dry-run` to preview
- **Default branch pointer**: `git remote set-head <name> -a` (auto-detect, needs a prior fetch), `... <branch>` (explicit), `... -d` (delete `refs/remotes/<name>/HEAD`)
- **Prune stale tracking refs without fetching**: `rtk git remote prune <name>`, with `--dry-run` to preview
- **Default branch pointer**: `rtk git remote set-head <name> -a` (auto-detect, needs a prior fetch), `... <branch>` (explicit), `... -d` (delete `refs/remotes/<name>/HEAD`)
## `set-url` — full form
```bash
git remote set-url <name> <newurl> # replace the first fetch URL
git remote set-url <name> <newurl> <oldurl-regex> # replace only the URL matching regex
git remote set-url --push <name> <url> # change push URL only (must point at same repo)
git remote set-url --add <name> <url> # add an extra push URL (push to multiple remotes)
git remote set-url --delete <name> <regex> # remove URLs matching regex
rtk git remote set-url <name> <newurl> # replace the first fetch URL
rtk git remote set-url <name> <newurl> <oldurl-regex> # replace only the URL matching regex
rtk git remote set-url --push <name> <url> # change push URL only (must point at same repo)
rtk git remote set-url --add <name> <url> # add an extra push URL (push to multiple remotes)
rtk git remote set-url --delete <name> <regex> # remove URLs matching regex
```
`--push` changes only where pushes go — fetch and push URLs must still reference the same repository. For genuine fetch-from-A / push-to-B workflows, use two separate named remotes instead; `--push` cannot do this.

View File

@@ -9,6 +9,7 @@ description: >
Not the superproject's own remotes -> `git-remotes`.
metadata:
version: "1.0.0"
category: git
source_keys:
- git-scm-submodule-docs

View File

@@ -8,6 +8,7 @@ description: >
agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`.
metadata:
version: "1.0.0"
category: git
source_keys:
- nvie-gitflow-post

View File

@@ -8,6 +8,7 @@ description: >
Not interactive multi-step git guidance -> `git-workflow`.
metadata:
version: "1.0.0"
category: git
source_keys:
- git-scm-worktree-docs
@@ -16,7 +17,7 @@ metadata:
## Gotchas
- **A branch can be checked out in only one worktree at a time.** `git worktree add` on an already-checked-out branch fails; `--force` is the only override, so use it only deliberately.
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `git worktree remove`, or `git worktree prune` afterwards.
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `rtk git worktree remove`, or `rtk git worktree prune` afterwards.
- **Submodules break worktree support.** A worktree containing submodules cannot be moved at all, and needs `--force` to remove.
- **`extensions.worktreeConfig = true` is a one-way door.** Without it, `git config --worktree` errors; with it, that flag writes to the worktree's own `config.worktree` file, and `core.bare`/`core.worktree` are forced there too. It also breaks older Git. Leave it off unless per-worktree config is needed.
@@ -24,19 +25,19 @@ metadata:
| Operation | Run |
|---|---|
| Create on a branch that already exists locally | `git worktree add <path> <branch>` |
| Create on a new branch | `git worktree add -b <branch> <path>` |
| Create on the branch named after the path basename | `git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD |
| Create and reset an existing branch to HEAD — discards its commits | `git worktree add -B <branch> <path>` |
| Create a local branch tracking a remote one | `git worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add <path> <branch>` expands to exactly this, but **only** under the conditions in `references/worktrees.md` |
| Throwaway experiment, no branch | `git worktree add -d <path>` — detached HEAD |
| Create on a branch that already exists locally | `rtk git worktree add <path> <branch>` |
| Create on a new branch | `rtk git worktree add -b <branch> <path>` |
| Create on the branch named after the path basename | `rtk git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD |
| Create and reset an existing branch to HEAD — discards its commits | `rtk git worktree add -B <branch> <path>` |
| Create a local branch tracking a remote one | `rtk git worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add <path> <branch>` expands to exactly this, but **only** under the conditions in `references/worktrees.md` |
| Throwaway experiment, no branch | `rtk git worktree add -d <path>` — detached HEAD |
| **Never** `git worktree add <path> <remote>/<branch>` | That ref resolves, so the shortcut never fires and you get **a detached HEAD, no branch, no upstream**. Commits there go unreachable once HEAD moves, and `git push` needs an explicit refspec. Use the tracking row above |
| List | `git worktree list -v`, or `--porcelain -z` to parse |
| Lock or unlock | `git worktree lock [--reason <str>] <path>` / `git worktree unlock <path>` |
| Move | `git worktree move <from> <to>` |
| Remove | `git worktree remove <path>` |
| Prune stale metadata | `git worktree prune --dry-run`, then without the flag |
| Repair after a manual move | `git worktree repair` — in the main worktree if *it* moved, or inside a linked worktree that moved. `git worktree repair <path>...` — from any worktree, naming each moved linked worktree's new path |
| List | `rtk git worktree list -v`, or `--porcelain -z` to parse |
| Lock or unlock | `rtk git worktree lock [--reason <str>] <path>` / `rtk git worktree unlock <path>` |
| Move | `rtk git worktree move <from> <to>` |
| Remove | `rtk git worktree remove <path>` |
| Prune stale metadata | `rtk git worktree prune --dry-run`, then without the flag |
| Repair after a manual move | `rtk git worktree repair` — in the main worktree if *it* moved, or inside a linked worktree that moved. `rtk git worktree repair <path>...` — from any worktree, naming each moved linked worktree's new path |
If the operation needs anything the table does not carry — the full `add` flag
table, orphan branches, sparse-checkout, locking for removable media, remote
@@ -48,7 +49,7 @@ Gates:
- **`move`, `remove` — the main worktree cannot be moved or removed.** Only linked worktrees, the ones `git worktree add` created, are candidates.
- **`add`, `move`, `remove` — escalate force flags one step at a time.** `-f` overrides a safeguard such as an unclean tree; `move` and `remove` need `-ff` on top of that when the worktree is locked. Confirm with the user before either — both discard state.
- **`add` — lock at creation, not after.** `git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected.
- **`add` — lock at creation, not after.** `rtk git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected.
## Step 2 — Report
@@ -61,6 +62,6 @@ worktrees:
lock_reason: <reason or empty>
```
Derive those fields from `git worktree list --porcelain -z`. For a single
Derive those fields from `rtk git worktree list --porcelain -z`. For a single
operation, report its outcome instead — `created: true`, `moved: true`,
`removed: true`.

View File

@@ -23,17 +23,17 @@ that are usable.
## `add` forms
```bash
git worktree add <path> <branch> # <branch> exists locally: check it out — non-destructive
git worktree add -b <branch> <path> # create a new branch; fails if it exists
git worktree add <path> # branch named after $(basename <path>): checked out
rtk git worktree add <path> <branch> # <branch> exists locally: check it out — non-destructive
rtk git worktree add -b <branch> <path> # create a new branch; fails if it exists
rtk git worktree add <path> # branch named after $(basename <path>): checked out
# if it exists, else created from HEAD
git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD,
rtk git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD,
# discarding the commits it carried
git worktree add --track -b <branch> <path> <remote>/<branch>
rtk git worktree add --track -b <branch> <path> <remote>/<branch>
# new local branch tracking the remote — always works
git worktree add <path> <branch> # <branch> absent locally and in exactly one remote:
rtk git worktree add <path> <branch> # <branch> absent locally and in exactly one remote:
# Git expands this to the --track -b form above
git worktree add -d <path> # detached HEAD, no branch
rtk git worktree add -d <path> # detached HEAD, no branch
```
The same `git worktree add <path> <branch>` spelling appears twice above and does two
@@ -65,19 +65,19 @@ Using `-` as `<commit-ish>` is shorthand for `@{-1}` (the branch checked out bef
## New unborn branch
```bash
git worktree add --orphan -b <branch> <path>
rtk git worktree add --orphan -b <branch> <path>
```
Creates an empty branch with no commits. **`--orphan` needs Git 2.42 or later** — it was added
upstream in 2.42, and on 2.39.5 this fails with `error: unknown option 'orphan'` and exit 129.
Check `git --version` before reaching for it.
Check `rtk git --version` before reaching for it.
Fallback on older Git, verified on 2.39.5 — detach first, then orphan the linked worktree in place,
which leaves the main worktree on its own branch throughout:
```bash
git worktree add -d <path> # linked worktree, detached HEAD
rtk git worktree add -d <path> # linked worktree, detached HEAD
cd <path>
git switch --orphan <branch> # unborn branch: empty index, empty working tree
rtk git switch --orphan <branch> # unborn branch: empty index, empty working tree
```
`git worktree list` then shows the new worktree at `0000000 [<branch>]` until its first commit.
@@ -88,25 +88,25 @@ the disruption worktrees exist to avoid.
Suppress the initial checkout to configure sparse-checkout first:
```bash
git worktree add --no-checkout ../sparse main
rtk git worktree add --no-checkout ../sparse main
cd ../sparse
git sparse-checkout init --cone
git sparse-checkout set src/
git checkout main
rtk git sparse-checkout init --cone
rtk git sparse-checkout set src/
rtk git checkout main
```
## Worktree on removable media
```bash
git worktree add --lock --reason "external SSD" <path> <branch>
git worktree unlock <path> # when reconnected
rtk git worktree add --lock --reason "external SSD" <path> <branch>
rtk git worktree unlock <path> # when reconnected
```
## Remote-branch disambiguation
```bash
git worktree add --track -b <branch> <path> <remote>/<branch> # explicit: no guessing at all
git worktree add <path> <branch> # shortcut: needs one clear remote
rtk git worktree add --track -b <branch> <path> <remote>/<branch> # explicit: no guessing at all
rtk git worktree add <path> <branch> # shortcut: needs one clear remote
```
**The bare-name shortcut needs exactly one remote.** It fires only when `<branch>` is not found
locally, none of `-b`/`-B`/`--detach` were given, and a tracking branch of that name exists in
@@ -122,10 +122,10 @@ exactly one remote has it, and marks that branch as upstream. Its default comes
## Repair after a manual move
```bash
git worktree repair # the MAIN worktree moved: run it there to reconnect every linked
rtk git worktree repair # the MAIN worktree moved: run it there to reconnect every linked
# worktree back to the main worktree
git worktree repair # a LINKED worktree moved: run it inside that recently-moved worktree
git worktree repair <path>... # reconnect a specific linked worktree — runnable from any worktree,
rtk git worktree repair # a LINKED worktree moved: run it inside that recently-moved worktree
rtk git worktree repair <path>... # reconnect a specific linked worktree — runnable from any worktree,
# naming each moved tree's new path
```
@@ -133,10 +133,10 @@ Which form applies depends on what moved:
| What moved | Remedy |
|---|---|
| The main worktree (or bare repo) | `git worktree repair` in the main worktree |
| One linked worktree | `git worktree repair` inside that worktree |
| Several linked worktrees | `git worktree repair <path>...` from any worktree, listing each new path |
| Both main and linked worktrees | `git worktree repair <path>...` in the main worktree, naming each linked worktree's new path — this restores the connections in both directions |
| The main worktree (or bare repo) | `rtk git worktree repair` in the main worktree |
| One linked worktree | `rtk git worktree repair` inside that worktree |
| Several linked worktrees | `rtk git worktree repair <path>...` from any worktree, listing each new path |
| Both main and linked worktrees | `rtk git worktree repair <path>...` in the main worktree, naming each linked worktree's new path — this restores the connections in both directions |
Only the no-argument form is tied to the current directory. The `<path>...` form is not — it
reestablishes the connection to every path you name, run from any worktree.
@@ -157,20 +157,20 @@ reestablishes the connection to every path you name, run from any worktree.
untouched throughout:
```bash
git worktree add -b emergency-fix ../temp main
rtk git worktree add -b emergency-fix ../temp main
cd ../temp
# fix, then commit
git commit -a -m "fix: critical production bug"
rtk git commit -a -m "fix: critical production bug"
cd -
git worktree remove ../temp
rtk git worktree remove ../temp
```
**Review a PR branch alongside your own work** — both branches stay checked out, so there is no
context switch:
```bash
git worktree add ../review-pr-123 origin/feature-xyz # detached HEAD — read-only review
git worktree add --track -b feature-xyz ../review-pr-123 origin/feature-xyz # if you will commit
rtk git worktree add ../review-pr-123 origin/feature-xyz # detached HEAD — read-only review
rtk git worktree add --track -b feature-xyz ../review-pr-123 origin/feature-xyz # if you will commit
# open ../review-pr-123 in a second editor window or terminal
```

View File

@@ -6,6 +6,7 @@ description: >
shellcheck"). Not running, installing, or updating hooks -> `pc-run`.
allowed-tools: Bash Read Write Edit
metadata:
version: "1.0.0"
category: devtools
source_keys:
- context7-pre-commit-com

View File

@@ -15,7 +15,7 @@ flow's file is not needed here. `SKILL.md`'s three common gates still apply.
inferring them from the project's name or README:
```bash
git ls-files | grep -oE '\.[a-z]+$' | sort | uniq -c | sort -rn
rtk git ls-files | grep -oE '\.[a-z]+$' | sort | uniq -c | sort -rn
```
2. Read `references/hooks-by-language.md` and map the detected extensions to recommended hooks.

View File

@@ -17,7 +17,7 @@ Read the existing `.pre-commit-config.yaml` before editing. Note any stale `rev`
1. Run a shallow extension scan, so the addition is judged against the languages actually present:
```bash
git ls-files | grep -oE '\.[a-z]+$' | sort | uniq -c | sort -rn
rtk git ls-files | grep -oE '\.[a-z]+$' | sort | uniq -c | sort -rn
```
2. Read `references/hooks-by-language.md` for the correct repo URL, `rev` and recommended args

View File

@@ -8,6 +8,7 @@ description: >
compatibility: Requires pre-commit installed and available on PATH.
metadata:
version: "1.0.0"
category: devtools
source_keys:
- context7-pre-commit-com
@@ -20,7 +21,7 @@ allowed-tools: Bash Read
- The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks git commit -m "msg"`. A space after a comma silently skips nothing instead of erroring.
- Never bypass a failing hook with `git commit --no-verify` (or `-n`). Hooks are the automated QA gate, so a bypassed commit pushes the failure downstream where it costs more — diagnose it instead.
- `- files were modified by this hook` is not a bug. A fixer hook rewrote a staged file, so the staged snapshot is stale and the commit is blocked on purpose. Re-stage and re-run the same commit: `git add -u && git commit`. Do NOT reach for `pre-commit install -f` here — it overwrites `.git/hooks/` and has nothing to do with re-staging.
- `- files were modified by this hook` is not a bug. A fixer hook rewrote a staged file, so the staged snapshot is stale and the commit is blocked on purpose. Re-stage and re-run the same commit: `rtk git add -u && rtk git commit`. Do NOT reach for `pre-commit install -f` here — it overwrites `.git/hooks/` and has nothing to do with re-staging.
## Gate — `pre-commit clean`

View File

@@ -14,8 +14,8 @@ Cause: A fixer hook (e.g. `trailing-whitespace`, `end-of-file-fixer`, `pretty-fo
Fix: Re-stage and recommit.
```bash
git add -u
git commit -m "same message"
rtk git add -u
rtk git commit -m "same message"
```
Do NOT reach for `pre-commit install -f` here. That flag overwrites existing hook files in `.git/hooks/`; it has nothing to do with re-staging.

View File

@@ -11,6 +11,7 @@ compatibility: Requires the Gitea MCP server configured with a token scoped to a
is not actually required for any of this domain's five tools.
metadata:
version: "1.0.0"
category: gitea
source_keys:
- gitea-mcp-repo

View File

@@ -7,6 +7,7 @@ description: >
directory -> skill-audit.
allowed-tools: Bash Read
metadata:
version: "1.0.0"
category: factory
source_keys:
- context7-websites-code-claude

View File

@@ -27,15 +27,19 @@ where the body's flowchart specified two.
Read the frontmatter before judging a single word.
- **`disable-model-invocation: true` or `user-invocable: false`** — the agent is hand-invoked. Its
description is never matched against user intent, so it is not a routing string. It carries **one
plain human-facing sentence** stating what the agent does. Audit it for that and nothing else.
Reporting a missing trigger clause, a missing boundary clause or absent indirect triggers on a
hand-invoked agent is a wrong finding, not a strict one. Both fields are Copilot-only and neither
is on the vendor-neutral APM allowlist, so this case arises in a Copilot `.agent.md` at
project/user scope and nowhere else. Its Claude Code counterpart has no equivalent field and stays
model-invoked, so the two halves of the pair carrying differently shaped descriptions is expected
there rather than a pair-consistency finding.
- **`disable-model-invocation: true`** — the agent is hand-invoked. Its description is never
matched against user intent, so it is not a routing string. It carries **one plain human-facing
sentence** stating what the agent does. Audit it for that and nothing else. Reporting a missing
trigger clause, a missing boundary clause or absent indirect triggers on a hand-invoked agent is a
wrong finding, not a strict one. The field is Copilot-only and not on the vendor-neutral APM
allowlist, so this case arises in a Copilot `.agent.md` at project/user scope and nowhere else.
Its Claude Code counterpart has no equivalent field and stays model-invoked, so the two halves of
the pair carrying differently shaped descriptions is expected there rather than a
pair-consistency finding.
`user-invocable: false` does not belong in this bullet: it only blocks manual invocation and is
independent of `disable-model-invocation` — an agent can be `user-invocable: false` and still
model-routed, in which case the three-part shape below still applies. It carries no
description-quality contract of its own and is out of this file's scope entirely.
- **No such flag** — the agent is model-invoked and the rest of this file applies.
## The three-part shape

View File

@@ -6,6 +6,7 @@ description: >
Not read-only review -> `agent-audit`. Not skills -> `skill-author`.
allowed-tools: Bash Read Write Edit
metadata:
version: "1.0.0"
category: factory
source_keys:
- context7-websites-code-claude

View File

@@ -6,6 +6,7 @@ description: >
authoring, publishing, auditing, or dependency installation for an apm
package -> `apm-workflow`.
metadata:
version: "1.0.0"
category: apm
source_keys:
- context7-microsoft-apm

View File

@@ -5,6 +5,7 @@ description: >
the dependencies it declares, or an apm marketplace — even when the user does
not say "apm". Not the apm binary or an agent runtime -> `apm-install`.
metadata:
version: "1.0.0"
category: apm
source_keys:
- context7-microsoft-apm

View File

@@ -8,6 +8,7 @@ description: >
already named — invoke `skill-author`, `agent-author` or `apm-workflow`
directly.
metadata:
version: "1.0.0"
category: factory
source_keys:
- claude-code-subagents-docs

View File

@@ -7,6 +7,7 @@ description: >
skill-author.
allowed-tools: Bash Read
metadata:
version: "1.0.0"
category: factory
source_keys:
- agentskills-home

View File

@@ -105,6 +105,19 @@ Three ways to read the result wrong:
is not a directory, a directory holding no `SKILL.md`, a missing or extra argument, and an absent
`python3` all exit **2** with a message on stderr. Exit 2 means the script never ran — report it
as an unaudited dimension, never as a pass and never as a finding. Exit 1 is findings.
- **A check-9 INFO — `'<field>' changed for '<slug>' since <ref>` — means go read, not just relay.**
Check 9 diffs the current `references/sources.md` against a base ref (default: the merge base with
`origin/main`) and flags a slug whose `Description` or `Contributing files` text differs. It is
structurally incapable of telling you whether the new wording is still *true* — it only detects
that the text changed — so when this INFO fires, open the Contributing files it names and the
document named in that slug's `Research doc:` field, and confirm by reading whether the (possibly
strengthened) claim genuinely holds. This is the one provenance finding this script cannot verify
for you: every other check here is a structural fact you can relay as-is, but check 9's job is
only to tell you *where* to spend that reading effort, not to replace it. Acknowledging the INFO
without opening those files is not auditing it. A single INFO naming "no base ref could be
resolved" or "no repo root above the skill directory" is the same graceful-skip pattern as every
other check here that cannot run — treat it as an unaudited dimension for that reason, not as a
finding about the skill.
- **`vale` reports `0 files`.** Treat the pass as NOT RUN, not as clean, and fall back to full
Step 3 judgment for the dimensions it would have covered. The bundled `Kyberforge` style is
scoped by glob in `assets/vale/.vale.ini`; a file outside those globs is silently not linted.

View File

@@ -3,12 +3,19 @@ set -euo pipefail
usage() {
cat <<EOF
Usage: validate-provenance.sh <skill-dir>
Usage: validate-provenance.sh <skill-dir> [--base-ref=<ref>]
Validate that a skill's sources provenance chain is complete and internally consistent.
Arguments:
skill-dir Path to the skill directory to validate.
skill-dir Path to the skill directory to validate.
--base-ref=REF Git ref to diff references/sources.md against for check 9.
Defaults to \`git merge-base HEAD origin/main\`. Override this
when origin/main is not the right comparison point (a fork,
a long-lived branch, a mirror with a different remote name).
The VALIDATE_PROVENANCE_BASE_REF environment variable is an
equivalent, lower-precedence way to set it — the flag wins
if both are given.
Exit codes:
0 All checks passed (or nothing to validate)
@@ -38,6 +45,12 @@ Checks performed:
resolved; a path that still does not resolve is reported as an INFO saying
checks 7 and 8 did not run, never skipped silently.
8 Extracted non-(none) slug in research doc present in sources.md
9 Description or Contributing files text changed since --base-ref (INFO
only — a bash script cannot verify the claim is still TRUE, only that it
changed; the auditor reads the named files to check that). A slug absent
at the base ref is a creation, not a change, and is not flagged. When the
base ref cannot be resolved at all, this is announced as ONE INFO for the
whole check, never a silent skip.
Checks 7 and 8 apply ONLY when the Research doc value names a research SOURCE
INDEX — a file whose basename is sources.md, whose H2 headings ARE source
@@ -52,13 +65,31 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
exit 0
fi
# --base-ref=<ref> is the only recognised flag, for check 9's base-ref
# override. It is pulled out before the positional-count checks below so it
# never counts against them — a caller passing it alongside skill-dir sees
# the same argument-count behaviour as one who does not pass it at all, and a
# genuinely extra positional argument is still rejected.
declare -a POSITIONAL_ARGS=()
BASE_REF_OVERRIDE=""
for arg in "$@"; do
case "$arg" in
--base-ref=*)
BASE_REF_OVERRIDE="${arg#--base-ref=}"
;;
*)
POSITIONAL_ARGS+=("$arg")
;;
esac
done
# Usage and environment problems exit 2, findings exit 1. See the usage text
# above for why the two must not share a code. This is a deliberate divergence
# from validate.sh, which has no 2 tier: validate.sh always prints PASS lines,
# so a usage error there is visibly not a findings report. This script prints
# NOTHING on a clean run, so exit 1 plus empty stdout was the only signal a
# caller got either way.
if [[ $# -lt 1 ]]; then
if [[ ${#POSITIONAL_ARGS[@]} -lt 1 ]]; then
echo "Error: skill-dir is required." >&2
echo "" >&2
usage >&2
@@ -67,13 +98,20 @@ fi
# Extra positional arguments were silently dropped, so a typo'd flag or a second
# path looked like it had been honoured.
if [[ $# -gt 1 ]]; then
echo "Error: expected exactly one argument, got $#: $*" >&2
if [[ ${#POSITIONAL_ARGS[@]} -gt 1 ]]; then
echo "Error: expected exactly one argument, got ${#POSITIONAL_ARGS[@]}: ${POSITIONAL_ARGS[*]}" >&2
echo "" >&2
usage >&2
exit 2
fi
SKILL_DIR_ARG="${POSITIONAL_ARGS[0]}"
# The flag wins over the environment variable when both are given; either is
# empty-string when unset, and an empty string tells the Python body to fall
# back to `git merge-base HEAD origin/main`.
BASE_REF="${BASE_REF_OVERRIDE:-${VALIDATE_PROVENANCE_BASE_REF:-}}"
# python3 is a HARD dependency. Without this preflight a missing interpreter
# produced 'line NN: python3: command not found' and exit 127 — an exit code no
# caller maps to anything, from a message that names this script's line number
@@ -91,24 +129,25 @@ fi
# references/validation-scripts.md explicitly told the auditor to read as a
# pass. A typo'd target was therefore indistinguishable from a clean skill.
# vale-wrap.sh hard-errors on a nonexistent path for exactly this reason.
if [[ ! -d "$1" ]]; then
echo "Error: not a directory: $1" >&2
if [[ ! -d "$SKILL_DIR_ARG" ]]; then
echo "Error: not a directory: $SKILL_DIR_ARG" >&2
echo " Why: a nonexistent target would otherwise report a silent pass." >&2
echo " Fix: pass the path of the skill directory to validate." >&2
exit 2
fi
if [[ ! -f "$1/SKILL.md" ]]; then
echo "Error: not a skill directory (no SKILL.md): $1" >&2
if [[ ! -f "$SKILL_DIR_ARG/SKILL.md" ]]; then
echo "Error: not a skill directory (no SKILL.md): $SKILL_DIR_ARG" >&2
echo " Why: a directory with no SKILL.md has no provenance chain to validate, and reporting that as a pass hides the wrong-target mistake." >&2
echo " Fix: pass the skill directory itself, not its parent or its references/ subdirectory." >&2
exit 2
fi
python3 -u - "$1" <<'PYTHON'
python3 -u - "$SKILL_DIR_ARG" "$BASE_REF" <<'PYTHON'
import sys
import os
import re
import subprocess
# Output is UTF-8 for the same reason input is: under LC_ALL=C the streams
# default to ASCII, and every finding this script prints contains an em dash.
@@ -125,6 +164,11 @@ skill_dir = os.path.abspath(sys.argv[1])
sources_md_path = os.path.join(skill_dir, "references", "sources.md")
refs_dir = os.path.join(skill_dir, "references")
# Empty string (the shell side passes "" when neither --base-ref nor
# VALIDATE_PROVENANCE_BASE_REF was given) means: resolve the default via
# `git merge-base HEAD origin/main` at check-9 time, below.
base_ref_override = sys.argv[2] if len(sys.argv) > 2 else ""
# --- Helpers ---
# The trailing character class used to be CONSUMING — `[^`\n]` — so a
@@ -444,6 +488,81 @@ def find_repo_root(start_dir):
return None
current = parent
# --- Check 9 helpers ---------------------------------------------------
# Check 9 needs a raw field VALUE (as text, to diff against an earlier
# version), not the parsed structure parse_contributing_files() and
# parse_status() return — a Contributing files list that reordered its
# entries without changing them is not what this check is looking for, but
# neither is normalizing so hard that a genuine rewrite disappears. Raw text,
# whitespace-normalized, is the middle ground.
def run_git(args, cwd):
"""Run `git <args>` in cwd. Returns (returncode, stdout, stderr) — never
raises, so a missing git binary or an unexpected OSError is just another
non-zero result the caller folds into "could not run", not a crash."""
try:
result = subprocess.run(
["git"] + args, cwd=cwd, capture_output=True, text=True,
encoding="utf-8", errors="replace"
)
return result.returncode, result.stdout, result.stderr
except OSError as exc:
return 1, "", str(exc)
def ref_exists(ref, cwd):
"""True when ref resolves to a commit in the repo at cwd."""
rc, _out, _err = run_git(["rev-parse", "--verify", "--quiet", ref + "^{commit}"], cwd)
return rc == 0
def find_slug_block(content, slug):
"""The raw text of a '## <slug>' entry's body, or None if no such H2."""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
return m.group(1) if m else None
def parse_field_raw(content, slug, field_name):
"""Raw text of a '**<field_name>:**' field under a slug H2.
Mirrors the two authored shapes parse_contributing_files() and
parse_status() already handle (inline value on the same line, or a
bare heading followed by '- ' bullets), but returns text rather than a
parsed structure, because check 9 diffs wording, not semantics.
Returns None when the H2 itself is absent (the slug did not exist at
this content's revision) or the field is absent — both read as "no
earlier claim to compare against" to the caller, which is deliberate:
a field appearing for the first time is a creation, not a change.
"""
block = find_slug_block(content, slug)
if block is None:
return None
inline_re = re.compile(r'^\- \*\*' + re.escape(field_name) + r':\*\* (.+)$', re.MULTILINE)
im = inline_re.search(block)
if im:
return im.group(1).strip()
heading_re = re.compile(r'^\*\*' + re.escape(field_name) + r':\*\*\s*$', re.MULTILINE)
hm = heading_re.search(block)
if not hm:
return None
lines = []
for line in block[hm.end():].splitlines():
line = line.strip()
if not line:
if lines:
break
continue
if not line.startswith("- "):
break
lines.append(line[2:].strip())
return ", ".join(lines) if lines else None
def normalize_field_text(value):
"""Collapse whitespace so reformatting alone never registers as a change."""
return re.sub(r'\s+', ' ', value).strip()
findings = []
has_fail = False
@@ -859,6 +978,89 @@ for rd_abs, (rd_rel, known_slugs, rd_content) in research_docs_seen.items():
f"Add '## {rd_slug}' to references/sources.md or mark it as '(none)' in the research doc's Contributing files."
)
# --- Check 9: Description / Contributing files changed since --base-ref ---
# A structural fact — the field's TEXT differs from an earlier revision — is
# all git can tell us. Whether the (possibly stronger) new wording is still
# TRUE is a semantic question no parser here can answer; that is what sent
# the earlier literal-text approaches (flagging a named-but-missing filename)
# to 3/3 false positives against the real corpus without even catching the
# bug that motivated this check. So check 9 does the one thing git reliably
# can: detect the change, and hand the auditor the slug and field to go read,
# never a verdict on the claim itself. Always INFO, never FAIL.
if repo_root is None:
emit_info(
"Check 9 skipped — no repo root above the skill directory",
"references/sources.md",
"No ancestor of the skill directory contains a .git entry, so there is no git history to diff "
"references/sources.md against. Check 9 did not run for any slug in this skill. "
"Run this script against a skill inside a checkout to get this check."
)
else:
resolved_base_ref = base_ref_override.strip()
resolve_error = None
if not resolved_base_ref:
rc, mb_out, mb_err = run_git(["merge-base", "HEAD", "origin/main"], repo_root)
if rc == 0 and mb_out.strip():
resolved_base_ref = mb_out.strip()
else:
resolve_error = (
"`git merge-base HEAD origin/main` could not resolve a base ref"
+ (f" ({mb_err.strip()})" if mb_err.strip() else "")
+ " — there may be no origin/main remote, HEAD may be detached, or the clone may be shallow."
)
elif not ref_exists(resolved_base_ref, repo_root):
resolve_error = f"--base-ref value '{resolved_base_ref}' does not resolve to a commit in this repository."
if resolve_error:
emit_info(
"Check 9 skipped — no base ref could be resolved",
"references/sources.md",
resolve_error + " Check 9 did not run for any slug in this skill. "
"Pass --base-ref=<ref>, or set the VALIDATE_PROVENANCE_BASE_REF environment variable, "
"to compare against something other than origin/main."
)
else:
sources_md_relpath = os.path.relpath(sources_md_path, repo_root)
rc, old_sources_content, show_err = run_git(
["show", f"{resolved_base_ref}:{sources_md_relpath}"], repo_root
)
if rc != 0:
# The base ref resolved fine, but references/sources.md did not
# exist there at all — the whole file is new. Every entry in it
# is therefore a creation, not a change: nothing to flag, and
# this is not a structural failure of the check, so no INFO
# either. Same reasoning applies per-slug below when the ref
# resolved but a given '## <slug>' heading did not exist yet.
old_sources_content = None
if old_sources_content is not None:
for slug in unique_slugs:
changed_fields = []
for field_name in ("Description", "Contributing files"):
old_value = parse_field_raw(old_sources_content, slug, field_name)
new_value = parse_field_raw(sources_content, slug, field_name)
if old_value is None or new_value is None:
# No earlier claim to compare against — a brand-new
# entry, or a field that did not exist yet at the
# base ref. That is a creation, not a change, and is
# never flagged.
continue
if normalize_field_text(old_value) != normalize_field_text(new_value):
changed_fields.append(field_name)
if changed_fields:
field_list = " and ".join(changed_fields)
emit_info(
f"'{field_list}' changed for '{slug}' since {resolved_base_ref}",
f"references/sources.md (## {slug})",
f"The '## {slug}' entry's {field_list} text differs from the version at "
f"{resolved_base_ref}. This script can confirm the entry is internally "
f"consistent, but it cannot verify whether the claim itself is still true — a "
f"retrofit once turned an honest hedge into an unsupported confident claim and "
f"every structural check here passed it silently. Re-read the upstream research "
f"doc named in this entry's Research doc field and the Contributing files it "
f"lists, and confirm by hand that the wording still holds."
)
print_findings()
sys.exit(1 if has_fail else 0)
PYTHON

View File

@@ -6,6 +6,7 @@ description: >
Not read-only review -> `skill-audit`. Not agent files -> `agent-author`.
allowed-tools: Bash Read Write Edit
metadata:
version: "1.0.0"
category: factory
source_keys:
- agentskills-home
@@ -56,6 +57,6 @@ Gates `/skill-audit` enforces in both flows:
Run `/skill-audit` on the resolved skill directory; resolve every FAIL before reporting done. It checks name-to-directory match, placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those. Hand-check the one thing it misses: an empty body reports `PASS SKILL.md body word count 0 (ADR-0020 target: 600)`, so confirm at least one non-empty section exists.
With `metadata.version` present, bump the **minor** version on create (new skills start at `0.1.0`) and the **patch** version on improve.
Bump `metadata.version`: the **minor** version on create (new skills start at `0.1.0`) and the **patch** version on improve.
**Commit verification.** Inside a git worktree: once the audit is clean, run `git add` and `git commit` — do not stop at staging. Re-run `git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up. Report done only once the hash has changed. Outside a worktree (a skill under `~/.claude/skills/`, say) nothing is committable — report done on a clean audit, naming that as the reason.

View File

@@ -101,8 +101,10 @@ plain sentence and `disable-model-invocation: true` instead.
- `license` — include when distributing the skill externally
- `compatibility` — include if the skill requires specific tools, runtimes, or network access
(max 500 characters)
- `metadata` — key-value map; use `author`, `version`, `category`; add `source_keys` now (Step 6)
if research sources are in context
- `metadata` — key-value map. `version` is **required** on every skill (ADR-0022), seeded at
`"1.0.0"` for a retrofitted skill with no prior version and at `"0.1.0"` for a newly created
skill; `author` and `category` stay optional; add `source_keys` now (Step 6) if research sources
are in context
- `allowed-tools` — space-separated pre-approved tools; reduces permission prompts (experimental —
support varies by client)
- `disable-model-invocation` — hand-invoked skills only

View File

@@ -105,6 +105,16 @@ them for you. After every retrofit that adds, removes or renames a file:
zero.
- [ ] Re-run `/skill-audit` and confirm its `### Provenance` dimension does not report the new
file as missing `source_keys`.
- [ ] **Compression must not add authority the source text didn't have.** The bullet above is
about a `sources.md` entry going *stale* — Contributing files left uncited after content
moves. This is a distinct failure: a compression or rewrite pass that upgrades an honest
hedge in a Description into an unsupported confident claim, without the underlying source
having changed at all — "no forge-specific content drawn directly from it beyond that"
quietly becoming "Grounds Step 2's dispatch table." Nothing in `/skill-audit`'s structural
checks catches this; a bash script can verify an entry is internally consistent, never
whether the claim is *true*. If a retrofit strengthens or otherwise changes the wording of a
provenance claim, re-read the upstream research doc first and confirm the stronger wording
is actually still true before committing it.
## Worked example — a description retrofit