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
169 lines
12 KiB
Markdown
169 lines
12 KiB
Markdown
# 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.
|