chore(plugins): sync generated content mirrors
Regenerates `plugins/*/skills`, `plugins/*/agents`, both per-plugin `plugin.json` manifests and the two marketplace mirrors from `.apm/` per ADR-0017, via `scripts/sync-plugin-content.sh --all`. The manifests matter beyond tidiness here: `plugin.json` carries the plugin version and wins over the marketplace entry at install time (calculatePluginVersion precedence). Until this ran, the patch bumps in the preceding commit were inert for anyone installing these plugins. ADR: 0017 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
2
plugins/bin/.github/plugin/plugin.json
vendored
2
plugins/bin/.github/plugin/plugin.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -9,7 +9,7 @@ description: >
|
||||
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"
|
||||
version: "1.0.0"
|
||||
category: implement
|
||||
source:
|
||||
- repo: anthropics/skills
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "git",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
2
plugins/git/.github/plugin/plugin.json
vendored
2
plugins/git/.github/plugin/plugin.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "git",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -9,7 +9,7 @@ description: >
|
||||
Not a Gitea remote's branches -> `gitea-branches`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- context7-git-htmldocs
|
||||
@@ -21,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 — `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>`.
|
||||
- **A branch and a tag can carry the same name.** Detect it before acting — `git branch --list <name>` (bare, not `rtk`: rtk prints a phantom `* ` line even on no match, which reports every name as ambiguous — ADR-0023) 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
|
||||
|
||||
@@ -43,9 +43,12 @@ past it: it shelves the working tree and index so the branch pointer can move.
|
||||
- **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** — `rtk git stash pop` applies the newest entry and deletes it. `rtk git stash apply stash@{n}`
|
||||
- **restore** — `git stash pop` applies the newest entry and deletes it. Bare, not `rtk`: on a
|
||||
conflict rtk prints only `FAILED: git stash pop` and swallows the conflict report the paragraph
|
||||
below tells you to read (ADR-0023). `rtk git stash apply stash@{n}`
|
||||
applies without deleting, for replaying one shelf onto more than one branch.
|
||||
- **list** — `rtk git stash list`; `rtk git stash show -p stash@{n}` prints that entry's diff.
|
||||
- **list** — `git stash list` — bare, not `rtk`: rtk prints `No stashes` where git prints nothing,
|
||||
so an empty-output test misfires (ADR-0023). `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** — `rtk git stash branch <branch> stash@{n}` creates a branch at the commit the
|
||||
|
||||
@@ -26,5 +26,6 @@ list the conflicted files, edit each to resolve its markers, then `rtk git add <
|
||||
`rtk git merge --continue`.
|
||||
|
||||
- `rtk git merge --abort` restores the pre-merge state.
|
||||
- `rtk git mergetool` opens the configured merge tool.
|
||||
- `git mergetool` opens the configured merge tool — bare, not `rtk`: it hands control to an
|
||||
interactive child process, and a token filter has nothing to offer there (ADR-0023).
|
||||
- `rtk git diff --diff-filter=U` shows only the still-conflicted files.
|
||||
|
||||
@@ -8,7 +8,7 @@ description: >
|
||||
Not branch lifecycle -> `git-branches`.
|
||||
|
||||
metadata:
|
||||
version: "0.1.3"
|
||||
version: "0.1.4"
|
||||
category: git
|
||||
source_keys:
|
||||
- conventional-commits-spec
|
||||
@@ -21,7 +21,7 @@ allowed-tools: Bash
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Run git as `rtk git <subcommand>`, never bare `git`** — org convention, in `&&` chains too.
|
||||
- **Run git as `rtk git <subcommand>`, never bare `git`** — org convention, in `&&` chains too. Exceptions: ADR-0023 clause 3.
|
||||
- **Refuse to force-push `main`/`master`** — a rewrite leaves the branch diverged and the reflex is to force it back; safe only where nobody else has based work on it.
|
||||
- **`reset --hard` is a confirmation gate, not a default.** It overwrites the working tree, and uncommitted edits it discards were never in git, so no reflog recovers them. Name what will be lost and offer a stash first.
|
||||
- **Never add `--no-verify`** — using it when a hook fails bypasses the QA gate the pipeline depends on. Only on the user's explicit demand, with a warning.
|
||||
|
||||
@@ -21,9 +21,9 @@ Prefer this whenever a commit is written to be folded, because git does the mark
|
||||
|
||||
1. `rtk git commit --fixup=<commit>` keeps the target's message; `rtk git commit --squash=<commit>` lets you edit the combined message later. Both prefix the message with `fixup!`/`squash!` and name the target commit.
|
||||
2. Get explicit approval — the rebase still rewrites history.
|
||||
3. Run `rtk git rebase -i --autosquash HEAD~N`. Git pre-fills the todo list with the tagged commits already reordered against their targets; save it unchanged to apply.
|
||||
3. Run `git rebase -i --autosquash HEAD~N` — bare, not `rtk`: `-i` opens an interactive sequence editor (ADR-0023). Git pre-fills the todo list with the tagged commits already reordered against their targets; save it unchanged to apply.
|
||||
|
||||
**`-i` is not optional here.** On Git 2.39.5, `rtk git rebase --autosquash HEAD~N` without `-i` prints `Successfully rebased and updated refs/heads/<branch>.` and exits 0 while leaving the `fixup!` commit in place at its original SHA — `--autosquash` is honoured only by the interactive machinery, and the false success is the trap: the fold is reported as done, and the surviving `fixup!` subject then fails the Conventional Commits `commit-msg` hook. Later Git versions taught the non-interactive rebase to honour the flag, but `-i --autosquash` is correct on every version, so always write that.
|
||||
**`-i` is not optional here.** On Git 2.39.5, `git rebase --autosquash HEAD~N` without `-i` prints `Successfully rebased and updated refs/heads/<branch>.` and exits 0 while leaving the `fixup!` commit in place at its original SHA — `--autosquash` is honoured only by the interactive machinery, and the false success is the trap: the fold is reported as done, and the surviving `fixup!` subject then fails the Conventional Commits `commit-msg` hook. Later Git versions taught the non-interactive rebase to honour the flag, but `-i --autosquash` is correct on every version, so always write that.
|
||||
|
||||
## Squash by hand (interactive rebase)
|
||||
|
||||
@@ -31,7 +31,7 @@ Use this when the commits were not tagged at commit time. **Interactive rebase h
|
||||
|
||||
1. Identify the commits to squash — typically the last N on the current branch.
|
||||
2. Get explicit approval.
|
||||
3. Run `rtk git rebase -i HEAD~N`, marking the older commits `squash` to keep their messages for editing, or `fixup` to discard them.
|
||||
3. Run `git rebase -i HEAD~N` — bare, not `rtk`, for the same interactive-editor reason — marking the older commits `squash` to keep their messages for editing, or `fixup` to discard them.
|
||||
4. Compose the combined message when the rebase stops to ask. For a non-trivial combined message, follow the structure in `references/commit-template.md`.
|
||||
|
||||
## When a rebase halts on a conflict
|
||||
|
||||
@@ -8,7 +8,7 @@ description: >
|
||||
`git-commits`. Not a Gitea server's history -> `gitea-branches`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- git-scm-bisect-docs
|
||||
@@ -38,7 +38,7 @@ allowed-tools: Bash
|
||||
Default to `rtk git log --oneline`, then narrow by whatever is known:
|
||||
|
||||
- **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 line or function**: `git log -L <start>,<end>:<file>` or `git log -L :<function>:<file>` — bare, not `rtk`: rtk truncates each diff line at ~72 characters (ADR-0023). 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**: `rtk git log --format="%h | %s | %an (%ar)"`.
|
||||
|
||||
@@ -161,11 +161,15 @@ rtk 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`.
|
||||
|
||||
Bare `git`, not `rtk git`, on every `-L` form below: rtk truncates each diff body
|
||||
line at roughly 72 characters with an ellipsis, on the one query whose whole point
|
||||
is showing line content.
|
||||
|
||||
```bash
|
||||
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/
|
||||
git log -L 10,20:file.txt # bare per ADR-0023
|
||||
git log -L /start_pattern/,/end_pattern/:file.txt # bare per ADR-0023
|
||||
git log -L :myfunction:src/app.c # bare per ADR-0023
|
||||
git log -L /init/,+15:config.py # bare per ADR-0023; 15 lines after first /init/ match
|
||||
```
|
||||
|
||||
Range formats:
|
||||
@@ -205,20 +209,26 @@ rtk git diff --numstat # machine-readable: <added>\t<deleted>\t<p
|
||||
|
||||
### --name-only / --name-status
|
||||
|
||||
Bare `git`, not `rtk git`: rtk appends a blank line and a `Changes:` trailer, so
|
||||
the output is no longer one record per line.
|
||||
|
||||
```bash
|
||||
rtk git diff --name-only # only filenames, one per line
|
||||
rtk git diff --name-status # status letter + filename per line
|
||||
git diff --name-only # bare per ADR-0023; only filenames, one per line
|
||||
git diff --name-status # bare per ADR-0023; status letter + filename per line
|
||||
```
|
||||
|
||||
`--name-status` uses the same status letters as `--diff-filter`.
|
||||
|
||||
### --word-diff
|
||||
|
||||
Bare `git`, not `rtk git`: rtk replaces the word-diff with its own diffstat
|
||||
renderer and emits none of the `[-removed-] {+added+}` markers.
|
||||
|
||||
```bash
|
||||
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"
|
||||
git diff --word-diff # bare per ADR-0023; inline word-level diff, [-removed-] {+added+} markers
|
||||
git diff --word-diff=color # bare per ADR-0023; color only, no markers
|
||||
git diff --word-diff=porcelain # bare per ADR-0023; machine-readable: +/- prefixed lines, ~ for newlines
|
||||
git diff --word-diff-regex=<re> # bare per ADR-0023; define what counts as a "word"
|
||||
```
|
||||
|
||||
### Whitespace Flags
|
||||
|
||||
@@ -10,7 +10,7 @@ description: >
|
||||
Not submodule pointers -> `git-submodules`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- git-scm-remote-docs
|
||||
|
||||
@@ -48,13 +48,15 @@ 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.
|
||||
rtk git remote add origin-push $(rtk git config remote.origin.url)
|
||||
# The inner `git config` is bare: its stdout becomes a remote URL, so any
|
||||
# output rewriting would poison the remote silently.
|
||||
rtk git remote add origin-push $(git config remote.origin.url) # inner bare per ADR-0023
|
||||
rtk git push --force-with-lease origin-push
|
||||
|
||||
# Option 2 — explicit SHA via a local tag, unaffected by tracking-branch state
|
||||
rtk git fetch
|
||||
rtk git tag base master
|
||||
rtk git rebase -i master
|
||||
git rebase -i master # bare, not `rtk` (ADR-0023): interactive sequence editor
|
||||
rtk git push --force-with-lease=master:base master:master
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: >
|
||||
Not interactive multi-step git guidance -> `git-workflow`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- git-scm-worktree-docs
|
||||
@@ -32,7 +32,7 @@ metadata:
|
||||
| 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 | `rtk git worktree list -v`, or `--porcelain -z` to parse |
|
||||
| List | `git worktree list -v` to read, or `git worktree list --porcelain -z` to parse — both bare per ADR-0023: rtk re-renders the output and drops the porcelain flags |
|
||||
| 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>` |
|
||||
@@ -62,6 +62,7 @@ worktrees:
|
||||
lock_reason: <reason or empty>
|
||||
```
|
||||
|
||||
Derive those fields from `rtk git worktree list --porcelain -z`. For a single
|
||||
Derive those fields from `git worktree list --porcelain -z` — bare, not `rtk`:
|
||||
rtk drops both flags and never emits `locked`/`lock_reason` (ADR-0023). For a single
|
||||
operation, report its outcome instead — `created: true`, `moved: true`,
|
||||
`removed: true`.
|
||||
|
||||
@@ -8,7 +8,7 @@ description: >
|
||||
compatibility: Requires pre-commit installed and available on PATH.
|
||||
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: devtools
|
||||
source_keys:
|
||||
- context7-pre-commit-com
|
||||
@@ -19,7 +19,7 @@ allowed-tools: Bash Read
|
||||
|
||||
## Gotchas
|
||||
|
||||
- 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.
|
||||
- The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks rtk 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: `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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitea",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.8",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API \u2014 the forge's own objects, as distinct from the local git clone.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
2
plugins/gitea/.github/plugin/plugin.json
vendored
2
plugins/gitea/.github/plugin/plugin.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitea",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.8",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API \u2014 the forge's own objects, as distinct from the local git clone.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -14,7 +14,7 @@ disallowedTools: Edit, Write, NotebookEdit
|
||||
|
||||
You are the orchestrator for the gitea plugin — a composable workflow dispatcher designed for other agents to invoke multi-step Gitea operations reliably. Your one job is routing and safety-gating: you do not call `mcp__gitea__*` tools yourself, you delegate to domain skills and enforce confirmation on destructive operations. You never edit files. Every write you cause reaches its target through a domain skill's Gitea API call — never through an edit you make to the local working tree.
|
||||
|
||||
You resolve `owner`/`repo` once per session (via `git remote -v` on `origin`) and carry that forward as session context to every domain skill you dispatch to, rather than making each skill re-resolve it.
|
||||
You resolve `owner`/`repo` once per session (via `rtk git remote -v` on `origin`) and carry that forward as session context to every domain skill you dispatch to, rather than making each skill re-resolve it.
|
||||
|
||||
**Scope:** this orchestrator routes Gitea-object operations across the six domain skills only: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-branches`, `gitea-files`, `gitea-releases`. `gitea-workflow` is also not routed here, but for a different reason than a missing domain: it is a human-facing conversational wrapper that gives status check-ins and resolves ambiguous bare numbers ("what's going on with #42") by reasoning about phrasing and context, and it composes the same six domain skills directly rather than calling this orchestrator. It is not a peer to invoke instead of this dispatcher — agent callers route Gitea-object operations here directly with an explicit `operation` field; direct human users to `gitea-workflow` when they want guided, conversational help. Never invoke `gitea-workflow` as an agent caller — resolve ambiguous issue/PR numbers yourself (see Number resolution below) instead of relying on its conversational disambiguation.
|
||||
|
||||
@@ -30,7 +30,7 @@ These are non-negotiable regardless of `confirm` or any skill-local override:
|
||||
- Issues and PRs share one number space. Before dispatching an operation keyed on a bare number, resolve whether it's an issue or a PR yourself (see Number resolution) — never infer the domain from operation phrasing alone.
|
||||
- `list_releases`/`list_tags` default to `per_page: 20` (other domains default to 30) with no server-side auto-pagination — when a caller needs a complete result set, loop `page` upward until a page returns fewer than `per_page` results before returning.
|
||||
- Never commit secrets, credentials, or environment-specific config into any file written via `gitea-files`.
|
||||
- You are read-only against the local working tree. Never create, edit, or delete a local file — not a manifest, not a config, not a scratch note. Local state is the caller's, and you only read it (e.g. `git remote -v`) to resolve context.
|
||||
- You are read-only against the local working tree. Never create, edit, or delete a local file — not a manifest, not a config, not a scratch note. Local state is the caller's, and you only read it (e.g. `rtk git remote -v`) to resolve context.
|
||||
|
||||
### Number resolution
|
||||
|
||||
@@ -68,7 +68,7 @@ When invoked, you:
|
||||
1. Validate the request structure and check if `operation` is known
|
||||
2. Check the request against the Hard rules above (default-branch deletion, release/tag id-vs-name asymmetry, label/milestone ID resolution, number-space ambiguity, pagination) — refuse outright on violation, independent of `confirm`
|
||||
3. If destructive operation: require `confirm: true`, else fail with structured "requires explicit confirmation" error
|
||||
4. Resolve `owner`/`repo` via `git remote -v` on `origin` if not already present in `context`, and reuse the resolution for the remainder of the request
|
||||
4. Resolve `owner`/`repo` via `rtk git remote -v` on `origin` if not already present in `context`, and reuse the resolution for the remainder of the request
|
||||
5. If the operation targets a bare number and the domain isn't specified, run Number resolution above before dispatch
|
||||
6. Invoke the appropriate domain skill via `Skill` with the operation, parameters, and resolved context (`owner`, `repo`)
|
||||
7. Catch and handle Gitea errors: disambiguate 404s (not-found vs. permission-hidden), retry transient failures, loop pagination for `list_releases`/`list_tags` until exhausted
|
||||
|
||||
@@ -12,7 +12,7 @@ compatibility: Requires Gitea MCP server configured with a token with write:repo
|
||||
|
||||
metadata:
|
||||
category: integration
|
||||
version: "0.1.2"
|
||||
version: "0.1.3"
|
||||
source_keys:
|
||||
- gitea-mcp-repo
|
||||
- gitea-mcp-slim-go
|
||||
@@ -32,7 +32,7 @@ allowed-tools: Bash mcp__gitea__list_branches mcp__gitea__create_branch mcp__git
|
||||
Before any tool call, extract `owner` and `repo` from the git remote:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
rtk git remote get-url origin
|
||||
```
|
||||
|
||||
`get_me` and `list_my_repos` are blocked under the token scope this skill assumes, so the remote is the only source. If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
|
||||
|
||||
@@ -48,7 +48,7 @@ create_branch owner: <owner> repo: <repo> branch: <new-name> old_branch: <source
|
||||
|
||||
Default dispatch: if the user gives a base ("branch off of X", "from X"), pass it as `old_branch`.
|
||||
If they don't specify a base and you're mid-task on a local branch, pass your current branch
|
||||
(`git branch --show-current`) as `old_branch` so the new branch forks from where you're actually
|
||||
(`rtk git branch --show-current`) as `old_branch` so the new branch forks from where you're actually
|
||||
working, rather than silently falling back to the repo default. If neither applies (e.g. a fresh
|
||||
top-level request with no working branch context), omit `old_branch` and let it default server-side.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ compatibility: Requires Gitea MCP server configured with write:issue and write:r
|
||||
|
||||
metadata:
|
||||
category: integration
|
||||
version: "0.1.3"
|
||||
version: "0.1.4"
|
||||
source_keys:
|
||||
- gitea-mcp-repo
|
||||
- gitea-mcp-slim-go
|
||||
@@ -36,7 +36,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
|
||||
An orchestrating caller may pass `owner` and `repo` in already, and the `search` row is cross-repository and needs only a query — both skip this step. Otherwise, before any tool call:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
rtk git remote get-url origin
|
||||
```
|
||||
|
||||
If origin is unset or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
|
||||
|
||||
@@ -16,7 +16,7 @@ metadata:
|
||||
- gitea-mcp-slim-go
|
||||
- context7-websites-gitea
|
||||
- context7-gitea-tea-cli
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
|
||||
allowed-tools: Bash mcp__gitea__label_read mcp__gitea__label_write mcp__gitea__milestone_read mcp__gitea__milestone_write
|
||||
---
|
||||
@@ -32,7 +32,7 @@ allowed-tools: Bash mcp__gitea__label_read mcp__gitea__label_write mcp__gitea__m
|
||||
Before any tool call, extract `owner` and `repo` from the git remote (skip this if an orchestrating caller already passed them in):
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
rtk git remote get-url origin
|
||||
```
|
||||
|
||||
If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
|
||||
|
||||
@@ -19,7 +19,7 @@ metadata:
|
||||
- gitea-mcp-slim-go
|
||||
- context7-websites-gitea
|
||||
- context7-gitea-tea-cli
|
||||
version: "0.1.2"
|
||||
version: "0.1.3"
|
||||
|
||||
allowed-tools: Bash mcp__gitea__list_pull_requests mcp__gitea__pull_request_read mcp__gitea__pull_request_write mcp__gitea__pull_request_review_write
|
||||
---
|
||||
@@ -34,7 +34,7 @@ allowed-tools: Bash mcp__gitea__list_pull_requests mcp__gitea__pull_request_read
|
||||
Extract them from the git remote before any tool call, skipping this when an orchestrating caller already passed them in:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
rtk git remote get-url origin
|
||||
```
|
||||
|
||||
If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
|
||||
|
||||
@@ -14,7 +14,7 @@ compatibility: Requires Gitea MCP server configured with a token with write:repo
|
||||
|
||||
metadata:
|
||||
category: integration
|
||||
version: "0.1.0"
|
||||
version: "0.1.1"
|
||||
source_keys:
|
||||
- gitea-mcp-repo
|
||||
- gitea-mcp-slim-go
|
||||
@@ -36,7 +36,7 @@ allowed-tools: Bash mcp__gitea__list_releases mcp__gitea__get_release mcp__gitea
|
||||
`owner` and `repo` are required on every tool below. Extract them from the git remote, unless an orchestrating caller passed them in already:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
rtk git remote get-url origin
|
||||
```
|
||||
|
||||
If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -7,7 +7,7 @@ description: >
|
||||
directory -> skill-audit.
|
||||
allowed-tools: Bash Read
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: factory
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
|
||||
@@ -36,9 +36,12 @@ Read the frontmatter before judging a single word.
|
||||
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
|
||||
`user-invocable: false` does not belong in this bullet. The two are separate fields with opposite
|
||||
defaults — `disable-model-invocation` (default `false`) governs runtime auto-selection,
|
||||
`user-invocable` (default `true`) governs manual invocation, and the retired `infer` field was
|
||||
replaced by the pair rather than by either one. So `user-invocable: false` says nothing about
|
||||
whether the agent is model-routed: judge that from `disable-model-invocation` alone, and where
|
||||
that is absent the three-part shape below still applies. `user-invocable` 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.
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ those same sections, so any restatement is a copy that can disagree with the che
|
||||
If it carries one, it still has to be kebab-case.
|
||||
- `Use proactively` is meaningful in a CC description and steers the runtime to offer the agent
|
||||
unprompted. In a Copilot description it does nothing; `KyberforgeCopilot.ProactivePhrase` flags
|
||||
it. The Copilot equivalent is `disable-model-invocation` / `user-invocable`, which changes the
|
||||
description contract entirely — see `references/description-quality.md`, Step 0.
|
||||
it. The Copilot equivalent is `disable-model-invocation`, which changes the description contract
|
||||
entirely — see `references/description-quality.md`, Step 0.
|
||||
|
||||
## Pair consistency
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >
|
||||
Not read-only review -> `agent-audit`. Not skills -> `skill-author`.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: factory
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
@@ -31,7 +31,7 @@ metadata:
|
||||
|
||||
Signals: grill output, `agent-audit` findings, inline feedback, session context describing what went wrong. With none, ask: "No improvement signals found. Did you mean to create a new agent, or do you have feedback to apply?"
|
||||
|
||||
Read only the reference for the resolved flow. Capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
Read only the reference for the resolved flow. Capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Scope
|
||||
|
||||
@@ -62,4 +62,4 @@ Invoke `agent-audit` on each file written and resolve every FAIL before reportin
|
||||
|
||||
At plugin/APM scope bump the resolved package's `apm.yml` `version` — **minor** on create, **patch** on improve — because consumers compare it to detect updates. Project and user scope have no manifest.
|
||||
|
||||
**Commit verification.** 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 lost if the tree is cleaned up. Report done only once the hash has changed.
|
||||
**Commit verification.** Once the audit is clean, run `rtk git add` and `rtk git commit` — do not stop at staging. Re-run `rtk 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 lost if the tree is cleaned up. Report done only once the hash has changed.
|
||||
|
||||
@@ -11,7 +11,7 @@ Audit a skill directory against the agentskills.io specification and the house c
|
||||
|
||||
`validate.sh` enforces two independent length families that must not be conflated: the agentskills.io spec conformance ceilings (500 lines, 2,770 words, both counting the whole file) and the ADR-0020 context budget (250/400 description characters, 600/900 body-only words).
|
||||
|
||||
Alongside those it runs four shape checks that are not length measurements at all. Two are FAILs: every routing target named in the description — in the compressed `Not <thing> -> <name>` arrow **and** in the prose form — must resolve to a real skill or agent, and every `references/<file>.md` the body names must exist on disk. Three are SUGGESTIONs: a missing boundary clause, a Gotchas section over five entries, and a Gotchas section over 25% of the body. The resolution universe for boundary targets is derived by walking up from the audited `SKILL.md` — the authoring root above it, its own apm package, and that package's declared `apm.yml` dependencies — so a fresh clone and a machine that has run `apm install` return the same verdict. When no universe can be determined the check prints `INFO ... DID NOT RUN` and does not silently pass.
|
||||
Alongside those it runs shape checks that are not length measurements at all. Three are FAILs: every routing target named in the description — in the compressed `Not <thing> -> <name>` arrow **and** in the prose form — must resolve to a real skill or agent; every `references/<file>.md` the body names must exist on disk; and `metadata.version` must be present and three-part semver (ADR-0022). That last one is FAIL rather than SUGGESTION because the `skill-frontmatter` pre-commit hook rejects the file without it — an audit grading it lower would report ready-to-ship on a file the commit gate refuses. Three are SUGGESTIONs: a missing boundary clause, a Gotchas section over five entries, and a Gotchas section over 25% of the body. The resolution universe for boundary targets is derived by walking up from the audited `SKILL.md` — the authoring root above it, its own apm package, and that package's declared `apm.yml` dependencies — so a fresh clone and a machine that has run `apm install` return the same verdict. When no universe can be determined the check prints `INFO ... DID NOT RUN` and does not silently pass.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -26,8 +26,8 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Skill instructions for agents |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description presence and length, body-only word count, line and whole-file word ceilings, boundary-clause presence, boundary-target resolution, `references/` pointer existence, Gotchas entry count and body share, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description presence and length, `metadata.version` presence and semver shape (ADR-0022), body-only word count, line and whole-file word ceilings, boundary-clause presence, boundary-target resolution, `references/` pointer existence, Gotchas entry count and body share, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, upstream research doc alignment, and (check 9, INFO only) whether a slug's `Description` or `Contributing files` text has changed since a base ref — `--base-ref=<ref>` or `VALIDATE_PROVENANCE_BASE_REF`, defaulting to the merge base with `origin/main` |
|
||||
| `scripts/vale-wrap.sh` | Vale prefilter wrapper — runs the bundled `Kyberforge` Vale styles against SKILL.md and reports alerts as deterministic FAILs ahead of Step 3's qualitative review |
|
||||
| `assets/vale/.vale.ini` | Vale configuration — points Vale at the bundled `Kyberforge` style path, self-located relative to `vale-wrap.sh` |
|
||||
| `assets/vale/styles/Kyberforge/CompositionNote.yml` | Vale rule — flags composition and architecture notes in a description (e.g. "cross-cutting", "entry point", "rather than duplicating") |
|
||||
@@ -41,7 +41,7 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| `references/patterns.md` | Rubric for the patterns dimension — which instruction construct fits which job, and how each is correctly formed |
|
||||
| `references/file-structure.md` | Rubric for the file-structure and internal-consistency dimensions — permitted directories, cross-plugin path rules and their two structural exemptions, README drift |
|
||||
| `references/formatting-and-scripts.md` | Rubric for the formatting and scripts dimensions — heading and fencing conventions, and the agentic-use criteria for bundled scripts |
|
||||
| `references/validation-scripts.md` | Step 1 troubleshooting — the manual structural fallback when `validate.sh` cannot run, and the script exit codes that are easy to misread (loaded only on a script failure) |
|
||||
| `references/validation-scripts.md` | Step 1 troubleshooting — the manual structural fallback when `validate.sh` cannot run, and the script exit codes that are easy to misread (loaded on a script failure, and on any exit-0 run that printed something — `validate-provenance.sh`'s check 9 is INFO-only, so its findings arrive that way) |
|
||||
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
|
||||
| `tests/validate.bats` | (source-only) Bats test suite for validate.sh |
|
||||
| `tests/validate-provenance.bats` | (source-only) Bats test suite for validate-provenance.sh |
|
||||
|
||||
@@ -7,7 +7,7 @@ description: >
|
||||
skill-author.
|
||||
allowed-tools: Bash Read
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
category: factory
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
@@ -36,9 +36,9 @@ bash scripts/vale-wrap.sh <skill-dir>/SKILL.md
|
||||
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both, at the tier the script assigned. Report each once; never re-grade one under another dimension. Unresolved boundary targets are where this bites, because their tier turns on notation.
|
||||
|
||||
If any of the three cannot run, or exits non-zero for a reason other than findings, read `references/validation-scripts.md` — it carries the manual fallback and the misleading exit codes. Ordinary content FAILs are the expected outcome here and need no fallback.
|
||||
Read `references/validation-scripts.md` when any of the three cannot run or exits non-zero for a reason other than findings, **and whenever `validate-provenance.sh` exits 0 having printed anything**. Ordinary content FAILs are the expected outcome here and need no fallback.
|
||||
|
||||
`validate-provenance.sh` prints nothing on success, so read its exit code before you read its silence. **0** is a genuine pass. **1** means real findings: its FAILs and INFOs become a separate `### Provenance` dimension, and it emits Why and Fix itself — surface those verbatim. **2** means the check never ran — a usage or environment error, reason on stderr, no findings and often no stdout at all. On a 2, report `### Provenance` as unverified and quote the stderr reason. Never grade an exit 2 as a clean pass: empty stdout there means nothing was checked, not that nothing was wrong.
|
||||
`validate-provenance.sh` reports through exit code **and** output; neither alone is the verdict. **0, silent** is a genuine pass. **0 with output** is INFO-only findings — still a `### Provenance` dimension; `references/validation-scripts.md` says what each obliges — for a check-9 INFO, reading rather than relaying. **1** is FAILs plus any INFOs; it emits Why and Fix itself — surface those verbatim. **2** means it never ran — a usage or environment error, reason on stderr, often no stdout — so report `### Provenance` unverified and quote that reason. Never grade an exit 2, or an exit 0 that printed, as a clean pass.
|
||||
|
||||
`vale-wrap.sh` applies the bundled `Kyberforge` style as a prefilter. Pass no `--config`; the wrapper locates its own. Every rule is graded `error`, so every alert is a FAIL. Report each one citing its rule ID, filed under the dimension it belongs to, and do not re-derive it by judgment:
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ source_keys:
|
||||
|
||||
# Validation Scripts Reference
|
||||
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting.
|
||||
Nothing here is needed on a clean run.
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting —
|
||||
including `validate-provenance.sh` exiting **0 having printed something**, which is INFO findings,
|
||||
not a clean run. Its silent exit 0 is the only outcome that needs nothing here.
|
||||
|
||||
## Report the gap, do not guess
|
||||
|
||||
@@ -106,18 +107,33 @@ Three ways to read the result wrong:
|
||||
`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
|
||||
Check 9 diffs the current `references/sources.md` against a base ref 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 that slug's own entry: the document named in its `Research doc:` field, and the
|
||||
files its `Contributing files` list names. Read whichever the changed field is a claim *about* —
|
||||
a Description-only change often leaves the file list untouched, so "open the Contributing files"
|
||||
is where to look, not proof that they are what moved. 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.
|
||||
without opening those files is not auditing it. Its companion — `'<field>' removed for '<slug>'
|
||||
since <ref>` — is the same obligation in the other direction: a claim withdrawn rather than
|
||||
rewritten. No other check here requires the field, so confirm the removal was deliberate.
|
||||
- **The check-9 base ref defaults to `git merge-base HEAD origin/main`, and there are two ways to
|
||||
override it.** `--base-ref=<ref>` on the command line, or the `VALIDATE_PROVENANCE_BASE_REF`
|
||||
environment variable; the flag wins when both are given, including when it is given empty
|
||||
(`--base-ref=`), which selects the default resolution and ignores the environment. Reach for one
|
||||
on a fork, a long-lived branch, or a mirror whose remote is not called `origin` — and when a
|
||||
review asks what changed since a specific commit rather than since the branch point.
|
||||
- **A single check-9 INFO naming a whole-check skip is an unaudited dimension, not a finding about
|
||||
the skill.** There are three: "no repo root above the skill directory", "no base ref could be
|
||||
resolved", and "`<path>` is not tracked at `<ref>`". The third is the one to read carefully — it
|
||||
fires when the base ref resolved but `git show <ref>:<path>` did not, which covers both a
|
||||
genuinely new `sources.md` (nothing to flag) and a path git does not know under that name: a
|
||||
renamed skill directory, or an installed, gitignored copy such as a deployed `.claude/skills/`
|
||||
tree. Auditing the deployed copy silently checks nothing; re-run against the authoring path under
|
||||
`plugins/*/.apm/skills/`.
|
||||
- **`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.
|
||||
|
||||
@@ -15,7 +15,8 @@ Arguments:
|
||||
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.
|
||||
if both are given, including when the flag is given empty
|
||||
(\`--base-ref=\`), which selects the default resolution.
|
||||
|
||||
Exit codes:
|
||||
0 All checks passed (or nothing to validate)
|
||||
@@ -47,10 +48,13 @@ Checks performed:
|
||||
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.
|
||||
changed; the auditor reads the named files to check that). Wrapped values
|
||||
are joined before comparison, so a re-wrap alone is not a change and a
|
||||
rewrite of any line of one is. A slug absent at the base ref is a
|
||||
creation, not a change, and is not flagged; a field that WAS there and is
|
||||
now gone is announced as a removal. When the base ref cannot be resolved,
|
||||
or references/sources.md is not tracked under this path at that ref, 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
|
||||
@@ -70,8 +74,15 @@ fi
|
||||
# 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.
|
||||
#
|
||||
# BASE_REF_OVERRIDE is deliberately left UNSET here rather than initialised to
|
||||
# the empty string. `--base-ref=` (given, but empty) and "no flag at all" are
|
||||
# different instructions — the first says "use the default resolution, ignoring
|
||||
# the environment", the second says "fall back to the environment" — and an
|
||||
# empty-string initialiser collapsed them: `${BASE_REF_OVERRIDE:-$ENV}` treats
|
||||
# an empty flag value as absent, so the environment variable won and the usage
|
||||
# text's "the flag wins if both are given" was false for exactly that spelling.
|
||||
declare -a POSITIONAL_ARGS=()
|
||||
BASE_REF_OVERRIDE=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--base-ref=*)
|
||||
@@ -107,10 +118,15 @@ 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
|
||||
# The flag wins over the environment variable whenever the flag was GIVEN —
|
||||
# `+x` tests for presence, not for a non-empty value, which is the distinction
|
||||
# `:-` could not make. An empty result either way tells the Python body to fall
|
||||
# back to `git merge-base HEAD origin/main`.
|
||||
BASE_REF="${BASE_REF_OVERRIDE:-${VALIDATE_PROVENANCE_BASE_REF:-}}"
|
||||
if [[ -n "${BASE_REF_OVERRIDE+x}" ]]; then
|
||||
BASE_REF="$BASE_REF_OVERRIDE"
|
||||
else
|
||||
BASE_REF="${VALIDATE_PROVENANCE_BASE_REF:-}"
|
||||
fi
|
||||
|
||||
# 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
|
||||
@@ -491,10 +507,21 @@ def find_repo_root(start_dir):
|
||||
# --- 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.
|
||||
# parse_status() return. The ONE normalization applied is whitespace
|
||||
# collapsing, which is what makes a re-wrap or a re-indent invisible; nothing
|
||||
# else is normalized away.
|
||||
#
|
||||
# In particular a REORDERED Contributing files list DOES fire this check, and
|
||||
# that is deliberate — the header here used to claim the opposite, which the
|
||||
# code never did. Order-insensitivity cannot be had for one field without
|
||||
# distorting the other: the two fields share this parser, and the only way to
|
||||
# ignore order is to split the value into items and sort them, which for a
|
||||
# prose Description means splitting on commas and would then hide a genuine
|
||||
# rewrite that merely permuted its clauses. Check 9 is always an INFO whose
|
||||
# whole job is to point a human at a place to read; a reordered list costs
|
||||
# that human one glance to dismiss, whereas a hidden rewrite is the exact
|
||||
# failure #118 exists to catch. False positive over false negative, on this
|
||||
# check, on purpose.
|
||||
|
||||
def run_git(args, cwd):
|
||||
"""Run `git <args>` in cwd. Returns (returncode, stdout, stderr) — never
|
||||
@@ -523,14 +550,44 @@ def find_slug_block(content, slug):
|
||||
m = pattern.search(content)
|
||||
return m.group(1) if m else None
|
||||
|
||||
# A field value ENDS at the next field, the next heading, or a blank line.
|
||||
# Every other non-blank line is a continuation of the value the author wrapped
|
||||
# across physical lines.
|
||||
#
|
||||
# This boundary is what the old `(.+)$` regex did not have. `.` does not cross
|
||||
# a newline, so only the FIRST physical line of a wrapped value was ever
|
||||
# compared — and a rewrite confined to a continuation line produced no finding
|
||||
# at all. That is verbatim the hedge-to-confident-claim regression #118 exists
|
||||
# to catch, invisible to the check written to catch it. The bullet branch had
|
||||
# the same defect one level down: a wrapped bullet's continuation does not
|
||||
# start with '- ', so the loop broke there and silently dropped every
|
||||
# remaining bullet.
|
||||
#
|
||||
# A continuation line that itself opens with bold text ('**note** — ...') is
|
||||
# read as a boundary and truncates the value. That is a known, narrow
|
||||
# false-negative, accepted because the alternative — no boundary at all —
|
||||
# is what produced the wide one above.
|
||||
FIELD_BOUNDARY_RE = re.compile(r'^(?:- )?\*\*|^#{1,6} ')
|
||||
|
||||
|
||||
def _is_field_boundary(stripped_line):
|
||||
"""True when a stripped line starts a new field, bullet-less heading or H2."""
|
||||
return bool(FIELD_BOUNDARY_RE.match(stripped_line))
|
||||
|
||||
|
||||
def parse_field_raw(content, slug, field_name):
|
||||
"""Raw text of a '**<field_name>:**' field under a slug H2.
|
||||
"""Raw text of a '**<field_name>:**' field under a slug H2, wrapping joined.
|
||||
|
||||
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.
|
||||
|
||||
Continuation lines are joined into the value they belong to before the
|
||||
caller normalizes and compares, so a value wrapped across two lines and
|
||||
the same value on one line are the same text — and a change made on any
|
||||
line of a wrapped value is visible, not just one made on the first.
|
||||
|
||||
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:
|
||||
@@ -539,28 +596,49 @@ def parse_field_raw(content, slug, field_name):
|
||||
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
|
||||
lines = block.splitlines()
|
||||
inline_re = re.compile(r'^\- \*\*' + re.escape(field_name) + r':\*\*[ \t]*(.*)$')
|
||||
heading_re = re.compile(r'^\*\*' + re.escape(field_name) + r':\*\*[ \t]*$')
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
im = inline_re.match(line)
|
||||
if im:
|
||||
parts = [im.group(1).strip()]
|
||||
for cont in lines[idx + 1:]:
|
||||
stripped = cont.strip()
|
||||
if not stripped or stripped.startswith("- ") or _is_field_boundary(stripped):
|
||||
break
|
||||
parts.append(stripped)
|
||||
joined = " ".join(p for p in parts if p).strip()
|
||||
return joined or None
|
||||
if heading_re.match(line):
|
||||
entries = []
|
||||
for cont in lines[idx + 1:]:
|
||||
stripped = cont.strip()
|
||||
if not stripped:
|
||||
if entries:
|
||||
break
|
||||
continue
|
||||
if _is_field_boundary(stripped):
|
||||
break
|
||||
if stripped.startswith("- "):
|
||||
entries.append(stripped[2:].strip())
|
||||
elif entries:
|
||||
# A wrapped bullet: fold it back into the bullet it
|
||||
# continues rather than ending the list here.
|
||||
entries[-1] = (entries[-1] + " " + stripped).strip()
|
||||
else:
|
||||
break
|
||||
return ", ".join(e for e in entries if e) or None
|
||||
return None
|
||||
|
||||
def normalize_field_text(value):
|
||||
"""Collapse whitespace so reformatting alone never registers as a change."""
|
||||
"""Collapse whitespace so reformatting alone never registers as a change.
|
||||
|
||||
True only because parse_field_raw() joins wrapped continuation lines
|
||||
first: collapsing whitespace inside a value that had already been
|
||||
truncated at its first newline normalized nothing a re-wrap could change.
|
||||
"""
|
||||
return re.sub(r'\s+', ' ', value).strip()
|
||||
|
||||
findings = []
|
||||
@@ -1025,28 +1103,82 @@ else:
|
||||
["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.
|
||||
# The base ref resolved fine but `git show <ref>:<path>` did not.
|
||||
# That single return code covers two situations this check cannot
|
||||
# tell apart, and only one of them is harmless:
|
||||
#
|
||||
# the file genuinely did not exist at the base ref — the whole
|
||||
# sources.md is new, every entry in it is a creation, and there
|
||||
# is nothing check 9 could have flagged;
|
||||
#
|
||||
# the path is not TRACKED under that name at the base ref — a
|
||||
# renamed skill directory, or a copy of the skill living
|
||||
# somewhere untracked or gitignored (an installed .claude/skills
|
||||
# tree is the everyday case).
|
||||
#
|
||||
# Treating both as "creation, nothing to flag" made the second one
|
||||
# a silent, whole-skill skip: the same directory audited at its
|
||||
# authoring path reported changed claims and at its deployed path
|
||||
# reported nothing, with no way to tell that from a clean run.
|
||||
# That is the exact fail-open this script's own header forbids —
|
||||
# "never a silent skip" — so announce it once for the whole check
|
||||
# and hand over git's own stderr, which is the only diagnostic
|
||||
# that separates the two cases.
|
||||
detail = show_err.strip().splitlines()
|
||||
detail = detail[0] if detail else "git gave no reason"
|
||||
emit_info(
|
||||
f"Check 9 skipped — '{sources_md_relpath}' is not tracked at {resolved_base_ref}",
|
||||
"references/sources.md",
|
||||
f"`git show {resolved_base_ref}:{sources_md_relpath}` failed ({detail}). "
|
||||
f"Either the file did not exist at that ref — in which case every entry is a "
|
||||
f"creation and there was nothing to flag — or this path is not tracked under "
|
||||
f"that name there: a renamed skill directory, or an untracked or gitignored copy "
|
||||
f"of the skill such as a deployed .claude/skills/ tree. "
|
||||
f"Check 9 did not run for any slug in this skill. "
|
||||
f"Re-run against the tracked authoring path, or pass --base-ref=<ref> naming a "
|
||||
f"commit where this path exists."
|
||||
)
|
||||
old_sources_content = None
|
||||
|
||||
if old_sources_content is not None:
|
||||
for slug in unique_slugs:
|
||||
changed_fields = []
|
||||
removed_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:
|
||||
if old_value is None and new_value is None:
|
||||
continue
|
||||
if old_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 new_value is None:
|
||||
# The field existed at the base ref and is gone now.
|
||||
# This was folded into the creation skip above, which
|
||||
# justified only the other half: deleting a whole
|
||||
# '- **Description:**' line left NO finding anywhere —
|
||||
# no other check in this script requires the field, so
|
||||
# a claim could be removed as invisibly as it could be
|
||||
# strengthened. Announce it; the auditor decides
|
||||
# whether the removal was intended.
|
||||
removed_fields.append(field_name)
|
||||
continue
|
||||
if normalize_field_text(old_value) != normalize_field_text(new_value):
|
||||
changed_fields.append(field_name)
|
||||
if removed_fields:
|
||||
removed_list = " and ".join(removed_fields)
|
||||
emit_info(
|
||||
f"'{removed_list}' removed for '{slug}' since {resolved_base_ref}",
|
||||
f"references/sources.md (## {slug})",
|
||||
f"The '## {slug}' entry had {removed_list} at {resolved_base_ref} and has "
|
||||
f"none now. Nothing else in this script requires the field, so the removal "
|
||||
f"is otherwise invisible. Confirm it was deliberate — a provenance claim "
|
||||
f"withdrawn is as much a change to the chain as one rewritten — and "
|
||||
f"restore the field if it was lost to an edit."
|
||||
)
|
||||
if changed_fields:
|
||||
field_list = " and ".join(changed_fields)
|
||||
emit_info(
|
||||
|
||||
@@ -1289,6 +1289,50 @@ else:
|
||||
if desc:
|
||||
ok("description has no unfilled placeholders")
|
||||
|
||||
# --- ADR-0022: metadata.version is mandatory -------------------------------
|
||||
# FAIL, not SUGGESTION, and the tier is set by the gate rather than by taste.
|
||||
# `.pre-commit-config.yaml`'s `skill-frontmatter` hook REJECTS a SKILL.md with
|
||||
# no `metadata.version`, and rejects a value that is not three-part semver.
|
||||
# skill-author's Step 4 says to run this audit and "resolve every FAIL", so any
|
||||
# tier below FAIL lets that step report done on a skill the commit gate then
|
||||
# refuses — the same audit-disagrees-with-the-gate failure the MAX_LINES note
|
||||
# below warns about, arrived at from the other direction. Verified before this
|
||||
# check existed: a SKILL.md with no `metadata:` block at all reported "All
|
||||
# checks passed".
|
||||
#
|
||||
# The rule is DUPLICATED from that hook for the same cache-isolation reason as
|
||||
# every other constant here — an installed plugin's scripts cannot read the
|
||||
# repo-root config. Keep the two in step: this check must accept exactly what
|
||||
# the hook accepts.
|
||||
SEMVER_RE = re.compile(r'^\d+\.\d+\.\d+$')
|
||||
|
||||
try:
|
||||
fm_data = yaml.safe_load(fm)
|
||||
except Exception:
|
||||
# Unreachable in practice: description_value() above parses the same text
|
||||
# and hard-exits on a YAML error, so anything arriving here already parsed.
|
||||
fm_data = None
|
||||
metadata_block = fm_data.get('metadata') if isinstance(fm_data, dict) else None
|
||||
|
||||
if not isinstance(metadata_block, dict) or metadata_block.get('version') is None:
|
||||
fail("frontmatter has no metadata.version — ADR-0022 makes it mandatory for "
|
||||
"every skill, and the skill-frontmatter pre-commit hook rejects the file "
|
||||
"without it. Add `metadata:` / ` version: \"1.0.0\"` (new skills start "
|
||||
"at \"0.1.0\")")
|
||||
else:
|
||||
version_value = metadata_block['version']
|
||||
# NOT str()-coerced blind: `version: 1.0` is a YAML float, and its "1.0"
|
||||
# spelling is exactly the two-part value the hook rejects — coercing and
|
||||
# then matching keeps this check and the hook agreeing on that case.
|
||||
version_text = version_value if isinstance(version_value, str) else str(version_value)
|
||||
version_text = version_text.strip()
|
||||
if SEMVER_RE.match(version_text):
|
||||
ok(f"metadata.version present: '{version_text}' (ADR-0022)")
|
||||
else:
|
||||
fail(f"metadata.version '{version_text}' is not three-part semver — the "
|
||||
f"skill-frontmatter pre-commit hook rejects it. Use MAJOR.MINOR.PATCH, "
|
||||
f"e.g. \"1.0.0\"")
|
||||
|
||||
# SKILL.md size ceilings (agentskills.io skill-authoring.md: 500 lines,
|
||||
# ~5,000 tokens). Both constants are DUPLICATED from the repo-root pre-commit
|
||||
# hook scripts/skill-size-check.sh — a plugin skill's scripts cannot read files
|
||||
@@ -1515,11 +1559,66 @@ def stdin_redirected(line, prev_line):
|
||||
unquoted = re.sub(r'"[^"]*"|\'[^\']*\'', '', line)
|
||||
return '<' in unquoted or prev_line.rstrip().endswith('|')
|
||||
|
||||
# A here-doc body is DATA, not command position. Every script in this corpus
|
||||
# carries a `usage() { cat <<EOF ... EOF; }`, and prose wrapped inside one puts
|
||||
# ordinary English at the start of a line — "read is reported as an INFO ..."
|
||||
# in this skill's own validate-provenance.sh, which made skill-audit hard-FAIL
|
||||
# on its own script. Reflowing that one sentence would have cleared the finding
|
||||
# and left the cause: every future usage text is one wrap away from the same
|
||||
# false positive, and the remedy an author reaches for is contorting working
|
||||
# source, which the note above records has already happened twice.
|
||||
#
|
||||
# Detection is deliberately conservative in the direction that matters. A
|
||||
# here-doc body is skipped only when its terminator is actually found further
|
||||
# down the file; an opener with no terminator — the shape a stray `<<` inside a
|
||||
# string would produce — is ignored rather than allowed to swallow the tail,
|
||||
# because swallowing the tail is a false NEGATIVE and this check exists to fail
|
||||
# closed. `<<<` here-strings open nothing and are excluded by the lookbehind.
|
||||
HEREDOC_START_RE = re.compile(r'(?<!<)<<-?\s*(["\']?)([A-Za-z_][A-Za-z0-9_]*)\1')
|
||||
|
||||
|
||||
def heredoc_delimiter(line):
|
||||
"""The here-doc terminator this line opens, or None."""
|
||||
m = HEREDOC_START_RE.search(line)
|
||||
return m.group(2) if m else None
|
||||
|
||||
|
||||
def heredoc_body_indices(lines):
|
||||
"""Line indices that are here-doc BODY (plus its terminator), not code."""
|
||||
skip = set()
|
||||
i, n = 0, len(lines)
|
||||
while i < n:
|
||||
stripped = lines[i].strip()
|
||||
delim = None if stripped.startswith('#') else heredoc_delimiter(lines[i])
|
||||
if delim:
|
||||
# `<<-` allows an indented terminator, so compare stripped.
|
||||
for j in range(i + 1, n):
|
||||
if lines[j].strip() == delim:
|
||||
skip.update(range(i + 1, j + 1))
|
||||
i = j
|
||||
break
|
||||
i += 1
|
||||
return skip
|
||||
|
||||
|
||||
# The here-doc exemption applies to the `read` heuristic ONLY, and the
|
||||
# asymmetry is the point. `read` is an ordinary English verb, so any prose a
|
||||
# script prints is one line-wrap away from opening with it. `input(` is not a
|
||||
# word — a line beginning `input(` inside a here-doc is an embedded Python
|
||||
# program pausing for a keypress, which is exactly what this check is for, and
|
||||
# these scripts embed Python in a here-doc as a matter of course. Exempting the
|
||||
# whole body would have disarmed the check across every script in the corpus.
|
||||
def interactive_reads(source):
|
||||
hits = []
|
||||
prev_line = ''
|
||||
for line in source.splitlines():
|
||||
lines = source.splitlines()
|
||||
in_heredoc = heredoc_body_indices(lines)
|
||||
for idx, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if idx in in_heredoc:
|
||||
if re.match(r'input\(', stripped):
|
||||
hits.append(stripped)
|
||||
continue
|
||||
if re.match(r'read(\s|$)', stripped):
|
||||
if not stdin_redirected(line, prev_line):
|
||||
hits.append(stripped)
|
||||
|
||||
@@ -4,7 +4,7 @@ Author and refine skills conforming to the [agentskills.io](https://agentskills.
|
||||
|
||||
## What it does
|
||||
|
||||
Routes to one of two flows based on context: if no skill directory exists at the target path, it scaffolds the directory from annotated templates, fills in `SKILL.md` and supporting files, and validates the result. If an existing skill directory and improvement signals are both present, it groups those signals by root cause and applies targeted edits, then re-validates. In both flows, bumps the skill's `metadata.version` when present (minor for create, patch for improve).
|
||||
Routes to one of two flows based on context: if no skill directory exists at the target path, it scaffolds the directory from annotated templates, fills in `SKILL.md` and supporting files, and validates the result. If an existing skill directory and improvement signals are both present, it groups those signals by root cause and applies targeted edits, then re-validates. In both flows, bumps the skill's `metadata.version` — minor for create, patch for improve — which every skill carries (ADR-0022).
|
||||
|
||||
`SKILL.md` itself carries only the dispatch table, the invocation-axis decision, the contract gates and the shared close; each flow lives in its own self-contained reference file, per ADR-0020.
|
||||
|
||||
|
||||
@@ -6,7 +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"
|
||||
version: "1.0.1"
|
||||
category: factory
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
@@ -34,7 +34,7 @@ metadata:
|
||||
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask whether the user meant to create a new skill or has feedback to apply.
|
||||
|
||||
Read only the reference matching the resolved flow — each is self-contained. If the target sits inside a git worktree, capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
Read only the reference matching the resolved flow — each is self-contained. If the target sits inside a git worktree, capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Invocation axis
|
||||
|
||||
@@ -59,4 +59,4 @@ Run `/skill-audit` on the resolved skill directory; resolve every FAIL before re
|
||||
|
||||
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.
|
||||
**Commit verification.** Inside a git worktree: once the audit is clean, run `rtk git add` and `rtk git commit` — do not stop at staging. Re-run `rtk 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.
|
||||
|
||||
@@ -45,14 +45,18 @@ description: >
|
||||
# Optional. 1–500 characters. State tool requirements, runtime versions,
|
||||
# and network access needs. Omit for skills with no special environment requirements.
|
||||
|
||||
# metadata:
|
||||
metadata:
|
||||
version: "0.1.0"
|
||||
# author: your-name
|
||||
# version: "1.0"
|
||||
# category: general
|
||||
# source_keys:
|
||||
# - source-slug-one
|
||||
# - source-slug-two
|
||||
# Optional. Arbitrary key-value map. Common keys: author, version, category.
|
||||
# `metadata.version` is REQUIRED on every skill (ADR-0022) and is enforced by the
|
||||
# `skill-frontmatter` pre-commit hook. Three-component semver. A newly created
|
||||
# skill starts at "0.1.0" — leave the seeded value as it is; "1.0.0" is the seed
|
||||
# for a pre-existing skill retrofitted into the rule, not for a new one.
|
||||
# The rest of the map is optional: author, category, source_keys.
|
||||
# source_keys: populated when built from /research output. Lists slugs from references/sources.md.
|
||||
# Also add source_keys to each references/*.md file that was informed by research.
|
||||
|
||||
|
||||
@@ -84,8 +84,10 @@ already covers the new skill. Use Read/Edit directly on `apm.yml`; this is not p
|
||||
## Step 3 — Fill in SKILL.md
|
||||
|
||||
Open the new skill's `SKILL.md` (the path Step 1 printed) and replace every `FILL IN:`
|
||||
placeholder. The scaffold template already carries the compliant frontmatter and body skeleton —
|
||||
fill it rather than restructuring it.
|
||||
placeholder. The scaffold template carries the ADR-0020 body skeleton and the two frontmatter
|
||||
fields that cannot be left as placeholders — `name`, substituted by the script, and
|
||||
`metadata.version`, seeded live at `"0.1.0"` — so fill the template in rather than restructuring
|
||||
it.
|
||||
|
||||
**`name`** — already set by the scaffold script. Must exactly match the directory name. Format:
|
||||
1–64 characters, lowercase letters, numbers and hyphens only; no leading, trailing or consecutive
|
||||
@@ -96,15 +98,18 @@ against `references/contract.md`, which holds the three-part shape, the banned c
|
||||
boundary-clause form and the length tiers. A hand-invoked skill (`SKILL.md` Step 2) takes one
|
||||
plain sentence and `disable-model-invocation: true` instead.
|
||||
|
||||
**`metadata.version`** — required on every skill (ADR-0022), not a per-skill or per-plugin choice,
|
||||
and enforced by the `skill-frontmatter` pre-commit hook. The scaffold seeds a new skill at
|
||||
`"0.1.0"`; leave that value alone here and let `SKILL.md` Step 4 bump it. (`"1.0.0"` is the seed
|
||||
for a pre-existing skill retrofitted into the rule, and never applies to a skill created here.)
|
||||
|
||||
**Optional frontmatter** — uncomment and fill in, or remove entirely:
|
||||
|
||||
- `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. `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
|
||||
- `metadata` — the rest of the map, all of it optional: `author` and `category`, plus `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
|
||||
|
||||
@@ -85,6 +85,10 @@ improvise the cuts — four dry runs invented six to ten different answers to th
|
||||
If a signal points to a script or reference file, edit that file directly rather than adding a
|
||||
workaround in SKILL.md.
|
||||
|
||||
**A skill carrying no `metadata.version` is seeded at `"1.0.0"`, not bumped** — `SKILL.md` Step 4's
|
||||
patch bump presumes a version to bump, and ADR-0022 reserves `"0.1.0"` for a newly created skill.
|
||||
`references/retrofit.md` carries the reasoning.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing audit against the skill's pre-edit state — a PASS that has become a SUGGESTION, or a
|
||||
|
||||
@@ -105,16 +105,37 @@ 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.
|
||||
|
||||
## Compression must not add authority the source text didn't have
|
||||
|
||||
This one is **not** part of the checklist above, and deliberately so: it fires on a wording change
|
||||
with no file change at all, so a retrofit that adds and removes nothing still owes it.
|
||||
|
||||
The `sources.md` bullet above is about an 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."
|
||||
|
||||
`/skill-audit`'s provenance script does now notice this class: it diffs each slug's `Description`
|
||||
and `Contributing files` text against a base ref and raises an **INFO** when the wording changed.
|
||||
That is a prompt, not a verdict — it reports only *that* the claim moved, never whether the new
|
||||
claim is true, because a bash script can verify an entry is internally consistent and nothing more.
|
||||
Answering it is this flow's job: 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.
|
||||
|
||||
## Versioning a retrofitted skill
|
||||
|
||||
`SKILL.md` Step 4 says to bump the **patch** version on improve, which presumes there is a version
|
||||
to bump. A pre-ADR-0020 skill often carries none — `metadata.version` only became mandatory under
|
||||
ADR-0022, and this flow is exactly where those skills surface.
|
||||
|
||||
A skill with no `metadata.version` is **seeded at `"1.0.0"`, not bumped**. `"0.1.0"` is reserved
|
||||
for a skill created new by the create flow: it means "created and never yet revised", which
|
||||
understates a skill that has been through retrofit and audit passes without tracking a version.
|
||||
Add the field in this retrofit — the `skill-frontmatter` pre-commit hook blocks the commit without
|
||||
it.
|
||||
|
||||
## Worked example — a description retrofit
|
||||
|
||||
|
||||
Reference in New Issue
Block a user