fix(git): stop prefixing rtk where it rewrites the output skills parse
The #113 sweep rested on CLAUDE.md's premise that rtk either filters or passes through unchanged, so prefixing is always safe. Measured against rtk 0.42.4, that premise is false for several of the commands the sweep prefixed, and two skills were left giving wrong answers silently. Why: - `rtk git worktree list --porcelain -z` discards both flags and renders its own format. The `locked`/`lock_reason` fields git-worktrees Step 2 must emit are absent entirely, and paths under $HOME are abbreviated to `~/`. - `rtk git branch --list <name>` prints a phantom `* ` line even when nothing matches, so git-branches' stated ambiguity test — "output from both means the name is ambiguous" — reported every name as ambiguous. `tag --list` is a clean passthrough, so only one half broke. - `rtk git diff --name-only`/`--name-status` append a `Changes:` trailer to output documented as "one per line"; `--word-diff` emits none of the `[-removed-] {+added+}` markers its table describes; `rtk git log -L` truncates each line at ~72 chars, on the one command whose purpose is showing line content. - `rtk git stash pop` prints only `FAILED: git stash pop`, swallowing the conflict diagnostic and retained-entry message the surrounding prose tells the agent to rely on. Implementation notes: - Eleven sites reverted to bare `git`, each carrying its reason inline so the next sweep does not undo it. `mergetool` and `rebase -i` are reverted on clause 3's interactive limb only: the TTY defect does not reproduce — rtk filters exactly twelve subcommands and execs the rest — and ADR-0023 records that measurement rather than a convenient one. - ADR-0023 states the rule repo-wide with a third clause: a command whose output the skill parses, or which is interactive, stays bare. `plugins/git/README.md` is reduced to a pointer; its claim that gitea skills "contain no git/rtk mentions at all" was false, and its citation of `hard-rules.md` pointed at a file containing no occurrence of "rtk". - Eight gitea sites swept, all verified byte-identical passthroughs first. - `scripts/check-rtk-prefix.sh` gates clause 1. Run against main's pre-sweep corpus it reports 99 findings including every gitea site, so it would have caught the drift #113 was filed about. Impact: the gate covers clause 1 only, in shell-tagged fences and the opening span of Run cells. Clause 2 is not gateable — "Run `git switch`" and "`git switch` refuses" are the same tokens — and prose bullets are invisible to it. Both limits are recorded in gates.md rather than left implied. Refs: #113 ADR: 0023 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
This commit is contained in:
@@ -346,6 +346,21 @@ repos:
|
||||
# records for Vale warnings. Costs nothing on a clean file: the script
|
||||
# prints only findings.
|
||||
|
||||
- id: check-rtk-prefix
|
||||
stages: ['pre-commit']
|
||||
name: ADR-0023 rtk prefix on executable git commands
|
||||
description: Enforce ADR-0023 clause 1 -- an executable, instructed git command in a shell code fence or a dispatch-table Run cell is written `rtk git`. Clauses 2 and 3 are not machine-decidable; a deliberately bare command opts out with the literal string ADR-0023 on its own line
|
||||
entry: scripts/check-rtk-prefix.sh
|
||||
language: script
|
||||
files: '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$'
|
||||
# README.md is excluded on purpose, not by oversight. A skill-directory
|
||||
# README is consumer-facing prose that no agent ever loads, and the
|
||||
# `git clone` lines in the seven tests/README.md files are setup
|
||||
# instructions for a third party who has no rtk installed. Prefixing
|
||||
# those would be actively wrong -- see ADR-0023's consumer section.
|
||||
exclude: '(^|/)README\.md$'
|
||||
pass_filenames: true
|
||||
|
||||
- id: vale-audit-prefilter-skill
|
||||
stages: ['pre-commit']
|
||||
name: Vale audit prefilter (SKILL.md)
|
||||
|
||||
168
docs/adr/0023-rtk-prefix-marks-executable-commands-only.md
Normal file
168
docs/adr/0023-rtk-prefix-marks-executable-commands-only.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# The `rtk` prefix marks executable commands only, and is repo-wide
|
||||
|
||||
**Status: accepted (2026-09-08).**
|
||||
|
||||
## Context
|
||||
|
||||
`CLAUDE.md` states the org convention as a golden rule: "Always prefix commands with `rtk`. If RTK
|
||||
has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always
|
||||
safe to use." Issue #113 observed that the rule had never been written down for skill *prose*, where
|
||||
a `git <subcommand>` mention can be either an instruction to execute or a reference to the concept,
|
||||
and that the corpus had drifted into carrying both spellings with no stated rule. PR #130 swept the
|
||||
`git` plugin and recorded a two-way split in `plugins/git/README.md`.
|
||||
|
||||
Review found two defects in that sweep, and both are in the premise rather than the execution.
|
||||
|
||||
**RTK is not output-transparent.** `rtk git --help` enumerates twelve filtered subcommands — `diff`,
|
||||
`log`, `status`, `show`, `add`, `commit`, `push`, `pull`, `branch`, `fetch`, `stash`, `worktree`.
|
||||
Everything else is a true passthrough. Inside that set the filter is not a formatting preference; it
|
||||
changes what the command *reports*. Measured against rtk 0.42.4:
|
||||
|
||||
| Command | What rtk does to it |
|
||||
|---|---|
|
||||
| `worktree list --porcelain -z` | discards both flags; no NUL separators, no `locked`/`lock_reason` field at all |
|
||||
| `worktree list -v` | abbreviates `/root/…` to `~/…`, collapses column alignment |
|
||||
| `branch --list <name>` | emits a phantom `* ` line even on no match |
|
||||
| `diff --name-only` / `--name-status` | appends a blank line and a `Changes:` trailer |
|
||||
| `diff --word-diff[=color\|=porcelain]` | emits no `[-removed-] {+added+}` markers; substitutes a diffstat |
|
||||
| `log -L` | truncates each diff body line at ~72 characters with an ellipsis |
|
||||
| `stash pop` (on conflict) | prints only `FAILED: git stash pop`, swallowing `CONFLICT`, `Unmerged paths` and the retained-entry notice |
|
||||
| `stash list` (empty) | prints `No stashes` where git prints nothing |
|
||||
|
||||
Every one of those falsified a skill that was written against the bare output. `git-worktrees`'s
|
||||
Step 2 required `locked` and `lock_reason` from a command whose rtk rendering has never carried
|
||||
them; `git-log-format.md` documented `[-removed-] {+added+}` markers beside a command that no longer
|
||||
produces them. The two-way split could not see any of this, because both halves of it are about what
|
||||
a *sentence* is doing and none of it is about what the *command* does.
|
||||
|
||||
**The rule is not `git`-plugin-scoped.** `plugins/git/README.md` claimed the `gitea-*` skills
|
||||
"contain no `git`/`rtk` mentions at all". Five `gitea-*` SKILL.md files run `git remote get-url
|
||||
origin` in a fenced ```bash Step block — the README's own canonical example of "executable,
|
||||
instructed" — plus `git branch --show-current` in a reference file and three `git remote -v` in
|
||||
`gitea-orchestrate.agent.md`. A convention stated inside one plugin's README is invisible from the
|
||||
plugin next door, which is how those eight sites stayed bare through the sweep that existed to find
|
||||
them.
|
||||
|
||||
## Decision
|
||||
|
||||
**One rule, three clauses, repo-wide** — every `plugins/*/.apm/skills/**` and
|
||||
`plugins/*/.apm/agents/**` file, not the `git` plugin alone.
|
||||
|
||||
1. **Executable and instructed → `rtk git`.** Anything telling the agent to run a command now: an
|
||||
imperative step, a dispatch-table "Run" cell, a fenced code-block procedure.
|
||||
`rtk git push -u origin <branch>`.
|
||||
2. **Illustrative or referential → bare `git`.** Naming a flag's behaviour, quoting a doc's own
|
||||
heading, describing a command in the abstract, warning against an anti-pattern. "`git switch`
|
||||
refuses rather than clobbering conflicting local edits."
|
||||
3. **Machine-parsed or interactive → bare `git`, and say why inline.** A command whose output the
|
||||
skill parses, where rtk is in the filtered set above; or a command that hands control to an
|
||||
interactive child process.
|
||||
|
||||
Clause 3 is the new one and it looks arbitrary without the table in Context, which is why the
|
||||
measurements are recorded here rather than left in a PR thread. It is applied per subcommand and per
|
||||
flag, not per skill: `tag --list` stays prefixed because rtk passes it through byte-identically,
|
||||
while `branch --list` two words away goes bare because it does not. `git remote get-url origin`,
|
||||
`git remote -v`, `git branch --show-current`, `git log --oneline -1` and `git add -u` were all
|
||||
re-measured as byte-identical passthroughs and are therefore prefixed, parsing notwithstanding.
|
||||
|
||||
Two consequences of that per-subcommand basis are worth stating, because both are load-bearing and
|
||||
neither is comfortable:
|
||||
|
||||
- **rtk's filtered set is a moving target.** `git rebase` and `git mergetool` are passthroughs on
|
||||
0.42.4 — verified under `script(1)`, both inherit a real TTY, contradicting an earlier report that
|
||||
they did not. They stay bare anyway, on the interactive limb: a token filter has nothing to offer a
|
||||
command that hands control to an editor, and the prefix would only buy exposure to whatever a later
|
||||
rtk version decides to do with those subcommands. The same reasoning makes the *inner* call in
|
||||
`` `rtk git remote add origin-push $(git config remote.origin.url)` `` bare while the outer stays
|
||||
prefixed — `config` passes through cleanly today, but its stdout becomes a remote URL that is then
|
||||
force-pushed to, and that is not a blast radius to lend to a future filter change.
|
||||
- **`branch --show-current` sits on the sharp edge.** It is in the filtered set, it is parsed, and it
|
||||
is prefixed — on a measurement, in a subcommand whose sibling `--list` is exactly the defect clause
|
||||
3 exists for. If rtk's `branch` filter is ever extended, that is the first site to break. It is
|
||||
called out rather than hedged, because a rule whose exceptions are unrecorded is the state this ADR
|
||||
is replacing.
|
||||
|
||||
**A clause-3 site says so inline, in a few words.** "bare, not `rtk`: rtk prints a phantom `* ` line
|
||||
even on no match". Without it the next sweep re-prefixes the command, which is how #113 recurs.
|
||||
|
||||
**The rule lives here, and `docs/spec/gates.md` carries the gate.** `plugins/git/README.md` is
|
||||
reduced to a pointer. It had also cited `git-workflow/references/hard-rules.md` as a place the rule
|
||||
was written down; that file contains no occurrence of "rtk", and the citation is removed rather than
|
||||
repaired.
|
||||
|
||||
**Clause 1 is enforced by a `check-rtk-prefix` pre-commit hook; clauses 2 and 3 are not enforceable
|
||||
and are not gated.** The hook checks the two places a `git` mention is unambiguously an instruction —
|
||||
a line in a shell-tagged code fence, and the opening backticked span of a "Run" column cell — and a
|
||||
deliberately-bare command opts out with the literal string `ADR-0023` on its own line. Its coverage
|
||||
limits are recorded in `docs/spec/gates.md`, not smoothed over.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Add `compatibility:` frontmatter to every skill.** These six plugins are installable by third
|
||||
parties, and a consumer who installs `git` from the marketplace has no `rtk` on their PATH. Every
|
||||
prefixed command in the corpus is a plain `git` invocation with a word in front of it, so the prefix
|
||||
is *droppable*: delete `rtk ` and the command is correct. A `compatibility:` line per skill would
|
||||
state that in a machine-readable field. Rejected on cost. It is 39 lines of frontmatter restating one
|
||||
sentence, it is preloaded into every agent's context every session under ADR-0020's budget — the
|
||||
field is not free the way a line in a doc is — and it has no consumer: nothing reads
|
||||
`compatibility:`, so the field would be a comment with a colon in it. The consumer situation is
|
||||
documented here and in `plugins/git/README.md` instead, which is where a human installing a plugin
|
||||
actually looks. The same two-line note is owed to the other five plugin READMEs and is not yet
|
||||
written.
|
||||
|
||||
**Move rtk to the execution layer entirely.** Skills instruct bare `git` throughout; `CLAUDE.md`'s
|
||||
session rule handles prefixing at the point of execution. This is the strongest rejected option and
|
||||
it deserves the space: it closes the consumer gap and all eight output defects at once, because the
|
||||
executing agent knows what it is about to parse and the skill does not have to predict it. It also
|
||||
removes clause 3 entirely — there is nothing to except. Rejected because the prefix is lost wherever
|
||||
an agent copies a command literally, which is the common case for a fenced procedure block and the
|
||||
whole reason dispatch tables exist. The org convention's value is that the prefix is *already there*
|
||||
in the text the agent lifts; a rule that relies on the agent remembering to add it is the rule that
|
||||
produced the drift in the first place. Worth revisiting if rtk ever ships a shell shim, which would
|
||||
make the execution layer transparent and this trade different.
|
||||
|
||||
**Keep the two-way split and fix the eight sites by hand.** Rejected: the split has no vocabulary for
|
||||
"this command is executable, instructed, and must still be bare", so the eight sites would be
|
||||
unexplained exceptions and the next sweep re-prefixes them. That is the failure this ADR exists to
|
||||
stop, not a smaller version of it.
|
||||
|
||||
**Gate clauses 2 and 3 as well.** Rejected as undecidable. "Run `git switch <branch>`" and "`git
|
||||
switch` refuses rather than clobbering local edits" are the same token sequence; separating them is a
|
||||
judgement about what a sentence is doing. A gate that guessed would fire on correct content, and a
|
||||
gate that fires on correct content gets added to `SKIP`, which disarms clause 1 along with it.
|
||||
|
||||
## The boundary the rule does not decide
|
||||
|
||||
Two shapes in the corpus resisted the two-way split. The three-clause rule resolves one and does not
|
||||
resolve the other; both are recorded so an author meeting a third one knows which kind it is.
|
||||
|
||||
**`git-worktrees/SKILL.md`'s tracking row carries both spellings in one Run cell** — `rtk git
|
||||
worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add
|
||||
<path> <branch>` expands to exactly this. **Resolved: the clauses apply per mention, not per row,
|
||||
per cell or per file.** The first is the instruction (clause 1), the second names what the first
|
||||
expands to (clause 2), and one table cell can hold one of each. The rule needed no change; the
|
||||
*gate* did, and it checks only a Run cell's opening span for exactly this reason.
|
||||
|
||||
**`git-submodules/references/setup-and-update.md:80` has a git command inside a quoted argument to
|
||||
another command** — `rtk git submodule foreach 'git pull origin main || :'`. **Not resolved: all
|
||||
three clauses describe a command the reading agent executes, and the inner `git pull` is not one.**
|
||||
It is the literal text of an argument that `git submodule foreach` hands to a subshell running inside
|
||||
each submodule's own working tree, where the local convention does not reach. The file already gets
|
||||
this right and already justifies it in prose two lines below ("the git calls in it are the
|
||||
submodule's own — that is the one place a bare `git` is correct"). **An author meeting this shape
|
||||
should do the same: leave the inner command bare and justify it inline.** It is deliberately not
|
||||
promoted to a fourth clause on one instance. The gate does not decide it either — it happens to pass
|
||||
this line, because the segment containing the inner command begins with `rtk`, and that is an
|
||||
accident of the split rather than an understanding of quoting.
|
||||
|
||||
## Consequences
|
||||
|
||||
Eleven sites in `plugins/git/.apm/skills/**` revert to bare `git` under clause 3, each carrying a
|
||||
short inline reason. Eight sites across `plugins/gitea/.apm/skills/**` and
|
||||
`plugins/gitea/.apm/agents/gitea-orchestrate.agent.md` gain the prefix under clause 1, and one in
|
||||
`pc-run/SKILL.md` that the #130 sweep's grep missed because the backtick opens with `SKIP=` rather
|
||||
than `git `. `plugins/git/README.md`'s Conventions section becomes a pointer here, minus a paragraph
|
||||
that was false about the `gitea-*` skills and a citation to a file that does not carry the rule.
|
||||
A `check-rtk-prefix` pre-commit hook and `tests/test-check-rtk-prefix.sh` land with it; the test runs
|
||||
the gate against the pre-sweep corpus on `main` and asserts it fails there, because a gate that only
|
||||
passes on the fixed tree proves nothing about the drift it was written for.
|
||||
@@ -561,6 +561,91 @@ pre-commit run --all-files # size AND Vale
|
||||
|
||||
Scoping a retrofit off `skill-size-check` output alone leaves you blocked at the second gate.
|
||||
|
||||
## The `rtk` prefix gate (ADR-0023)
|
||||
|
||||
`check-rtk-prefix` is a `repo: local` pre-commit hook running `scripts/check-rtk-prefix.sh` over
|
||||
`^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$`, with `README.md` excluded. It enforces
|
||||
**ADR-0023 clause 1 and nothing else**: an executable, instructed local git command in plugin skill
|
||||
or agent content is written `rtk git`.
|
||||
|
||||
It is wider in file scope than the ADR-0020 hooks — every markdown file under a plugin's
|
||||
`.apm/skills/` and `.apm/agents/`, not `SKILL.md` alone — because the rule it enforces is about
|
||||
commands an agent runs, and most of those live in `references/`, which the ADR-0020 gates do not
|
||||
reach ([the `references/` blind spot](#the-blind-spot-references-is-unlinted-for-two-independent-reasons)).
|
||||
|
||||
### What it can decide, and what it declines to
|
||||
|
||||
ADR-0023 has three clauses and only the first is a pattern:
|
||||
|
||||
| Clause | Rule | Gated |
|
||||
|---|---|---|
|
||||
| 1 | executable + instructed → `rtk git` | yes |
|
||||
| 2 | illustrative / referential → bare `git` | no — undecidable |
|
||||
| 3 | machine-parsed or interactive → bare `git` | no — opt-out marker |
|
||||
|
||||
Clause 2 is a judgement about what a sentence is *doing*. "Run `git switch <branch>`" and "`git
|
||||
switch` refuses rather than clobbering local edits" are the same token sequence. A gate that guessed
|
||||
would fire on correct prose, and **a gate that fires on correct content gets added to `SKIP`** —
|
||||
which disarms clause 1 along with it. So the hook looks only at the two contexts where a `git`
|
||||
mention is unambiguously an instruction to execute:
|
||||
|
||||
- a line inside a fenced code block whose info string names a shell — `bash`, `sh`, `shell`, `zsh`,
|
||||
`console`, `shell-session`. Fences tagged `text`, `yaml`, `json`, or tagged with nothing, are **not**
|
||||
checked;
|
||||
- the **opening** backticked span of a "Run" column cell in a markdown dispatch table, and only the
|
||||
opening span.
|
||||
|
||||
That last narrowing is not fussiness. A Run cell routinely carries a command followed by prose about
|
||||
it, and the prose is clause 2. `git-worktrees/SKILL.md` has both shapes on adjacent rows — one cell
|
||||
reading `` `rtk git worktree add --track …` `` — always correct. `` `git worktree add <path>
|
||||
<branch>` `` expands to exactly this (instruction, then reference), and a `**Never** …` row whose Run
|
||||
cell is entirely explanation containing a bare `git push`. Checking every backticked span flags both;
|
||||
checking only a leading span flags neither, and still catches the ordinary
|
||||
`` | List | `git worktree list -v` | `` case the gate exists for.
|
||||
|
||||
### The clause-3 opt-out
|
||||
|
||||
A command that is deliberately bare — because rtk rewrites the output the skill parses, or because
|
||||
the command is interactive — is exempted by putting the literal string `ADR-0023` **on the same
|
||||
line**: in a shell comment for a code line, in the cell text for a table row.
|
||||
|
||||
Per line, never per block. A fenced procedure routinely mixes `rtk git` steps with one deliberately
|
||||
bare command (`git-remotes/references/push.md` does exactly that), and a block-level marker would
|
||||
silently disarm every checked line around the marked one. The cost is a repeated `# bare per
|
||||
ADR-0023` in the three blocks of `git-log-format.md` where every line is deliberately bare; that
|
||||
repetition is the price of the marked line being the only line the marker speaks for.
|
||||
|
||||
The marker is a plain substring match, so a line that mentions `ADR-0023` for an unrelated reason is
|
||||
also exempt. Accepted deliberately: the marker records an author's opt-out, it is not a security
|
||||
boundary, and a stricter form would only move the same trust to a different string.
|
||||
|
||||
### What it deliberately does not cover
|
||||
|
||||
- **Clause 2.** Nothing checks that an illustrative mention stayed bare. A sweep that re-prefixes a
|
||||
referential `git` passes this gate. The inline reasons ADR-0023 requires on clause-3 sites are the
|
||||
only defence, and they are prose.
|
||||
- **Prose bullets.** Most of `branch-operations.md`, `merging.md` and `rewrite-history.md` instruct
|
||||
in list items, not fences. Those are clause-1 sites the gate cannot see, because it cannot
|
||||
distinguish them from clause-2 mentions in the same list.
|
||||
- **`README.md`, excluded by pattern.** A skill-directory README is consumer-facing prose no agent
|
||||
loads, and the `git clone https://github.com/bats-core/…` lines in the seven `tests/README.md`
|
||||
files are setup instructions for a third party who has no `rtk`. Prefixing those would be actively
|
||||
wrong, not merely noisy — see ADR-0023's consumer section.
|
||||
- **Quoting.** The line splitter breaks on `;`, `|`, `&&`, `||`, `$(` and backticks without tracking
|
||||
quotes, so a git command inside a quoted argument is decided by accident.
|
||||
`rtk git submodule foreach 'git pull origin main || :'` passes because the segment holding the
|
||||
inner command begins with `rtk` — the right answer for the wrong reason. Write
|
||||
`foreach 'git a; git b'` and the second inner command is a false positive needing the marker.
|
||||
ADR-0023 records this shape as one the rule itself does not decide.
|
||||
- **Non-git commands.** Only `git` is checked. `rtk` fronts `gh`, `docker`, `kubectl` and others; no
|
||||
gate covers those, and the corpus does not currently instruct them.
|
||||
|
||||
`tests/test-check-rtk-prefix.sh` pins all of it, including the false-positive cases. Its first case
|
||||
reconstructs the plugin corpus as it stood on `main` before the #113 sweep and asserts the gate
|
||||
fails there with at least 20 findings, one of them the `gitea-*` `git remote get-url origin` drift
|
||||
the sweep missed — a gate that only passes on the already-fixed tree proves nothing about the drift
|
||||
it was written for.
|
||||
|
||||
## Vale
|
||||
|
||||
Install the `vale` binary — `brew install vale` (macOS), `snap install vale` (Linux),
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -30,12 +30,9 @@ copilot plugin install ./plugins/git
|
||||
|
||||
## Conventions
|
||||
|
||||
Every skill in this plugin runs local git commands through the org's `rtk` wrapper, never bare `git` — see `git-commits/SKILL.md`'s Gotchas and `git-workflow/references/hard-rules.md`. Issue #113 made that rule precise for skill *prose*, where a `git <subcommand>` mention can be either an instruction to execute or just a reference to the concept:
|
||||
Skills here run local git commands through the org's `rtk` wrapper. **When a command is prefixed, when it stays bare, and why some executable commands must stay bare are all decided by ADR-0023** (`docs/adr/0023-rtk-prefix-marks-executable-commands-only.md`), which is repo-wide and not specific to this plugin. `check-rtk-prefix` enforces the part of it that is machine-decidable.
|
||||
|
||||
- **Executable, instructed commands** — anything telling the agent to run a command right now (an imperative step, a dispatch-table "Run" cell, a fenced code-block procedure) — use `rtk git`. Example: `rtk git push -u origin <branch>`.
|
||||
- **Illustrative or referential mentions** — naming a flag's behavior, quoting a doc's own heading, describing what a command does in the abstract, or warning against an anti-pattern — stay bare `git`. Example: "`git switch` refuses rather than clobbering conflicting local edits."
|
||||
|
||||
This applies within this plugin's own skill files (`git-*`, `pc-*`) — it does not generalize to other plugins. The `gitea-*` skills, for instance, talk to a remote Gitea server through the `gitea` MCP tools and contain no `git`/`rtk` mentions at all; the distinction has nothing to hold onto there.
|
||||
Installing this plugin without `rtk`? Every prefixed command is a plain `git` invocation with a word in front of it — drop the `rtk ` and it is correct.
|
||||
|
||||
## Contents
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
198
scripts/check-rtk-prefix.sh
Executable file
198
scripts/check-rtk-prefix.sh
Executable file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ADR-0023 clause 1, and ONLY clause 1: an executable, instructed local git
|
||||
# command in plugin skill or agent content is written `rtk git`, never bare
|
||||
# `git`.
|
||||
#
|
||||
# WHY THIS IS NARROW ON PURPOSE. ADR-0023 has three clauses, and only the first
|
||||
# is machine-decidable:
|
||||
#
|
||||
# 1. executable + instructed -> `rtk git` <- this hook
|
||||
# 2. illustrative/referential -> bare `git` <- undecidable, not gated
|
||||
# 3. machine-parsed or interactive -> bare `git`, marked <- opt-out, below
|
||||
#
|
||||
# Clause 2 is a judgement about what a sentence is doing, not a pattern. "`git
|
||||
# switch` refuses rather than clobbering conflicting local edits" and "run `git
|
||||
# switch <branch>`" are the same token sequence in prose. A gate that guessed
|
||||
# would fire on every doc paragraph in the corpus, and a gate that fires on
|
||||
# correct content gets disabled. So this hook looks only at the two places where
|
||||
# a `git` mention is unambiguously an instruction to execute:
|
||||
#
|
||||
# (a) a line inside a fenced code block whose info string names a shell
|
||||
# (bash / sh / shell / zsh / console);
|
||||
# (b) the OPENING backticked span of a "Run" column cell in a markdown
|
||||
# dispatch table -- and only the opening span.
|
||||
#
|
||||
# (b) is that narrow because a Run cell routinely carries a command followed by
|
||||
# prose about it, and that prose is clause 2. git-worktrees/SKILL.md has both
|
||||
# shapes on adjacent rows: a cell reading `rtk git worktree add --track ...` --
|
||||
# always correct. `git worktree add <path> <branch>` expands to exactly this
|
||||
# (instruction first, reference second), and a `**Never** ...` row whose Run cell
|
||||
# is entirely explanatory prose containing a bare `git push`. Checking every span
|
||||
# flags both; checking only a leading span flags neither, and still catches the
|
||||
# ordinary `| List | `git worktree list -v` |` case this gate exists for.
|
||||
#
|
||||
# Prose bullets, prose-leading Run cells, table cells outside a Run column, and
|
||||
# fences tagged `text`, `yaml`, `json` etc. are NOT checked. That is a real
|
||||
# coverage gap, recorded in docs/spec/gates.md rather than papered over.
|
||||
#
|
||||
# CLAUSE-3 OPT-OUT. A site that is deliberately bare because rtk rewrites the
|
||||
# output the skill parses, or because the command is interactive, is exempted by
|
||||
# putting the literal string `ADR-0023` on the SAME LINE — in a shell comment for
|
||||
# a code line, in the cell text for a table row. Per-line, never per-block: a
|
||||
# fenced block routinely mixes `rtk git` steps with one deliberately-bare
|
||||
# command (references/push.md does exactly that), and a block-level marker would
|
||||
# silently disarm the checked lines around the marked one.
|
||||
#
|
||||
# The marker is a bare substring match, so a line that merely *mentions*
|
||||
# ADR-0023 for an unrelated reason is also exempt. That is accepted: the marker
|
||||
# is an author's deliberate opt-out, not a security boundary, and a stricter
|
||||
# form would only move the same trust to a different string.
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v python3 > /dev/null 2>&1; then
|
||||
echo "ERROR: python3 is required for the ADR-0023 rtk-prefix gate but was not found on PATH." >&2
|
||||
echo " Fix: install python3 (pre-commit itself is a Python application, so it is almost certainly already present)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No PyYAML here, unlike skill-size-check.sh: this gate never reads frontmatter,
|
||||
# only the markdown body, so it has no folded scalar to measure.
|
||||
exec python3 -u - "$@" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
MARKER = "ADR-0023"
|
||||
SHELL_INFO = {"bash", "sh", "shell", "zsh", "console", "shell-session"}
|
||||
|
||||
FENCE_OPEN = re.compile(r"^\s*(?P<f>`{3,}|~{3,})\s*(?P<info>[^\s`]*)")
|
||||
ASSIGNMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=\S*\s+")
|
||||
# Shell separators that begin a fresh command word.
|
||||
SPLIT = re.compile(r"(?:\|\||&&|[;|&\n]|\$\(|`|\()")
|
||||
BACKTICKED = re.compile(r"`([^`]+)`")
|
||||
|
||||
|
||||
def strip_shell_comment(line: str) -> str:
|
||||
"""Drop a trailing `#` comment, ignoring `#` inside single/double quotes."""
|
||||
out = []
|
||||
quote = None
|
||||
prev = ""
|
||||
for ch in line:
|
||||
if quote:
|
||||
if ch == quote and prev != "\\":
|
||||
quote = None
|
||||
elif ch in "'\"":
|
||||
quote = ch
|
||||
elif ch == "#" and (not out or out[-1].isspace()):
|
||||
break
|
||||
out.append(ch)
|
||||
prev = ch
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def bare_git_in_shell(code: str) -> bool:
|
||||
for segment in SPLIT.split(code):
|
||||
seg = segment.lstrip()
|
||||
if seg.startswith("$ "): # a copied prompt
|
||||
seg = seg[2:].lstrip()
|
||||
while True: # VAR=x VAR2=y git ...
|
||||
m = ASSIGNMENT.match(seg)
|
||||
if not m:
|
||||
break
|
||||
seg = seg[m.end():]
|
||||
if re.match(r"git(\s|$)", seg):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run_column(header: str):
|
||||
"""Index of the 'Run' column in a markdown header row, or None."""
|
||||
cells = [c.strip().strip("`*_ ").lower() for c in header.strip().strip("|").split("|")]
|
||||
return cells.index("run") if "run" in cells else None
|
||||
|
||||
|
||||
def check(path: str):
|
||||
problems = []
|
||||
try:
|
||||
lines = open(path, encoding="utf-8").read().splitlines()
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
# Unreadable is an error, never a silent pass.
|
||||
return [(0, f"could not read file: {exc}")]
|
||||
|
||||
fence = None # closing marker of the open fence, or None
|
||||
fence_is_shell = False
|
||||
run_col = None # active Run-column index, or None
|
||||
pending_header = None
|
||||
|
||||
for n, raw in enumerate(lines, start=1):
|
||||
if fence is not None:
|
||||
if re.match(r"^\s*" + re.escape(fence) + r"\s*$", raw):
|
||||
fence, fence_is_shell = None, False
|
||||
continue
|
||||
if fence_is_shell and MARKER not in raw:
|
||||
if bare_git_in_shell(strip_shell_comment(raw)):
|
||||
problems.append((n, raw.strip()))
|
||||
continue
|
||||
|
||||
m = FENCE_OPEN.match(raw)
|
||||
if m:
|
||||
fence = m.group("f")
|
||||
fence_is_shell = m.group("info").lower() in SHELL_INFO
|
||||
run_col, pending_header = None, None
|
||||
continue
|
||||
|
||||
stripped = raw.strip()
|
||||
if not stripped.startswith("|"):
|
||||
run_col, pending_header = None, None
|
||||
continue
|
||||
|
||||
# A markdown table: header row, delimiter row, then data rows.
|
||||
if run_col is None:
|
||||
if pending_header is not None and set(stripped) <= set("|-: "):
|
||||
run_col = run_column(pending_header)
|
||||
pending_header = None
|
||||
else:
|
||||
pending_header = stripped
|
||||
run_col = None
|
||||
continue
|
||||
|
||||
if MARKER in raw:
|
||||
continue
|
||||
cells = stripped.strip("|").split("|")
|
||||
if run_col >= len(cells):
|
||||
continue
|
||||
cell = cells[run_col].strip()
|
||||
# Only a cell that OPENS with a backticked command is a dispatch entry.
|
||||
# A cell opening with prose is explanation, and explanation is clause 2.
|
||||
if not cell.startswith("`"):
|
||||
continue
|
||||
opening = BACKTICKED.match(cell)
|
||||
if opening and re.match(r"git(\s|$)", opening.group(1).strip()):
|
||||
problems.append((n, opening.group(1).strip()))
|
||||
|
||||
return problems
|
||||
|
||||
|
||||
failed = False
|
||||
for path in sys.argv[1:]:
|
||||
for line_no, text in check(path):
|
||||
failed = True
|
||||
print(
|
||||
f"ADR-0023: {path}:{line_no}: executable git command is not prefixed with `rtk`: {text}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if failed:
|
||||
print("", file=sys.stderr)
|
||||
print(
|
||||
"Fix: write `rtk git <subcommand>` (ADR-0023 clause 1). If this command must stay bare\n"
|
||||
"because rtk rewrites output the skill parses, or because it is interactive (clause 3),\n"
|
||||
"say so inline and put the literal string ADR-0023 on the same line to record the opt-out.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
PY
|
||||
224
tests/test-check-rtk-prefix.sh
Normal file
224
tests/test-check-rtk-prefix.sh
Normal file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Tests for scripts/check-rtk-prefix.sh — ADR-0023 clause 1.
|
||||
#
|
||||
# Case 1 is the one that earns the rest: it runs the gate against the corpus as
|
||||
# it stood on `main` BEFORE the #113 sweep, and asserts it fails there. A gate
|
||||
# that only passes on the already-fixed tree proves nothing about whether it
|
||||
# would have caught the drift it was written for.
|
||||
#
|
||||
# Everything after that is synthetic. The false-NEGATIVE cases (a bare command
|
||||
# the gate must catch) and the false-POSITIVE cases (correct content the gate
|
||||
# must leave alone) carry equal weight: this hook's failure mode is not missing
|
||||
# a violation, it is firing on deliberately-bare clause-2 and clause-3 content
|
||||
# until someone adds it to SKIP.
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/check-rtk-prefix.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "SKIP: python3 is not installed — the script under test fails closed on it, so every case here would only re-assert the missing-dependency guard"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
RUN_TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$RUN_TMP"' EXIT
|
||||
|
||||
# Writes $2 to a .md file and asserts the gate's verdict. $1 is "clean" or
|
||||
# "dirty"; $3 is the case description.
|
||||
expect() {
|
||||
local want="$1" body="$2" desc="$3"
|
||||
local f="$RUN_TMP/case.md"
|
||||
printf '%s\n' "$body" > "$f"
|
||||
local rc=0
|
||||
bash "$SCRIPT" "$f" > "$RUN_TMP/out" 2>&1 || rc=$?
|
||||
if [[ "$want" == "dirty" && $rc -eq 0 ]]; then
|
||||
fail "$desc — expected a violation, exited 0"
|
||||
elif [[ "$want" == "clean" && $rc -ne 0 ]]; then
|
||||
fail "$desc — expected no violation, exited $rc"
|
||||
sed 's/^/ /' "$RUN_TMP/out"
|
||||
else
|
||||
pass "$desc"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "--- the pre-#113 corpus on main trips the gate ---"
|
||||
|
||||
# Derived from git, not hardcoded: the point is "the drift this gate exists for",
|
||||
# and a hardcoded path list goes stale the moment a file is renamed.
|
||||
if ! git -C "$REPO_ROOT" rev-parse --verify -q main >/dev/null; then
|
||||
echo "SKIP: no local 'main' ref — the historical-corpus case cannot be reconstructed"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
PRE="$RUN_TMP/pre"
|
||||
# A read loop, not the bash-4 array builtin: tests/test-vale-wrap.sh scans every
|
||||
# script under tests/ for bash-4-only constructs, because these run on macOS's
|
||||
# bash 3.2. Same reason for the `[@]+` guards on every array expansion below.
|
||||
PRE_FILES=()
|
||||
PRE_PATHS=()
|
||||
while IFS= read -r PRE_REL; do
|
||||
PRE_FILES+=("$PRE_REL")
|
||||
PRE_PATHS+=("$PRE/$PRE_REL")
|
||||
done < <(
|
||||
git -C "$REPO_ROOT" ls-tree -r --name-only main -- 'plugins' \
|
||||
| grep -E '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$' \
|
||||
| grep -v '/README\.md$'
|
||||
)
|
||||
if [[ ${#PRE_FILES[@]} -eq 0 ]]; then
|
||||
fail "no plugin skill/agent files found on main — the historical case checked nothing"
|
||||
else
|
||||
for f in ${PRE_FILES[@]+"${PRE_FILES[@]}"}; do
|
||||
mkdir -p "$PRE/$(dirname "$f")"
|
||||
git -C "$REPO_ROOT" show "main:$f" > "$PRE/$f"
|
||||
done
|
||||
if bash "$SCRIPT" ${PRE_PATHS[@]+"${PRE_PATHS[@]}"} > "$RUN_TMP/pre.out" 2>&1; then
|
||||
fail "the pre-sweep corpus passed — the gate would not have caught the #113 drift"
|
||||
else
|
||||
hits="$(grep -c '^ADR-0023: ' "$RUN_TMP/pre.out" || true)"
|
||||
if [[ "$hits" -lt 20 ]]; then
|
||||
fail "the pre-sweep corpus produced only $hits findings — too few to be the known drift"
|
||||
elif ! grep -q 'gitea-issues/SKILL.md.*git remote get-url origin' "$RUN_TMP/pre.out"; then
|
||||
fail "the pre-sweep corpus failed, but not on the known gitea drift"
|
||||
sed 's/^/ /' "$RUN_TMP/pre.out" | head -5
|
||||
else
|
||||
pass "the pre-sweep corpus fails with $hits findings, including the gitea sweep gap"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- shell fences: what must fail ---"
|
||||
|
||||
expect dirty '```bash
|
||||
git commit -m "x"
|
||||
```' "a bare git command in a bash fence"
|
||||
|
||||
expect dirty '```sh
|
||||
rtk git add -u && git commit -m "x"
|
||||
```' "a bare git after && on a line that starts with rtk git"
|
||||
|
||||
expect dirty '```bash
|
||||
SKIP=check-yaml git commit -m "x"
|
||||
```' "a bare git behind an environment-variable prefix"
|
||||
|
||||
expect dirty '```bash
|
||||
url=$(git config remote.origin.url)
|
||||
```' "a bare git inside a command substitution"
|
||||
|
||||
expect dirty '```console
|
||||
$ git status
|
||||
```' "a bare git behind a copied shell prompt"
|
||||
|
||||
echo ""
|
||||
echo "--- shell fences: what must NOT fail ---"
|
||||
|
||||
expect clean '```bash
|
||||
rtk git commit -m "x"
|
||||
rtk git push
|
||||
```' "prefixed commands"
|
||||
|
||||
expect clean '```bash
|
||||
git stash list # bare per ADR-0023: rtk prints "No stashes" where git prints nothing
|
||||
```' "a bare command carrying the ADR-0023 opt-out marker"
|
||||
|
||||
expect clean '```text
|
||||
git worktree list --porcelain -z
|
||||
```' "a non-shell fence (text) is out of scope"
|
||||
|
||||
expect clean '```yaml
|
||||
entry: git
|
||||
```' "a yaml fence is out of scope"
|
||||
|
||||
expect clean '```bash
|
||||
# never reach for git commit --no-verify here
|
||||
rtk git commit
|
||||
```' "a bare git inside a shell comment"
|
||||
|
||||
expect clean 'Run `git switch <branch>` — no wait, this is prose, not a fence.' \
|
||||
"a bare git in a prose line outside any fence"
|
||||
|
||||
echo ""
|
||||
echo "--- dispatch tables ---"
|
||||
|
||||
expect dirty '| Operation | Run |
|
||||
|---|---|
|
||||
| List | `git worktree list -v` |' "a bare command opening a Run cell"
|
||||
|
||||
expect clean '| Operation | Run |
|
||||
|---|---|
|
||||
| List | `rtk git worktree list -v` |' "a prefixed command in a Run cell"
|
||||
|
||||
expect clean '| Operation | Run |
|
||||
|---|---|
|
||||
| List | `git worktree list -v` — bare per ADR-0023 |' \
|
||||
"a bare Run cell carrying the opt-out marker"
|
||||
|
||||
# The clause-2 shapes that made an every-span check unusable. Both are real rows
|
||||
# from git-worktrees/SKILL.md.
|
||||
expect clean '| Operation | Run |
|
||||
|---|---|
|
||||
| Track | `rtk git worktree add --track -b <b> <p> <r>/<b>` — always correct. `git worktree add <p> <b>` expands to exactly this |' \
|
||||
"a referential bare mention AFTER the instructed command in a Run cell"
|
||||
|
||||
expect clean '| Operation | Run |
|
||||
|---|---|
|
||||
| **Never** `git worktree add <p> <r>/<b>` | That ref resolves, so `git push` needs an explicit refspec |' \
|
||||
"an anti-pattern row whose Run cell opens with prose"
|
||||
|
||||
expect clean '| Flag | Meaning |
|
||||
|---|---|
|
||||
| `-L` | as in `git log -L` |' "a table with no Run column is out of scope"
|
||||
|
||||
echo ""
|
||||
echo "--- degenerate inputs ---"
|
||||
|
||||
expect clean '' "an empty file"
|
||||
|
||||
expect clean '```bash
|
||||
rtk git status' "an unterminated fence does not crash the parser"
|
||||
|
||||
if bash "$SCRIPT" > "$RUN_TMP/noargs.out" 2>&1; then
|
||||
pass "no filenames exits 0 rather than erroring"
|
||||
else
|
||||
fail "no filenames should exit 0 — pre-commit calls hooks with an empty file list"
|
||||
fi
|
||||
|
||||
if bash "$SCRIPT" "$RUN_TMP/does-not-exist.md" > "$RUN_TMP/missing.out" 2>&1; then
|
||||
fail "an unreadable file exited 0 — unreadable must be an error, never a silent pass"
|
||||
elif ! grep -q 'could not read file' "$RUN_TMP/missing.out"; then
|
||||
fail "an unreadable file failed for the wrong reason"
|
||||
sed 's/^/ /' "$RUN_TMP/missing.out"
|
||||
else
|
||||
pass "an unreadable file exits 1 and says so"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- the live corpus is clean ---"
|
||||
|
||||
LIVE=()
|
||||
while IFS= read -r LIVE_REL; do
|
||||
LIVE+=("$LIVE_REL")
|
||||
done < <(
|
||||
git -C "$REPO_ROOT" ls-files -- 'plugins' \
|
||||
| grep -E '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$' \
|
||||
| grep -v '/README\.md$'
|
||||
)
|
||||
if [[ ${#LIVE[@]} -eq 0 ]]; then
|
||||
fail "no plugin skill/agent files matched the hook's files: pattern"
|
||||
elif (cd "$REPO_ROOT" && bash "$SCRIPT" ${LIVE[@]+"${LIVE[@]}"} > "$RUN_TMP/live.out" 2>&1); then
|
||||
pass "the ${#LIVE[@]} in-scope corpus files pass"
|
||||
else
|
||||
fail "the live corpus has ADR-0023 clause-1 violations"
|
||||
sed 's/^/ /' "$RUN_TMP/live.out"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Reference in New Issue
Block a user