10 Commits

Author SHA1 Message Date
0427422765 chore(release): bump the six plugins and the catalog to patch, not minor
The branch is 31 fix, 30 refactor, 11 docs, 4 chore, 2 test — zero feat, zero `!`,
zero BREAKING CHANGE — and it adds no skill, agent, command or hook. Two rules
shipped in this repo both make that a patch: forge's version-bump.md, which lands in
this very branch ("minor for new capability, patch for a fix/refactor"), and
git-commits' conventional-commits-spec.md, which maps fix to PATCH and refactor to
none. Minor was the one answer neither rule produces, and the release was arguing
with a policy it was simultaneously introducing.

  bin 1.1.6, core 1.1.2, git 1.3.6, gitea 1.3.7, kyberforge 1.6.1, lint 1.1.7

The catalog goes 0.5.0 to 0.4.6 for the same reason: apm-workflow's marketplace.md
reserves a catalog minor for a packages[] entry added or removed and assigns patch
to an existing entry's version moving. The set is 7 entries on both sides with
unchanged names, so only the patch trigger applies.

Each number is +1 patch on the pre-bump value rather than stacked on the minor, and
executables.allow follows kyberforge to 1.6.1 so the ADR-0019 SessionStart hook does
not orphan.

Note what this does not settle: four published files were removed from the installed
tree on this branch, three more moved, and caveman gained disable-model-invocation,
which retires its old triggers. Under a strict reading of the repo's own breaking
rule those are major-class and they currently ship under refactor: with no marker.
Patch is right for the code; whether the deployed skill surface is a public contract
is still unwritten, and that question outlives this commit.
2026-09-01 12:39:54 +00:00
ccc54cbb58 docs: retire ADR-0020's stale measurements and fix a CONTEXT.md code span
971e148 de-pinned three stale present-tense figures in gates.md and left ADR-0020's
copy of each, while CONTEXT.md points readers at the ADR for the current number. All
three were wrong at HEAD, re-measured against a `git archive` of the tree rather
than the working copy:

- "26 description FAILs, 9 body FAILs, 2 dangling targets, 58 SUGGESTIONs" is now
  0 / 0 / 0 / 29.
- "pre-commit run --all-files is red on 10 alerts" is 0 errors in 39 files.
- apm-orchestrate was cited as a 900-word body FAIL. It is 876 words, a SUGGESTION.
  git-orchestrate at 933 and gitea-orchestrate at 1,199 were correct.

Marked historical with a dated amendment and the measured current values, following
the convention already in this file. The "realistic landing is somewhere in that
33-58% band" projection is left alone: it is a forecast rather than a measurement,
and the realised 55.3% falls inside it.

The Enforcement table claimed exhaustiveness while listing only ERROR and SUGGESTION
for boundary targets; gates.md documents a third verdict, INFO "DID NOT RUN". Added.

CONTEXT.md carried shell-escaped backticks inside a markdown code span, which closes
the span early and leaves an unterminated double-backtick span that swallows the
rest of the glossary entry — in the definition of a term this branch introduces. It
also called the ADR's pre-retrofit 23,427 the current preload figure; the measured
value is 10,478, recorded here so the glossary and the gate agree.

ADR: 0020
2026-09-01 12:39:42 +00:00
f40deada86 fix(lint): restore the glob-scoping rule and warn about the MDX invocation kill
vale-config deleted "settings in a glob section only apply to files matching that
glob" — the single most common Vale misconfiguration. Nothing in either lint skill
told an agent that BasedOnStyles under `[*.md]` governs `.md` only; the nearest
survivor was a parenthetical in a reference file the skill opens only for the full
field listing. Restored to the always-loaded body, at the point of use.

vale-config also never mentioned `[formats]` or MDX at all, leaving the whole
decision in that same reference file. Verified against Vale 3.15.2: an unmapped
`.mdx` covered by one of your globs is `E100 [lintMDX]`, exit 2, and every other
file in that invocation produces no output whatsoever. Because the failure is
invocation-wide rather than per-file, it belongs in the Gotchas.

The crash requires a glob that actually covers `.mdx` — under `[*.md]` the file is
skipped and nothing fails — so the Gotcha states that condition rather than the
broader claim that any `.mdx` in the tree kills the run.

The mapping-versus-mdx2vast attribution disagreed across the two skills: vale-run
said `[formats] mdx = md` is "what vale-config recommends" while vale-config
presented it as a bare either/or. Fixed on the source side by having vale-config
actually recommend it and say why, which makes vale-run's existing sentence true
with no edit to vale-run.

Refs: #117
2026-09-01 12:39:31 +00:00
40ff89eabf fix(bin): restore prototype's deleted anti-patterns and two routing triggers
ADR-0020's stated anti-goal is satisfying the size gate by deleting content rather
than relocating it. prototype's logic.md lost three anti-patterns, including
"Don't generalise" — the one with a distinct failure mode, a throwaway growing
abstractions for hypothetical futures, and the one the logic branch is most exposed
to. It survived nowhere in the repo.

The deletion bought nothing measurable: references/ sits outside the body FAIL,
outside the 600-word suggestion and outside the Vale gate, and prototype's body is
483 words. 4011d14 restored the byte-identical defect in the sibling ui.md with
exactly that reasoning in its message and left this file alone. Restored verbatim
from main.

improve-codebase-architecture had dropped "refactoring" from its description
entirely, so "find refactoring opportunities in this repo" had no lexical match,
while spending characters on a boundary against tdd — which cannot plausibly steal
an architecture request. retrofit.md names that exact failure: an invented boundary
costs characters and buys no routing accuracy.

write-docs had dropped all four literal trigger phrasings, leaving them only in the
body and a `when:` field, neither visible to the router at routing time. Its
boundary also sent PRDs to grill-with-docs, which has no PRD flow, and the body
repeated that at two more places. Per #123 nothing in the corpus produces a PRD, so
no target was invented — the boundary is now honest about the ADR case only.

Two READMEs added by this branch contradicted the SKILL.md they document: triage's
label resolution, and grill-with-docs' fifth during-session behaviour. Unconditional
reference pointers in tdd and improve-codebase-architecture are now conditional; the
files stay at the skill root, which is #122's scope.

Refs: #114, #122, #123
ADR: 0020
2026-09-01 12:39:22 +00:00
be9b8d277f fix(gitea): restore the withLines exception and route rename_branch through the orchestrator
gitea-files' always-loaded Gotchas said "content is base64 both ways" without
qualification. The `main` text carried an exception for `withLines: true` and both
halves were dropped. Verified live: `get_file_contents` with `withLines: true`
returns plain JSON text while the same response still reports `"encoding":"base64"`.
An agent that follows the recommendation two sentences later and applies the
unconditional decode gets garbage, with the response's own field confirming the
wrong answer. Exception restored, and the lying field named.

gitea-orchestrate was never updated for `rename_branch`: absent from the operation
enum, so an agent caller got "unknown operation", and absent from the
destructive-confirm list, though branches.md requires a rename with open PRs or a
protection rule to be confirmed exactly as `delete_branch` is. Added to both — the
confirm gate rather than the enum alone, because accepting the operation without it
routes around a rule the skill states while appearing to support it. The
compatibility frontmatter, which the agent reads, still omitted the tool too.

Two more always-loaded Gotchas contradicted their own reference files, and the
Gotcha was wrong both times: issues and PRs are distinguishable on a list item by
the `html_url` path segment (confirmed live — #129 at /pulls/, #128 at /issues/),
and `get_repository_tree` takes `tree_sha`, not `ref`.

`review_comments` was asserted as unconditionally present on the PR get response.
It is absent on a PR with no review comments, so the claim is downgraded to
present-when-non-zero rather than stated as response shape.

The label-exclusivity relocation moved the rule out of label-inference.md and into
labels.md without updating sources.md, leaving the one rule in this branch that
writes differently to live repos citing a file that no longer carries it. The rule
itself is correct as it stands and `main` was wrong — every Kind/* label on this
instance is exclusive:false, every Priority/* and Status/* is true — so only the
provenance record is corrected.

Routing: gitea-workflow lost the human-caller discriminator and widened from status
checks to any request, which sent "close #42" to a branch that resolves the number
and presents detail without ever closing it. gitea-branches and gitea-issues regain
trigger phrasings the retrofit dropped.

Refs: #92
2026-09-01 12:38:53 +00:00
20e5627fd7 fix(git): correct three command claims the plugin got wrong, and give rebase an owner
Three statements were false against the git in use (2.39.5), each verified by
running it.

`git rebase --autosquash HEAD~N` without `-i` is a silent no-op: git prints
"Successfully rebased", the `fixup!` commit survives with the same SHA, and
rewrite-history.md presented that as the preferred flow with `-i` as an optional
review step. The agent reports the squash as done and the `fixup!` subject then
trips this repo's own commit-msg gate. `-i` is now the command, not the alternative.

"Fetch never modifies a local branch, so it is always safe to run" — new text on
this branch — is false: `git fetch origin main:probe` fast-forwarded the local
branch, and a `+` prefix force-updates it, losing commits. The claim is scoped to
the no-refspec form.

`git worktree add --orphan` does not exist before Git 2.42; on 2.39.5 it is
`error: unknown option 'orphan'`, exit 129. The same file gives a version floor for
`--recurse-submodules` three sections earlier. Floor added, with a fallback that
was tested before being documented.

git-workflow claimed "every request resolves to exactly one of these six" while
rebase, reset and stash were owned by no skill — `git reset` appeared nowhere in
the plugin, old tree or new — so "undo my last commit" routed nowhere. The claim is
gone, the router gains rows for all three, and the procedures now exist: plain
rebase with `--onto` and conflict handling, a reset mode table gated on `--hard`,
and stash save/pop/list/drop. `--hard` gets an always-loaded Gotcha, matching the
register the force-push refusal already sets.

Cherry-pick had three claimants pointing at git-history while git-commits owned the
flow, and the two copies were not equivalent — git-history's lacked the destination
check, the rtk prefix and `--abort`. Resolved to git-commits per #112; the weaker
duplicate is replaced by a hand-off.

git-commits' metadata.version was deleted rather than bumped in 14af50b, leaving two
house-contract documents citing a worked v0.1.2 to v0.1.3 transition against a file
declaring no version. Restored to 0.1.3.

Also: the divergent-pull explanation stated a `--ff-only` default that does not
exist (it is a hard error); `--remote` "requires" a configured branch where it uses
one; `git push origin --delete` moved to git-remotes, which owns the remote-side
gates; seven retired `git:<name>` source slugs; git-orchestrate quoted a
git-workflow sentence that no longer exists; git-submodules regains the indirect
trigger that made "add a dependency repo" routable; and commit atomicity is a common
gate rather than reachable only from the create flow.

Refs: #112, #113
2026-09-01 12:38:38 +00:00
fc305ba7d9 fix(kyberforge): correct the routing-tier contract and make agent-audit rubrics conditional
Five documents told authors that a prose-form dangling routing target blocks. The
gate reports it as a SUGGESTION and exits 0. Verified on fixtures: `-> name` and
`/name` are blocking ERRORs, the prose form is SUGGESTION-tier unless a second
resolving target in the same sentence corroborates it. ADR-0020 and gates.md were
right; contract.md, retrofit.md, description-quality.md, finding-criteria.md and
agent-author's contract.md were wrong — and they are what an author and an auditor
actually read. The whole 39-skill corpus was retrofitted against them.

skill-audit was also self-contradictory: it imports validate.sh's SUGGESTIONs into
the Structure dimension verbatim while its own rubric grades the same target a FAIL,
so one target got reported twice at two tiers. The script owns the grade; the rubric
now says so.

The YAML-fold trap that broke gitea-labels-milestones (#100) was warned about only
in retrofit.md, reachable only from the improve flow when a budget is exceeded. It
is now in both contract.md files, which SKILL.md mandates on the create flow too.

agent-audit loaded both rubrics unconditionally on every run — 3,323 words for a
clean audit against skill-audit's 1,636. dac9cad fixed exactly this in skill-audit
and edited agent-audit in the same commit without applying it. Same treatment: the
criteria move to a new finding-criteria.md and load per dimension. Clean run now
2,083 words, a 37% cut.

Routing: apm-workflow's description shed dependency installation while still owning
the flow, and apm-install's boundary did not exclude it, so "install my apm
dependencies" matched the CLI-binary skill with no route back. Fixed on both sides.
forge regains two of the three phrasings the retrofit deleted.

forge Step 1 called grill-with-docs unconditionally — a skill in plugins/bin, which
kyberforge does not declare as a dependency. It resolves here only because the
walk-up sweeps sibling plugins; a standalone install dead-ends. Step 1 now names
the cross-plugin dependency and gives an inline fallback. Declaring it properly in
apm.yml remains the better fix.

Also: both audit SKILL.md files now grade exit 2 as "did not run, dimension
unverified" rather than as findings; skill-audit's README row described content that
moved, which its own finding-criteria.md grades a FAIL; and body-discipline.md's
`git show <sha>:plugins/...` command is fenced, since an installed plugin cache has
no repo and file-structure.md makes a bare repo path a FAIL.

Refs: #100, #101, #125
ADR: 0020
2026-09-01 12:38:22 +00:00
59f27dbd94 fix(core): close five ways validate-adapter.sh graded an adapter it had not read
The adapter check shipped green on files it should have failed, and failed files it
should have passed. Each defect is a residual of the fix that closes #115.

A fenced, indented or HTML-commented `@AGENTS.md` counted as an import, though
Claude Code resolves none of them — the adapter deferred to nothing and the gate
said so approvingly. Import matching now runs against a character mask that marks
fenced blocks and HTML comments inert, and applies CommonMark's four-space rule.
The mask is deliberately not applied to prose pointers, where four-space
indentation is ordinary list continuation.

The encoding fix reached only BOM-carrying UTF-16/32. BOM-less UTF-16LE/BE and
UTF-32LE are valid UTF-8, so they still produced the exact false diagnosis the fix
was written to remove: "no reference to AGENTS.md" on a file whose first line is
`@AGENTS.md`. A NUL-byte check is the complete signal. BOM stripping is no longer
positional, which also drops the mirror-image false FAILs on a doubled or mid-file
BOM.

`@NOTAGENTS.md`, `@zzzAGENTS.md` and `@docs/does/not/exist/AGENTS.md` all passed:
the pattern had no path-segment boundary and the target was never resolved on disk.
Both now hold, and a zero-byte or blank target is reported rather than credited.
Unresolved candidates print as `Near miss:` lines so the author sees why a line was
not counted.

`--no-import-syntax` still used substring matching, so `Do NOT read AGENTS.md; it
is obsolete.` passed as a pointer. That is #115's own defect surviving in the flag's
other mode. A mention must now carry a deference cue and must not be negated.

An unreadable file passed `isfile()`, raised, and exited 1 with a traceback and no
FAIL line — the one exit code no document covered, while Step 3 says to re-run
until it exits 0. It now exits 3 with a diagnostic, and the README states all four
codes and what to do about each instead of "exits non-zero on any failure".

Tests 18 to 42. Two of the new cases initially survived their own mutation and were
strengthened: `@NOTAGENTS.md` was being rejected by the disk check before the token
boundary ran, and stripping that boundary leaves the fragment `NOT`, which the
negation cue then rejects for an unrelated reason.

Refs: #115
2026-09-01 12:38:04 +00:00
88866b81a3 fix(kyberforge): announce every provenance skip and scope checks 7-8 to source indexes
The clean provenance bill was an artifact. Checks 7 and 8 assume `Research doc:`
names a source index whose H2s are slugs, but 30 of 121 corpus entries point at
topic content documents whose H2s are topics. Those 30 produced every new check-7
INFO — all false positives. Check 8 aimed at the same documents, which carry no
`Status:` line at all, would have emitted a large false-FAIL flood; the only thing
preventing it was an unannounced `rd_status != extracted` skip. So "0 new FAILs"
rested on exactly the fail-open class this branch exists to remove, and naively
fixing the skip would have turned the branch red.

Checks 7/8 now run only when the research doc's basename is `sources.md`, and every
other case emits a visible INFO naming the slug. The dangling-path INFO stays ahead
of the basename gate, because a path that does not resolve is rot whatever it is
named. `parse_status` accepts the bullet form and a trailing note after the
backticked value, so a status it cannot read no longer reads as "nothing to check".

Corpus: 36 INFOs of which 30 were false, to 56 of which none are. FAIL stays 0, and
no Status line flipped to `extracted` under the new parser, so no FAIL was
suppressed by luck.

Also closed, each a silent pass: a nonexistent directory, a directory with no
SKILL.md, and extra arguments now exit 2; a UTF-8 BOM no longer defeats frontmatter
parsing; the bare `except Exception: return False` that turned an unreadable file
into a clean pass is gone, with all reads pinned to UTF-8; check 3 walks nested
`references/` subdirectories; `FILL IN:` at end of line no longer escapes checks 1
and 6; duplicate `## slug` blocks and repeated `Research doc:` lines are announced
rather than half-read.

The agent-audit copy carried all of the above unfixed and is now ported, minus the
four fixes that are genuinely N/A at agent scope — it reads a plugin-root
`sources.md` and has no checks 7/8 and no `references/` tree. Its silent exit 0 for
a file outside plugin scope is preserved deliberately: that is a verdict about a
valid file, not a skip, and `check-scope-walkup-sync.sh` pins it. Every exit-2 gate
therefore decides from the argument alone, before the walk-up runs.

`validation-scripts.md` said flatly that silence from the validator is a pass, not
a skip. That sentence is what made a typo'd path dangerous, and both copies are
corrected here. The matching SKILL.md exit-code guidance lands with the audit
rubric change, which touches the same files.

Tests: skill-audit 45 to 65, agent-audit 24 to 43, every new case proven by mutation.

Refs: #111, #118, #121
2026-09-01 12:37:50 +00:00
9fe734573d fix(gates): close the /name fail-open and stop the path guard inventing targets
Two defects in the routing-target resolver, both latent in the corpus but hot for
anything written next.

The free-standing `/name` sweep sat inside `if boundary:`, so route notation in a
sentence carrying no boundary marker was never extracted at all — not an ERROR, not
a SUGGESTION, not an INFO. That contradicted ADR-0020's amendment and gates.md,
which both promise `/name` blocks unconditionally. The sweep now runs over every
sentence. `-> name` and backticked forms stay gated deliberately: an arrow also
writes a process chain and a code span cites tools, files and skills alike, so
ungating either fires on ordinary prose.

The path guard used `\b`, which still holds after a hyphen, so the engine
backtracked to a shorter hyphen-terminated prefix whenever the lookahead rejected
the full segment. `/api-docs/v2.md` in a boundary clause raised blocking ERRORs for
'api' and 'api-docs' — names no author wrote, with no corroboration escape.
`(?![\w-])` forbids the shortened prefix outright; MARKED_TARGET, which had no
trailing guard at all, gained one.

Zero arguments now exits 2 rather than 0, so a mis-scoped `files:` pattern is no
longer indistinguishable from a clean corpus. Both hook manifests pass filenames
and pre-commit skips a filename-passing hook when nothing matches, so the hook
never sees an empty argv — that contract is now asserted by a test rather than
left in prose.

Deleting the sweep entirely used to leave every suite green. It now kills eight
assertions. The suite also gains its first slash-path and URL fixtures, in both
directions.

Refs: #107, #110, #124
ADR: 0020
2026-09-01 12:37:12 +00:00
155 changed files with 4672 additions and 869 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "holocron", "name": "holocron",
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.", "description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
"version": "0.5.0", "version": "0.4.6",
"owner": { "owner": {
"name": "Defame1297", "name": "Defame1297",
"email": "defame1297@rkdr.net", "email": "defame1297@rkdr.net",
@@ -11,35 +11,35 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"version": "1.7.0", "version": "1.6.1",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/kyberforge" "source": "./plugins/kyberforge"
}, },
{ {
"name": "bin", "name": "bin",
"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.", "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.",
"version": "1.2.0", "version": "1.1.6",
"category": "Utilities", "category": "Utilities",
"source": "./plugins/bin" "source": "./plugins/bin"
}, },
{ {
"name": "git", "name": "git",
"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.", "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.",
"version": "1.4.0", "version": "1.3.6",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/git" "source": "./plugins/git"
}, },
{ {
"name": "gitea", "name": "gitea",
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
"version": "1.4.0", "version": "1.3.7",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/gitea" "source": "./plugins/gitea"
}, },
{ {
"name": "core", "name": "core",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"version": "1.2.0", "version": "1.1.2",
"category": "Productivity", "category": "Productivity",
"source": "./plugins/core" "source": "./plugins/core"
}, },
@@ -59,7 +59,7 @@
{ {
"name": "lint", "name": "lint",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"version": "1.2.0", "version": "1.1.7",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/lint" "source": "./plugins/lint"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "holocron", "name": "holocron",
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.", "description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
"version": "0.5.0", "version": "0.4.6",
"owner": { "owner": {
"name": "Defame1297", "name": "Defame1297",
"email": "defame1297@rkdr.net", "email": "defame1297@rkdr.net",
@@ -11,35 +11,35 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"version": "1.7.0", "version": "1.6.1",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/kyberforge" "source": "./plugins/kyberforge"
}, },
{ {
"name": "bin", "name": "bin",
"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.", "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.",
"version": "1.2.0", "version": "1.1.6",
"category": "Utilities", "category": "Utilities",
"source": "./plugins/bin" "source": "./plugins/bin"
}, },
{ {
"name": "git", "name": "git",
"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.", "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.",
"version": "1.4.0", "version": "1.3.6",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/git" "source": "./plugins/git"
}, },
{ {
"name": "gitea", "name": "gitea",
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
"version": "1.4.0", "version": "1.3.7",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/gitea" "source": "./plugins/gitea"
}, },
{ {
"name": "core", "name": "core",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"version": "1.2.0", "version": "1.1.2",
"category": "Productivity", "category": "Productivity",
"source": "./plugins/core" "source": "./plugins/core"
}, },
@@ -59,7 +59,7 @@
{ {
"name": "lint", "name": "lint",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"version": "1.2.0", "version": "1.1.7",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/lint" "source": "./plugins/lint"
} }

View File

@@ -16,8 +16,12 @@ decisions.
**Preload tax**: **Preload tax**:
The always-on context cost of every installed skill's `name` and `description`, charged from the The always-on context cost of every installed skill's `name` and `description`, charged from the
first token of every session whether the skill is invoked or not. Measurement method and current first token of every session whether the skill is invoked or not. Measurement method: ADR-0020. Its
figure: ADR-0020. **23,427 characters is the pre-retrofit baseline, not a current reading** — measured at the decision
commit, before #99. Across the same 39 skills it is **10,478 characters** (~2,620 tokens) as of
2026-09-01. Both figures move with the corpus, so re-derive rather than quote either: sum
`len(name) + len(description)` over the frontmatter of every `plugins/*/.apm/skills/*/SKILL.md`,
folding block scalars as `scripts/skill-size-check.sh` does.
_Avoid_: context cost, token overhead _Avoid_: context cost, token overhead
**Skill context contract**: **Skill context contract**:
@@ -44,7 +48,7 @@ _Avoid_: router body, thin body
A skill reached only by typing its slash command, declared `disable-model-invocation: true`. The host A skill reached only by typing its slash command, declared `disable-model-invocation: true`. The host
withholds it from the model-visible listing entirely, so it pays no preload tax and its description withholds it from the model-visible listing entirely, so it pays no preload tax and its description
becomes human-facing text. The flag also hard-blocks the Skill tool, so **no other skill can route to becomes human-facing text. The flag also hard-blocks the Skill tool, so **no other skill can route to
a hand-invoked skill** — a `Call \`x\`` step in another skill's body stops working the moment `x` a hand-invoked skill** — a `` Call `x` `` step in another skill's body stops working the moment `x`
takes the flag. Check inbound routes before declaring one. Exemplar: `zoom-out`. takes the flag. Check inbound routes before declaring one. Exemplar: `zoom-out`.
_Avoid_: manual skill, disabled skill _Avoid_: manual skill, disabled skill

18
apm.yml
View File

@@ -1,5 +1,5 @@
name: holocron name: holocron
version: 0.5.0 version: 0.4.6
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows. description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
license: MIT license: MIT
@@ -42,7 +42,7 @@ dependencies:
# after a kyberforge release, check this first. # after a kyberforge release, check this first.
executables: executables:
allow: allow:
kyberforge#1.7.0: kyberforge#1.6.1:
hooks: true hooks: true
bin: true bin: true
@@ -52,7 +52,7 @@ marketplace:
# top-level apm.yml description:/version: above are NOT inherited into the # top-level apm.yml description:/version: above are NOT inherited into the
# compiled output despite being used elsewhere (e.g. by `apm audit`). # compiled output despite being used elsewhere (e.g. by `apm audit`).
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows. description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
version: 0.5.0 version: 0.4.6
owner: owner:
name: Defame1297 name: Defame1297
email: defame1297@rkdr.net email: defame1297@rkdr.net
@@ -79,31 +79,31 @@ marketplace:
- name: kyberforge - name: kyberforge
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace. description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
source: ./plugins/kyberforge source: ./plugins/kyberforge
version: 1.7.0 version: 1.6.1
category: Developer Tools category: Developer Tools
- name: bin - name: bin
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. 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.
source: ./plugins/bin source: ./plugins/bin
version: 1.2.0 version: 1.1.6
category: Utilities category: Utilities
- name: git - name: git
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. 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.
source: ./plugins/git source: ./plugins/git
version: 1.4.0 version: 1.3.6
category: Version Control category: Version Control
- name: gitea - name: gitea
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone. description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
source: ./plugins/gitea source: ./plugins/gitea
version: 1.4.0 version: 1.3.7
category: Version Control category: Version Control
- name: core - name: core
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it. description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
source: ./plugins/core source: ./plugins/core
version: 1.2.0 version: 1.1.2
category: Productivity category: Productivity
- name: mattpocock-skills - name: mattpocock-skills
@@ -115,5 +115,5 @@ marketplace:
- name: lint - name: lint
description: Skills and agents for configuring and running linters. description: Skills and agents for configuring and running linters.
source: ./plugins/lint source: ./plugins/lint
version: 1.2.0 version: 1.1.7
category: Developer Tools category: Developer Tools

View File

@@ -127,8 +127,15 @@ clause**, and a **boundary clause**. Capability enumeration, output-format detai
gate shipping hot with no baseline cannot give two answers. Under the walk-up those four resolve gate shipping hot with no baseline cannot give two answers. Under the walk-up those four resolve
because sibling plugins are in the universe — no plugin here declares a cross-plugin apm because sibling plugins are in the universe — no plugin here declares a cross-plugin apm
dependency, and none needs to. Verified: a tree holding only `plugins/` and the root `apm.yml`, dependency, and none needs to. Verified: a tree holding only `plugins/` and the root `apm.yml`,
with no `.claude/` or `.agents/` anywhere, now produces findings identical to the working tree — with no `.claude/` or `.agents/` anywhere, produced findings identical to the working tree. The
26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs. figures that reproduction recorded — 26 description FAILs, 9 body FAILs, 2 dangling targets, 0
missing references, 58 SUGGESTIONs — are the **pre-retrofit** corpus as it stood when the
experiment ran, kept here as the evidence for the install-independence claim, not as a current
reading. *Amended 2026-09-01: the #99 retrofit took the first three to zero. Measured at that
date over the same install-free tree: 0 description FAILs, 0 body FAILs, 0 dangling targets, 0
missing references, 29 SUGGESTIONs.* What the experiment establishes is that the two trees agree,
not what either measured; re-derive rather than quote —
`bash scripts/skill-size-check.sh plugins/*/.apm/skills/*/SKILL.md`.
- **The universe is the apm marketplace, and nothing else.** A routing target resolves to a skill or - **The universe is the apm marketplace, and nothing else.** A routing target resolves to a skill or
an agent, or it does not resolve. Host built-ins are deliberately outside it: `/compact`, `/clear` an agent, or it does not resolve. Host built-ins are deliberately outside it: `/compact`, `/clear`
and `/init` are Claude Code slash commands with no counterpart in Copilot CLI or Codex, so a and `/init` are Claude Code slash commands with no counterpart in Copilot CLI or Codex, so a
@@ -185,8 +192,11 @@ becomes the system prompt of a fresh context. The rationale for the 900-word FAI
That exemption is expressed in `agent-audit/scripts/validate.sh`, which has no body constant, and in That exemption is expressed in `agent-audit/scripts/validate.sh`, which has no body constant, and in
the `files:` pattern of the `skill-size-check` pre-commit hook, which is `SKILL.md`-only. It is *not* the `files:` pattern of the `skill-size-check` pre-commit hook, which is `SKILL.md`-only. It is *not*
expressed in `scripts/skill-size-check.sh` itself, which measures whatever path it is handed — expressed in `scripts/skill-size-check.sh` itself, which measures whatever path it is handed —
running it directly over `plugins/*/.apm/agents/*.agent.md` today reports 900-word body FAILs on running it directly over `plugins/*/.apm/agents/*.agent.md` exits 1 with 900-word body FAILs on
`git-orchestrate` (933), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080). Agents escape by `git-orchestrate` and `gitea-orchestrate`. *Amended 2026-09-01: this sentence named a third agent,
`apm-orchestrate`, at 1,080 words. It is 876 today — a SUGGESTION, not a FAIL. Counts are
deliberately no longer pinned here: agent bodies are edited like any other file and a figure in this
paragraph goes stale the moment one is trimmed. Run the command.* Agents escape by
file pattern, not by the script knowing the difference. Anyone widening that pattern to cover agents file pattern, not by the script knowing the difference. Anyone widening that pattern to cover agents
would silently enforce a gate this ADR declines to set. would silently enforce a gate this ADR declines to set.
@@ -245,7 +255,7 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
| description characters (250 SUGGESTION † / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` | | description characters (250 SUGGESTION † / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` | | body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
| description present and non-empty (ERROR) | skills, agents | deterministic | same | | description present and non-empty (ERROR) | skills, agents | deterministic | same |
| boundary target resolves to a real skill or agent (ERROR when written in route notation — `/name`, or any arrow form; or when a *terminal* bare name's own sentence names another target that resolves; SUGGESTION otherwise) | skills, agents | deterministic | same | | boundary target resolves to a real skill or agent — **three** verdicts, not two (ERROR when written in route notation — `/name`, or any arrow form; or when a *terminal* bare name's own sentence names another target that resolves. SUGGESTION otherwise. INFO, "DID NOT RUN", exit 0, when no skill universe could be determined for the path at all — no authoring root above it, no apm package root, no declared apm dependencies, no deployed `.claude/` or `.agents/` tree: the targets are named and left unchecked) | skills, agents | deterministic | same |
| boundary clause absent — `absent` (SUGGESTION) † | skills, agents | deterministic | same | | boundary clause absent — `absent` (SUGGESTION) † | skills, agents | deterministic | same |
| an arrow clause is present but no target can be read out of it — `unparsed` (SUGGESTION) † | skills, agents | deterministic | same | | an arrow clause is present but no target can be read out of it — `unparsed` (SUGGESTION) † | skills, agents | deterministic | same |
| one arrow clause naming two or more targets, of which only the first is resolved (SUGGESTION, issue #107) † | skills, agents | deterministic | same | | one arrow clause naming two or more targets, of which only the first is resolved (SUGGESTION, issue #107) † | skills, agents | deterministic | same |
@@ -378,10 +388,17 @@ carries is the ordinary one for hot gates: a gate expensive enough to be inconve
with `SKIP=` and loses its authority. with `SKIP=` and loses its authority.
**A second hot gate ships alongside it, and it is easy to miss.** `Kyberforge.CompositionNote` is **A second hot gate ships alongside it, and it is easy to miss.** `Kyberforge.CompositionNote` is
`level: error` like every other rule in that style, so `pre-commit run --all-files` is red on 10 `level: error` like every other rule in that style, so at decision time `pre-commit run --all-files`
alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and `gitea-workflow` was red on 10 alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and
independently of anything `skill-size-check` reports. Someone scoping the #99 retrofit off the size `gitea-workflow` independently of anything `skill-size-check` reports. Someone scoping the #99
findings alone will fix those and still be blocked. The two gates want fixing together. retrofit off the size findings alone would have fixed those and still been blocked. The two gates
wanted fixing together, and were. *Amended 2026-09-01: that figure is historical. The Vale prefilter
over the same 39 files now reports 0 errors, 0 warnings and 0 suggestions, so
`Kyberforge.CompositionNote` fires nowhere in the corpus today. The rule is still hot and still
independent of `skill-size-check`, so a new description can reintroduce it; `skill-size-check` does
not cover the Vale half, and no `references/` file is linted by anything (`docs/spec/gates.md` has
both causes, issue #117 tracks them). Re-derive rather than quote —*
`bash plugins/kyberforge/.apm/skills/skill-audit/scripts/vale-wrap.sh plugins/*/.apm/skills/*/SKILL.md`.
**A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the **A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the
preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to

View File

@@ -4,10 +4,11 @@ The grilling interview, run against the project's domain model — and writing d
## What it does ## What it does
Runs the same relentless one-question-at-a-time interview as `grill-me`, with the project's own documentation as an active participant. During codebase exploration it also locates the domain documentation — a root `CONTEXT.md` and `docs/adr/`, or a `CONTEXT-MAP.md` pointing at per-context glossaries and ADR directories in a multi-context repo — and then uses it four ways: Runs the same relentless one-question-at-a-time interview as `grill-me`, with the project's own documentation as an active participant. During codebase exploration it also locates the domain documentation — a root `CONTEXT.md` and `docs/adr/`, or a `CONTEXT-MAP.md` pointing at per-context glossaries and ADR directories in a multi-context repo — and then uses it five ways:
- **Challenges terms against the glossary.** When the user's usage conflicts with what `CONTEXT.md` already defines, that is raised immediately rather than absorbed. - **Challenges terms against the glossary.** When the user's usage conflicts with what `CONTEXT.md` already defines, that is raised immediately rather than absorbed.
- **Sharpens fuzzy language** by proposing a precise canonical term ("you're saying 'account' — do you mean the Customer or the User?"). - **Sharpens fuzzy language** by proposing a precise canonical term ("you're saying 'account' — do you mean the Customer or the User?").
- **Stress-tests domain relationships with concrete scenarios**, inventing edge cases that force the user to be precise about where one concept ends and the next begins.
- **Cross-references claims against the code**, and surfaces contradictions between what the user says happens and what the code does. - **Cross-references claims against the code**, and surfaces contradictions between what the user says happens and what the code does.
- **Updates `CONTEXT.md` inline**, the moment a term is resolved, rather than batching changes to the end of the session where they get lost. - **Updates `CONTEXT.md` inline**, the moment a term is resolved, rather than batching changes to the end of the session where they get lost.
@@ -31,6 +32,6 @@ Describe the plan or design. Expect questions one at a time, each with a recomme
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the four during-session behaviours, and the three-part ADR test | | `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the five during-session behaviours, and the three-part ADR test |
| `CONTEXT-FORMAT.md` | Skill-root document, cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry | | `CONTEXT-FORMAT.md` | Skill-root document, cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry |
| `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template | | `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |

View File

@@ -1,10 +1,11 @@
--- ---
name: improve-codebase-architecture name: improve-codebase-architecture
description: > description: >
Use when the user wants a codebase's architecture improved — deepening Use when the user wants to improve architecture, find refactoring
opportunities that turn shallow modules into deep ones, informed by opportunities, consolidate tightly-coupled modules, or make a codebase more
`CONTEXT.md` and `docs/adr/`. Not debugging a failure -> `diagnose`. Not testable and AI-navigable — deepening opportunities that turn shallow modules
test-first feature work -> `tdd`. into deep ones, informed by `CONTEXT.md` and `docs/adr/`. Not debugging a
failure -> `diagnose`.
--- ---
# Improve Codebase Architecture # Improve Codebase Architecture
@@ -13,7 +14,7 @@ Surface architectural friction and propose **deepening opportunities** — refac
## Glossary ## Glossary
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary."
- **Module** — anything with an interface and an implementation (function, class, package, slice). - **Module** — anything with an interface and an implementation (function, class, package, slice).
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. - **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
@@ -24,12 +25,14 @@ Use these terms exactly in every suggestion. Consistent language is the point
- **Leverage** — what callers get from depth. - **Leverage** — what callers get from depth.
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. - **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): Key principles:
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. - **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.** - **The interface is the test surface.**
- **One adapter = hypothetical seam. Two adapters = real seam.** - **One adapter = hypothetical seam. Two adapters = real seam.**
If a term or principle above is ambiguous in the case in front of you, or you need the definitions and the principles the two lists leave out, read `LANGUAGE.md`.
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
## Process ## Process
@@ -57,7 +60,7 @@ Present a numbered list of deepening opportunities. For each candidate:
- **Solution** — plain English description of what would change - **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve - **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." **Use CONTEXT.md vocabulary for the domain, and the architecture glossary above for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. **ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
@@ -72,4 +75,4 @@ Side effects happen inline as decisions crystallize:
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `grill-with-docs`, in the format `grill-with-docs`'s `CONTEXT-FORMAT.md` defines. Create the file lazily if it doesn't exist. - **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `grill-with-docs`, in the format `grill-with-docs`'s `CONTEXT-FORMAT.md` defines. Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. - **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See `grill-with-docs`'s `ADR-FORMAT.md`. - **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See `grill-with-docs`'s `ADR-FORMAT.md`.
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). - **Want to explore alternative interfaces for the deepened module?** Read `INTERFACE-DESIGN.md`.

View File

@@ -72,5 +72,8 @@ When the prototype has done its job, the answer to the question is the only thin
## Anti-patterns ## Anti-patterns
- **Don't add tests.** A prototype that needs tests is no longer a prototype.
- **Don't wire it to the real database.** Use an in-memory store unless the question is specifically about persistence.
- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question.
- **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module. - **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module.
- **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping. - **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping.

View File

@@ -16,7 +16,7 @@ description: >
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior. **Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. If you need worked examples of the difference — a behaviour-level test beside the implementation-coupled version of the same check — read `tests.md`. If a test needs a collaborator faked, read `mocking.md` before reaching for a mock.
## Anti-Pattern: Horizontal Slices ## Anti-Pattern: Horizontal Slices

View File

@@ -12,7 +12,7 @@ A run does one of three things depending on what the maintainer asks for:
- **Triage a specific issue** — gather context (including prior triage notes, so resolved questions are not re-asked, and `.out-of-scope/` records that resemble the issue), recommend a category and state with reasoning, attempt reproduction for bugs *before* any grilling, run a `grill-with-docs` session if the issue needs fleshing out, then apply the outcome. - **Triage a specific issue** — gather context (including prior triage notes, so resolved questions are not re-asked, and `.out-of-scope/` records that resemble the issue), recommend a category and state with reasoning, attempt reproduction for bugs *before* any grilling, run a `grill-with-docs` session if the issue needs fleshing out, then apply the outcome.
- **Quick state override** — "move #42 to ready-for-agent" is trusted and applied directly, skipping grilling, after confirming the exact changes. - **Quick state override** — "move #42 to ready-for-agent" is trusted and applied directly, skipping grilling, after confirming the exact changes.
Two hard rules: every comment or issue the skill posts during triage must open with the AI-generated disclaimer, and the canonical role names above are *not* necessarily the label strings in the tracker — the mapping has to be supplied to the run. Two hard rules: every comment or issue the skill posts during triage must open with the AI-generated disclaimer, and the canonical role names above are *not* necessarily the label strings in the tracker — each is resolved against the tracker's live label set before it is applied, and a name with no counterpart there is reported to the maintainer as a gap rather than guessed at.
## Composition ## Composition

View File

@@ -2,8 +2,10 @@
name: write-docs name: write-docs
description: > description: >
Use when the user wants technical documentation produced or updated from code Use when the user wants technical documentation produced or updated from code
or spec, every claim traced to a source. Not a PRD, ADR, or decision doc -> or spec, every claim traced to a source — "write docs for X", "document this
`grill-with-docs`. Not an external tool researched from its docs -> `research`. module", "create docs for this feature", "write a README for this". Not an ADR
or other decision record -> `grill-with-docs`. Not an external tool researched
from its docs -> `research`.
version: "1.0" version: "1.0"
updated: 2026-05-17 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 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
@@ -38,7 +40,8 @@ You are a technical writer that produces documentation by reading code and spec
- User says "write docs for X", "document this", "create docs for this feature", "write a README for this" - User says "write docs for X", "document this", "create docs for this feature", "write a README for this"
**Do not use when:** **Do not use when:**
- User wants a PRD, decision doc, or architecture proposal → `grill-me` or `grill-with-docs` - User wants an ADR, decision doc, or architecture proposal → `grill-with-docs`, which writes ADRs
- User wants a PRD → no skill in this set produces one; say so rather than redirecting
- User wants to document a skill file (skill files are self-describing) - User wants to document a skill file (skill files are self-describing)
- User wants marketing or blog copy - User wants marketing or blog copy
- Documentation requires tacit organisational knowledge that cannot be read from code or spec - Documentation requires tacit organisational knowledge that cannot be read from code or spec
@@ -91,7 +94,7 @@ You are a technical writer that produces documentation by reading code and spec
- Stage skipped without a logged reason → flag and require the one-sentence log before continuing - Stage skipped without a logged reason → flag and require the one-sentence log before continuing
- Code behaviour is undocumentable (internal implementation detail, no public spec) → note as out-of-scope in the doc; do not invent an explanation - Code behaviour is undocumentable (internal implementation detail, no public spec) → note as out-of-scope in the doc; do not invent an explanation
- Reader Testing sub-agent fails on multiple questions → surface the failures, return to step 4; do not mark complete - Reader Testing sub-agent fails on multiple questions → surface the failures, return to step 4; do not mark complete
- Requested output is a PRD, decision doc, or architecture proposal → redirect to `grill-me` or `grill-with-docs` - Requested output is an ADR, decision doc, or architecture proposal → redirect to `grill-with-docs`; for a PRD, say no skill here produces one instead of redirecting
## Self-check ## Self-check

View File

@@ -1,6 +1,6 @@
{ {
"name": "bin", "name": "bin",
"version": "1.2.0", "version": "1.1.6",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "bin", "name": "bin",
"version": "1.2.0", "version": "1.1.6",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,5 +1,5 @@
name: bin name: bin
version: 1.2.0 version: 1.1.6
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. 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: author:
name: Defame1297 name: Defame1297

View File

@@ -4,10 +4,11 @@ The grilling interview, run against the project's domain model — and writing d
## What it does ## What it does
Runs the same relentless one-question-at-a-time interview as `grill-me`, with the project's own documentation as an active participant. During codebase exploration it also locates the domain documentation — a root `CONTEXT.md` and `docs/adr/`, or a `CONTEXT-MAP.md` pointing at per-context glossaries and ADR directories in a multi-context repo — and then uses it four ways: Runs the same relentless one-question-at-a-time interview as `grill-me`, with the project's own documentation as an active participant. During codebase exploration it also locates the domain documentation — a root `CONTEXT.md` and `docs/adr/`, or a `CONTEXT-MAP.md` pointing at per-context glossaries and ADR directories in a multi-context repo — and then uses it five ways:
- **Challenges terms against the glossary.** When the user's usage conflicts with what `CONTEXT.md` already defines, that is raised immediately rather than absorbed. - **Challenges terms against the glossary.** When the user's usage conflicts with what `CONTEXT.md` already defines, that is raised immediately rather than absorbed.
- **Sharpens fuzzy language** by proposing a precise canonical term ("you're saying 'account' — do you mean the Customer or the User?"). - **Sharpens fuzzy language** by proposing a precise canonical term ("you're saying 'account' — do you mean the Customer or the User?").
- **Stress-tests domain relationships with concrete scenarios**, inventing edge cases that force the user to be precise about where one concept ends and the next begins.
- **Cross-references claims against the code**, and surfaces contradictions between what the user says happens and what the code does. - **Cross-references claims against the code**, and surfaces contradictions between what the user says happens and what the code does.
- **Updates `CONTEXT.md` inline**, the moment a term is resolved, rather than batching changes to the end of the session where they get lost. - **Updates `CONTEXT.md` inline**, the moment a term is resolved, rather than batching changes to the end of the session where they get lost.
@@ -31,6 +32,6 @@ Describe the plan or design. Expect questions one at a time, each with a recomme
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the four during-session behaviours, and the three-part ADR test | | `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the five during-session behaviours, and the three-part ADR test |
| `CONTEXT-FORMAT.md` | Skill-root document, cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry | | `CONTEXT-FORMAT.md` | Skill-root document, cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry |
| `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template | | `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |

View File

@@ -1,10 +1,11 @@
--- ---
name: improve-codebase-architecture name: improve-codebase-architecture
description: > description: >
Use when the user wants a codebase's architecture improved — deepening Use when the user wants to improve architecture, find refactoring
opportunities that turn shallow modules into deep ones, informed by opportunities, consolidate tightly-coupled modules, or make a codebase more
`CONTEXT.md` and `docs/adr/`. Not debugging a failure -> `diagnose`. Not testable and AI-navigable — deepening opportunities that turn shallow modules
test-first feature work -> `tdd`. into deep ones, informed by `CONTEXT.md` and `docs/adr/`. Not debugging a
failure -> `diagnose`.
--- ---
# Improve Codebase Architecture # Improve Codebase Architecture
@@ -13,7 +14,7 @@ Surface architectural friction and propose **deepening opportunities** — refac
## Glossary ## Glossary
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary."
- **Module** — anything with an interface and an implementation (function, class, package, slice). - **Module** — anything with an interface and an implementation (function, class, package, slice).
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. - **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
@@ -24,12 +25,14 @@ Use these terms exactly in every suggestion. Consistent language is the point
- **Leverage** — what callers get from depth. - **Leverage** — what callers get from depth.
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. - **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): Key principles:
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. - **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.** - **The interface is the test surface.**
- **One adapter = hypothetical seam. Two adapters = real seam.** - **One adapter = hypothetical seam. Two adapters = real seam.**
If a term or principle above is ambiguous in the case in front of you, or you need the definitions and the principles the two lists leave out, read `LANGUAGE.md`.
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
## Process ## Process
@@ -57,7 +60,7 @@ Present a numbered list of deepening opportunities. For each candidate:
- **Solution** — plain English description of what would change - **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve - **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." **Use CONTEXT.md vocabulary for the domain, and the architecture glossary above for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. **ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
@@ -72,4 +75,4 @@ Side effects happen inline as decisions crystallize:
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `grill-with-docs`, in the format `grill-with-docs`'s `CONTEXT-FORMAT.md` defines. Create the file lazily if it doesn't exist. - **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `grill-with-docs`, in the format `grill-with-docs`'s `CONTEXT-FORMAT.md` defines. Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. - **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See `grill-with-docs`'s `ADR-FORMAT.md`. - **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See `grill-with-docs`'s `ADR-FORMAT.md`.
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). - **Want to explore alternative interfaces for the deepened module?** Read `INTERFACE-DESIGN.md`.

View File

@@ -72,5 +72,8 @@ When the prototype has done its job, the answer to the question is the only thin
## Anti-patterns ## Anti-patterns
- **Don't add tests.** A prototype that needs tests is no longer a prototype.
- **Don't wire it to the real database.** Use an in-memory store unless the question is specifically about persistence.
- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question.
- **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module. - **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module.
- **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping. - **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping.

View File

@@ -16,7 +16,7 @@ description: >
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior. **Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. If you need worked examples of the difference — a behaviour-level test beside the implementation-coupled version of the same check — read `tests.md`. If a test needs a collaborator faked, read `mocking.md` before reaching for a mock.
## Anti-Pattern: Horizontal Slices ## Anti-Pattern: Horizontal Slices

View File

@@ -12,7 +12,7 @@ A run does one of three things depending on what the maintainer asks for:
- **Triage a specific issue** — gather context (including prior triage notes, so resolved questions are not re-asked, and `.out-of-scope/` records that resemble the issue), recommend a category and state with reasoning, attempt reproduction for bugs *before* any grilling, run a `grill-with-docs` session if the issue needs fleshing out, then apply the outcome. - **Triage a specific issue** — gather context (including prior triage notes, so resolved questions are not re-asked, and `.out-of-scope/` records that resemble the issue), recommend a category and state with reasoning, attempt reproduction for bugs *before* any grilling, run a `grill-with-docs` session if the issue needs fleshing out, then apply the outcome.
- **Quick state override** — "move #42 to ready-for-agent" is trusted and applied directly, skipping grilling, after confirming the exact changes. - **Quick state override** — "move #42 to ready-for-agent" is trusted and applied directly, skipping grilling, after confirming the exact changes.
Two hard rules: every comment or issue the skill posts during triage must open with the AI-generated disclaimer, and the canonical role names above are *not* necessarily the label strings in the tracker — the mapping has to be supplied to the run. Two hard rules: every comment or issue the skill posts during triage must open with the AI-generated disclaimer, and the canonical role names above are *not* necessarily the label strings in the tracker — each is resolved against the tracker's live label set before it is applied, and a name with no counterpart there is reported to the maintainer as a gap rather than guessed at.
## Composition ## Composition

View File

@@ -2,8 +2,10 @@
name: write-docs name: write-docs
description: > description: >
Use when the user wants technical documentation produced or updated from code Use when the user wants technical documentation produced or updated from code
or spec, every claim traced to a source. Not a PRD, ADR, or decision doc -> or spec, every claim traced to a source — "write docs for X", "document this
`grill-with-docs`. Not an external tool researched from its docs -> `research`. module", "create docs for this feature", "write a README for this". Not an ADR
or other decision record -> `grill-with-docs`. Not an external tool researched
from its docs -> `research`.
version: "1.0" version: "1.0"
updated: 2026-05-17 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 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
@@ -38,7 +40,8 @@ You are a technical writer that produces documentation by reading code and spec
- User says "write docs for X", "document this", "create docs for this feature", "write a README for this" - User says "write docs for X", "document this", "create docs for this feature", "write a README for this"
**Do not use when:** **Do not use when:**
- User wants a PRD, decision doc, or architecture proposal → `grill-me` or `grill-with-docs` - User wants an ADR, decision doc, or architecture proposal → `grill-with-docs`, which writes ADRs
- User wants a PRD → no skill in this set produces one; say so rather than redirecting
- User wants to document a skill file (skill files are self-describing) - User wants to document a skill file (skill files are self-describing)
- User wants marketing or blog copy - User wants marketing or blog copy
- Documentation requires tacit organisational knowledge that cannot be read from code or spec - Documentation requires tacit organisational knowledge that cannot be read from code or spec
@@ -91,7 +94,7 @@ You are a technical writer that produces documentation by reading code and spec
- Stage skipped without a logged reason → flag and require the one-sentence log before continuing - Stage skipped without a logged reason → flag and require the one-sentence log before continuing
- Code behaviour is undocumentable (internal implementation detail, no public spec) → note as out-of-scope in the doc; do not invent an explanation - Code behaviour is undocumentable (internal implementation detail, no public spec) → note as out-of-scope in the doc; do not invent an explanation
- Reader Testing sub-agent fails on multiple questions → surface the failures, return to step 4; do not mark complete - Reader Testing sub-agent fails on multiple questions → surface the failures, return to step 4; do not mark complete
- Requested output is a PRD, decision doc, or architecture proposal → redirect to `grill-me` or `grill-with-docs` - Requested output is an ADR, decision doc, or architecture proposal → redirect to `grill-with-docs`; for a PRD, say no skill here produces one instead of redirecting
## Self-check ## Self-check

View File

@@ -30,8 +30,8 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file: Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it. - **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it — nor one inside a code fence, an indented block, or an HTML comment, nor one whose path does not resolve to a real, non-empty file on disk.
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket. - **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short sentence pointing at `AGENTS.md` ("See AGENTS.md at the repo root for ..."), keep the provider-specific bucket. A bare or negated mention is not a pointer and will not be credited.
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like. The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
@@ -45,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file> bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
``` ```
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exit `2` is not a `FAIL`: it means the invocation or the input is wrong — a bad or missing argument, or a file that is not UTF-8 — so fix that, not the adapter. Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exits `2` and `3` are not `FAIL`s and nothing was graded under either, so neither is a reason to touch the adapter: `2` means the invocation or the input is wrong (a bad, missing, or extra argument, an unknown option, or a file that is not UTF-8), and `3` means a named file exists but could not be read.
## Step 4 — Report ## Step 4 — Report

View File

@@ -4,6 +4,25 @@ Deterministic self-check this skill shells out to instead of relying on LLM judg
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `validate-adapter.sh` | Checks a rewritten provider file (CLAUDE.md, etc.) has a reference to AGENTS.md, doesn't duplicate its content, and stays under a thin-file line threshold | | `validate-adapter.sh` | Checks a rewritten provider file (CLAUDE.md, etc.) has a working reference to AGENTS.md, doesn't duplicate its content, and stays under a thin-file line threshold |
Takes `<adapter-file> <agents-md-file>`, with optional `--no-import-syntax` and `--max-lines N` flags. Prints `FAIL` findings to stdout and exits non-zero on any failure. Takes exactly `<adapter-file> <agents-md-file>`, with optional `--no-import-syntax` and `--max-lines N` flags (also accepted as `--max-lines=N`). A third positional argument or an unknown option is an error, not something quietly ignored.
## What counts as a reference to AGENTS.md
Both modes require the named path to be a real path segment ending in `AGENTS.md` — `AGENTS.md` or `…/AGENTS.md`, not `NOTAGENTS.md` — that resolves on disk, relative to the adapter file, to a non-empty file. An adapter deferring to a path that is not there defers to nothing, so the check has to touch the disk rather than pattern-match the line.
A reference only counts where something would actually resolve it. A line inside a fenced code block, an indented code block, or an HTML comment is not credited in either mode: Claude Code resolves an import in none of those, so a fenced `@AGENTS.md` is the silent-drop failure this gate exists to catch, not a pass.
Default mode wants a real import: `@AGENTS.md` alone on its own line, indented no more than three spaces. `--no-import-syntax` wants a prose pointer that reads as one — the sentence naming `AGENTS.md` must carry a deference cue (see, read, refer to, documented in, conventions, …) and must not be negated. `Do NOT read AGENTS.md; it is obsolete.` and `We deleted AGENTS.md last year.` name the file while pointing the reader away from it, and neither is a pointer.
## Exit codes
The distinction matters because the skill's closeout tells the agent to fix any non-zero exit by editing the provider file. That is right for exactly one of these.
| Code | Meaning | What to do |
|------|---------|------------|
| `0` | Passes every check | Nothing |
| `1` | One or more `FAIL` findings printed to stdout — empty adapter, no working reference to AGENTS.md, excessive duplication, or not thin | Edit the provider file |
| `2` | Usage or input error: a bad, missing, or extra argument, an unknown option, a path that is not a file, or a file that is not UTF-8. Nothing was graded, so there is no `FAIL` line | Fix the invocation or the file's encoding — do not edit the adapter |
| `3` | A named input file exists but could not be read (permissions, I/O error). Nothing was graded and the adapter's contents are unknown | Fix the file's readability — do not edit the adapter |

View File

@@ -14,6 +14,10 @@ Arguments:
adapter-file Path to the provider-specific file to check. adapter-file Path to the provider-specific file to check.
agents-md-file Path to the AGENTS.md file it should defer to. agents-md-file Path to the AGENTS.md file it should defer to.
Exactly two positional arguments are accepted. Extra ones are rejected
rather than ignored: a third path silently graded nothing but the first
two, so a typo'd invocation passed against the wrong file.
Options: Options:
--no-import-syntax The target provider has no native cross-file import --no-import-syntax The target provider has no native cross-file import
mechanism. Require a plain-text pointer line naming mechanism. Require a plain-text pointer line naming
@@ -26,26 +30,69 @@ Options:
it's considered no longer "thin". Must be a it's considered no longer "thin". Must be a
non-negative integer. Default: 60. non-negative integer. Default: 60.
--help, -h Show this help and exit 0. --help, -h Show this help and exit 0.
-- End of options; every later argument is positional.
Both flags also accept the --flag=value form (--max-lines=40). An unknown
option is reported as an unknown option, not as a missing file.
What counts as a reference:
In both modes the named path must be a real path segment ending in
AGENTS.md ("AGENTS.md" or ".../AGENTS.md" — not NOTAGENTS.md), and it must
resolve on disk, relative to the adapter file, to a non-empty file. An
adapter deferring to a path that is not there defers to nothing.
A mention inside a fenced code block, an indented code block, or an HTML
comment is not credited in either mode. Nothing resolves those, so an
adapter whose only "import" is fenced silently defers to nothing.
With --no-import-syntax the pointer must read as a pointer: the sentence
naming AGENTS.md has to carry a deference cue (see, read, refer to,
documented in, conventions, ...) and must not be a negation ("do not read
AGENTS.md", "we deleted AGENTS.md"). A bare mention is not a pointer.
Exit codes: Exit codes:
0 Adapter file passes all checks 0 Adapter file passes all checks
1 One or more checks failed (empty file, no reference to AGENTS.md, 1 One or more checks failed (empty file, no reference to AGENTS.md,
excessive duplication, or file too long) excessive duplication, or file too long)
2 Usage or input error — a bad or missing argument, a path that is not a 2 Usage or input error — a bad, missing, or extra argument, an unknown
file, or a file that is not UTF-8. Nothing was graded, so there is no option, a path that is not a file, or a file that is not UTF-8. Nothing
FAIL line and no adapter edit to make: fix the invocation or the file's was graded, so there is no FAIL line and no adapter edit to make: fix
encoding and re-run. Kept distinct from 1 because the skill's own the invocation or the file's encoding and re-run. Kept distinct from 1
closeout tells the agent to fix every non-zero exit by editing the because the skill's own closeout tells the agent to fix every non-zero
provider file, which for a mistyped flag edits the wrong file forever. exit by editing the provider file, which for a mistyped flag edits the
wrong file forever.
3 A named input file exists but could not be read (permissions, a
directory swapped in mid-run, I/O error). Also not a FAIL: nothing was
graded and the adapter's contents are unknown, so editing it is
guesswork. Fix the file's readability and re-run.
EOF EOF
} }
NO_IMPORT_SYNTAX=0 NO_IMPORT_SYNTAX=0
MAX_LINES=60 MAX_LINES=60
ARGS=() ARGS=()
END_OF_OPTS=0
require_int() {
# $1 = the value to validate
if [[ ! "$1" =~ ^[0-9]+$ ]]; then
echo "Error: --max-lines expects a non-negative integer, got '$1'." >&2
exit 2
fi
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
if [[ $END_OF_OPTS -eq 1 ]]; then
ARGS+=("$1")
shift
continue
fi
case "$1" in case "$1" in
--)
END_OF_OPTS=1
shift
;;
--help|-h) --help|-h)
usage usage
exit 0 exit 0
@@ -54,17 +101,37 @@ while [[ $# -gt 0 ]]; do
NO_IMPORT_SYNTAX=1 NO_IMPORT_SYNTAX=1
shift shift
;; ;;
--no-import-syntax=*)
echo "Error: --no-import-syntax is a flag and takes no value (got '$1')." >&2
exit 2
;;
--max-lines) --max-lines)
if [[ $# -lt 2 ]]; then if [[ $# -lt 2 ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2 echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 2 exit 2
fi fi
MAX_LINES="$2" MAX_LINES="$2"
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then require_int "$MAX_LINES"
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2 shift 2
;;
--max-lines=*)
MAX_LINES="${1#--max-lines=}"
if [[ -z "$MAX_LINES" ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 2 exit 2
fi fi
shift 2 require_int "$MAX_LINES"
shift
;;
-*)
# Reported as an unknown option rather than falling through to the
# positional bucket, where it used to surface as "'--bogus' is not a
# file" — the right exit code attached to a diagnostic that sends the
# reader looking for a path they never typed.
echo "Error: unknown option '$1'." >&2
echo "" >&2
usage >&2
exit 2
;; ;;
*) *)
ARGS+=("$1") ARGS+=("$1")
@@ -80,6 +147,13 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
exit 2 exit 2
fi fi
if [[ ${#ARGS[@]} -gt 2 ]]; then
echo "Error: expected exactly 2 positional arguments (adapter-file and agents-md-file), got ${#ARGS[@]}: ${ARGS[*]}." >&2
echo "" >&2
usage >&2
exit 2
fi
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON' python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
import sys import sys
import os import os
@@ -89,45 +163,80 @@ adapter_path, agents_md_path, no_import_syntax, max_lines = sys.argv[1:5]
no_import_syntax = no_import_syntax == "1" no_import_syntax = no_import_syntax == "1"
max_lines = int(max_lines) max_lines = int(max_lines)
EXIT_FAIL = 1
EXIT_USAGE = 2
EXIT_UNREADABLE = 3
if not os.path.isfile(adapter_path): if not os.path.isfile(adapter_path):
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr) print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
if not os.path.isfile(agents_md_path): if not os.path.isfile(agents_md_path):
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr) print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
def read_text(path): def read_text(path):
r"""File contents as text, UTF-8, BOM stripped. r"""File contents as text, UTF-8, every BOM stripped.
The BOM strip is not cosmetic. IMPORT_RE anchors on `^\s*@`, and a BOM is The BOM strip is not cosmetic. IMPORT_RE anchors on `^ {0,3}@`, and a BOM
not `\s` in Python, so a CLAUDE.md saved by an editor that emits one had is not whitespace in Python, so a CLAUDE.md saved by an editor that emits
its first line — the `@AGENTS.md` import, which is the whole adapter — one had its first line — the `@AGENTS.md` import, which is the whole
silently treated as prose. The check then said "no reference to AGENTS.md" adapter — silently treated as prose. The check then said "no reference to
told the author to add the line already sitting in front of them. Same AGENTS.md" and told the author to add the line already sitting in front of
class of silent BOM miss recorded in scripts/skill-size-check.sh; strip it them. Same class of silent BOM miss recorded in scripts/skill-size-check.sh;
at the reader so no later check has to know about it. strip it at the reader so no later check has to know about it.
Every U+FEFF goes, not just one at offset 0. Stripping exactly the first
one left the mirror-image false FAIL for a doubled BOM (two concatenated
files, or a tool that re-adds one) and for a BOM mid-file at the head of
the import line. U+FEFF has no meaning as a character in a markdown
instruction file, so removing all of them cannot lose signal.
Decoding is strict, not errors="replace". Replacement mangles the file and Decoding is strict, not errors="replace". Replacement mangles the file and
the checks then grade the mangling: a UTF-16 adapter whose first line is the checks then grade the mangling: a UTF-16 adapter whose first line is
`@AGENTS.md` decoded to interleaved NULs and failed as "no reference", `@AGENTS.md` decoded to interleaved NULs and failed as "no reference",
which is a true FAIL for a false reason and points the fix at the wrong which is a true FAIL for a false reason and points the fix at the wrong
thing. A file this gate cannot read gets an encoding diagnostic and exit 2, thing. But strict UTF-8 alone does not catch it — BOM-less UTF-16LE/BE and
UTF-32LE are *valid* UTF-8, because NUL is a legal code point, so they
decoded clean and produced exactly that false diagnosis anyway. The NUL
byte is the complete signal and is checked first: no plausible markdown
adapter contains one, and every UTF-16/32 encoding of ASCII is full of
them. A file this gate cannot read gets an encoding diagnostic and exit 2,
the same policy the ADR-0020 validators' read_text() uses. the same policy the ADR-0020 validators' read_text() uses.
A file that exists but cannot be read at all is neither a pass nor a FAIL —
nothing was graded — so it exits 3 rather than 1. Exit 1 sends the skill's
closeout into "fix the FAIL by editing the provider file", which for a file
it cannot open is an instruction to edit blind.
""" """
try: try:
with open(path, encoding="utf-8") as fh: with open(path, "rb") as fh:
text = fh.read() raw = fh.read()
except OSError as exc:
print(f"Error: '{path}' exists but could not be read ({exc.strerror}). "
"Nothing was checked — fix whatever is blocking the read "
"(permissions, ownership, the underlying device) and re-run; do "
"not edit the adapter on the strength of this.", file=sys.stderr)
sys.exit(EXIT_UNREADABLE)
if b"\x00" in raw:
print(f"Error: '{path}' is not valid UTF-8 — it contains NUL bytes, so "
"it is almost certainly UTF-16 or UTF-32 (with or without a BOM). "
"Re-save it as UTF-8; this check does not guess at other "
"encodings.", file=sys.stderr)
sys.exit(EXIT_USAGE)
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as exc: except UnicodeDecodeError as exc:
print(f"Error: '{path}' is not valid UTF-8 ({exc.reason} at byte " print(f"Error: '{path}' is not valid UTF-8 ({exc.reason} at byte "
f"{exc.start}) — re-save it as UTF-8; this check does not guess " f"{exc.start}) — re-save it as UTF-8; this check does not guess "
"at other encodings.", file=sys.stderr) "at other encodings.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
return text[1:] if text.startswith("\ufeff") else text return text.replace("\ufeff", "")
adapter_content = read_text(adapter_path) adapter_content = read_text(adapter_path)
agents_md_content = read_text(agents_md_path) agents_md_content = read_text(agents_md_path)
adapter_dir = os.path.dirname(os.path.abspath(adapter_path))
has_fail = False has_fail = False
@@ -136,34 +245,229 @@ if not adapter_content.strip():
print(" Why: An empty adapter carries no reference to AGENTS.md and no provider-specific content.") print(" Why: An empty adapter carries no reference to AGENTS.md and no provider-specific content.")
print(" Fix: Add at least an import (or text pointer) to AGENTS.md.") print(" Fix: Add at least an import (or text pointer) to AGENTS.md.")
print() print()
sys.exit(1) sys.exit(EXIT_FAIL)
IMPORT_RE = re.compile(r'(?m)^\s*@\S*AGENTS\.md\s*$')
lines = adapter_content.splitlines() # --- Inert regions -----------------------------------------------------------
import_lines = [ln for ln in lines if IMPORT_RE.match(ln)] #
# A prose pointer is any line naming AGENTS.md that is not itself an import # A reference only counts where something would actually resolve it. Fenced
# line — an inert `@AGENTS.md` in a provider that resolves no imports points # code blocks, indented code blocks and HTML comments are shown to the reader
# a reader at nothing. # (or hidden from them) as literal text; Claude Code resolves an @import in
pointer_lines = [ln for ln in lines if not IMPORT_RE.match(ln) and "AGENTS.md" in ln] # none of them. Without this, a ```-fenced `@AGENTS.md` — the exact
# copy-the-example-into-the-file mistake this gate exists to catch — exited 0
# with the adapter deferring to nothing.
#
# Indented code blocks are handled by IMPORT_RE's `^ {0,3}` instead of by the
# mask: four leading spaces is what opens an indented code block in CommonMark,
# so an import has to sit within three. The mask deliberately does not apply
# that rule to prose pointers, where four-space indentation is ordinary list
# continuation rather than code.
FENCE_RE = re.compile(r'^( {0,3})(`{3,}|~{3,})(.*)$')
COMMENT_RE = re.compile(r'<!--.*?(?:-->|\Z)', re.DOTALL)
def line_offsets(text):
"""[(char offset, line without its terminator)] over `text`."""
out = []
off = 0
for raw in text.splitlines(keepends=True):
out.append((off, raw.rstrip("\r\n")))
off += len(raw)
return out
def build_inert_mask(text, offsets):
"""Per-character flags: 1 where a reference would never be resolved."""
mask = bytearray(len(text))
fence = None # (fence char, opening run length)
for start, line in offsets:
m = FENCE_RE.match(line)
if fence is None:
if m:
fence = (m.group(2)[0], len(m.group(2)))
for i in range(start, start + len(line)):
mask[i] = 1
continue
for i in range(start, start + len(line)):
mask[i] = 1
if (m and m.group(2)[0] == fence[0]
and len(m.group(2)) >= fence[1]
and not m.group(3).strip()):
fence = None
for m in COMMENT_RE.finditer(text):
if m.start() < len(mask) and mask[m.start()]:
continue # a literal "<!--" printed inside a fence opens nothing
for i in range(m.start(), min(m.end(), len(mask))):
mask[i] = 1
return mask
# --- Reference shapes --------------------------------------------------------
#
# `\S*AGENTS\.md` had no path-separator boundary, so `@NOTAGENTS.md` and
# `@zzzAGENTS.md` counted as imports of AGENTS.md. The matched path must end in
# AGENTS.md as a whole segment.
IMPORT_RE = re.compile(r'^ {0,3}@(?P<path>\S+?)\s*$')
# A mention in prose: an optional relative path, then AGENTS.md, with no
# identifier character glued to the front (so NOTAGENTS.md does not match) and
# nothing glued to the back.
MENTION_RE = re.compile(r'(?<![0-9A-Za-z_.\-/])((?:[\w.\-~]+/)*AGENTS\.md)(?![0-9A-Za-z])')
# A pointer has to read as a pointer. `"AGENTS.md" in ln` passed
# "Do NOT read AGENTS.md; it is obsolete." and "We deleted AGENTS.md last
# year." — both of which point the reader away from the file. Require a
# deference cue in the naming sentence, and reject a negated one.
DIRECTIVE_RE = re.compile(
r'\b(see|read|refer|refers|referring|consult|consults|follow|follows|'
r'defer|defers|deferring|described|documented|documents|covered|covers|'
r'found|listed|specified|defined|governed|per|use|uses|using|apply|obey|'
r'start|check|live|lives|contains|holds|carries|inherit|inherits|import|'
r'imports|conventions|instructions|guidelines|guidance|rules|standards|'
r'reference|setup)\b', re.I)
NEGATION_RE = re.compile(
r"(\bnot\b|n't\b|\bnever\b|\bno longer\b|\bdeleted\b|\bremoved\b|"
r"\bobsolete\b|\bdeprecated\b|\bignore\b|\bignores\b|\bignoring\b|"
r"\bdisregard\b|\bsuperseded\b|\bgone\b|\bunused\b|\bstale\b)", re.I)
SENTENCE_SPLIT_RE = re.compile(r'(?<=[.;:!?])\s+')
def sentence_around(line, index):
"""(sentence of `line` containing character `index`, its start offset)."""
bounds = [0]
for m in SENTENCE_SPLIT_RE.finditer(line):
bounds.append(m.end())
bounds.append(len(line) + 1)
for i in range(len(bounds) - 1):
if bounds[i] <= index < bounds[i + 1]:
return line[bounds[i]:bounds[i + 1]], bounds[i]
return line, 0
def reads_as_pointer(line, match):
"""Does the sentence naming AGENTS.md actually point the reader at it?
The matched path is blanked out before the cues are applied. It is a
filename, not prose, and leaving it in let its own characters vote: the
perfectly ordinary `docs/does/not/exist/AGENTS.md` tripped the negation
cue on the `not` path segment, so a pointer got rejected for the wrong
reason and the near-miss line then reported the wrong diagnosis.
"""
sentence, sentence_start = sentence_around(line, match.start())
rel_start = match.start() - sentence_start
rel_end = match.end() - sentence_start
probe = sentence[:rel_start] + " AGENTS.md " + sentence[rel_end:]
if NEGATION_RE.search(probe):
return False
return bool(DIRECTIVE_RE.search(probe))
def resolve(raw_path):
"""An import/pointer path resolved the way the provider would resolve it."""
p = os.path.expanduser(raw_path)
if not os.path.isabs(p):
p = os.path.join(adapter_dir, p)
return os.path.normpath(p)
def target_problem(raw_path):
"""None if `raw_path` names a real, non-empty file; else why not."""
resolved = resolve(raw_path)
if not os.path.isfile(resolved):
return f"'{raw_path}' resolves to {resolved}, which does not exist"
try:
if os.path.getsize(resolved) == 0:
return f"'{raw_path}' resolves to {resolved}, which is empty"
with open(resolved, "rb") as fh:
if not fh.read().strip():
return f"'{raw_path}' resolves to {resolved}, which is blank"
except OSError as exc:
return f"'{raw_path}' resolves to {resolved}, which cannot be read ({exc.strerror})"
return None
def names_agents_md(path):
return path == "AGENTS.md" or path.endswith("/AGENTS.md")
offsets = line_offsets(adapter_content)
lines = [line for _, line in offsets]
mask = build_inert_mask(adapter_content, offsets)
def is_inert(abs_index):
return abs_index < len(mask) and bool(mask[abs_index])
# Lines shaped like an @AGENTS.md import, whether or not the target resolves.
# Used to exclude them from the duplication denominator and from the prose
# pointer scan, both of which only care about the shape.
import_shaped_lines = set()
# (line, raw path) for every import whose target actually resolves.
live_imports = []
# Diagnostics for imports that are the right shape but resolve to nothing.
dead_imports = []
# Imports that exist only inside a fence or an HTML comment.
inert_imports = []
for start, line in offsets:
m = IMPORT_RE.match(line)
if not m or not names_agents_md(m.group("path")):
continue
at_index = start + line.index("@")
if is_inert(at_index):
inert_imports.append(line.strip())
continue
import_shaped_lines.add(line)
problem = target_problem(m.group("path"))
if problem:
dead_imports.append(problem)
else:
live_imports.append(line)
live_pointers = []
dead_pointers = []
inert_pointers = []
mention_only = []
for start, line in offsets:
if line in import_shaped_lines:
continue
for m in MENTION_RE.finditer(line):
if is_inert(start + m.start()):
inert_pointers.append(line.strip())
continue
if not reads_as_pointer(line, m):
mention_only.append(sentence_around(line, m.start())[0].strip())
continue
problem = target_problem(m.group(1))
if problem:
dead_pointers.append(problem)
else:
live_pointers.append(line)
if no_import_syntax: if no_import_syntax:
has_reference = bool(pointer_lines) has_reference = bool(live_pointers)
near_misses = dead_pointers + [f"{d} (inside a code fence or HTML comment)" for d in inert_pointers]
near_misses += [f"'{s}' names AGENTS.md but does not point at it" for s in mention_only]
else: else:
has_reference = bool(import_lines) has_reference = bool(live_imports)
near_misses = dead_imports + [f"'{d}' is inside a code fence or HTML comment, where no import is resolved" for d in inert_imports]
if not has_reference: if not has_reference:
has_fail = True has_fail = True
print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}") print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}")
if no_import_syntax: if no_import_syntax:
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.") print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text. The pointer has to read as a pointer and name a file that is really there — a bare or negated mention (\"we deleted AGENTS.md\") defers nothing, and neither does a mention buried in a code fence or an HTML comment.")
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"") print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\", outside any fence, naming a path that exists relative to this file.")
else: else:
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line of its own; naming the file mid-sentence or inside backticks is prose this check will not credit, and merely naming it defers nothing.") print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line of its own, indented no more than three spaces, and the path must resolve to a real non-empty file. Naming the file mid-sentence or inside backticks is prose this check will not credit; putting the line inside a ``` fence, an indented code block, or an HTML comment is worse, because nothing resolves it and it looks right.")
print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line, or pass --no-import-syntax if this provider resolves no imports.") print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line at the top level of the file, or pass --no-import-syntax if this provider resolves no imports.")
for miss in near_misses:
print(f" Near miss: {miss}")
print() print()
# --- Duplication check --- # --- Duplication check ---
non_import_lines = [ln for ln in lines if not IMPORT_RE.match(ln)] non_import_lines = [ln for ln in lines if ln not in import_shaped_lines]
adapter_lines = [ln.strip() for ln in non_import_lines if ln.strip()] adapter_lines = [ln.strip() for ln in non_import_lines if ln.strip()]
agents_lines = {ln.strip() for ln in agents_md_content.splitlines() if ln.strip()} agents_lines = {ln.strip() for ln in agents_md_content.splitlines() if ln.strip()}
@@ -187,6 +491,6 @@ if non_blank_count > max_lines:
print() print()
if has_fail: if has_fail:
sys.exit(1) sys.exit(EXIT_FAIL)
sys.exit(0) sys.exit(0)
PYTHON PYTHON

View File

@@ -212,3 +212,308 @@ EOF
assert_output --partial "no reference" assert_output --partial "no reference"
assert_output --partial "line of its own" assert_output --partial "line of its own"
} }
# --- Q1: a reference only counts where something would resolve it -------------
@test "an @AGENTS.md inside a backtick code fence is not credited as an import" {
ADAPTER="$TMPDIR/CLAUDE.md"
cat > "$ADAPTER" <<'EOF'
# Claude notes
Put this at the top of the file:
```
@AGENTS.md
```
EOF
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "an @AGENTS.md inside a tilde code fence is not credited as an import" {
ADAPTER="$TMPDIR/CLAUDE.md"
cat > "$ADAPTER" <<'EOF'
~~~
@AGENTS.md
~~~
EOF
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "an @AGENTS.md in a four-space indented code block is not credited as an import" {
ADAPTER="$TMPDIR/CLAUDE.md"
cat > "$ADAPTER" <<'EOF'
# Claude notes
@AGENTS.md
EOF
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "an @AGENTS.md inside a multi-line HTML comment is not credited as an import" {
ADAPTER="$TMPDIR/CLAUDE.md"
cat > "$ADAPTER" <<'EOF'
# Claude notes
<!--
@AGENTS.md
-->
EOF
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "an @AGENTS.md indented up to three spaces is still credited" {
ADAPTER="$TMPDIR/CLAUDE.md"
printf ' @AGENTS.md\n' > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_success
}
# --- Q2: encodings that decode as valid UTF-8 but are not UTF-8 ---------------
@test "a BOM-less UTF-16LE adapter is an encoding error, not a missing reference" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'wb').write('@AGENTS.md\n'.encode('utf-16-le'))" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 2
assert_output --partial "not valid UTF-8"
refute_output --partial "no reference"
}
@test "a BOM-less UTF-32LE adapter is an encoding error, not a missing reference" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'wb').write('@AGENTS.md\n'.encode('utf-32-le'))" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 2
assert_output --partial "not valid UTF-8"
refute_output --partial "no reference"
}
@test "a doubled UTF-8 BOM does not hide the @import line" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'wb').write(('@AGENTS.md\n').encode('utf-8'))" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_success
}
@test "a BOM in front of a mid-file @import line does not hide it" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'wb').write(('# Claude notes\n\n@AGENTS.md\n').encode('utf-8'))" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_success
}
# --- Q3: a file that exists but cannot be read is not a FAIL -----------------
@test "an adapter that exists but cannot be read exits 3 with a diagnostic and no FAIL" {
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER"
chmod 000 "$ADAPTER"
# chmod is not enough under a uid that bypasses it (root in CI containers).
# /proc/self/mem is a regular file whose read returns EIO for every uid, so
# it exercises the same branch where chmod cannot.
if cat "$ADAPTER" >/dev/null 2>&1; then
if [ -e /proc/self/mem ]; then
ADAPTER=/proc/self/mem
else
chmod 644 "$TMPDIR/CLAUDE.md"
skip "no way to make a readable-by-stat, unreadable-by-open file here"
fi
fi
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
chmod 644 "$TMPDIR/CLAUDE.md"
assert_failure 3
assert_output --partial "could not be read"
refute_output --partial "FAIL"
}
# --- Q4: the reference has to name, and resolve to, a real AGENTS.md ---------
@test "an @import naming a path that does not exist is not credited" {
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@docs/does/not/exist/AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
assert_output --partial "does not exist"
}
@test "@NOTAGENTS.md and @zzzAGENTS.md are not imports of AGENTS.md" {
# The decoys are real files, so the on-disk resolution check cannot be what
# rejects them. Only the path-segment boundary can — without the fixtures
# this test passes against a substring match and proves nothing.
cp "$AGENTS_MD" "$TMPDIR/NOTAGENTS.md"
cp "$AGENTS_MD" "$TMPDIR/zzzAGENTS.md"
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@NOTAGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
echo "@zzzAGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "an @import resolving to a zero-byte AGENTS.md is not credited" {
SUB="$TMPDIR/empty"
mkdir -p "$SUB"
: > "$SUB/AGENTS.md"
ADAPTER="$SUB/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
assert_output --partial "empty"
}
@test "an @import naming a real relative path to AGENTS.md is credited" {
mkdir -p "$TMPDIR/docs"
cp "$AGENTS_MD" "$TMPDIR/docs/AGENTS.md"
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@docs/AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_success
}
# --- Q5: --no-import-syntax needs a pointer, not a mention -------------------
@test "with --no-import-syntax, a negated mention of AGENTS.md is not a pointer" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
Do NOT read AGENTS.md; it is obsolete.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "with --no-import-syntax, a past-tense mention of a deleted AGENTS.md is not a pointer" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
We deleted AGENTS.md last year.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "with --no-import-syntax, a pointer inside a code fence is not credited" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
Example of what to write:
```
See AGENTS.md at the repo root for shared conventions.
```
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "with --no-import-syntax, a pointer inside an HTML comment is not credited" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
# Copilot instructions
<!-- See AGENTS.md at the repo root for shared conventions. -->
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "with --no-import-syntax, a name merely ending in AGENTS.md is not a pointer to it" {
# Real decoy files, so the on-disk resolution check cannot be what rejects
# these — only the token boundary in the mention pattern can. zzzAGENTS.md
# is the load-bearing case: dropping the boundary from NOTAGENTS.md leaves
# the fragment "NOT" behind, which the negation cue then rejects for an
# unrelated reason, so that case alone would prove nothing.
cp "$AGENTS_MD" "$TMPDIR/zzzAGENTS.md"
cp "$AGENTS_MD" "$TMPDIR/NOTAGENTS.md"
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
See zzzAGENTS.md at the repo root for shared conventions.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
cat > "$ADAPTER" <<'EOF'
See NOTAGENTS.md at the repo root for shared conventions.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}
@test "with --no-import-syntax, a pointer naming a path that does not exist is not credited" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
See docs/does/not/exist/AGENTS.md for shared conventions.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
assert_output --partial "does not exist"
}
# --- argument handling -------------------------------------------------------
@test "a third positional argument is rejected instead of silently ignored" {
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" "$TMPDIR/also-not-graded.md"
assert_failure 2
assert_output --partial "exactly 2 positional arguments"
# The usage text this prints mentions the word FAIL, so refute the shape of
# a real finding line rather than the bare word.
refute_output --partial "FAIL Adapter"
}
@test "an unknown option is reported as an unknown option, not as a missing file" {
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" --bogus "$ADAPTER" "$AGENTS_MD"
assert_failure 2
assert_output --partial "unknown option '--bogus'"
refute_output --partial "'--bogus' is not a file"
refute_output --partial "FAIL Adapter"
}
@test "--max-lines=N is accepted in the equals form" {
ADAPTER="$TMPDIR/CLAUDE.md"
{
echo "@AGENTS.md"
for i in $(seq 1 10); do echo "Provider-specific line $i unrelated to AGENTS.md content."; done
} > "$ADAPTER"
run bash "$SCRIPT" --max-lines=5 "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "thin"
run bash "$SCRIPT" --max-lines=40 "$ADAPTER" "$AGENTS_MD"
assert_success
}
@test "with --no-import-syntax, a bare mention with no deference cue is not a pointer" {
ADAPTER="$TMPDIR/copilot-instructions.md"
cat > "$ADAPTER" <<'EOF'
# Copilot instructions
This repo also has an AGENTS.md.
Prefer inline suggestions over chat for one-line edits.
EOF
run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "core", "name": "core",
"version": "1.2.0", "version": "1.1.2",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "core", "name": "core",
"version": "1.2.0", "version": "1.1.2",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,5 +1,5 @@
name: core name: core
version: 1.2.0 version: 1.1.2
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it. description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
author: author:
name: Defame1297 name: Defame1297

View File

@@ -30,8 +30,8 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file: Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it. - **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it — nor one inside a code fence, an indented block, or an HTML comment, nor one whose path does not resolve to a real, non-empty file on disk.
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket. - **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short sentence pointing at `AGENTS.md` ("See AGENTS.md at the repo root for ..."), keep the provider-specific bucket. A bare or negated mention is not a pointer and will not be credited.
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like. The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
@@ -45,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file> bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
``` ```
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exit `2` is not a `FAIL`: it means the invocation or the input is wrong — a bad or missing argument, or a file that is not UTF-8 — so fix that, not the adapter. Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exits `2` and `3` are not `FAIL`s and nothing was graded under either, so neither is a reason to touch the adapter: `2` means the invocation or the input is wrong (a bad, missing, or extra argument, an unknown option, or a file that is not UTF-8), and `3` means a named file exists but could not be read.
## Step 4 — Report ## Step 4 — Report

View File

@@ -4,6 +4,25 @@ Deterministic self-check this skill shells out to instead of relying on LLM judg
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `validate-adapter.sh` | Checks a rewritten provider file (CLAUDE.md, etc.) has a reference to AGENTS.md, doesn't duplicate its content, and stays under a thin-file line threshold | | `validate-adapter.sh` | Checks a rewritten provider file (CLAUDE.md, etc.) has a working reference to AGENTS.md, doesn't duplicate its content, and stays under a thin-file line threshold |
Takes `<adapter-file> <agents-md-file>`, with optional `--no-import-syntax` and `--max-lines N` flags. Prints `FAIL` findings to stdout and exits non-zero on any failure. Takes exactly `<adapter-file> <agents-md-file>`, with optional `--no-import-syntax` and `--max-lines N` flags (also accepted as `--max-lines=N`). A third positional argument or an unknown option is an error, not something quietly ignored.
## What counts as a reference to AGENTS.md
Both modes require the named path to be a real path segment ending in `AGENTS.md` — `AGENTS.md` or `…/AGENTS.md`, not `NOTAGENTS.md` — that resolves on disk, relative to the adapter file, to a non-empty file. An adapter deferring to a path that is not there defers to nothing, so the check has to touch the disk rather than pattern-match the line.
A reference only counts where something would actually resolve it. A line inside a fenced code block, an indented code block, or an HTML comment is not credited in either mode: Claude Code resolves an import in none of those, so a fenced `@AGENTS.md` is the silent-drop failure this gate exists to catch, not a pass.
Default mode wants a real import: `@AGENTS.md` alone on its own line, indented no more than three spaces. `--no-import-syntax` wants a prose pointer that reads as one — the sentence naming `AGENTS.md` must carry a deference cue (see, read, refer to, documented in, conventions, …) and must not be negated. `Do NOT read AGENTS.md; it is obsolete.` and `We deleted AGENTS.md last year.` name the file while pointing the reader away from it, and neither is a pointer.
## Exit codes
The distinction matters because the skill's closeout tells the agent to fix any non-zero exit by editing the provider file. That is right for exactly one of these.
| Code | Meaning | What to do |
|------|---------|------------|
| `0` | Passes every check | Nothing |
| `1` | One or more `FAIL` findings printed to stdout — empty adapter, no working reference to AGENTS.md, excessive duplication, or not thin | Edit the provider file |
| `2` | Usage or input error: a bad, missing, or extra argument, an unknown option, a path that is not a file, or a file that is not UTF-8. Nothing was graded, so there is no `FAIL` line | Fix the invocation or the file's encoding — do not edit the adapter |
| `3` | A named input file exists but could not be read (permissions, I/O error). Nothing was graded and the adapter's contents are unknown | Fix the file's readability — do not edit the adapter |

View File

@@ -14,6 +14,10 @@ Arguments:
adapter-file Path to the provider-specific file to check. adapter-file Path to the provider-specific file to check.
agents-md-file Path to the AGENTS.md file it should defer to. agents-md-file Path to the AGENTS.md file it should defer to.
Exactly two positional arguments are accepted. Extra ones are rejected
rather than ignored: a third path silently graded nothing but the first
two, so a typo'd invocation passed against the wrong file.
Options: Options:
--no-import-syntax The target provider has no native cross-file import --no-import-syntax The target provider has no native cross-file import
mechanism. Require a plain-text pointer line naming mechanism. Require a plain-text pointer line naming
@@ -26,26 +30,69 @@ Options:
it's considered no longer "thin". Must be a it's considered no longer "thin". Must be a
non-negative integer. Default: 60. non-negative integer. Default: 60.
--help, -h Show this help and exit 0. --help, -h Show this help and exit 0.
-- End of options; every later argument is positional.
Both flags also accept the --flag=value form (--max-lines=40). An unknown
option is reported as an unknown option, not as a missing file.
What counts as a reference:
In both modes the named path must be a real path segment ending in
AGENTS.md ("AGENTS.md" or ".../AGENTS.md" — not NOTAGENTS.md), and it must
resolve on disk, relative to the adapter file, to a non-empty file. An
adapter deferring to a path that is not there defers to nothing.
A mention inside a fenced code block, an indented code block, or an HTML
comment is not credited in either mode. Nothing resolves those, so an
adapter whose only "import" is fenced silently defers to nothing.
With --no-import-syntax the pointer must read as a pointer: the sentence
naming AGENTS.md has to carry a deference cue (see, read, refer to,
documented in, conventions, ...) and must not be a negation ("do not read
AGENTS.md", "we deleted AGENTS.md"). A bare mention is not a pointer.
Exit codes: Exit codes:
0 Adapter file passes all checks 0 Adapter file passes all checks
1 One or more checks failed (empty file, no reference to AGENTS.md, 1 One or more checks failed (empty file, no reference to AGENTS.md,
excessive duplication, or file too long) excessive duplication, or file too long)
2 Usage or input error — a bad or missing argument, a path that is not a 2 Usage or input error — a bad, missing, or extra argument, an unknown
file, or a file that is not UTF-8. Nothing was graded, so there is no option, a path that is not a file, or a file that is not UTF-8. Nothing
FAIL line and no adapter edit to make: fix the invocation or the file's was graded, so there is no FAIL line and no adapter edit to make: fix
encoding and re-run. Kept distinct from 1 because the skill's own the invocation or the file's encoding and re-run. Kept distinct from 1
closeout tells the agent to fix every non-zero exit by editing the because the skill's own closeout tells the agent to fix every non-zero
provider file, which for a mistyped flag edits the wrong file forever. exit by editing the provider file, which for a mistyped flag edits the
wrong file forever.
3 A named input file exists but could not be read (permissions, a
directory swapped in mid-run, I/O error). Also not a FAIL: nothing was
graded and the adapter's contents are unknown, so editing it is
guesswork. Fix the file's readability and re-run.
EOF EOF
} }
NO_IMPORT_SYNTAX=0 NO_IMPORT_SYNTAX=0
MAX_LINES=60 MAX_LINES=60
ARGS=() ARGS=()
END_OF_OPTS=0
require_int() {
# $1 = the value to validate
if [[ ! "$1" =~ ^[0-9]+$ ]]; then
echo "Error: --max-lines expects a non-negative integer, got '$1'." >&2
exit 2
fi
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
if [[ $END_OF_OPTS -eq 1 ]]; then
ARGS+=("$1")
shift
continue
fi
case "$1" in case "$1" in
--)
END_OF_OPTS=1
shift
;;
--help|-h) --help|-h)
usage usage
exit 0 exit 0
@@ -54,17 +101,37 @@ while [[ $# -gt 0 ]]; do
NO_IMPORT_SYNTAX=1 NO_IMPORT_SYNTAX=1
shift shift
;; ;;
--no-import-syntax=*)
echo "Error: --no-import-syntax is a flag and takes no value (got '$1')." >&2
exit 2
;;
--max-lines) --max-lines)
if [[ $# -lt 2 ]]; then if [[ $# -lt 2 ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2 echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 2 exit 2
fi fi
MAX_LINES="$2" MAX_LINES="$2"
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then require_int "$MAX_LINES"
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2 shift 2
;;
--max-lines=*)
MAX_LINES="${1#--max-lines=}"
if [[ -z "$MAX_LINES" ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 2 exit 2
fi fi
shift 2 require_int "$MAX_LINES"
shift
;;
-*)
# Reported as an unknown option rather than falling through to the
# positional bucket, where it used to surface as "'--bogus' is not a
# file" — the right exit code attached to a diagnostic that sends the
# reader looking for a path they never typed.
echo "Error: unknown option '$1'." >&2
echo "" >&2
usage >&2
exit 2
;; ;;
*) *)
ARGS+=("$1") ARGS+=("$1")
@@ -80,6 +147,13 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
exit 2 exit 2
fi fi
if [[ ${#ARGS[@]} -gt 2 ]]; then
echo "Error: expected exactly 2 positional arguments (adapter-file and agents-md-file), got ${#ARGS[@]}: ${ARGS[*]}." >&2
echo "" >&2
usage >&2
exit 2
fi
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON' python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
import sys import sys
import os import os
@@ -89,45 +163,80 @@ adapter_path, agents_md_path, no_import_syntax, max_lines = sys.argv[1:5]
no_import_syntax = no_import_syntax == "1" no_import_syntax = no_import_syntax == "1"
max_lines = int(max_lines) max_lines = int(max_lines)
EXIT_FAIL = 1
EXIT_USAGE = 2
EXIT_UNREADABLE = 3
if not os.path.isfile(adapter_path): if not os.path.isfile(adapter_path):
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr) print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
if not os.path.isfile(agents_md_path): if not os.path.isfile(agents_md_path):
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr) print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
def read_text(path): def read_text(path):
r"""File contents as text, UTF-8, BOM stripped. r"""File contents as text, UTF-8, every BOM stripped.
The BOM strip is not cosmetic. IMPORT_RE anchors on `^\s*@`, and a BOM is The BOM strip is not cosmetic. IMPORT_RE anchors on `^ {0,3}@`, and a BOM
not `\s` in Python, so a CLAUDE.md saved by an editor that emits one had is not whitespace in Python, so a CLAUDE.md saved by an editor that emits
its first line — the `@AGENTS.md` import, which is the whole adapter — one had its first line — the `@AGENTS.md` import, which is the whole
silently treated as prose. The check then said "no reference to AGENTS.md" adapter — silently treated as prose. The check then said "no reference to
told the author to add the line already sitting in front of them. Same AGENTS.md" and told the author to add the line already sitting in front of
class of silent BOM miss recorded in scripts/skill-size-check.sh; strip it them. Same class of silent BOM miss recorded in scripts/skill-size-check.sh;
at the reader so no later check has to know about it. strip it at the reader so no later check has to know about it.
Every U+FEFF goes, not just one at offset 0. Stripping exactly the first
one left the mirror-image false FAIL for a doubled BOM (two concatenated
files, or a tool that re-adds one) and for a BOM mid-file at the head of
the import line. U+FEFF has no meaning as a character in a markdown
instruction file, so removing all of them cannot lose signal.
Decoding is strict, not errors="replace". Replacement mangles the file and Decoding is strict, not errors="replace". Replacement mangles the file and
the checks then grade the mangling: a UTF-16 adapter whose first line is the checks then grade the mangling: a UTF-16 adapter whose first line is
`@AGENTS.md` decoded to interleaved NULs and failed as "no reference", `@AGENTS.md` decoded to interleaved NULs and failed as "no reference",
which is a true FAIL for a false reason and points the fix at the wrong which is a true FAIL for a false reason and points the fix at the wrong
thing. A file this gate cannot read gets an encoding diagnostic and exit 2, thing. But strict UTF-8 alone does not catch it — BOM-less UTF-16LE/BE and
UTF-32LE are *valid* UTF-8, because NUL is a legal code point, so they
decoded clean and produced exactly that false diagnosis anyway. The NUL
byte is the complete signal and is checked first: no plausible markdown
adapter contains one, and every UTF-16/32 encoding of ASCII is full of
them. A file this gate cannot read gets an encoding diagnostic and exit 2,
the same policy the ADR-0020 validators' read_text() uses. the same policy the ADR-0020 validators' read_text() uses.
A file that exists but cannot be read at all is neither a pass nor a FAIL —
nothing was graded — so it exits 3 rather than 1. Exit 1 sends the skill's
closeout into "fix the FAIL by editing the provider file", which for a file
it cannot open is an instruction to edit blind.
""" """
try: try:
with open(path, encoding="utf-8") as fh: with open(path, "rb") as fh:
text = fh.read() raw = fh.read()
except OSError as exc:
print(f"Error: '{path}' exists but could not be read ({exc.strerror}). "
"Nothing was checked — fix whatever is blocking the read "
"(permissions, ownership, the underlying device) and re-run; do "
"not edit the adapter on the strength of this.", file=sys.stderr)
sys.exit(EXIT_UNREADABLE)
if b"\x00" in raw:
print(f"Error: '{path}' is not valid UTF-8 — it contains NUL bytes, so "
"it is almost certainly UTF-16 or UTF-32 (with or without a BOM). "
"Re-save it as UTF-8; this check does not guess at other "
"encodings.", file=sys.stderr)
sys.exit(EXIT_USAGE)
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as exc: except UnicodeDecodeError as exc:
print(f"Error: '{path}' is not valid UTF-8 ({exc.reason} at byte " print(f"Error: '{path}' is not valid UTF-8 ({exc.reason} at byte "
f"{exc.start}) — re-save it as UTF-8; this check does not guess " f"{exc.start}) — re-save it as UTF-8; this check does not guess "
"at other encodings.", file=sys.stderr) "at other encodings.", file=sys.stderr)
sys.exit(2) sys.exit(EXIT_USAGE)
return text[1:] if text.startswith("\ufeff") else text return text.replace("\ufeff", "")
adapter_content = read_text(adapter_path) adapter_content = read_text(adapter_path)
agents_md_content = read_text(agents_md_path) agents_md_content = read_text(agents_md_path)
adapter_dir = os.path.dirname(os.path.abspath(adapter_path))
has_fail = False has_fail = False
@@ -136,34 +245,229 @@ if not adapter_content.strip():
print(" Why: An empty adapter carries no reference to AGENTS.md and no provider-specific content.") print(" Why: An empty adapter carries no reference to AGENTS.md and no provider-specific content.")
print(" Fix: Add at least an import (or text pointer) to AGENTS.md.") print(" Fix: Add at least an import (or text pointer) to AGENTS.md.")
print() print()
sys.exit(1) sys.exit(EXIT_FAIL)
IMPORT_RE = re.compile(r'(?m)^\s*@\S*AGENTS\.md\s*$')
lines = adapter_content.splitlines() # --- Inert regions -----------------------------------------------------------
import_lines = [ln for ln in lines if IMPORT_RE.match(ln)] #
# A prose pointer is any line naming AGENTS.md that is not itself an import # A reference only counts where something would actually resolve it. Fenced
# line — an inert `@AGENTS.md` in a provider that resolves no imports points # code blocks, indented code blocks and HTML comments are shown to the reader
# a reader at nothing. # (or hidden from them) as literal text; Claude Code resolves an @import in
pointer_lines = [ln for ln in lines if not IMPORT_RE.match(ln) and "AGENTS.md" in ln] # none of them. Without this, a ```-fenced `@AGENTS.md` — the exact
# copy-the-example-into-the-file mistake this gate exists to catch — exited 0
# with the adapter deferring to nothing.
#
# Indented code blocks are handled by IMPORT_RE's `^ {0,3}` instead of by the
# mask: four leading spaces is what opens an indented code block in CommonMark,
# so an import has to sit within three. The mask deliberately does not apply
# that rule to prose pointers, where four-space indentation is ordinary list
# continuation rather than code.
FENCE_RE = re.compile(r'^( {0,3})(`{3,}|~{3,})(.*)$')
COMMENT_RE = re.compile(r'<!--.*?(?:-->|\Z)', re.DOTALL)
def line_offsets(text):
"""[(char offset, line without its terminator)] over `text`."""
out = []
off = 0
for raw in text.splitlines(keepends=True):
out.append((off, raw.rstrip("\r\n")))
off += len(raw)
return out
def build_inert_mask(text, offsets):
"""Per-character flags: 1 where a reference would never be resolved."""
mask = bytearray(len(text))
fence = None # (fence char, opening run length)
for start, line in offsets:
m = FENCE_RE.match(line)
if fence is None:
if m:
fence = (m.group(2)[0], len(m.group(2)))
for i in range(start, start + len(line)):
mask[i] = 1
continue
for i in range(start, start + len(line)):
mask[i] = 1
if (m and m.group(2)[0] == fence[0]
and len(m.group(2)) >= fence[1]
and not m.group(3).strip()):
fence = None
for m in COMMENT_RE.finditer(text):
if m.start() < len(mask) and mask[m.start()]:
continue # a literal "<!--" printed inside a fence opens nothing
for i in range(m.start(), min(m.end(), len(mask))):
mask[i] = 1
return mask
# --- Reference shapes --------------------------------------------------------
#
# `\S*AGENTS\.md` had no path-separator boundary, so `@NOTAGENTS.md` and
# `@zzzAGENTS.md` counted as imports of AGENTS.md. The matched path must end in
# AGENTS.md as a whole segment.
IMPORT_RE = re.compile(r'^ {0,3}@(?P<path>\S+?)\s*$')
# A mention in prose: an optional relative path, then AGENTS.md, with no
# identifier character glued to the front (so NOTAGENTS.md does not match) and
# nothing glued to the back.
MENTION_RE = re.compile(r'(?<![0-9A-Za-z_.\-/])((?:[\w.\-~]+/)*AGENTS\.md)(?![0-9A-Za-z])')
# A pointer has to read as a pointer. `"AGENTS.md" in ln` passed
# "Do NOT read AGENTS.md; it is obsolete." and "We deleted AGENTS.md last
# year." — both of which point the reader away from the file. Require a
# deference cue in the naming sentence, and reject a negated one.
DIRECTIVE_RE = re.compile(
r'\b(see|read|refer|refers|referring|consult|consults|follow|follows|'
r'defer|defers|deferring|described|documented|documents|covered|covers|'
r'found|listed|specified|defined|governed|per|use|uses|using|apply|obey|'
r'start|check|live|lives|contains|holds|carries|inherit|inherits|import|'
r'imports|conventions|instructions|guidelines|guidance|rules|standards|'
r'reference|setup)\b', re.I)
NEGATION_RE = re.compile(
r"(\bnot\b|n't\b|\bnever\b|\bno longer\b|\bdeleted\b|\bremoved\b|"
r"\bobsolete\b|\bdeprecated\b|\bignore\b|\bignores\b|\bignoring\b|"
r"\bdisregard\b|\bsuperseded\b|\bgone\b|\bunused\b|\bstale\b)", re.I)
SENTENCE_SPLIT_RE = re.compile(r'(?<=[.;:!?])\s+')
def sentence_around(line, index):
"""(sentence of `line` containing character `index`, its start offset)."""
bounds = [0]
for m in SENTENCE_SPLIT_RE.finditer(line):
bounds.append(m.end())
bounds.append(len(line) + 1)
for i in range(len(bounds) - 1):
if bounds[i] <= index < bounds[i + 1]:
return line[bounds[i]:bounds[i + 1]], bounds[i]
return line, 0
def reads_as_pointer(line, match):
"""Does the sentence naming AGENTS.md actually point the reader at it?
The matched path is blanked out before the cues are applied. It is a
filename, not prose, and leaving it in let its own characters vote: the
perfectly ordinary `docs/does/not/exist/AGENTS.md` tripped the negation
cue on the `not` path segment, so a pointer got rejected for the wrong
reason and the near-miss line then reported the wrong diagnosis.
"""
sentence, sentence_start = sentence_around(line, match.start())
rel_start = match.start() - sentence_start
rel_end = match.end() - sentence_start
probe = sentence[:rel_start] + " AGENTS.md " + sentence[rel_end:]
if NEGATION_RE.search(probe):
return False
return bool(DIRECTIVE_RE.search(probe))
def resolve(raw_path):
"""An import/pointer path resolved the way the provider would resolve it."""
p = os.path.expanduser(raw_path)
if not os.path.isabs(p):
p = os.path.join(adapter_dir, p)
return os.path.normpath(p)
def target_problem(raw_path):
"""None if `raw_path` names a real, non-empty file; else why not."""
resolved = resolve(raw_path)
if not os.path.isfile(resolved):
return f"'{raw_path}' resolves to {resolved}, which does not exist"
try:
if os.path.getsize(resolved) == 0:
return f"'{raw_path}' resolves to {resolved}, which is empty"
with open(resolved, "rb") as fh:
if not fh.read().strip():
return f"'{raw_path}' resolves to {resolved}, which is blank"
except OSError as exc:
return f"'{raw_path}' resolves to {resolved}, which cannot be read ({exc.strerror})"
return None
def names_agents_md(path):
return path == "AGENTS.md" or path.endswith("/AGENTS.md")
offsets = line_offsets(adapter_content)
lines = [line for _, line in offsets]
mask = build_inert_mask(adapter_content, offsets)
def is_inert(abs_index):
return abs_index < len(mask) and bool(mask[abs_index])
# Lines shaped like an @AGENTS.md import, whether or not the target resolves.
# Used to exclude them from the duplication denominator and from the prose
# pointer scan, both of which only care about the shape.
import_shaped_lines = set()
# (line, raw path) for every import whose target actually resolves.
live_imports = []
# Diagnostics for imports that are the right shape but resolve to nothing.
dead_imports = []
# Imports that exist only inside a fence or an HTML comment.
inert_imports = []
for start, line in offsets:
m = IMPORT_RE.match(line)
if not m or not names_agents_md(m.group("path")):
continue
at_index = start + line.index("@")
if is_inert(at_index):
inert_imports.append(line.strip())
continue
import_shaped_lines.add(line)
problem = target_problem(m.group("path"))
if problem:
dead_imports.append(problem)
else:
live_imports.append(line)
live_pointers = []
dead_pointers = []
inert_pointers = []
mention_only = []
for start, line in offsets:
if line in import_shaped_lines:
continue
for m in MENTION_RE.finditer(line):
if is_inert(start + m.start()):
inert_pointers.append(line.strip())
continue
if not reads_as_pointer(line, m):
mention_only.append(sentence_around(line, m.start())[0].strip())
continue
problem = target_problem(m.group(1))
if problem:
dead_pointers.append(problem)
else:
live_pointers.append(line)
if no_import_syntax: if no_import_syntax:
has_reference = bool(pointer_lines) has_reference = bool(live_pointers)
near_misses = dead_pointers + [f"{d} (inside a code fence or HTML comment)" for d in inert_pointers]
near_misses += [f"'{s}' names AGENTS.md but does not point at it" for s in mention_only]
else: else:
has_reference = bool(import_lines) has_reference = bool(live_imports)
near_misses = dead_imports + [f"'{d}' is inside a code fence or HTML comment, where no import is resolved" for d in inert_imports]
if not has_reference: if not has_reference:
has_fail = True has_fail = True
print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}") print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}")
if no_import_syntax: if no_import_syntax:
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.") print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text. The pointer has to read as a pointer and name a file that is really there — a bare or negated mention (\"we deleted AGENTS.md\") defers nothing, and neither does a mention buried in a code fence or an HTML comment.")
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"") print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\", outside any fence, naming a path that exists relative to this file.")
else: else:
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line of its own; naming the file mid-sentence or inside backticks is prose this check will not credit, and merely naming it defers nothing.") print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line of its own, indented no more than three spaces, and the path must resolve to a real non-empty file. Naming the file mid-sentence or inside backticks is prose this check will not credit; putting the line inside a ``` fence, an indented code block, or an HTML comment is worse, because nothing resolves it and it looks right.")
print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line, or pass --no-import-syntax if this provider resolves no imports.") print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line at the top level of the file, or pass --no-import-syntax if this provider resolves no imports.")
for miss in near_misses:
print(f" Near miss: {miss}")
print() print()
# --- Duplication check --- # --- Duplication check ---
non_import_lines = [ln for ln in lines if not IMPORT_RE.match(ln)] non_import_lines = [ln for ln in lines if ln not in import_shaped_lines]
adapter_lines = [ln.strip() for ln in non_import_lines if ln.strip()] adapter_lines = [ln.strip() for ln in non_import_lines if ln.strip()]
agents_lines = {ln.strip() for ln in agents_md_content.splitlines() if ln.strip()} agents_lines = {ln.strip() for ln in agents_md_content.splitlines() if ln.strip()}
@@ -187,6 +491,6 @@ if non_blank_count > max_lines:
print() print()
if has_fail: if has_fail:
sys.exit(1) sys.exit(EXIT_FAIL)
sys.exit(0) sys.exit(0)
PYTHON PYTHON

View File

@@ -16,7 +16,7 @@ You are the orchestrator for the git plugin—a composable workflow dispatcher d
You act on the caller's real branch and session context (you explicitly carry forward `current_branch`), not a disposable copy — you do not run in an isolated worktree. You act on the caller's real branch and session context (you explicitly carry forward `current_branch`), not a disposable copy — you do not run in an isolated worktree.
**Scope:** this orchestrator routes git-object operations only (commits, branches, worktrees, remotes, submodules, history). `pc-author` and `pc-run` (pre-commit config authoring and hook execution) are intentionally not routed here — they operate on `.pre-commit-config.yaml` and hook installation, not git objects. `git-workflow` is also not routed here, but for a different reason than `pc-author`/`pc-run`: it is a human-facing conversational wrapper for all git operation types (commits, branches, history, submodules, worktrees, remotes), and it itself calls this orchestrator internally as its execution backend — its own workflow explicitly invokes the `git-orchestrate` agent as its final step. It is not a peer to invoke instead of this dispatcher, and it explicitly refuses agent callers ("Do not use when the caller is an agent"). Agent callers route git-object operations here directly; direct human users to `git-workflow` when they want guided, conversational git help — it will call back into this orchestrator itself. Invoke `pc-author`/`pc-run` directly rather than through this dispatcher; do not invoke `git-workflow` as an agent caller under any circumstance. **Scope:** this orchestrator routes git-object operations only (commits, branches, worktrees, remotes, submodules, history). `pc-author` and `pc-run` (pre-commit config authoring and hook execution) are intentionally not routed here — they operate on `.pre-commit-config.yaml` and hook installation, not git objects. `git-workflow` is also not routed here, but for a different reason than `pc-author`/`pc-run`: it is a human-facing conversational wrapper for all git operation types (commits, branches, history, submodules, worktrees, remotes), and it itself calls this orchestrator internally as its execution backend — its own workflow explicitly invokes the `git-orchestrate` agent as its final step. It is not a peer to invoke instead of this dispatcher, and its own boundary clause sends agent callers here ("Not an agent caller -> `git-orchestrate`"). Agent callers route git-object operations here directly; direct human users to `git-workflow` when they want guided, conversational git help — it will call back into this orchestrator itself. Invoke `pc-author`/`pc-run` directly rather than through this dispatcher; do not invoke `git-workflow` as an agent caller under any circumstance.
## Hard rules ## Hard rules

View File

@@ -20,7 +20,7 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
|------|---------| |------|---------|
| `SKILL.md` | Skill instructions for agents | | `SKILL.md` | Skill instructions for agents |
| `references/branch-patterns.md` | Loaded when a branch's base, name prefix, or merge rule depends on GitHub Flow vs. Gitflow | | `references/branch-patterns.md` | Loaded when a branch's base, name prefix, or merge rule depends on GitHub Flow vs. Gitflow |
| `references/branch-operations.md` | Loaded when running a create/switch/delete/rename/track/list action, or resolving `get-intent` | | `references/branch-operations.md` | Loaded when running a create/switch/delete/rename/track/list/stash action, or resolving `get-intent` |
| `references/merging.md` | Loaded when merging one branch into another or resolving merge conflicts | | `references/merging.md` | Loaded when merging one branch into another or resolving merge conflicts |
| `references/comparing-branches.md` | Loaded when comparing two branches or finding where they diverged | | `references/comparing-branches.md` | Loaded when comparing two branches or finding where they diverged |
| `references/orchestrator-contract.md` | Loaded when `git-orchestrate` or another calling agent supplies a structured request rather than prose | | `references/orchestrator-contract.md` | Loaded when `git-orchestrate` or another calling agent supplies a structured request rather than prose |
@@ -29,5 +29,6 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
## Composition ## Composition
`git-orchestrate` calls this skill for the branch step of a multi-step workflow and parses its `git-orchestrate` calls this skill for the branch step of a multi-step workflow and parses its
structured result. Cherry-pick and revert are `git-history`'s; commit authoring and rebase are structured result. Revert is `git-history`'s; commit authoring, rebase, reset and cherry-pick are
`git-commits`'; branch operations against a Gitea-hosted remote are `gitea-branches`'. `git-commits`'; deleting a remote branch is `git-remotes`'; branch operations against a
Gitea-hosted remote are `gitea-branches`'.

View File

@@ -33,7 +33,7 @@ The two patterns are not mixable, and the wrong merge rule silently damages hist
| Action | Reference | | Action | Reference |
|---|---| |---|---|
| create, switch, delete, rename, track, list, get-intent | `references/branch-operations.md` | | create, switch, delete, rename, track, list, get-intent, stash | `references/branch-operations.md` |
| merge a branch, resolve merge conflicts | `references/merging.md` | | merge a branch, resolve merge conflicts | `references/merging.md` |
| compare two branches, find their divergence | `references/comparing-branches.md` | | compare two branches, find their divergence | `references/comparing-branches.md` |
@@ -51,7 +51,7 @@ These gates are passable. The `main`/`master` refusal in Gotchas is not.
## Step 4 — Set tracking ## Step 4 — Set tracking
When pushing a branch for the first time, always `git push -u origin <branch>`. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened. A new branch's first push must be `git push -u origin <branch>`. The push itself is `git-remotes`' — every remote-side gate lives there, which is why Step 2's remote-delete row hands off the same way — but the upstream requirement originates here, so carry it in the handoff. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened.
## Step 5 — Return a structured result ## Step 5 — Return a structured result

View File

@@ -15,7 +15,9 @@ hatch.
- **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits, - **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits,
which is why it is the default. `git branch -D <branch>` forces the deletion and discards that which is why it is the default. `git branch -D <branch>` forces the deletion and discards that
work — only after the destructive-operation gates pass and `confirm: true` is set. work — only after the destructive-operation gates pass and `confirm: true` is set.
- **delete (remote)** — `git push origin --delete <branch>`. - **delete (remote)** — not this skill's. Deleting a remote branch is a push, and every remote-side
gate lives in `git-remotes`; hand it there rather than running the push from here. Its
`references/push.md` carries the command and the refspec form.
- **rename** — `git branch -m <old> <new>`. - **rename** — `git branch -m <old> <new>`.
- **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only), - **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
`--merged` / `--no-merged` (filter by merge status into the current branch). `--merged` / `--no-merged` (filter by merge status into the current branch).
@@ -32,3 +34,23 @@ On `get-intent`, either parse the intent back out of the branch-name convention
(`feature/<intent-slug>`) or return `{ "intent": null }` when the caller never persisted the (`feature/<intent-slug>`) or return `{ "intent": null }` when the caller never persisted the
create-time value. Never fabricate an intent: a downstream commit message built on a guessed create-time value. Never fabricate an intent: a downstream commit message built on a guessed
intent is worse than one built on none. intent is worse than one built on none.
## Stashing work in progress
A switch aborts rather than clobbering conflicting local changes (see Gotchas). Stash is the way
past it: it shelves the working tree and index so the branch pointer can move.
- **save** — `git stash push -m "<message>"`. Add `-u` to include untracked files; verified on Git
2.39.5, a plain `push` leaves them in place, and a plain `push` with *only* untracked changes
reports `No local changes to save` and stashes nothing. Bare `git stash` is `push` with no message.
- **restore** — `git stash pop` applies the newest entry and deletes it. `git stash apply stash@{n}`
applies without deleting, for replaying one shelf onto more than one branch.
- **list** — `git stash list`; `git stash show -p stash@{n}` prints that entry's diff.
- **drop** — `git stash drop stash@{n}` deletes one entry. `git stash clear` deletes all of them
and nothing recovers them — confirm before running it.
- **branch from a stash** — `git stash branch <branch> stash@{n}` creates a branch at the commit the
stash was taken from and pops it there. Use it when the stash no longer applies to the current tip.
**A conflicting `pop` keeps the entry.** Verified on 2.39.5: it exits 1, writes conflict markers,
prints "The stash entry is kept in case you need it again", and `git stash list` still shows it.
Resolve, `git add`, then `git stash drop` the entry by hand — otherwise it silently accumulates.

View File

@@ -6,8 +6,8 @@ source_keys:
# Merging one branch into another # Merging one branch into another
Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase belongs to Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase and cherry-pick
`git-commits`; cherry-pick and revert to `git-history`. belong to `git-commits`; revert to `git-history`.
- **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the - **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the
target has not diverged. target has not diverged.

View File

@@ -8,6 +8,7 @@ description: >
Not branch lifecycle -> `git-branches`. Not branch lifecycle -> `git-branches`.
metadata: metadata:
version: "0.1.3"
category: git category: git
source_keys: source_keys:
- conventional-commits-spec - conventional-commits-spec
@@ -22,6 +23,7 @@ allowed-tools: Bash
- **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.
- **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. - **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. - **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.
## Dispatch ## Dispatch
@@ -31,18 +33,19 @@ Read exactly one flow file. Each is self-contained.
| Condition | Flow | Read | | Condition | Flow | Read |
|---|---|---| |---|---|---|
| Composing a new commit from staged changes | create | `references/create-commit.md` | | Composing a new commit from staged changes | create | `references/create-commit.md` |
| Amending, squashing, or folding a fixup into an earlier commit | rewrite | `references/rewrite-history.md` | | Amending, squashing, folding a fixup, rebasing onto a new base, or resetting HEAD | rewrite | `references/rewrite-history.md` |
| Replaying an existing commit onto the current branch | cherry-pick | `references/cherry-pick.md` | | Replaying an existing commit onto the current branch | cherry-pick | `references/cherry-pick.md` |
## Gates on every flow ## Gates on every flow
1. **Confirmation.** No history rewrite executes without explicit approval from the user or the calling agent. Cherry-pick needs the destination branch confirmed first. 1. **Confirmation.** No history rewrite executes without explicit approval from the user or the calling agent. Cherry-pick needs the destination branch confirmed first.
2. **Secrets.** Before any commit or amend, scan the staged diff for anything resembling an API key, 2. **Atomicity.** The result must be one logical, independently reviewable and reversible change that leaves the repository buildable and testable. This binds an amend or a squashed result as much as a fresh commit — say so before writing it, not after.
3. **Secrets.** Before any commit or amend, scan the staged diff for anything resembling an API key,
token, password, connection string, or environment-specific config. Stop and flag it rather than token, password, connection string, or environment-specific config. Stop and flag it rather than
committing it. committing it.
3. **Validation.** Check the message against commitlint `config-conventional` before committing. If a type, footer, or breaking-change edge case is not obvious, read `references/conventional-commits-spec.md` — it carries the constraint table, the 11-type set, and the footer token rules. 4. **Validation.** Check the message against commitlint `config-conventional` before committing. If a type, footer, or breaking-change edge case is not obvious, read `references/conventional-commits-spec.md` — it carries the constraint table, the 11-type set, and the footer token rules.
4. **SemVer impact.** Report the bump the commit implies: `feat` → MINOR, `fix`/`perf`/`revert` → PATCH, any breaking change → MAJOR, everything else → none. Callers decide releases from this, so never omit it. 5. **SemVer impact.** Report the bump the commit implies: `feat` → MINOR, `fix`/`perf`/`revert` → PATCH, any breaking change → MAJOR, everything else → none. Callers decide releases from this, so never omit it.
5. **Conflicts.** If a rebase or cherry-pick halts, offer resolution or an abort. Do not resolve automatically without confirmation. 6. **Conflicts.** If a rebase or cherry-pick halts, offer resolution or an abort. Do not resolve automatically without confirmation.
## Output ## Output

View File

@@ -21,7 +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. 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. 2. Get explicit approval — the rebase still rewrites history.
3. Run `rtk git rebase --autosquash HEAD~N`, or `-i --autosquash` to review the plan first. Git reorders the tagged commits against their targets automatically. 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.
**`-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.
## Squash by hand (interactive rebase) ## Squash by hand (interactive rebase)
@@ -35,3 +37,40 @@ Use this when the commits were not tagged at commit time. **Interactive rebase h
## When a rebase halts on a conflict ## When a rebase halts on a conflict
Offer conflict resolution or `rtk git rebase --abort`. Do not resolve conflicts automatically without confirmation. Offer conflict resolution or `rtk git rebase --abort`. Do not resolve conflicts automatically without confirmation.
## Rebase the branch onto a new base
Replays this branch's commits on top of another branch's tip — bringing a feature branch up to
date without a merge commit.
1. Confirm nothing being replayed has been pushed anywhere someone else has based work on. A rebase
gives every replayed commit a new SHA, which breaks any clone that already has the old ones.
2. Get explicit approval — this rewrites history like every other flow on this page.
3. `rtk git fetch origin` first, so `<newbase>` is the real tip rather than a stale local copy.
4. `rtk git rebase <newbase>` — for example `rtk git rebase main`. Use
`rtk git rebase --onto <newbase> <upstream> <branch>` to replay only the commits after
`<upstream>`, which is how a branch started from the wrong base gets moved.
5. The branch has now diverged from its remote. It needs
`--force-with-lease --force-if-includes` to push, never a bare `--force`, and never on
`main`/`master` — refuse that and explain.
## Move the branch pointer back (`git reset`)
`reset` moves the current branch to another commit. The mode decides what survives:
| Mode | Branch pointer | Index | Working tree |
|---|---|---|---|
| `--soft` | moves | untouched — the changes stay staged | untouched |
| `--mixed` (default) | moves | reset — the changes become unstaged | untouched |
| `--hard` | moves | reset | **overwritten; uncommitted work is destroyed** |
- "Undo my last commit but keep the changes" is `rtk git reset --soft HEAD~1`. That is the default
answer to the request; reach for anything else only when the caller asked for it.
- `rtk git reset --mixed HEAD~1` when the staging should be redone from scratch too.
- `rtk git reset --hard <ref>` is gated like a force-push: state exactly which uncommitted changes
will be discarded, get approval for that specific reset, and offer `rtk git stash push -u` first.
The commits it drops stay reachable through `git reflog`; the uncommitted edits never entered git
at all and nothing recovers them.
Reset does not rewrite the commits it leaves behind, so no force-push is needed unless the branch
was already pushed at the newer commit.

View File

@@ -8,7 +8,7 @@ This skill handles history inspection within the git workflow suite. It queries
## Composition ## Composition
`git-branches` delegates cherry-pick and revert here (see `git-branches`'s `references/merging.md`), which is why this skill carries those two operations rather than treating them as out of scope. They are general git knowledge, not drawn from the `history-inspection.md` research corpus. Server-side commit history on a Gitea-hosted repository belongs to `gitea-branches`; this skill reads the local working copy. `git-branches` delegates revert here (see `git-branches`'s `references/merging.md`), which is why this skill carries that operation rather than treating it as out of scope; it is general git knowledge, not drawn from the `history-inspection.md` research corpus. Cherry-pick is **not** this skill's: `git-commits` owns it, and this skill's job ends at locating the SHA to hand over. Server-side commit history on a Gitea-hosted repository belongs to `gitea-branches`; this skill reads the local working copy.
## Usage ## Usage

View File

@@ -48,7 +48,7 @@ If you need the placeholder catalogue, format presets, `--diff-filter` letters,
Offer the operation and its consequence; run it only once the user has chosen. Offer the operation and its consequence; run it only once the user has chosen.
- `git cherry-pick <commit>` copies the commit's changes onto the current HEAD — for backporting a fix to another branch. - Backporting the commit to another branch is a cherry-pick, and cherry-pick is `git-commits`' — it owns the destination-branch check, the `rtk git` wrapper and the `--abort` path. Hand it the SHA; do not run `git cherry-pick` from here.
- `git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history. - `git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history.
- `git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line. - `git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line.

View File

@@ -7,7 +7,9 @@ source_keys:
# Fetching # Fetching
Fetch updates remote-tracking branches (`refs/remotes/<name>/*`) and never modifies a local branch, so it is always safe to run. Fetch **with no refspec** updates remote-tracking branches (`refs/remotes/<name>/*`) and leaves every local branch alone.
That safety comes from the default refspec, not from `fetch` itself. Give it an explicit one and it writes to local branches: verified on Git 2.39.5, `git fetch origin main:probe` fast-forwarded the local `probe` branch, and a `+` prefix force-updates the destination, discarding whatever commits it held. Treat any `fetch` carrying a `<src>:<dst>` refspec as a branch update, not a read.
- **One remote**: `git fetch <remote>` — all branches - **One remote**: `git fetch <remote>` — all branches
- **One branch**: `git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref - **One branch**: `git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref

View File

@@ -23,9 +23,11 @@ A pull that diverges with no strategy configured fails, and that failure is the
## Config precedence ## Config precedence
The installed default varies by Git version — older versions merge on divergence, newer ones `--ff-only` is not Git's default on an unset config, and never has been. Older versions silently
default to `--ff-only` — so an unset `pull.ff` means the same pull behaves differently on different merged on divergence; current ones refuse outright — verified on Git 2.39.5, a divergent pull with
machines. Set it explicitly. nothing configured prints the reconciliation hint and exits 128 with
`fatal: Need to specify how to reconcile divergent branches.` The behaviour therefore still varies
by installed version, and neither variant is the one you want. Set it explicitly.
Highest wins: Highest wins:

View File

@@ -3,7 +3,8 @@ name: git-submodules
description: > description: >
Use when managing Git submodules — the full lifecycle of a nested Use when managing Git submodules — the full lifecycle of a nested
repository inside a superproject. repository inside a superproject — including phrasings that never say the
word, such as "add a dependency repo" or "vendor this repo inside ours".
Not multiple checkouts of one repo -> `git-worktrees`. Not multiple checkouts of one repo -> `git-worktrees`.
Not the superproject's own remotes -> `git-remotes`. Not the superproject's own remotes -> `git-remotes`.

View File

@@ -70,8 +70,8 @@ rtk git submodule update --remote --merge --recursive
rtk git commit -am "chore: update submodules to latest" rtk git commit -am "chore: update submodules to latest"
``` ```
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default `--remote` uses `submodule.<name>.branch` when it is set; without it Git falls back to the remote's
branch. Commit the superproject afterwards or the new pin is lost on the next `update`. default branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
## Run one command across every submodule ## Run one command across every submodule

View File

@@ -22,10 +22,10 @@ source_keys:
Other source keys extracted during the git plugin research phase inform sibling skills in the git workflow suite, not this one: Other source keys extracted during the git plugin research phase inform sibling skills in the git workflow suite, not this one:
- `context7-git-htmldocs` — git:branches, git:history, git:remotes - `context7-git-htmldocs` — git-branches, git-history, git-remotes
- `git-scm-docs` — git:configuration - `git-scm-docs` — no current skill; it backed a git-configuration skill that no longer exists and survives here as provenance only
- `git-scm-worktree-docs` — git:worktrees - `git-scm-worktree-docs` — git-worktrees
- `nvie-gitflow-post`, `atlassian-gitflow-tutorial`, `gitflow-cheatsheet` — git:branches - `nvie-gitflow-post`, `atlassian-gitflow-tutorial`, `gitflow-cheatsheet` — git-branches
- `conventional-commits-spec`, `commitlint-config-conventional` — git:commits - `conventional-commits-spec`, `commitlint-config-conventional` — git-commits
- `git-scm-push-docs`, `git-scm-fetch-docs`, `git-scm-pull-docs`, `git-scm-remote-docs` — git:remotes - `git-scm-push-docs`, `git-scm-fetch-docs`, `git-scm-pull-docs`, `git-scm-remote-docs` — git-remotes
- `git-scm-bisect-docs`, `git-scm-log-docs`, `git-scm-diff-docs` — git:history - `git-scm-bisect-docs`, `git-scm-log-docs`, `git-scm-diff-docs` — git-history

View File

@@ -28,13 +28,16 @@ metadata:
## Domains ## Domains
Every request resolves to exactly one of these six. An unambiguous one should have gone straight Route to the domain that owns the operation, and in sequence when a request spans two — a rebase
to the domain skill; this skill exists for the ones that did not. that ends in a force-push is `git-commits`, then `git-remotes`. An unambiguous request should have
gone straight to the domain skill; this one exists for the ones that did not.
| The request is about | Domain | | The request is about | Domain |
|---|---| |---|---|
| Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` | | Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` |
| Rebasing onto a new base, or undoing a commit with `reset` | `git-commits` |
| Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` | | Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` |
| Stashing work in progress to move between branches | `git-branches` |
| When a change landed, which commit broke something, what to revert or backport | `git-history` | | When a change landed, which commit broke something, what to revert or backport | `git-history` |
| Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` | | Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` |
| A nested repository pinned inside this one by a recorded commit | `git-submodules` | | A nested repository pinned inside this one by a recorded commit | `git-submodules` |

View File

@@ -53,7 +53,7 @@ final argument of the `--track -b` form.
| `-b <branch>` | Create and check out a new branch; fails if it exists | | `-b <branch>` | Create and check out a new branch; fails if it exists |
| `-B <branch>` | Like `-b` but resets the branch if it already exists | | `-B <branch>` | Like `-b` but resets the branch if it already exists |
| `-d` / `--detach` | Detach HEAD; useful for throwaway experiments | | `-d` / `--detach` | Detach HEAD; useful for throwaway experiments |
| `--orphan` | Create empty unborn branch | | `--orphan` | Create empty unborn branch — **Git 2.42+**; earlier versions exit 129 with `error: unknown option 'orphan'`. Fallback below |
| `--no-checkout` | Suppress initial checkout (for sparse-checkout setup) | | `--no-checkout` | Suppress initial checkout (for sparse-checkout setup) |
| `--guess-remote` | Look for a matching remote-tracking branch by path basename | | `--guess-remote` | Look for a matching remote-tracking branch by path basename |
| `--lock [--reason <str>]` | Lock immediately on creation (atomic; avoids race vs. add-then-lock) | | `--lock [--reason <str>]` | Lock immediately on creation (atomic; avoids race vs. add-then-lock) |
@@ -67,7 +67,22 @@ Using `-` as `<commit-ish>` is shorthand for `@{-1}` (the branch checked out bef
```bash ```bash
git worktree add --orphan -b <branch> <path> git worktree add --orphan -b <branch> <path>
``` ```
Creates an empty branch with no commits. Creates an empty branch with no commits. **`--orphan` needs Git 2.42 or later** — it was added
upstream in 2.42, and on 2.39.5 this fails with `error: unknown option 'orphan'` and exit 129.
Check `git --version` before reaching for it.
Fallback on older Git, verified on 2.39.5 — detach first, then orphan the linked worktree in place,
which leaves the main worktree on its own branch throughout:
```bash
git worktree add -d <path> # linked worktree, detached HEAD
cd <path>
git switch --orphan <branch> # unborn branch: empty index, empty working tree
```
`git worktree list` then shows the new worktree at `0000000 [<branch>]` until its first commit.
Do not substitute `git switch --orphan` in the *main* worktree: it clears that checkout, which is
the disruption worktrees exist to avoid.
## Sparse-checkout worktree ## Sparse-checkout worktree

View File

@@ -1,6 +1,6 @@
{ {
"name": "git", "name": "git",
"version": "1.4.0", "version": "1.3.6",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "git", "name": "git",
"version": "1.4.0", "version": "1.3.6",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -16,7 +16,7 @@ You are the orchestrator for the git plugin—a composable workflow dispatcher d
You act on the caller's real branch and session context (you explicitly carry forward `current_branch`), not a disposable copy — you do not run in an isolated worktree. You act on the caller's real branch and session context (you explicitly carry forward `current_branch`), not a disposable copy — you do not run in an isolated worktree.
**Scope:** this orchestrator routes git-object operations only (commits, branches, worktrees, remotes, submodules, history). `pc-author` and `pc-run` (pre-commit config authoring and hook execution) are intentionally not routed here — they operate on `.pre-commit-config.yaml` and hook installation, not git objects. `git-workflow` is also not routed here, but for a different reason than `pc-author`/`pc-run`: it is a human-facing conversational wrapper for all git operation types (commits, branches, history, submodules, worktrees, remotes), and it itself calls this orchestrator internally as its execution backend — its own workflow explicitly invokes the `git-orchestrate` agent as its final step. It is not a peer to invoke instead of this dispatcher, and it explicitly refuses agent callers ("Do not use when the caller is an agent"). Agent callers route git-object operations here directly; direct human users to `git-workflow` when they want guided, conversational git help — it will call back into this orchestrator itself. Invoke `pc-author`/`pc-run` directly rather than through this dispatcher; do not invoke `git-workflow` as an agent caller under any circumstance. **Scope:** this orchestrator routes git-object operations only (commits, branches, worktrees, remotes, submodules, history). `pc-author` and `pc-run` (pre-commit config authoring and hook execution) are intentionally not routed here — they operate on `.pre-commit-config.yaml` and hook installation, not git objects. `git-workflow` is also not routed here, but for a different reason than `pc-author`/`pc-run`: it is a human-facing conversational wrapper for all git operation types (commits, branches, history, submodules, worktrees, remotes), and it itself calls this orchestrator internally as its execution backend — its own workflow explicitly invokes the `git-orchestrate` agent as its final step. It is not a peer to invoke instead of this dispatcher, and its own boundary clause sends agent callers here ("Not an agent caller -> `git-orchestrate`"). Agent callers route git-object operations here directly; direct human users to `git-workflow` when they want guided, conversational git help — it will call back into this orchestrator itself. Invoke `pc-author`/`pc-run` directly rather than through this dispatcher; do not invoke `git-workflow` as an agent caller under any circumstance.
## Hard rules ## Hard rules

View File

@@ -1,5 +1,5 @@
name: git name: git
version: 1.4.0 version: 1.3.6
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. 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: author:
name: Defame1297 name: Defame1297

View File

@@ -20,7 +20,7 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
|------|---------| |------|---------|
| `SKILL.md` | Skill instructions for agents | | `SKILL.md` | Skill instructions for agents |
| `references/branch-patterns.md` | Loaded when a branch's base, name prefix, or merge rule depends on GitHub Flow vs. Gitflow | | `references/branch-patterns.md` | Loaded when a branch's base, name prefix, or merge rule depends on GitHub Flow vs. Gitflow |
| `references/branch-operations.md` | Loaded when running a create/switch/delete/rename/track/list action, or resolving `get-intent` | | `references/branch-operations.md` | Loaded when running a create/switch/delete/rename/track/list/stash action, or resolving `get-intent` |
| `references/merging.md` | Loaded when merging one branch into another or resolving merge conflicts | | `references/merging.md` | Loaded when merging one branch into another or resolving merge conflicts |
| `references/comparing-branches.md` | Loaded when comparing two branches or finding where they diverged | | `references/comparing-branches.md` | Loaded when comparing two branches or finding where they diverged |
| `references/orchestrator-contract.md` | Loaded when `git-orchestrate` or another calling agent supplies a structured request rather than prose | | `references/orchestrator-contract.md` | Loaded when `git-orchestrate` or another calling agent supplies a structured request rather than prose |
@@ -29,5 +29,6 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
## Composition ## Composition
`git-orchestrate` calls this skill for the branch step of a multi-step workflow and parses its `git-orchestrate` calls this skill for the branch step of a multi-step workflow and parses its
structured result. Cherry-pick and revert are `git-history`'s; commit authoring and rebase are structured result. Revert is `git-history`'s; commit authoring, rebase, reset and cherry-pick are
`git-commits`'; branch operations against a Gitea-hosted remote are `gitea-branches`'. `git-commits`'; deleting a remote branch is `git-remotes`'; branch operations against a
Gitea-hosted remote are `gitea-branches`'.

View File

@@ -33,7 +33,7 @@ The two patterns are not mixable, and the wrong merge rule silently damages hist
| Action | Reference | | Action | Reference |
|---|---| |---|---|
| create, switch, delete, rename, track, list, get-intent | `references/branch-operations.md` | | create, switch, delete, rename, track, list, get-intent, stash | `references/branch-operations.md` |
| merge a branch, resolve merge conflicts | `references/merging.md` | | merge a branch, resolve merge conflicts | `references/merging.md` |
| compare two branches, find their divergence | `references/comparing-branches.md` | | compare two branches, find their divergence | `references/comparing-branches.md` |
@@ -51,7 +51,7 @@ These gates are passable. The `main`/`master` refusal in Gotchas is not.
## Step 4 — Set tracking ## Step 4 — Set tracking
When pushing a branch for the first time, always `git push -u origin <branch>`. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened. A new branch's first push must be `git push -u origin <branch>`. The push itself is `git-remotes`' — every remote-side gate lives there, which is why Step 2's remote-delete row hands off the same way — but the upstream requirement originates here, so carry it in the handoff. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened.
## Step 5 — Return a structured result ## Step 5 — Return a structured result

View File

@@ -15,7 +15,9 @@ hatch.
- **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits, - **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits,
which is why it is the default. `git branch -D <branch>` forces the deletion and discards that which is why it is the default. `git branch -D <branch>` forces the deletion and discards that
work — only after the destructive-operation gates pass and `confirm: true` is set. work — only after the destructive-operation gates pass and `confirm: true` is set.
- **delete (remote)** — `git push origin --delete <branch>`. - **delete (remote)** — not this skill's. Deleting a remote branch is a push, and every remote-side
gate lives in `git-remotes`; hand it there rather than running the push from here. Its
`references/push.md` carries the command and the refspec form.
- **rename** — `git branch -m <old> <new>`. - **rename** — `git branch -m <old> <new>`.
- **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only), - **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
`--merged` / `--no-merged` (filter by merge status into the current branch). `--merged` / `--no-merged` (filter by merge status into the current branch).
@@ -32,3 +34,23 @@ On `get-intent`, either parse the intent back out of the branch-name convention
(`feature/<intent-slug>`) or return `{ "intent": null }` when the caller never persisted the (`feature/<intent-slug>`) or return `{ "intent": null }` when the caller never persisted the
create-time value. Never fabricate an intent: a downstream commit message built on a guessed create-time value. Never fabricate an intent: a downstream commit message built on a guessed
intent is worse than one built on none. intent is worse than one built on none.
## Stashing work in progress
A switch aborts rather than clobbering conflicting local changes (see Gotchas). Stash is the way
past it: it shelves the working tree and index so the branch pointer can move.
- **save** — `git stash push -m "<message>"`. Add `-u` to include untracked files; verified on Git
2.39.5, a plain `push` leaves them in place, and a plain `push` with *only* untracked changes
reports `No local changes to save` and stashes nothing. Bare `git stash` is `push` with no message.
- **restore** — `git stash pop` applies the newest entry and deletes it. `git stash apply stash@{n}`
applies without deleting, for replaying one shelf onto more than one branch.
- **list** — `git stash list`; `git stash show -p stash@{n}` prints that entry's diff.
- **drop** — `git stash drop stash@{n}` deletes one entry. `git stash clear` deletes all of them
and nothing recovers them — confirm before running it.
- **branch from a stash** — `git stash branch <branch> stash@{n}` creates a branch at the commit the
stash was taken from and pops it there. Use it when the stash no longer applies to the current tip.
**A conflicting `pop` keeps the entry.** Verified on 2.39.5: it exits 1, writes conflict markers,
prints "The stash entry is kept in case you need it again", and `git stash list` still shows it.
Resolve, `git add`, then `git stash drop` the entry by hand — otherwise it silently accumulates.

View File

@@ -6,8 +6,8 @@ source_keys:
# Merging one branch into another # Merging one branch into another
Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase belongs to Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase and cherry-pick
`git-commits`; cherry-pick and revert to `git-history`. belong to `git-commits`; revert to `git-history`.
- **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the - **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the
target has not diverged. target has not diverged.

View File

@@ -8,6 +8,7 @@ description: >
Not branch lifecycle -> `git-branches`. Not branch lifecycle -> `git-branches`.
metadata: metadata:
version: "0.1.3"
category: git category: git
source_keys: source_keys:
- conventional-commits-spec - conventional-commits-spec
@@ -22,6 +23,7 @@ allowed-tools: Bash
- **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.
- **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. - **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. - **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.
## Dispatch ## Dispatch
@@ -31,18 +33,19 @@ Read exactly one flow file. Each is self-contained.
| Condition | Flow | Read | | Condition | Flow | Read |
|---|---|---| |---|---|---|
| Composing a new commit from staged changes | create | `references/create-commit.md` | | Composing a new commit from staged changes | create | `references/create-commit.md` |
| Amending, squashing, or folding a fixup into an earlier commit | rewrite | `references/rewrite-history.md` | | Amending, squashing, folding a fixup, rebasing onto a new base, or resetting HEAD | rewrite | `references/rewrite-history.md` |
| Replaying an existing commit onto the current branch | cherry-pick | `references/cherry-pick.md` | | Replaying an existing commit onto the current branch | cherry-pick | `references/cherry-pick.md` |
## Gates on every flow ## Gates on every flow
1. **Confirmation.** No history rewrite executes without explicit approval from the user or the calling agent. Cherry-pick needs the destination branch confirmed first. 1. **Confirmation.** No history rewrite executes without explicit approval from the user or the calling agent. Cherry-pick needs the destination branch confirmed first.
2. **Secrets.** Before any commit or amend, scan the staged diff for anything resembling an API key, 2. **Atomicity.** The result must be one logical, independently reviewable and reversible change that leaves the repository buildable and testable. This binds an amend or a squashed result as much as a fresh commit — say so before writing it, not after.
3. **Secrets.** Before any commit or amend, scan the staged diff for anything resembling an API key,
token, password, connection string, or environment-specific config. Stop and flag it rather than token, password, connection string, or environment-specific config. Stop and flag it rather than
committing it. committing it.
3. **Validation.** Check the message against commitlint `config-conventional` before committing. If a type, footer, or breaking-change edge case is not obvious, read `references/conventional-commits-spec.md` — it carries the constraint table, the 11-type set, and the footer token rules. 4. **Validation.** Check the message against commitlint `config-conventional` before committing. If a type, footer, or breaking-change edge case is not obvious, read `references/conventional-commits-spec.md` — it carries the constraint table, the 11-type set, and the footer token rules.
4. **SemVer impact.** Report the bump the commit implies: `feat` → MINOR, `fix`/`perf`/`revert` → PATCH, any breaking change → MAJOR, everything else → none. Callers decide releases from this, so never omit it. 5. **SemVer impact.** Report the bump the commit implies: `feat` → MINOR, `fix`/`perf`/`revert` → PATCH, any breaking change → MAJOR, everything else → none. Callers decide releases from this, so never omit it.
5. **Conflicts.** If a rebase or cherry-pick halts, offer resolution or an abort. Do not resolve automatically without confirmation. 6. **Conflicts.** If a rebase or cherry-pick halts, offer resolution or an abort. Do not resolve automatically without confirmation.
## Output ## Output

View File

@@ -21,7 +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. 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. 2. Get explicit approval — the rebase still rewrites history.
3. Run `rtk git rebase --autosquash HEAD~N`, or `-i --autosquash` to review the plan first. Git reorders the tagged commits against their targets automatically. 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.
**`-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.
## Squash by hand (interactive rebase) ## Squash by hand (interactive rebase)
@@ -35,3 +37,40 @@ Use this when the commits were not tagged at commit time. **Interactive rebase h
## When a rebase halts on a conflict ## When a rebase halts on a conflict
Offer conflict resolution or `rtk git rebase --abort`. Do not resolve conflicts automatically without confirmation. Offer conflict resolution or `rtk git rebase --abort`. Do not resolve conflicts automatically without confirmation.
## Rebase the branch onto a new base
Replays this branch's commits on top of another branch's tip — bringing a feature branch up to
date without a merge commit.
1. Confirm nothing being replayed has been pushed anywhere someone else has based work on. A rebase
gives every replayed commit a new SHA, which breaks any clone that already has the old ones.
2. Get explicit approval — this rewrites history like every other flow on this page.
3. `rtk git fetch origin` first, so `<newbase>` is the real tip rather than a stale local copy.
4. `rtk git rebase <newbase>` — for example `rtk git rebase main`. Use
`rtk git rebase --onto <newbase> <upstream> <branch>` to replay only the commits after
`<upstream>`, which is how a branch started from the wrong base gets moved.
5. The branch has now diverged from its remote. It needs
`--force-with-lease --force-if-includes` to push, never a bare `--force`, and never on
`main`/`master` — refuse that and explain.
## Move the branch pointer back (`git reset`)
`reset` moves the current branch to another commit. The mode decides what survives:
| Mode | Branch pointer | Index | Working tree |
|---|---|---|---|
| `--soft` | moves | untouched — the changes stay staged | untouched |
| `--mixed` (default) | moves | reset — the changes become unstaged | untouched |
| `--hard` | moves | reset | **overwritten; uncommitted work is destroyed** |
- "Undo my last commit but keep the changes" is `rtk git reset --soft HEAD~1`. That is the default
answer to the request; reach for anything else only when the caller asked for it.
- `rtk git reset --mixed HEAD~1` when the staging should be redone from scratch too.
- `rtk git reset --hard <ref>` is gated like a force-push: state exactly which uncommitted changes
will be discarded, get approval for that specific reset, and offer `rtk git stash push -u` first.
The commits it drops stay reachable through `git reflog`; the uncommitted edits never entered git
at all and nothing recovers them.
Reset does not rewrite the commits it leaves behind, so no force-push is needed unless the branch
was already pushed at the newer commit.

View File

@@ -8,7 +8,7 @@ This skill handles history inspection within the git workflow suite. It queries
## Composition ## Composition
`git-branches` delegates cherry-pick and revert here (see `git-branches`'s `references/merging.md`), which is why this skill carries those two operations rather than treating them as out of scope. They are general git knowledge, not drawn from the `history-inspection.md` research corpus. Server-side commit history on a Gitea-hosted repository belongs to `gitea-branches`; this skill reads the local working copy. `git-branches` delegates revert here (see `git-branches`'s `references/merging.md`), which is why this skill carries that operation rather than treating it as out of scope; it is general git knowledge, not drawn from the `history-inspection.md` research corpus. Cherry-pick is **not** this skill's: `git-commits` owns it, and this skill's job ends at locating the SHA to hand over. Server-side commit history on a Gitea-hosted repository belongs to `gitea-branches`; this skill reads the local working copy.
## Usage ## Usage

View File

@@ -48,7 +48,7 @@ If you need the placeholder catalogue, format presets, `--diff-filter` letters,
Offer the operation and its consequence; run it only once the user has chosen. Offer the operation and its consequence; run it only once the user has chosen.
- `git cherry-pick <commit>` copies the commit's changes onto the current HEAD — for backporting a fix to another branch. - Backporting the commit to another branch is a cherry-pick, and cherry-pick is `git-commits`' — it owns the destination-branch check, the `rtk git` wrapper and the `--abort` path. Hand it the SHA; do not run `git cherry-pick` from here.
- `git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history. - `git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history.
- `git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line. - `git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line.

View File

@@ -7,7 +7,9 @@ source_keys:
# Fetching # Fetching
Fetch updates remote-tracking branches (`refs/remotes/<name>/*`) and never modifies a local branch, so it is always safe to run. Fetch **with no refspec** updates remote-tracking branches (`refs/remotes/<name>/*`) and leaves every local branch alone.
That safety comes from the default refspec, not from `fetch` itself. Give it an explicit one and it writes to local branches: verified on Git 2.39.5, `git fetch origin main:probe` fast-forwarded the local `probe` branch, and a `+` prefix force-updates the destination, discarding whatever commits it held. Treat any `fetch` carrying a `<src>:<dst>` refspec as a branch update, not a read.
- **One remote**: `git fetch <remote>` — all branches - **One remote**: `git fetch <remote>` — all branches
- **One branch**: `git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref - **One branch**: `git fetch <remote> <branch>` — the result lands in `FETCH_HEAD`, not a tracking ref

View File

@@ -23,9 +23,11 @@ A pull that diverges with no strategy configured fails, and that failure is the
## Config precedence ## Config precedence
The installed default varies by Git version — older versions merge on divergence, newer ones `--ff-only` is not Git's default on an unset config, and never has been. Older versions silently
default to `--ff-only` — so an unset `pull.ff` means the same pull behaves differently on different merged on divergence; current ones refuse outright — verified on Git 2.39.5, a divergent pull with
machines. Set it explicitly. nothing configured prints the reconciliation hint and exits 128 with
`fatal: Need to specify how to reconcile divergent branches.` The behaviour therefore still varies
by installed version, and neither variant is the one you want. Set it explicitly.
Highest wins: Highest wins:

View File

@@ -3,7 +3,8 @@ name: git-submodules
description: > description: >
Use when managing Git submodules — the full lifecycle of a nested Use when managing Git submodules — the full lifecycle of a nested
repository inside a superproject. repository inside a superproject — including phrasings that never say the
word, such as "add a dependency repo" or "vendor this repo inside ours".
Not multiple checkouts of one repo -> `git-worktrees`. Not multiple checkouts of one repo -> `git-worktrees`.
Not the superproject's own remotes -> `git-remotes`. Not the superproject's own remotes -> `git-remotes`.

View File

@@ -70,8 +70,8 @@ rtk git submodule update --remote --merge --recursive
rtk git commit -am "chore: update submodules to latest" rtk git commit -am "chore: update submodules to latest"
``` ```
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default `--remote` uses `submodule.<name>.branch` when it is set; without it Git falls back to the remote's
branch. Commit the superproject afterwards or the new pin is lost on the next `update`. default branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
## Run one command across every submodule ## Run one command across every submodule

View File

@@ -22,10 +22,10 @@ source_keys:
Other source keys extracted during the git plugin research phase inform sibling skills in the git workflow suite, not this one: Other source keys extracted during the git plugin research phase inform sibling skills in the git workflow suite, not this one:
- `context7-git-htmldocs` — git:branches, git:history, git:remotes - `context7-git-htmldocs` — git-branches, git-history, git-remotes
- `git-scm-docs` — git:configuration - `git-scm-docs` — no current skill; it backed a git-configuration skill that no longer exists and survives here as provenance only
- `git-scm-worktree-docs` — git:worktrees - `git-scm-worktree-docs` — git-worktrees
- `nvie-gitflow-post`, `atlassian-gitflow-tutorial`, `gitflow-cheatsheet` — git:branches - `nvie-gitflow-post`, `atlassian-gitflow-tutorial`, `gitflow-cheatsheet` — git-branches
- `conventional-commits-spec`, `commitlint-config-conventional` — git:commits - `conventional-commits-spec`, `commitlint-config-conventional` — git-commits
- `git-scm-push-docs`, `git-scm-fetch-docs`, `git-scm-pull-docs`, `git-scm-remote-docs` — git:remotes - `git-scm-push-docs`, `git-scm-fetch-docs`, `git-scm-pull-docs`, `git-scm-remote-docs` — git-remotes
- `git-scm-bisect-docs`, `git-scm-log-docs`, `git-scm-diff-docs` — git:history - `git-scm-bisect-docs`, `git-scm-log-docs`, `git-scm-diff-docs` — git-history

View File

@@ -28,13 +28,16 @@ metadata:
## Domains ## Domains
Every request resolves to exactly one of these six. An unambiguous one should have gone straight Route to the domain that owns the operation, and in sequence when a request spans two — a rebase
to the domain skill; this skill exists for the ones that did not. that ends in a force-push is `git-commits`, then `git-remotes`. An unambiguous request should have
gone straight to the domain skill; this one exists for the ones that did not.
| The request is about | Domain | | The request is about | Domain |
|---|---| |---|---|
| Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` | | Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` |
| Rebasing onto a new base, or undoing a commit with `reset` | `git-commits` |
| Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` | | Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` |
| Stashing work in progress to move between branches | `git-branches` |
| When a change landed, which commit broke something, what to revert or backport | `git-history` | | When a change landed, which commit broke something, what to revert or backport | `git-history` |
| Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` | | Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` |
| A nested repository pinned inside this one by a recorded commit | `git-submodules` | | A nested repository pinned inside this one by a recorded commit | `git-submodules` |

View File

@@ -53,7 +53,7 @@ final argument of the `--track -b` form.
| `-b <branch>` | Create and check out a new branch; fails if it exists | | `-b <branch>` | Create and check out a new branch; fails if it exists |
| `-B <branch>` | Like `-b` but resets the branch if it already exists | | `-B <branch>` | Like `-b` but resets the branch if it already exists |
| `-d` / `--detach` | Detach HEAD; useful for throwaway experiments | | `-d` / `--detach` | Detach HEAD; useful for throwaway experiments |
| `--orphan` | Create empty unborn branch | | `--orphan` | Create empty unborn branch — **Git 2.42+**; earlier versions exit 129 with `error: unknown option 'orphan'`. Fallback below |
| `--no-checkout` | Suppress initial checkout (for sparse-checkout setup) | | `--no-checkout` | Suppress initial checkout (for sparse-checkout setup) |
| `--guess-remote` | Look for a matching remote-tracking branch by path basename | | `--guess-remote` | Look for a matching remote-tracking branch by path basename |
| `--lock [--reason <str>]` | Lock immediately on creation (atomic; avoids race vs. add-then-lock) | | `--lock [--reason <str>]` | Lock immediately on creation (atomic; avoids race vs. add-then-lock) |
@@ -67,7 +67,22 @@ Using `-` as `<commit-ish>` is shorthand for `@{-1}` (the branch checked out bef
```bash ```bash
git worktree add --orphan -b <branch> <path> git worktree add --orphan -b <branch> <path>
``` ```
Creates an empty branch with no commits. Creates an empty branch with no commits. **`--orphan` needs Git 2.42 or later** — it was added
upstream in 2.42, and on 2.39.5 this fails with `error: unknown option 'orphan'` and exit 129.
Check `git --version` before reaching for it.
Fallback on older Git, verified on 2.39.5 — detach first, then orphan the linked worktree in place,
which leaves the main worktree on its own branch throughout:
```bash
git worktree add -d <path> # linked worktree, detached HEAD
cd <path>
git switch --orphan <branch> # unborn branch: empty index, empty working tree
```
`git worktree list` then shows the new worktree at `0000000 [<branch>]` until its first commit.
Do not substitute `git switch --orphan` in the *main* worktree: it clears that checkout, which is
the disruption worktrees exist to avoid.
## Sparse-checkout worktree ## Sparse-checkout worktree

View File

@@ -23,6 +23,7 @@ You resolve `owner`/`repo` once per session (via `git remote -v` on `origin`) an
These are non-negotiable regardless of `confirm` or any skill-local override: These are non-negotiable regardless of `confirm` or any skill-local override:
- Never delete the repository's default branch (typically `main` or `master`) — refused outright, independent of `confirm`. - Never delete the repository's default branch (typically `main` or `master`) — refused outright, independent of `confirm`.
- `delete_release` takes a numeric `id`; `delete_tag` takes a `tag_name` string. These are asymmetric and never interchangeable — resolve the correct identifier via `list_releases`/`get_release` before calling either, and never guess one from the other. - `delete_release` takes a numeric `id`; `delete_tag` takes a `tag_name` string. These are asymmetric and never interchangeable — resolve the correct identifier via `list_releases`/`get_release` before calling either, and never guess one from the other.
- `rename-branch` is gated like a delete even though it destroys nothing: what a rename does to open PRs using the branch as head or base, to a matching protection rule, and to every other clone's tracking branch is unconfirmed by `gitea-branches`' sources. Require `confirm: true`, and verify the PR and protection sides afterwards.
- Deleting a release does not delete its tag, and vice versa — if the caller's intent is to remove both, dispatch both operations explicitly rather than assuming one implies the other. - Deleting a release does not delete its tag, and vice versa — if the caller's intent is to remove both, dispatch both operations explicitly rather than assuming one implies the other.
- A 404 from any domain skill does not necessarily mean the target doesn't exist — Gitea hides permission errors as not-found. Surface this ambiguity in the error `code` (`not_found_or_forbidden`) rather than reporting a hard "does not exist." - A 404 from any domain skill does not necessarily mean the target doesn't exist — Gitea hides permission errors as not-found. Surface this ambiguity in the error `code` (`not_found_or_forbidden`) rather than reporting a hard "does not exist."
- Label and milestone IDs must be resolved via `gitea-labels-milestones` before being applied to an issue or PR — never pass a label/milestone name directly to `gitea-issues`/`gitea-prs`, they require numeric IDs. - Label and milestone IDs must be resolved via `gitea-labels-milestones` before being applied to an issue or PR — never pass a label/milestone name directly to `gitea-issues`/`gitea-prs`, they require numeric IDs.
@@ -43,7 +44,7 @@ Sub-skills carry their own local copies of relevant gotchas for humans who invok
When invoked, you: When invoked, you:
1. Parse the incoming workflow request (operation type, parameters, context overrides) 1. Parse the incoming workflow request (operation type, parameters, context overrides)
2. Check safety gates: if the operation is destructive (delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) and the request lacks explicit `confirm: true`, fail immediately with "requires explicit confirmation"; deleting the default branch is refused outright regardless of `confirm` 2. Check safety gates: if the operation is destructive (rename-branch, delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) and the request lacks explicit `confirm: true`, fail immediately with "requires explicit confirmation"; deleting the default branch is refused outright regardless of `confirm`
3. Route to the appropriate domain skill: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-branches`, `gitea-files`, `gitea-releases` 3. Route to the appropriate domain skill: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-branches`, `gitea-files`, `gitea-releases`
4. Manage session context: resolve and carry forward `owner`/`repo` and any cached number-space resolutions, passing them explicitly to each skill 4. Manage session context: resolve and carry forward `owner`/`repo` and any cached number-space resolutions, passing them explicitly to each skill
5. Handle error recovery: for recoverable failures (rate limiting, transient 5xx, pagination gaps) retry or complete the operation; for ambiguous 404s, attempt the permission-vs-not-found disambiguation before failing 5. Handle error recovery: for recoverable failures (rate limiting, transient 5xx, pagination gaps) retry or complete the operation; for ambiguous 404s, attempt the permission-vs-not-found disambiguation before failing
@@ -55,12 +56,12 @@ When invoked, you:
- issues: list-issues, get-issue, create-issue, update-issue, comment-issue, search-issues - issues: list-issues, get-issue, create-issue, update-issue, comment-issue, search-issues
- labels/milestones: list-labels, create-label, update-label, delete-label, list-milestones, create-milestone, update-milestone, close-milestone, delete-milestone, resolve-labels - labels/milestones: list-labels, create-label, update-label, delete-label, list-milestones, create-milestone, update-milestone, close-milestone, delete-milestone, resolve-labels
- prs: list-prs, get-pr, create-pr, update-pr, close-pr, reopen-pr, merge-pr, review-pr - prs: list-prs, get-pr, create-pr, update-pr, close-pr, reopen-pr, merge-pr, review-pr
- branches/commits: list-branches, create-branch, delete-branch, list-commits, get-commit - branches/commits: list-branches, create-branch, rename-branch, delete-branch, list-commits, get-commit
- files: get-file, get-dir, get-tree, write-file, delete-file - files: get-file, get-dir, get-tree, write-file, delete-file
- releases/tags: list-releases, get-release, create-release, delete-release, list-tags, create-tag, delete-tag - releases/tags: list-releases, get-release, create-release, delete-release, list-tags, create-tag, delete-tag
- **parameters:** object, operation-specific arguments (issue/PR number, title, body, label names, tag name, file path, etc.) - **parameters:** object, operation-specific arguments (issue/PR number, title, body, label names, tag name, file path, etc.)
- **context:** object (optional), session state to carry forward (`owner`, `repo`, cached number-space resolutions) - **context:** object (optional), session state to carry forward (`owner`, `repo`, cached number-space resolutions)
- **confirm:** boolean (optional), explicit confirmation for destructive operations (required if not set for delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) - **confirm:** boolean (optional), explicit confirmation for destructive operations (required if not set for rename-branch, delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr)
## Process ## Process

View File

@@ -3,11 +3,12 @@ name: gitea-branches
description: > description: >
Use when listing, creating, renaming, or deleting branches in a Gitea repository, Use when listing, creating, renaming, or deleting branches in a Gitea repository,
or reading its commit history — even when the user does not say "Gitea". Not a or reading its commit history — "what commits are on this branch", "what changed
in that commit" — even when the user does not say "Gitea". Not a
local checkout's branches -> `git-branches`. Not local history -> local checkout's branches -> `git-branches`. Not local history ->
`git-history`. Not a PR's head or base -> `gitea-prs`. `git-history`. Not a PR's head or base -> `gitea-prs`.
compatibility: Requires Gitea MCP server configured with a token with write:repository scope; this is confirmed to gate list_branches, create_branch, and delete_branch (Gitea gates reads behind write scope for repo-scoped operations), and is inferred by analogy (not explicitly confirmed by source docs) to also gate list_commits and get_commit. Requires git remote "origin" pointing to the Gitea instance. compatibility: Requires Gitea MCP server configured with a token with write:repository scope; this is confirmed to gate list_branches, create_branch, and delete_branch (Gitea gates reads behind write scope for repo-scoped operations), and is inferred by analogy (not explicitly confirmed by source docs) to also gate rename_branch, list_commits, and get_commit. Requires git remote "origin" pointing to the Gitea instance.
metadata: metadata:
category: integration category: integration

View File

@@ -23,8 +23,8 @@ allowed-tools: mcp__gitea__get_file_contents mcp__gitea__get_dir_contents mcp__g
## Gotchas ## Gotchas
- **A 404 may mean an under-scoped token, not a missing path.** Every tool here gates on `write:repository`, and Gitea masks insufficient scope as 404. Check scopes first. - **A 404 may mean an under-scoped token, not a missing path.** Every tool here gates on `write:repository`, and Gitea masks insufficient scope as 404. Check scopes first.
- **Reads take `ref`, writes take `branch_name`.** One concept, two parameter names — chaining a read into a write drops the branch if you carry the wrong key. - **Reads take `ref` (`tree_sha` on `get_repository_tree`), writes take `branch_name`.** One concept, three names — carry the wrong key and the branch is dropped.
- **`content` is base64 both ways.** Encode before a write, decode after a read. - **`content` is base64 both ways — except under `withLines: true`.** Encode before a write, decode after a read; but with `withLines: true` `content` is already plain JSON text and the reported `"encoding": "base64"` is a lie. Decoding it yields garbage.
## Inputs ## Inputs

View File

@@ -14,9 +14,13 @@ All three read calls select what to read with `ref` — a branch name, tag, or c
`get_file_contents(owner, repo, ref, path)`. `get_file_contents(owner, repo, ref, path)`.
The response carries the file's `sha` at the **top level**, not nested under `content`. That field The response carries the file's `sha` at the **top level**, not nested under `content`. That field
is the write-ready SHA, so capture it whenever a write may follow. Content comes back is the write-ready SHA, so capture it whenever a write may follow.
base64-encoded — decode it. Pass `withLines: true` only when you need numbered lines to quote
specific lines back to the user; omit it for a normal content fetch. Content comes back base64-encoded — decode it — **unless `withLines: true` was passed**, in which
case `content` is already plain text: a JSON array of `{"line": N, "content": "..."}` objects.
The response reports `"encoding": "base64"` either way, so that field is wrong under `withLines`
and decoding on its word yields garbage. Pass `withLines: true` only when you need numbered lines
to quote specific lines back to the user; omit it for a normal content fetch.
## One directory level ## One directory level

View File

@@ -3,8 +3,8 @@ name: gitea-issues
description: > description: >
Use when reading or writing Gitea issues — "create an issue", "what issues are open", Use when reading or writing Gitea issues — "create an issue", "what issues are open",
"close issue #N", "comment on issue #N", "search issues for X" — even when the user does not "close issue #N", "comment on issue #N", "label issue #N", "search issues for X" — even when
say "Gitea". Not pull requests -> `gitea-prs`. the user does not say "Gitea". Not pull requests -> `gitea-prs`.
Not label or milestone definitions -> `gitea-labels-milestones`. Not label or milestone definitions -> `gitea-labels-milestones`.
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
@@ -26,7 +26,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
## Gotchas ## Gotchas
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`. - **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one number space; pass `type: "issues"` to exclude PRs (or `"pulls"`). `is_pull` is returned only by `issue_read method: "get"` — on a list item the only tell is `html_url`'s path segment (`/issues/` vs `/pulls/`).
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels. - **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
- **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually. - **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually.
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist. - **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.

View File

@@ -3,6 +3,7 @@ topic: labels
source_keys: source_keys:
- gitea-mcp-repo - gitea-mcp-repo
- gitea-mcp-slim-go - gitea-mcp-slim-go
- context7-websites-gitea
--- ---
# Label operations # Label operations

View File

@@ -21,7 +21,7 @@
- **URL:** context7:/websites/gitea - **URL:** context7:/websites/gitea
- **Description:** Official Gitea docs mirror on Context7 (docs.gitea.com content) — scoped/exclusive label conventions and milestone/label state-transition semantics - **Description:** Official Gitea docs mirror on Context7 (docs.gitea.com content) — scoped/exclusive label conventions and milestone/label state-transition semantics
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md - **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** SKILL.md, references/label-inference.md - **Contributing files:** SKILL.md, references/labels.md, references/label-inference.md
- **Status:** `extracted` - **Status:** `extracted`
## context7-gitea-tea-cli ## context7-gitea-tea-cli

View File

@@ -36,7 +36,7 @@ List responses trim PRs down to summary fields — `head`/`base` are bare ref st
`"get"`, `"get_diff"`, `"get_files"`, and `"get_status"` are covered here. `"get_reviews"`, `"get_review"`, and `"get_review_comments"` are covered in `references/reviews.md`. `"get"`, `"get_diff"`, `"get_files"`, and `"get_status"` are covered here. `"get_reviews"`, `"get_review"`, and `"get_review_comments"` are covered in `references/reviews.md`.
- `"get"` returns the full PR object: state, draft, merged, mergeable flags; `head`/`base` as full objects (`{ref, sha, repo?}`); `milestone` as a bare title string (not `{id, title}`); and `review_comments` as an integer count, not comment objects (see `references/reviews.md`). - `"get"` returns the full PR object: state, draft, merged, mergeable flags; `head`/`base` as full objects (`{ref, sha, repo?}`); `milestone` as a bare title string (not `{id, title}`); and `review_comments`, *when present*, as an integer count rather than comment objects — it was absent from a live `"get"` on a PR with no inline comments, so verify the key before reading it (see `references/reviews.md`).
- `"get_diff"` returns raw diff text. - `"get_diff"` returns raw diff text.
- `"get_files"` returns the list of changed file objects. - `"get_files"` returns the list of changed file objects.
- `"get_status"` returns the combined commit status for the PR's head commit — CI result only, not review/approval state (see `references/merging.md`). - `"get_status"` returns the combined commit status for the PR's head commit — CI result only, not review/approval state (see `references/merging.md`).

View File

@@ -49,6 +49,6 @@ Get the `comment_id` from `pull_request_read method: "get_review_comments"`. Cal
- `method: "get_review"` (requires `review_id` — omitting it fails with `review_id is required`) — single review detail. - `method: "get_review"` (requires `review_id` — omitting it fails with `review_id is required`) — single review detail.
- `method: "get_review_comments"` (`review_id` **optional** — omit it to list every inline comment on the PR in one call, rather than one review's) — array of inline comments: `id`, `body`, `path`, `position`, `old_position`, `diff_hunk`, `user`, `html_url`, `created_at`, `updated_at`. - `method: "get_review_comments"` (`review_id` **optional** — omit it to list every inline comment on the PR in one call, rather than one review's) — array of inline comments: `id`, `body`, `path`, `position`, `old_position`, `diff_hunk`, `user`, `html_url`, `created_at`, `updated_at`.
**`review_comments` on the `"get"` response is a count, not the comments.** The full PR object returned by `pull_request_read method: "get"` carries `review_comments` as an integer — the number of inline review comments. It is distinct from the `get_review_comments` method above, which returns the actual comment objects; reading the count is no substitute for that call. Older gitea-mcp releases misspelled this key as `review_scomments`; the misspelling was corrected upstream and the deployed v1.7.0 response carries no such key, so treat any instruction that reaches for `review_scomments` as stale. **`review_comments` on the `"get"` response is a count, not the comments — and it may be absent.** Where the full PR object returned by `pull_request_read method: "get"` carries `review_comments`, it is an integer: the number of inline review comments. Presence is not guaranteed. A live `"get"` against a PR with zero inline comments carried no such key at all — only `comments`, which counts issue-style comments, not review ones. Treat it as present only when non-zero, and check for the key before reading it rather than assuming the response shape. Either way it is distinct from the `get_review_comments` method above, which returns the actual comment objects; reading the count is no substitute for that call. Older gitea-mcp releases misspelled this key as `review_scomments`; the misspelling was corrected upstream and the deployed v1.7.0 response carries no such key, so treat any instruction that reaches for `review_scomments` as stale.
**Inline-comment field names differ between write and read.** The `comments` array on `pull_request_review_write method: "create"` uses `old_line_num`/`new_line_num`. The `get_review_comments` read response uses different field names for the same concept — `position` (new-side line) and `old_position` (old-side line). Do not assume the same key names apply on both sides of the round trip. **Inline-comment field names differ between write and read.** The `comments` array on `pull_request_review_write method: "create"` uses `old_line_num`/`new_line_num`. The `get_review_comments` read response uses different field names for the same concept — `position` (new-side line) and `old_position` (old-side line). Do not assume the same key names apply on both sides of the round trip.

View File

@@ -2,11 +2,11 @@
name: gitea-workflow name: gitea-workflow
description: > description: >
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that Use when a human wants an ambiguous Gitea status check — a no-args check-in, a bare number
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which asked *about* without saying issue or PR ("status of #42"), or a capability whose owning skill
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an is unclear. Not an agent caller -> `gitea-orchestrate`. Not a stated action on a number
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component -> ("close #42") -> `gitea-issues`. Not an unambiguous PR request -> `gitea-prs`. Not local-only
`git-workflow`. git -> `git-workflow`.
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
domain skills, which in turn require write:issue and write:repository scopes at minimum. domain skills, which in turn require write:issue and write:repository scopes at minimum.
@@ -31,9 +31,11 @@ The invocation's shape selects exactly one branch.
| Invocation shape | Flow | Reference | | Invocation shape | Flow | Reference |
|---|---|---| |---|---|---|
| No arguments, no specific request | Repo status check-in | `references/status-checkin.md` | | No arguments, no specific request | Repo status check-in | `references/status-checkin.md` |
| A bare number, with neither "issue" nor "PR" said | Resolve which domain the number belongs to | `references/number-resolution.md` | | A bare number the user asks *about*, with neither "issue" nor "PR" said and no action stated | Resolve which domain the number belongs to | `references/number-resolution.md` |
| A named capability whose owning skill is unclear | Route to the domain skill that owns it | `references/skill-index.md` | | A named capability whose owning skill is unclear | Route to the domain skill that owns it | `references/skill-index.md` |
A bare number carrying a stated action ("close #42", "merge #42", "label #42") is not row 2: that is a write, and row 2 only presents detail. Resolve the domain per `references/number-resolution.md`, then hand the action to `gitea-issues` or `gitea-prs` to perform.
Read only the reference file matching the selected branch — each is self-contained for its concern. Read only the reference file matching the selected branch — each is self-contained for its concern.
## Report ## Report

View File

@@ -16,4 +16,6 @@ The user has referenced a bare number without saying "issue" or "PR" (e.g. "what
- `false` or absent → it's an issue. Present the issue detail already retrieved. - `false` or absent → it's an issue. Present the issue detail already retrieved.
3. If the resolution call 404s, don't conclude the number doesn't exist. Gitea hides permission errors as not-found (documented in `gitea-issues`' Gotchas), so report the 404 and suggest verifying the token carries `write:issue` rather than reporting "no such issue or PR." 3. If the resolution call 404s, don't conclude the number doesn't exist. Gitea hides permission errors as not-found (documented in `gitea-issues`' Gotchas), so report the 404 and suggest verifying the token carries `write:issue` rather than reporting "no such issue or PR."
If the user stated an action on the number rather than asking about it, resolution is only step one: hand the action, with the resolved domain, to `gitea-issues` or `gitea-prs` to carry out. Presenting detail is not a substitute for performing the write.
Then report per `SKILL.md`'s Report section, saying which domain the number turned out to be before showing detail ("That's a pull request:" / "That's an issue:") — otherwise the user cannot tell the resolution happened. Then report per `SKILL.md`'s Report section, saying which domain the number turned out to be before showing detail ("That's a pull request:" / "That's an issue:") — otherwise the user cannot tell the resolution happened.

View File

@@ -1,6 +1,6 @@
{ {
"name": "gitea", "name": "gitea",
"version": "1.4.0", "version": "1.3.7",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "gitea", "name": "gitea",
"version": "1.4.0", "version": "1.3.7",
"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.", "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": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -23,6 +23,7 @@ You resolve `owner`/`repo` once per session (via `git remote -v` on `origin`) an
These are non-negotiable regardless of `confirm` or any skill-local override: These are non-negotiable regardless of `confirm` or any skill-local override:
- Never delete the repository's default branch (typically `main` or `master`) — refused outright, independent of `confirm`. - Never delete the repository's default branch (typically `main` or `master`) — refused outright, independent of `confirm`.
- `delete_release` takes a numeric `id`; `delete_tag` takes a `tag_name` string. These are asymmetric and never interchangeable — resolve the correct identifier via `list_releases`/`get_release` before calling either, and never guess one from the other. - `delete_release` takes a numeric `id`; `delete_tag` takes a `tag_name` string. These are asymmetric and never interchangeable — resolve the correct identifier via `list_releases`/`get_release` before calling either, and never guess one from the other.
- `rename-branch` is gated like a delete even though it destroys nothing: what a rename does to open PRs using the branch as head or base, to a matching protection rule, and to every other clone's tracking branch is unconfirmed by `gitea-branches`' sources. Require `confirm: true`, and verify the PR and protection sides afterwards.
- Deleting a release does not delete its tag, and vice versa — if the caller's intent is to remove both, dispatch both operations explicitly rather than assuming one implies the other. - Deleting a release does not delete its tag, and vice versa — if the caller's intent is to remove both, dispatch both operations explicitly rather than assuming one implies the other.
- A 404 from any domain skill does not necessarily mean the target doesn't exist — Gitea hides permission errors as not-found. Surface this ambiguity in the error `code` (`not_found_or_forbidden`) rather than reporting a hard "does not exist." - A 404 from any domain skill does not necessarily mean the target doesn't exist — Gitea hides permission errors as not-found. Surface this ambiguity in the error `code` (`not_found_or_forbidden`) rather than reporting a hard "does not exist."
- Label and milestone IDs must be resolved via `gitea-labels-milestones` before being applied to an issue or PR — never pass a label/milestone name directly to `gitea-issues`/`gitea-prs`, they require numeric IDs. - Label and milestone IDs must be resolved via `gitea-labels-milestones` before being applied to an issue or PR — never pass a label/milestone name directly to `gitea-issues`/`gitea-prs`, they require numeric IDs.
@@ -43,7 +44,7 @@ Sub-skills carry their own local copies of relevant gotchas for humans who invok
When invoked, you: When invoked, you:
1. Parse the incoming workflow request (operation type, parameters, context overrides) 1. Parse the incoming workflow request (operation type, parameters, context overrides)
2. Check safety gates: if the operation is destructive (delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) and the request lacks explicit `confirm: true`, fail immediately with "requires explicit confirmation"; deleting the default branch is refused outright regardless of `confirm` 2. Check safety gates: if the operation is destructive (rename-branch, delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) and the request lacks explicit `confirm: true`, fail immediately with "requires explicit confirmation"; deleting the default branch is refused outright regardless of `confirm`
3. Route to the appropriate domain skill: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-branches`, `gitea-files`, `gitea-releases` 3. Route to the appropriate domain skill: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-branches`, `gitea-files`, `gitea-releases`
4. Manage session context: resolve and carry forward `owner`/`repo` and any cached number-space resolutions, passing them explicitly to each skill 4. Manage session context: resolve and carry forward `owner`/`repo` and any cached number-space resolutions, passing them explicitly to each skill
5. Handle error recovery: for recoverable failures (rate limiting, transient 5xx, pagination gaps) retry or complete the operation; for ambiguous 404s, attempt the permission-vs-not-found disambiguation before failing 5. Handle error recovery: for recoverable failures (rate limiting, transient 5xx, pagination gaps) retry or complete the operation; for ambiguous 404s, attempt the permission-vs-not-found disambiguation before failing
@@ -55,12 +56,12 @@ When invoked, you:
- issues: list-issues, get-issue, create-issue, update-issue, comment-issue, search-issues - issues: list-issues, get-issue, create-issue, update-issue, comment-issue, search-issues
- labels/milestones: list-labels, create-label, update-label, delete-label, list-milestones, create-milestone, update-milestone, close-milestone, delete-milestone, resolve-labels - labels/milestones: list-labels, create-label, update-label, delete-label, list-milestones, create-milestone, update-milestone, close-milestone, delete-milestone, resolve-labels
- prs: list-prs, get-pr, create-pr, update-pr, close-pr, reopen-pr, merge-pr, review-pr - prs: list-prs, get-pr, create-pr, update-pr, close-pr, reopen-pr, merge-pr, review-pr
- branches/commits: list-branches, create-branch, delete-branch, list-commits, get-commit - branches/commits: list-branches, create-branch, rename-branch, delete-branch, list-commits, get-commit
- files: get-file, get-dir, get-tree, write-file, delete-file - files: get-file, get-dir, get-tree, write-file, delete-file
- releases/tags: list-releases, get-release, create-release, delete-release, list-tags, create-tag, delete-tag - releases/tags: list-releases, get-release, create-release, delete-release, list-tags, create-tag, delete-tag
- **parameters:** object, operation-specific arguments (issue/PR number, title, body, label names, tag name, file path, etc.) - **parameters:** object, operation-specific arguments (issue/PR number, title, body, label names, tag name, file path, etc.)
- **context:** object (optional), session state to carry forward (`owner`, `repo`, cached number-space resolutions) - **context:** object (optional), session state to carry forward (`owner`, `repo`, cached number-space resolutions)
- **confirm:** boolean (optional), explicit confirmation for destructive operations (required if not set for delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr) - **confirm:** boolean (optional), explicit confirmation for destructive operations (required if not set for rename-branch, delete-branch, delete-release, delete-tag, delete-label, delete-milestone, delete-file, merge-pr)
## Process ## Process

View File

@@ -1,5 +1,5 @@
name: gitea name: gitea
version: 1.4.0 version: 1.3.7
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone. description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
author: author:
name: Defame1297 name: Defame1297

View File

@@ -3,11 +3,12 @@ name: gitea-branches
description: > description: >
Use when listing, creating, renaming, or deleting branches in a Gitea repository, Use when listing, creating, renaming, or deleting branches in a Gitea repository,
or reading its commit history — even when the user does not say "Gitea". Not a or reading its commit history — "what commits are on this branch", "what changed
in that commit" — even when the user does not say "Gitea". Not a
local checkout's branches -> `git-branches`. Not local history -> local checkout's branches -> `git-branches`. Not local history ->
`git-history`. Not a PR's head or base -> `gitea-prs`. `git-history`. Not a PR's head or base -> `gitea-prs`.
compatibility: Requires Gitea MCP server configured with a token with write:repository scope; this is confirmed to gate list_branches, create_branch, and delete_branch (Gitea gates reads behind write scope for repo-scoped operations), and is inferred by analogy (not explicitly confirmed by source docs) to also gate list_commits and get_commit. Requires git remote "origin" pointing to the Gitea instance. compatibility: Requires Gitea MCP server configured with a token with write:repository scope; this is confirmed to gate list_branches, create_branch, and delete_branch (Gitea gates reads behind write scope for repo-scoped operations), and is inferred by analogy (not explicitly confirmed by source docs) to also gate rename_branch, list_commits, and get_commit. Requires git remote "origin" pointing to the Gitea instance.
metadata: metadata:
category: integration category: integration

View File

@@ -23,8 +23,8 @@ allowed-tools: mcp__gitea__get_file_contents mcp__gitea__get_dir_contents mcp__g
## Gotchas ## Gotchas
- **A 404 may mean an under-scoped token, not a missing path.** Every tool here gates on `write:repository`, and Gitea masks insufficient scope as 404. Check scopes first. - **A 404 may mean an under-scoped token, not a missing path.** Every tool here gates on `write:repository`, and Gitea masks insufficient scope as 404. Check scopes first.
- **Reads take `ref`, writes take `branch_name`.** One concept, two parameter names — chaining a read into a write drops the branch if you carry the wrong key. - **Reads take `ref` (`tree_sha` on `get_repository_tree`), writes take `branch_name`.** One concept, three names — carry the wrong key and the branch is dropped.
- **`content` is base64 both ways.** Encode before a write, decode after a read. - **`content` is base64 both ways — except under `withLines: true`.** Encode before a write, decode after a read; but with `withLines: true` `content` is already plain JSON text and the reported `"encoding": "base64"` is a lie. Decoding it yields garbage.
## Inputs ## Inputs

View File

@@ -14,9 +14,13 @@ All three read calls select what to read with `ref` — a branch name, tag, or c
`get_file_contents(owner, repo, ref, path)`. `get_file_contents(owner, repo, ref, path)`.
The response carries the file's `sha` at the **top level**, not nested under `content`. That field The response carries the file's `sha` at the **top level**, not nested under `content`. That field
is the write-ready SHA, so capture it whenever a write may follow. Content comes back is the write-ready SHA, so capture it whenever a write may follow.
base64-encoded — decode it. Pass `withLines: true` only when you need numbered lines to quote
specific lines back to the user; omit it for a normal content fetch. Content comes back base64-encoded — decode it — **unless `withLines: true` was passed**, in which
case `content` is already plain text: a JSON array of `{"line": N, "content": "..."}` objects.
The response reports `"encoding": "base64"` either way, so that field is wrong under `withLines`
and decoding on its word yields garbage. Pass `withLines: true` only when you need numbered lines
to quote specific lines back to the user; omit it for a normal content fetch.
## One directory level ## One directory level

View File

@@ -3,8 +3,8 @@ name: gitea-issues
description: > description: >
Use when reading or writing Gitea issues — "create an issue", "what issues are open", Use when reading or writing Gitea issues — "create an issue", "what issues are open",
"close issue #N", "comment on issue #N", "search issues for X" — even when the user does not "close issue #N", "comment on issue #N", "label issue #N", "search issues for X" — even when
say "Gitea". Not pull requests -> `gitea-prs`. the user does not say "Gitea". Not pull requests -> `gitea-prs`.
Not label or milestone definitions -> `gitea-labels-milestones`. Not label or milestone definitions -> `gitea-labels-milestones`.
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
@@ -26,7 +26,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
## Gotchas ## Gotchas
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`. - **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one number space; pass `type: "issues"` to exclude PRs (or `"pulls"`). `is_pull` is returned only by `issue_read method: "get"` — on a list item the only tell is `html_url`'s path segment (`/issues/` vs `/pulls/`).
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels. - **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
- **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually. - **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually.
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist. - **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.

View File

@@ -3,6 +3,7 @@ topic: labels
source_keys: source_keys:
- gitea-mcp-repo - gitea-mcp-repo
- gitea-mcp-slim-go - gitea-mcp-slim-go
- context7-websites-gitea
--- ---
# Label operations # Label operations

View File

@@ -21,7 +21,7 @@
- **URL:** context7:/websites/gitea - **URL:** context7:/websites/gitea
- **Description:** Official Gitea docs mirror on Context7 (docs.gitea.com content) — scoped/exclusive label conventions and milestone/label state-transition semantics - **Description:** Official Gitea docs mirror on Context7 (docs.gitea.com content) — scoped/exclusive label conventions and milestone/label state-transition semantics
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md - **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** SKILL.md, references/label-inference.md - **Contributing files:** SKILL.md, references/labels.md, references/label-inference.md
- **Status:** `extracted` - **Status:** `extracted`
## context7-gitea-tea-cli ## context7-gitea-tea-cli

View File

@@ -36,7 +36,7 @@ List responses trim PRs down to summary fields — `head`/`base` are bare ref st
`"get"`, `"get_diff"`, `"get_files"`, and `"get_status"` are covered here. `"get_reviews"`, `"get_review"`, and `"get_review_comments"` are covered in `references/reviews.md`. `"get"`, `"get_diff"`, `"get_files"`, and `"get_status"` are covered here. `"get_reviews"`, `"get_review"`, and `"get_review_comments"` are covered in `references/reviews.md`.
- `"get"` returns the full PR object: state, draft, merged, mergeable flags; `head`/`base` as full objects (`{ref, sha, repo?}`); `milestone` as a bare title string (not `{id, title}`); and `review_comments` as an integer count, not comment objects (see `references/reviews.md`). - `"get"` returns the full PR object: state, draft, merged, mergeable flags; `head`/`base` as full objects (`{ref, sha, repo?}`); `milestone` as a bare title string (not `{id, title}`); and `review_comments`, *when present*, as an integer count rather than comment objects — it was absent from a live `"get"` on a PR with no inline comments, so verify the key before reading it (see `references/reviews.md`).
- `"get_diff"` returns raw diff text. - `"get_diff"` returns raw diff text.
- `"get_files"` returns the list of changed file objects. - `"get_files"` returns the list of changed file objects.
- `"get_status"` returns the combined commit status for the PR's head commit — CI result only, not review/approval state (see `references/merging.md`). - `"get_status"` returns the combined commit status for the PR's head commit — CI result only, not review/approval state (see `references/merging.md`).

View File

@@ -49,6 +49,6 @@ Get the `comment_id` from `pull_request_read method: "get_review_comments"`. Cal
- `method: "get_review"` (requires `review_id` — omitting it fails with `review_id is required`) — single review detail. - `method: "get_review"` (requires `review_id` — omitting it fails with `review_id is required`) — single review detail.
- `method: "get_review_comments"` (`review_id` **optional** — omit it to list every inline comment on the PR in one call, rather than one review's) — array of inline comments: `id`, `body`, `path`, `position`, `old_position`, `diff_hunk`, `user`, `html_url`, `created_at`, `updated_at`. - `method: "get_review_comments"` (`review_id` **optional** — omit it to list every inline comment on the PR in one call, rather than one review's) — array of inline comments: `id`, `body`, `path`, `position`, `old_position`, `diff_hunk`, `user`, `html_url`, `created_at`, `updated_at`.
**`review_comments` on the `"get"` response is a count, not the comments.** The full PR object returned by `pull_request_read method: "get"` carries `review_comments` as an integer — the number of inline review comments. It is distinct from the `get_review_comments` method above, which returns the actual comment objects; reading the count is no substitute for that call. Older gitea-mcp releases misspelled this key as `review_scomments`; the misspelling was corrected upstream and the deployed v1.7.0 response carries no such key, so treat any instruction that reaches for `review_scomments` as stale. **`review_comments` on the `"get"` response is a count, not the comments — and it may be absent.** Where the full PR object returned by `pull_request_read method: "get"` carries `review_comments`, it is an integer: the number of inline review comments. Presence is not guaranteed. A live `"get"` against a PR with zero inline comments carried no such key at all — only `comments`, which counts issue-style comments, not review ones. Treat it as present only when non-zero, and check for the key before reading it rather than assuming the response shape. Either way it is distinct from the `get_review_comments` method above, which returns the actual comment objects; reading the count is no substitute for that call. Older gitea-mcp releases misspelled this key as `review_scomments`; the misspelling was corrected upstream and the deployed v1.7.0 response carries no such key, so treat any instruction that reaches for `review_scomments` as stale.
**Inline-comment field names differ between write and read.** The `comments` array on `pull_request_review_write method: "create"` uses `old_line_num`/`new_line_num`. The `get_review_comments` read response uses different field names for the same concept — `position` (new-side line) and `old_position` (old-side line). Do not assume the same key names apply on both sides of the round trip. **Inline-comment field names differ between write and read.** The `comments` array on `pull_request_review_write method: "create"` uses `old_line_num`/`new_line_num`. The `get_review_comments` read response uses different field names for the same concept — `position` (new-side line) and `old_position` (old-side line). Do not assume the same key names apply on both sides of the round trip.

View File

@@ -2,11 +2,11 @@
name: gitea-workflow name: gitea-workflow
description: > description: >
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that Use when a human wants an ambiguous Gitea status check — a no-args check-in, a bare number
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which asked *about* without saying issue or PR ("status of #42"), or a capability whose owning skill
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an is unclear. Not an agent caller -> `gitea-orchestrate`. Not a stated action on a number
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component -> ("close #42") -> `gitea-issues`. Not an unambiguous PR request -> `gitea-prs`. Not local-only
`git-workflow`. git -> `git-workflow`.
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
domain skills, which in turn require write:issue and write:repository scopes at minimum. domain skills, which in turn require write:issue and write:repository scopes at minimum.
@@ -31,9 +31,11 @@ The invocation's shape selects exactly one branch.
| Invocation shape | Flow | Reference | | Invocation shape | Flow | Reference |
|---|---|---| |---|---|---|
| No arguments, no specific request | Repo status check-in | `references/status-checkin.md` | | No arguments, no specific request | Repo status check-in | `references/status-checkin.md` |
| A bare number, with neither "issue" nor "PR" said | Resolve which domain the number belongs to | `references/number-resolution.md` | | A bare number the user asks *about*, with neither "issue" nor "PR" said and no action stated | Resolve which domain the number belongs to | `references/number-resolution.md` |
| A named capability whose owning skill is unclear | Route to the domain skill that owns it | `references/skill-index.md` | | A named capability whose owning skill is unclear | Route to the domain skill that owns it | `references/skill-index.md` |
A bare number carrying a stated action ("close #42", "merge #42", "label #42") is not row 2: that is a write, and row 2 only presents detail. Resolve the domain per `references/number-resolution.md`, then hand the action to `gitea-issues` or `gitea-prs` to perform.
Read only the reference file matching the selected branch — each is self-contained for its concern. Read only the reference file matching the selected branch — each is self-contained for its concern.
## Report ## Report

View File

@@ -16,4 +16,6 @@ The user has referenced a bare number without saying "issue" or "PR" (e.g. "what
- `false` or absent → it's an issue. Present the issue detail already retrieved. - `false` or absent → it's an issue. Present the issue detail already retrieved.
3. If the resolution call 404s, don't conclude the number doesn't exist. Gitea hides permission errors as not-found (documented in `gitea-issues`' Gotchas), so report the 404 and suggest verifying the token carries `write:issue` rather than reporting "no such issue or PR." 3. If the resolution call 404s, don't conclude the number doesn't exist. Gitea hides permission errors as not-found (documented in `gitea-issues`' Gotchas), so report the 404 and suggest verifying the token carries `write:issue` rather than reporting "no such issue or PR."
If the user stated an action on the number rather than asking about it, resolution is only step one: hand the action, with the resolved domain, to `gitea-issues` or `gitea-prs` to carry out. Presenting detail is not a substitute for performing the write.
Then report per `SKILL.md`'s Report section, saying which domain the number turned out to be before showing detail ("That's a pull request:" / "That's an issue:") — otherwise the user cannot tell the resolution happened. Then report per `SKILL.md`'s Report section, saying which domain the number turned out to be before showing detail ("That's a pull request:" / "That's an issue:") — otherwise the user cannot tell the resolution happened.

View File

@@ -57,8 +57,9 @@ Pass the path to either agent file as the argument.
| `assets/vale/styles/Kyberforge/VagueWording.yml` | Flags vague capability wording ("helps with", "utilize", "assists with", "used for") in descriptions | | `assets/vale/styles/Kyberforge/VagueWording.yml` | Flags vague capability wording ("helps with", "utilize", "assists with", "used for") in descriptions |
| `assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml` | Flags CC-specific "Use proactively" phrasing with no effect in Copilot descriptions | | `assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml` | Flags CC-specific "Use proactively" phrasing with no effect in Copilot descriptions |
| `references/README.md` | Directory documentation for references/ | | `references/README.md` | Directory documentation for references/ |
| `references/description-quality.md` | Rubric for the description dimension — three-part shape, the 250/400-character budget, the hand-invoked contract, and the internal-mechanics FAIL | | `references/finding-criteria.md` | Every dimension's FAIL and SUGGESTION criteria — the one Step 3 file read on every run; it decides which rubrics below are worth loading |
| `references/body-and-delegation.md` | Rubric for the body, delegation and comment-discipline dimensions — the delegation FAIL and why agents take no body word gate | | `references/description-quality.md` | Rubric for the description dimension — why the description is the expensive part, the hand-invoked contract, the three-part shape, indirect triggers, and near-miss exclusions |
| `references/body-and-delegation.md` | Rubric for the body, delegation and comment-discipline dimensions — the core test, the delegation FAIL, why agents take no body word gate, and what an agent body is for |
| `references/scope-plugin-apm.md` | Scope contract for a single vendor-neutral APM agent file — allowlist, dimension routing, and the dimensions that do not apply | | `references/scope-plugin-apm.md` | Scope contract for a single vendor-neutral APM agent file — allowlist, dimension routing, and the dimensions that do not apply |
| `references/scope-project-user.md` | Scope contract for a CC / Copilot pair — counterpart derivation, provider field rules, pair consistency | | `references/scope-project-user.md` | Scope contract for a CC / Copilot pair — counterpart derivation, provider field rules, pair consistency |
| `references/validation-scripts.md` | Loaded only when a Step 1 script fails or cannot run — scope-detection walk-up, manual fallback checks, known script failures | | `references/validation-scripts.md` | Loaded only when a Step 1 script fails or cannot run — scope-detection walk-up, manual fallback checks, known script failures |

View File

@@ -37,7 +37,7 @@ bash scripts/vale-wrap.sh <agent-file> [<counterpart-file>]
If a validation script fails or cannot run — Bash denied, `python3` or `vale` absent, `references/field-inventory.md` missing — read `references/validation-scripts.md`; what these scripts measure is not reproducible by reading. If a validation script fails or cannot run — Bash denied, `python3` or `vale` absent, `references/field-inventory.md` missing — read `references/validation-scripts.md`; what these scripts measure is not reproducible by reading.
`validate-provenance.sh` prints nothing on success and runs at plugin/APM scope only, exiting 0 silently elsewhere. Its FAIL findings become a separate `### Provenance` dimension, and it emits Why and Fix itself — surface those verbatim. `validate-provenance.sh` prints nothing on success, so read its exit code before you read its silence. **0** is a genuine pass, including the silent exit 0 at project or user scope, where plugin-scope provenance does not apply. **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 bad argument or a missing dependency, 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 it as a clean pass. `validate.sh` uses the same 2 tier.
`vale-wrap.sh` applies the bundled `Kyberforge` style as a prefilter. Pass no `--config`; the wrapper locates its own. At project/user scope pass both files of the pair, not only the one you were handed. 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: `vale-wrap.sh` applies the bundled `Kyberforge` style as a prefilter. Pass no `--config`; the wrapper locates its own. At project/user scope pass both files of the pair, not only the one you were handed. 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:
@@ -57,14 +57,14 @@ Read the agent file end to end, and at project/user scope its counterpart too. A
## Step 3 — Qualitative audit ## Step 3 — Qualitative audit
Load a dimension's rubric before judging that dimension. Read `references/finding-criteria.md` first — every dimension's FAIL and SUGGESTION criteria. Load the rubric below only for a dimension the criteria put in play: one carrying a candidate finding, or one where the criterion alone does not settle the call.
| Dimension | Read | | Dimension | Rubric |
|---|---| |---|---|
| description | `references/description-quality.md` | | description | `references/description-quality.md` |
| body, delegation, comment-discipline | `references/body-and-delegation.md` | | body, delegation, comment-discipline | `references/body-and-delegation.md` |
Cite file and line number for every finding. Each rubric is the reasoning behind its criteria, not a second copy of them. Cite file and line number for every finding.
## Step 4 — Report ## Step 4 — Report

View File

@@ -10,8 +10,9 @@ Additional documentation agents load on demand.
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `description-quality.md` | Rubric for the description dimension — three-part shape, the 250/400-character budget, the hand-invoked contract, and the internal-mechanics FAIL. | | `finding-criteria.md` | Every dimension's FAIL and SUGGESTION criteria — the one Step 3 file read on every run; it decides which rubrics below are worth loading. |
| `body-and-delegation.md` | Rubric for the body, delegation and comment-discipline dimensions — the delegation FAIL, why agents take no body word gate, and what an agent body is for. | | `description-quality.md` | Rubric for the description dimension — why the description is the expensive part, the hand-invoked contract, the three-part shape, indirect triggers, and near-miss exclusions. |
| `body-and-delegation.md` | Rubric for the body, delegation and comment-discipline dimensions — the core test, the delegation FAIL, why agents take no body word gate, and what an agent body is for. |
| `scope-plugin-apm.md` | Contract for a single vendor-neutral `.apm/agents/<name>.agent.md` file — allowlist, dimension routing, and the dimensions that do not apply. | | `scope-plugin-apm.md` | Contract for a single vendor-neutral `.apm/agents/<name>.agent.md` file — allowlist, dimension routing, and the dimensions that do not apply. |
| `scope-project-user.md` | Contract for a Claude Code / Copilot file pair — counterpart derivation, provider field rules, and pair consistency. | | `scope-project-user.md` | Contract for a Claude Code / Copilot file pair — counterpart derivation, provider field rules, and pair consistency. |
| `validation-scripts.md` | Loaded only when a Step 1 script fails or cannot run — scope-detection walk-up, manual fallback checks, and known script failures. | | `validation-scripts.md` | Loaded only when a Step 1 script fails or cannot run — scope-detection walk-up, manual fallback checks, and known script failures. |

View File

@@ -98,24 +98,8 @@ to a shipped file. At plugin/APM scope the stakes are higher than tidiness: `apm
frontmatter verbatim to every target, `<!-- ... -->` is not valid YAML, and `validate.sh` FAILs a frontmatter verbatim to every target, `<!-- ... -->` is not valid YAML, and `validate.sh` FAILs a
frontmatter block that still contains one. frontmatter block that still contains one.
## Auditing guidance ## Where the criteria live
Flag as FAIL if: Every FAIL and SUGGESTION criterion for these dimensions is in `references/finding-criteria.md`,
which Step 3 reads on every run. This file is the reasoning behind them, loaded only when that file
- The body restates a procedure owned by a skill the agent can invoke — Fix: invoke `<skill>` puts the body, delegation or comment-discipline dimension in play.
instead
- A sentence answers "no" to the core test — it is padding
- A decision point presents a menu of options with no default
- An instruction repeats content already in the description
- Frontmatter comments are template scaffolding rather than instruction, or are HTML comments at
plugin/APM scope
- A prescriptive sequence is used where flexibility is fine, or the reverse
Flag as SUGGESTION if:
- The body does not open with a direct role instruction
- The body specifies no error handling — nothing tells the agent what to do with malformed,
missing or contradictory input
- The job the agent describes is unbounded, or bounded only implicitly
- A rationale is missing from a rule the agent is expected to enforce — present but unexplained
- Comments are useful but verbose enough to bury the field they annotate

View File

@@ -82,49 +82,8 @@ description: >
dispatched and safety-gated. Not conversational git help -> git-workflow. dispatched and safety-gated. Not conversational git help -> git-workflow.
``` ```
## Auditing guidance ## Where the criteria live
Flag as FAIL if: Every FAIL and SUGGESTION criterion for this dimension is in `references/finding-criteria.md`,
which Step 3 reads on every run. This file is the reasoning behind them, loaded only when that file
- **Over 400 characters.** Measured on the folded YAML value, not the raw source lines. puts the description dimension in play.
`validate.sh` reports the number; do not re-derive it, but do point the Fix at what to cut. Agent
descriptions have no platform-documented ceiling of their own — unlike a skill's 1,024-character
spec limit, the 400-character house ceiling is the only hard limit there is, so do not go looking
for a backstop behind it.
- **Internal mechanics appear in the description.** Any of:
- capability enumeration or a feature list;
- output-format detail ("Produces a compact findings report with Why and Fix per finding");
- composition or architecture notes ("composes X rather than duplicating Y", "a cross-cutting
shared agent", "the human-facing entry point", "replaces the old flat invocation");
- implementation detail ("self-validates via a bundled deterministic script").
None of it can change a routing decision and all of it is preloaded.
`Kyberforge.CompositionNote` catches the common phrasings deterministically; the rest is
judgment. This is the rule that deflates a description, so apply it before reaching for length.
- **The same trigger stated twice in two registers** — a verb list, then the same verbs re-quoted
as user phrasings, usually in the same order. One register, whichever routes better.
- **Descriptive rather than imperative phrasing** (`This agent ...`, `This is the ...`).
`Kyberforge.DescriptionOpener` catches any opener matching `^This`. There is no action-verb rule
here and never was a defensible one: an `Orchestrates ...` or `Audits ...` opener is a catalogue
entry, not a trigger.
- **Vague capabilities** ("helps with agents" where "audits an agent definition pair" was
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
judgment.
- **A boundary clause naming a target that does not resolve** to a real skill directory or agent
file in the authoring source. `validate.sh` resolves this for agent files at both scopes and
reports each unresolved target itself — take its verdict rather than re-resolving the name by
hand, because a hand-walk over a different universe can contradict it. What is left to you is
semantic and the script cannot reach it: whether a target that *does* resolve is the right
sibling to exclude, and whether a clause naming no target at all ("examine the files manually")
should have named one.
- **`Use proactively` in a Copilot or vendor-neutral description.**
`KyberforgeCopilot.ProactivePhrase` catches it. The phrase steers the Claude Code runtime and
does nothing anywhere else, so in a `.agent.md` it is preloaded text that buys no behaviour.
- **Trigger-list, boundary or indirect-trigger content on a hand-invoked agent** — see Step 0.
Flag as SUGGESTION if:
- **Over 250 characters** but at or under 400. This tier is what moves the corpus average; the FAIL
tier only stops outliers. Report it rather than treating a 399-character description as clean.
- A near-miss exclusion is present but targets a weak near-miss.
- An indirect trigger is present and warranted but could name the omitted phrasing more precisely.

View File

@@ -0,0 +1,98 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
---
# Finding Criteria
Every FAIL and SUGGESTION criterion, for every qualitative dimension, and nothing else. The
reasoning each criterion stands on, its worked examples and its house rules stay in that
dimension's rubric, which Step 3 loads only for a dimension this file puts in play.
Two rules on using it:
- A criterion that plainly applies is a finding. Write it up citing file and line.
- A criterion that might apply, or whose call the wording here does not settle, is a reason to load
that dimension's rubric — never a reason to drop the candidate. This file decides which rubrics
to read; it does not settle a close call on its own.
## description — `references/description-quality.md`
Flag as FAIL if:
- **Over 400 characters.** Measured on the folded YAML value, not the raw source lines.
`validate.sh` reports the number; do not re-derive it, but do point the Fix at what to cut. Agent
descriptions have no platform-documented ceiling of their own, so 400 is the only hard limit
there is — do not go looking for a backstop behind it.
- **Internal mechanics appear in the description.** Any of:
- capability enumeration or a feature list;
- output-format detail ("Produces a compact findings report with Why and Fix per finding");
- composition or architecture notes ("composes X rather than duplicating Y", "a cross-cutting
shared agent", "the human-facing entry point", "replaces the old flat invocation");
- implementation detail ("self-validates via a bundled deterministic script").
None of it can change a routing decision and all of it is preloaded.
`Kyberforge.CompositionNote` catches the common phrasings deterministically; the rest is
judgment. This is the rule that deflates a description, so apply it before reaching for length.
- **The same trigger stated twice in two registers** — a verb list, then the same verbs re-quoted
as user phrasings, usually in the same order. One register, whichever routes better.
- **Descriptive rather than imperative phrasing** (`This agent ...`, `This is the ...`).
`Kyberforge.DescriptionOpener` catches any opener matching `^This`.
- **Vague capabilities** ("helps with agents" where "audits an agent definition pair" was
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
judgment.
- **`Use proactively` in a Copilot or vendor-neutral description.**
`KyberforgeCopilot.ProactivePhrase` catches it. The phrase steers the Claude Code runtime and
does nothing anywhere else, so in a `.agent.md` it is preloaded text that buys no behaviour.
- **Trigger-list, boundary or indirect-trigger content on a hand-invoked agent** — see Step 0 of
`references/description-quality.md`.
Flag as SUGGESTION if:
- **Over 250 characters** but at or under 400. This tier is what moves the corpus average; the FAIL
tier only stops outliers. Report it rather than treating a 399-character description as clean.
- A near-miss exclusion is present but targets a weak near-miss.
- An indirect trigger is present and warranted but could name the omitted phrasing more precisely.
**An unresolved boundary target is not graded here.** `validate.sh` resolves boundary targets for
agent files at both scopes and tiers the verdict itself — route notation (`/name`, an arrow form)
is an ERROR, the bare prose form a SUGGESTION unless a second target in the same sentence resolves.
Step 1 has already filed it under `### Structure` at that tier. Take the script's verdict rather
than re-resolving the name by hand, and do not re-grade it under description: a hand-walk over a
different universe can contradict the script, and re-grading puts one target in the report twice.
What is left to judgment is semantic and the script cannot reach it: whether a target that *does*
resolve is the right sibling to exclude, and whether a clause naming no target at all ("examine the
files manually") should have named one.
## body, delegation and comment-discipline — `references/body-and-delegation.md`
Flag as FAIL if:
- The body restates a procedure owned by a skill the agent can invoke — Fix: invoke `<skill>`
instead
- A sentence answers "no" to the core test — it is padding
- A decision point presents a menu of options with no default
- An instruction repeats content already in the description
- Frontmatter comments are template scaffolding rather than instruction, or are HTML comments at
plugin/APM scope
- A prescriptive sequence is used where flexibility is fine, or the reverse
Flag as SUGGESTION if:
- The body does not open with a direct role instruction
- The body specifies no error handling — nothing tells the agent what to do with malformed,
missing or contradictory input
- The job the agent describes is unbounded, or bounded only implicitly
- A rationale is missing from a rule the agent is expected to enforce — present but unexplained
- Comments are useful but verbose enough to bury the field they annotate
**Never report an agent body as too long on a word count.** ADR-0020 gates a skill body at
600/900 words and deliberately gates an agent body at nothing, because an agent body *becomes* the
system prompt of a fresh context rather than competing with a live conversation. There is no number
to cite. The one length signal that applies is the Copilot runtime's 30,000-character body limit,
which `validate.sh` already reports as a SUGGESTION. Length is judged through the delegation FAIL
above instead.

View File

@@ -14,7 +14,7 @@ source_keys:
- **URL:** context7:/websites/code_claude - **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md - **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — plugin manifest schema, subagent definition types, marketplace JSON format, agent markdown file format - **Description:** Official Claude Code documentation site indexed by Context7 — plugin manifest schema, subagent definition types, marketplace JSON format, agent markdown file format
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.md - **Contributing files:** SKILL.md, references/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.md
- **Status:** `extracted` - **Status:** `extracted`
## claude-code-plugins-docs ## claude-code-plugins-docs
@@ -22,7 +22,7 @@ source_keys:
- **URL:** https://code.claude.com/docs/en/plugins - **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md - **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — plugin structure, manifest fields, loading methods, skill namespacing, agent activation, marketplace submission - **Description:** Official Claude Code plugin authoring guide — plugin structure, manifest fields, loading methods, skill namespacing, agent activation, marketplace submission
- **Contributing files:** SKILL.md, references/field-inventory.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/validation-scripts.md - **Contributing files:** SKILL.md, references/finding-criteria.md, references/field-inventory.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/validation-scripts.md
- **Status:** `extracted` - **Status:** `extracted`
## claude-code-subagents-docs ## claude-code-subagents-docs
@@ -30,7 +30,7 @@ source_keys:
- **URL:** https://code.claude.com/docs/en/sub-agents - **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md - **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations - **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.md - **Contributing files:** SKILL.md, references/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.md
- **Status:** `extracted` - **Status:** `extracted`
## context7-github-en-copilot ## context7-github-en-copilot
@@ -38,7 +38,7 @@ source_keys:
- **URL:** context7:/websites/github_en_copilot - **URL:** context7:/websites/github_en_copilot
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md - **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace - **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.md - **Contributing files:** SKILL.md, references/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.md
- **Status:** `extracted` - **Status:** `extracted`
## github-custom-agents-configuration ## github-custom-agents-configuration
@@ -46,7 +46,7 @@ source_keys:
- **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration - **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md - **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy - **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.md - **Contributing files:** SKILL.md, references/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.md
- **Status:** `extracted` - **Status:** `extracted`
## github-cli-plugin-reference ## github-cli-plugin-reference

View File

@@ -34,8 +34,14 @@ first of these:
`plugin.json` and no `apm.yml` falls through to project or user scope. `plugin.json` and no `apm.yml` falls through to project or user scope.
`validate-provenance.sh` exits 0 silently when that walk does not land on a package root, and again `validate-provenance.sh` exits 0 silently when that walk does not land on a package root, and again
when the package has no provenance data. Silence from it is a pass, not a skip you need to when the package has no provenance data. Check the exit code before you believe the silence:
investigate.
- **0** — a pass, not a skip you need to investigate. Both silent cases above land here.
- **1** — real findings, on stdout with Why and Fix.
- **2** — the check never ran. A missing, doubled, non-file or wrongly-named argument, an
undecodable `apm.yml`, or an absent `python3`, each with a diagnostic on stderr and no findings
at all. Report the `### Provenance` dimension as unverified and quote the reason. An exit 2 is
never a clean pass: empty stdout there means nothing was checked, not that nothing was wrong.
## Manual fallback ## Manual fallback

View File

@@ -16,7 +16,30 @@ Arguments:
Exit codes: Exit codes:
0 All checks passed (or nothing to validate, or not plugin scope) 0 All checks passed (or nothing to validate, or not plugin scope)
1 One or more checks failed 1 One or more checks failed
2 Script error (unrecognized file extension — expected .md or .agent.md) 2 Usage error, or the argument is not an agent file this script can read
An exit code of 2 is NOT a finding. SKILL.md tells the auditor to surface a
non-zero exit as findings, so a usage error leaving exit 1 with nothing on
stdout was indistinguishable from a clean-but-failing run. Environment and
argument problems exit 2; only real findings exit 1.
Exit 2 and the silent exit 0 answer two DIFFERENT questions, and neither may
be spelled with the other's code:
exit 2 the argument is not something this script can audit at all — it is
missing, doubled, not a file, or not named .md / .agent.md. Decided
before the scope walk-up runs, from the argument alone.
exit 0 the argument IS a readable agent file, and the scope walk-up found
no type:-bearing apm.yml above it before hitting the \$HOME, .git or
filesystem-root boundary. That is a real verdict about a real file —
"this agent is user or project scope, so plugin-scope provenance
does not apply to it" — not a rejected input.
scripts/check-scope-walkup-sync.sh's fixture 6 pins the second: a real agent
file under a \$HOME with a type-bearing apm.yml ABOVE it must exit 0 with empty
output. Widening exit 2 to cover "the walk-up found no package" would break
that fixture AND would be wrong on its own terms, because new-agent.sh happily
scaffolds exactly that layout.
Checks performed: Checks performed:
0 source_keys present in agent pair but sources.md absent 0 source_keys present in agent pair but sources.md absent
@@ -28,6 +51,13 @@ Checks performed:
not run, never skipped silently. not run, never skipped silently.
4 Contributing files back-reference the parent slug in their source_keys 4 Contributing files back-reference the parent slug in their source_keys
5 Research doc field present and not placeholder 5 Research doc field present and not placeholder
This script has no counterpart to skill-audit's checks 6, 7 and 8 (Research
doc field / upstream forward / upstream reverse are numbered 6, 7, 8 there and
5 here): an agent at plugin scope is a single file with a plugin-root
sources.md, so there is no references/ tree to walk and no upstream research
source index to cross-check. parse_status() and the sources.md-basename gate
that those checks need exist only in the skill-audit copy.
EOF EOF
} }
@@ -36,26 +66,131 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
exit 0 exit 0
fi fi
# Usage and environment problems exit 2, findings exit 1. See the usage text
# above for why the two must not share a code, and for why "not plugin scope"
# is neither of them. This is a deliberate divergence from validate.sh, which
# has no 2 tier for content: validate.sh always prints PASS lines, so a usage
# error there is visibly not a findings report. This script prints NOTHING on a
# clean run, so exit 1 plus empty stdout was the only signal a caller got
# either way.
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
echo "Error: agent-file is required." >&2 echo "Error: agent-file is required." >&2
echo "" >&2 echo "" >&2
usage >&2 usage >&2
exit 1 exit 2
fi fi
# Extra positional arguments were silently dropped, so a typo'd flag or a second
# path looked like it had been honoured.
if [[ $# -gt 1 ]]; then
echo "Error: expected exactly one argument, got $#: $*" >&2
echo "" >&2
usage >&2
exit 2
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
# caller maps to anything, from a message that names this script's line number
# rather than the missing dependency.
if ! command -v python3 > /dev/null 2>&1; then
echo "Error: python3 is required but was not found on PATH." >&2
echo " Why: skipping the provenance checks entirely would be a vacuous pass." >&2
echo " Fix: install python3 (pre-commit itself is a Python application, so it is almost certainly already present)." >&2
exit 2
fi
# A path that does not exist, or exists but is not a regular file, used to reach
# the Python body, get os.path.dirname()'d into some ancestor directory and then
# either report a silent exit 0 (no package above it) or — worse — audit a
# DIFFERENT agent's package while naming the typo'd path. A typo'd target was
# indistinguishable from a clean agent. vale-wrap.sh hard-errors on a
# nonexistent path for exactly this reason.
#
# This is decided from the argument alone, before any walk-up runs, so it cannot
# collide with the not-plugin-scope exit 0: that verdict is only ever reached by
# a file that got past here.
if [[ ! -e "$1" ]]; then
echo "Error: no such file: $1" >&2
echo " Why: a nonexistent target would otherwise report a silent pass." >&2
echo " Fix: pass the path of the agent file to validate." >&2
exit 2
fi
if [[ ! -f "$1" ]]; then
echo "Error: not a regular file: $1" >&2
echo " Why: this script audits one agent file, not a directory of them, and reporting a directory as a pass hides the wrong-target mistake." >&2
echo " Fix: pass the agent file itself — .apm/agents/<name>.agent.md — not its parent directory." >&2
exit 2
fi
# The extension check used to live inside the Python body. It stays exit 2 and
# keeps its wording; it moves up here so that every "this argument is not
# auditable" verdict is reached in one place, before the interpreter starts and
# before the scope walk-up can turn a bad argument into a silent exit 0.
case "$1" in
*.agent.md | *.md) ;;
*)
echo "Error: unrecognized extension '$(basename "$1")' — expected .md or .agent.md" >&2
exit 2
;;
esac
python3 -u - "$1" <<'PYTHON' python3 -u - "$1" <<'PYTHON'
import sys import sys
import os import os
import re import re
# Output is UTF-8 for the same reason input is: under LC_ALL=C the streams
# default to ASCII, and every finding this script prints contains an em dash.
# Pinning only the reads moved the crash from the read to the write — a
# UnicodeEncodeError inside print_findings(), which loses the whole report
# after all the checks have already run.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding='utf-8')
except AttributeError: # pragma: no cover — Python < 3.7
pass
agent_file = os.path.abspath(sys.argv[1]) agent_file = os.path.abspath(sys.argv[1])
fname = os.path.basename(agent_file)
agent_dir = os.path.dirname(agent_file) agent_dir = os.path.dirname(agent_file)
# --- Sanity-check extension (single vendor-neutral .agent.md file at plugin/APM scope) --- # --- Input ----------------------------------------------------------------
if not (fname.endswith('.agent.md') or fname.endswith('.md')): # Ported from the skill-audit copy, where the same two problems were already
print(f"Error: unrecognized extension '{fname}' — expected .md or .agent.md", file=sys.stderr) # fixed.
sys.exit(2) #
# read_text() pins UTF-8 explicitly instead of inheriting
# locale.getpreferredencoding(), which is ASCII under LC_ALL=C — an ordinary em
# dash in an agent file or in sources.md then aborted the run with a bare
# UnicodeDecodeError traceback, or, at the one call site that wrapped its read
# in `except Exception: return []`, reported the unreadable file as having no
# source_keys and therefore as clean. A file that genuinely is not UTF-8 still
# fails; it just says which file and why.
#
# strip_bom() runs on every read because a leading BOM defeats
# parse_frontmatter()'s `^---` anchor, which silently disabled check 2 on a
# BOM-prefixed agent file: no frontmatter parsed means no source_keys parsed
# means nothing to validate.
class EncodingError(Exception):
pass
def strip_bom(text):
return text[1:] if text.startswith(u'\ufeff') else text
def read_text(path):
"""File contents as text, UTF-8 and BOM-free, with a diagnostic instead of a traceback."""
try:
with open(path, encoding='utf-8') as fh:
return strip_bom(fh.read())
except UnicodeDecodeError as exc:
raise EncodingError(
"not valid UTF-8 (%s at byte %d) — re-save the file as UTF-8; "
"this gate does not guess at other encodings"
% (exc.reason, exc.start))
# Matches a top-level `type:` line whose value is exactly one of the four # Matches a top-level `type:` line whose value is exactly one of the four
# package content types — identical to validate.sh's APM_TYPE_RE. Group 1's # package content types — identical to validate.sh's APM_TYPE_RE. Group 1's
@@ -70,15 +205,31 @@ TYPE_RE = re.compile(r"^type:\s*(['\"]?)(instructions|skill|hybrid|prompts)\1(?:
# keep walking. Stop at a $HOME boundary, a .git boundary, or the filesystem # keep walking. Stop at a $HOME boundary, a .git boundary, or the filesystem
# root: none of these is plugin/APM scope, so this script has nothing to # root: none of these is plugin/APM scope, so this script has nothing to
# check there. # check there.
#
# Returning None here means NOT PLUGIN SCOPE, which is a verdict, not an error:
# the caller exits 0 silently, and scripts/check-scope-walkup-sync.sh fixture 6
# pins that. It is deliberately NOT folded into the exit-2 tier above.
def find_plugin_root(start_dir): def find_plugin_root(start_dir):
home = os.path.expanduser('~') home = os.path.expanduser('~')
current = os.path.abspath(start_dir) current = os.path.abspath(start_dir)
while True: while True:
apm_yml = os.path.join(current, 'apm.yml') apm_yml = os.path.join(current, 'apm.yml')
if os.path.isfile(apm_yml): if os.path.isfile(apm_yml):
with open(apm_yml) as f: # An apm.yml is a manifest this script must be able to READ to
if any(TYPE_RE.match(line) for line in f): # classify scope at all. Under LC_ALL=C the old bare open() decoded
return current # as ASCII, so a manifest with an accented author name raised
# UnicodeDecodeError mid-walk and killed the run with a traceback.
# It is an environment problem, not a finding, so it exits 2 rather
# than being swallowed into a silent "no package here".
try:
content = read_text(apm_yml)
except EncodingError as exc:
print(
"Error: %s is %s" % (apm_yml, exc),
file=sys.stderr)
sys.exit(2)
if any(TYPE_RE.match(line) for line in content.splitlines()):
return current
# $HOME is a non-plugin-scope boundary — checked before the .git test # $HOME is a non-plugin-scope boundary — checked before the .git test
# below (mirrors validate.sh's detect_scope ordering), so a # below (mirrors validate.sh's detect_scope ordering), so a
# dotfiles-managed $HOME (yadm, chezmoi bare-repo, etc.) can't shadow # dotfiles-managed $HOME (yadm, chezmoi bare-repo, etc.) can't shadow
@@ -104,7 +255,14 @@ if plugin_root is None:
sources_md_path = os.path.join(plugin_root, 'sources.md') sources_md_path = os.path.join(plugin_root, 'sources.md')
# --- Helpers --- # --- Helpers ---
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:[^`\n]')
# The trailing character class used to be CONSUMING — `[^`\n]` — so a
# `FILL IN:` at end of line matched nothing and escaped checks 1 and 5
# entirely. `- **Description:** FILL IN:` is the most likely spelling of a
# half-written entry, and it was the one spelling the placeholder gate could
# not see. The exclusion it was really expressing is "not inside backticks",
# which a lookahead states without eating a character.
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:(?!`)')
def parse_frontmatter(content): def parse_frontmatter(content):
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
@@ -227,33 +385,48 @@ def parse_contributing_files(content, slug):
return files or None return files or None
# ===== END SHARED CONTRIBUTING-FILES PARSER ===== # ===== END SHARED CONTRIBUTING-FILES PARSER =====
def parse_research_doc(content, slug): def parse_research_docs(content, slug):
"""Every Research doc value under a given slug H2, in document order.
The caller uses the first and reports the rest. Returning only the first —
what this did before — meant a second '- **Research doc:**' line in one
entry was silently ignored, so an author who added a doc rather than
replacing one got check 5 run against the old value and no hint that the
new one was never looked at.
"""
pattern = re.compile( pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL re.MULTILINE | re.DOTALL
) )
m = pattern.search(content) m = pattern.search(content)
if not m: if not m:
return None return []
block = m.group(1) block = m.group(1)
rd_m = re.search(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE) return [v.strip() for v in
if not rd_m: re.findall(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)]
return None
return rd_m.group(1).strip()
findings = [] findings = []
has_fail = False has_fail = False
# A finding identical in every field is the same finding, and the same file is
# now reached by more than one check — the agent file is read once for its own
# source_keys and again as a contributing file, so an unreadable one would
# otherwise be reported twice with the same words. Distinct findings about the
# same file still both appear.
def _record(entry):
if entry not in findings:
findings.append(entry)
def emit_fail(desc, fpath, why, fix): def emit_fail(desc, fpath, why, fix):
global has_fail global has_fail
has_fail = True has_fail = True
findings.append(("FAIL", desc, fpath, why, fix, None)) _record(("FAIL", desc, fpath, why, fix, None))
# INFO does not set has_fail and does not change the exit code. It is for a # INFO does not set has_fail and does not change the exit code. It is for a
# check that could not RUN — an unverified entry, not a broken one — and it # check that could not RUN — an unverified entry, not a broken one — and it
# exists so that "did not run" is never spelled the same way as "passed". # exists so that "did not run" is never spelled the same way as "passed".
def emit_info(desc, fpath, note): def emit_info(desc, fpath, note):
findings.append(("INFO", desc, fpath, None, None, note)) _record(("INFO", desc, fpath, None, None, note))
def print_findings(): def print_findings():
for entry in findings: for entry in findings:
@@ -273,38 +446,56 @@ def print_findings():
print(f" Note: {note}") print(f" Note: {note}")
print() print()
def emit_unreadable(rel, exc):
"""Report a file this script cannot decode. Never a silent skip."""
emit_fail(
f"File is {exc}",
rel,
f"'{rel}' cannot be decoded, so its frontmatter — and any source_keys in it — "
f"cannot be read. This used to be swallowed by a bare 'except Exception: return []', "
f"which reported the unreadable file as having no source_keys and therefore as clean.",
f"Re-save '{rel}' as UTF-8."
)
# --- Collect source_keys from agent pair --- # --- Collect source_keys from agent pair ---
def get_source_keys_from_file(fpath): def get_source_keys_from_file(fpath, rel):
if not os.path.isfile(fpath): if not os.path.isfile(fpath):
return [] return []
try: try:
with open(fpath) as f: content = read_text(fpath)
content = f.read() except EncodingError as exc:
except Exception: emit_unreadable(rel, exc)
return [] return []
fm, _ = parse_frontmatter(content) fm, _ = parse_frontmatter(content)
return parse_source_keys(fm) return parse_source_keys(fm)
# Plugin/APM scope is a single vendor-neutral file — no counterpart to merge. # Plugin/APM scope is a single vendor-neutral file — no counterpart to merge.
given_keys = get_source_keys_from_file(agent_file) rel_given = os.path.relpath(agent_file, plugin_root)
given_keys = get_source_keys_from_file(agent_file, rel_given)
all_source_keys = given_keys all_source_keys = given_keys
sources_md_exists = os.path.isfile(sources_md_path) sources_md_exists = os.path.isfile(sources_md_path)
# Early exit: nothing to validate # Early exit: nothing to validate. The read above can itself raise a finding —
# an unreadable agent file — so print before leaving; the clean case still
# prints nothing and exits 0.
if not all_source_keys and not sources_md_exists: if not all_source_keys and not sources_md_exists:
sys.exit(0) print_findings()
sys.exit(1 if has_fail else 0)
sources_content = None sources_content = None
sources_slugs = set() sources_slugs = set()
if sources_md_exists: if sources_md_exists:
with open(sources_md_path) as f: try:
sources_content = f.read() sources_content = read_text(sources_md_path)
except EncodingError as exc:
emit_unreadable("sources.md", exc)
print_findings()
sys.exit(1)
sources_slugs = set(parse_h2_slugs(sources_content)) sources_slugs = set(parse_h2_slugs(sources_content))
# --- Check 0: source_keys present but sources.md absent --- # --- Check 0: source_keys present but sources.md absent ---
if not sources_md_exists and all_source_keys: if not sources_md_exists and all_source_keys:
rel_given = os.path.relpath(agent_file, plugin_root)
emit_fail( emit_fail(
"source_keys declared but sources.md is absent", "source_keys declared but sources.md is absent",
rel_given, rel_given,
@@ -340,7 +531,31 @@ for fpath, keys in [(agent_file, given_keys)]:
) )
# --- Checks 3, 4, 5: Per-slug checks in sources.md --- # --- Checks 3, 4, 5: Per-slug checks in sources.md ---
for slug in parse_h2_slugs(sources_content):
# Every per-slug parser below — parse_contributing_files, parse_research_docs —
# locates its block with pattern.search(), so a slug written twice resolves to
# the FIRST block every time. Iterating the raw heading list therefore checked
# the first block's fields twice and the second block's never: a duplicated slug
# is half-validated, and looked fully validated. The duplicate is announced and
# the repeat visit dropped.
all_slugs = parse_h2_slugs(sources_content)
unique_slugs = []
for _slug in all_slugs:
if _slug in unique_slugs:
continue
unique_slugs.append(_slug)
_count = all_slugs.count(_slug)
if _count > 1:
emit_info(
f"Duplicate '## {_slug}' entry in sources.md — only the first block is checked",
f"sources.md (## {_slug})",
f"'## {_slug}' appears {_count} times. Every field parser here takes the first match, so the "
f"second and later blocks' Contributing files and Research doc are never validated — "
f"checks 3, 4 and 5 did not run for them. "
f"Merge the blocks into one entry, or give each a distinct slug and reference it from source_keys."
)
for slug in unique_slugs:
# Checks 3 and 4: Contributing files exist (paths relative to plugin root), # Checks 3 and 4: Contributing files exist (paths relative to plugin root),
# and back-reference the slug. `[]` and None are NOT the same answer here. # and back-reference the slug. `[]` and None are NOT the same answer here.
# `[]` is the author writing "(none)" — there is nothing to check and the # `[]` is the author writing "(none)" — there is nothing to check and the
@@ -370,8 +585,11 @@ for slug in parse_h2_slugs(sources_content):
) )
else: else:
# Check 4: Bidirectional — file should list slug in its source_keys # Check 4: Bidirectional — file should list slug in its source_keys
with open(cf_abs) as f: try:
cf_content = f.read() cf_content = read_text(cf_abs)
except EncodingError as exc:
emit_unreadable(cf_rel, exc)
continue
cf_fm, _ = parse_frontmatter(cf_content) cf_fm, _ = parse_frontmatter(cf_content)
cf_keys = parse_source_keys(cf_fm) cf_keys = parse_source_keys(cf_fm)
if slug not in cf_keys: if slug not in cf_keys:
@@ -383,7 +601,17 @@ for slug in parse_h2_slugs(sources_content):
) )
# Check 5: Research doc field required # Check 5: Research doc field required
rd_value = parse_research_doc(sources_content, slug) rd_values = parse_research_docs(sources_content, slug)
if len(rd_values) > 1:
emit_info(
f"Multiple '- **Research doc:**' lines for '{slug}' — only the first is used",
f"sources.md (## {slug})",
f"The '## {slug}' entry has {len(rd_values)} Research doc lines; check 5 ran against the first "
f"('{rd_values[0]}') and never looked at the rest. "
f"Keep one Research doc line per entry — if a slug genuinely came from two documents, split it into two slugs, "
f"or name the extra document inside the first value's annotation where it is at least visible."
)
rd_value = rd_values[0] if rd_values else None
if rd_value is None: if rd_value is None:
emit_fail( emit_fail(
"Research doc field missing", "Research doc field missing",

View File

@@ -69,6 +69,24 @@ import glob
import yaml import yaml
# Output is UTF-8 for the same reason input is: under LC_ALL=C the streams
# default to ASCII, and this script's own message text carries em dashes (the
# ADR-0020 boundary SUGGESTION is one). Pinning only the reads moved the crash
# from the read to the write — a UnicodeEncodeError raised while PRINTING, after
# every check has already run, which loses the whole report and (here) flips a
# clean exit 0 into a traceback and an exit 1. read_text() in the shared
# resolver block below pins the reads; this pins the writes.
#
# Deliberately OUTSIDE the ADR-0020 shared boundary resolver block: the two
# validate.sh copies print findings, skill-size-check.sh has its own top-level
# equivalent, and tests/test-adr0020-contract.sh hashes that block for
# byte-identity across all three.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding='utf-8')
except AttributeError: # pragma: no cover — Python < 3.7
pass
agent_file = os.path.abspath(sys.argv[1]) agent_file = os.path.abspath(sys.argv[1])
script_dir = sys.argv[2] script_dir = sys.argv[2]
@@ -552,9 +570,9 @@ def known_targets(start_dir):
# ambiguity to resolve, and an author who wants a route checked unconditionally # ambiguity to resolve, and an author who wants a route checked unconditionally
# has two ways to say so. # has two ways to say so.
# #
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is # BOTH FORMS ARE SWEPT FOR ON THEIR OWN, and that is a repair of the promise
# a repair of the promise above rather than a widening of it. Until the sweeps # above rather than a widening of it. Until the sweeps existed, notation was
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use # only ever seen as the OBJECT OF A ROUTE VERB (`use
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in # /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
# between. Every one of these therefore exited 0 in total silence — no ERROR, no # between. Every one of these therefore exited 0 in total silence — no ERROR, no
# SUGGESTION, not even the target's name: # SUGGESTION, not even the target's name:
@@ -565,6 +583,7 @@ def known_targets(start_dir):
# Do not use for Y — defer to /no-such-skill. # Do not use for Y — defer to /no-such-skill.
# Do not use for Y — /no-such-skill. # Do not use for Y — /no-such-skill.
# Do not use for Y; -> no-such-skill covers it. # Do not use for Y; -> no-such-skill covers it.
# For W, /no-such-skill is the right entry point.
# The target was never EXTRACTED, so the notation-first rule in _add() had # The target was never EXTRACTED, so the notation-first rule in _add() had
# nothing to apply itself to and the "always blocks" promise was false for the # nothing to apply itself to and the "always blocks" promise was false for the
# ordinary way an author writes the thing. The SUGGESTION tier made it worse # ordinary way an author writes the thing. The SUGGESTION tier made it worse
@@ -573,12 +592,43 @@ def known_targets(start_dir):
# visible SUGGESTION into silence — the gate teaching the one edit that blinds # visible SUGGESTION into silence — the gate teaching the one edit that blinds
# it. # it.
# #
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate # THE TWO SWEEPS ARE GATED DIFFERENTLY, and the asymmetry is the whole point.
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a # `/name` is Claude Code's invocation syntax and nothing else — no English
# PATH: a following `/`, or a `.` followed by a non-space, means # sentence contains one by accident — so the ADR-0020 amendment and
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's # docs/spec/gates.md both promise it blocks UNCONDITIONALLY, for any name. So
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still # NOTATION_SLASH is swept over every sentence, boundary marker or not. Gating it
# counts. # on BOUNDARY_MARKER made that promise false for the last sentence of
# Do not use for Z — use /real-skill instead.
# For W, /no-such-skill is the right entry point.
# which exited 0 in total silence: the boundary clause is one sentence up, so
# the sweep never looked at the sentence carrying the broken route. Extraction is
# per-sentence by design (corroboration is scoped to one sentence), which is
# exactly what made the gap invisible.
#
# NOTATION_ARROW stays gated on BOUNDARY_MARKER, and so does the backtick sweep.
# Neither form is unambiguous: `-> name` is also how a process chain is written
# ("reproduce -> minimise -> regression-test") and a code span is how a tool, a
# file and a skill are all cited. Ungating either would fire on prose that
# carries no routing intent at all — the false-positive class this whole
# extractor is tuned against.
#
# BOTH `/name` PATTERNS REFUSE A TOKEN THAT IS PART OF A PATH: a following `/`,
# or a `.` followed by a non-space, means `references/foo.md`, `docs/a/b.md` or
# `https://x/y`, not a route. A sentence's closing `.` is not followed by a
# non-space, so `— /no-such-skill.` still counts.
#
# THAT GUARD IS WRITTEN `(?![\w-])` AND NOT `\b`, because `\b` is not a guard at
# all here: it holds after a hyphen, so when the trailing lookahead rejected the
# full segment the engine simply backtracked to a shorter hyphen-terminated
# prefix and reported THAT as a route. Every one of these was a hard blocking
# ERROR naming a skill nobody had written:
# the config lives at /opt-tools/bin/thing. -> 'opt'
# see /api-docs/v2.md for the schema. -> 'api' AND 'api-docs'
# the file /no-such-skill.md documents it. -> 'no-such'
# `(?![\w-])` forbids the shortened prefix outright, so the whole segment is
# rejected as the path it is. MARKED_TARGET carries the same guard: it had no
# trailing lookahead whatsoever, so `see /api-docs/v2.md` raised the second of
# the two errors above through the route-verb path rather than the sweep.
# #
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
# still resolve `gitea:gitea-prs`), so the patterns admit an optional # still resolve `gitea:gitea-prs`), so the patterns admit an optional
@@ -590,7 +640,8 @@ ROUTE_VERB = (r"(?:use|uses|using|run|runs|invoke|invokes|invoking|try|see"
r"|that'?s|compose|composes|call|calls" r"|that'?s|compose|composes|call|calls"
r"|routes?\s+to|delegates?\s+to|prefers?|switch(?:es)?\s+to" r"|routes?\s+to|delegates?\s+to|prefers?|switch(?:es)?\s+to"
r"|hands?\s+off\s+to)") r"|hands?\s+off\s+to)")
MARKED_TARGET = r"(?:`/?(%s)`|(?<![\w./*-])/(%s)\b)" % (NAME_ANY, NAME_ANY) MARKED_TARGET = (r"(?:`/?(%s)`|(?<![\w./*-])/(%s)(?![\w-])(?!/|\.\S))"
% (NAME_ANY, NAME_ANY))
ANY_TARGET = r"(?:%s|(%s)\b)" % (MARKED_TARGET, NAME_HYPH) ANY_TARGET = r"(?:%s|(%s)\b)" % (MARKED_TARGET, NAME_HYPH)
ROUTE_MARKED = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, MARKED_TARGET), re.I) ROUTE_MARKED = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, MARKED_TARGET), re.I)
ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET), re.I) ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET), re.I)
@@ -603,10 +654,12 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their # The two EXPLICIT ROUTE NOTATION sweeps. NOTATION_SLASH runs over EVERY
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` # sentence; NOTATION_ARROW is scoped to a boundary sentence by its caller (see
# alternative: that one only ever runs behind a route verb or an arrow, and the # the asymmetry note in the header). NOTATION_SLASH is deliberately not a reuse
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. # of MARKED_TARGET's `/name` alternative: that one only ever runs behind a route
# verb or an arrow, and it may match a namespaced or path-adjacent token in
# positions this free-standing sweep must refuse.
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is # NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
# what made `Do not use for Y; -> no-such-skill covers it.` invisible: # what made `Do not use for Y; -> no-such-skill covers it.` invisible:
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the # CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
@@ -618,7 +671,8 @@ ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the # hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
# arrow, not whether prose can. An author who means the chain and not a route # arrow, not whether prose can. An author who means the chain and not a route
# writes it in its own sentence, where neither pattern looks. # writes it in its own sentence, where neither pattern looks.
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I) NOTATION_SLASH = re.compile(
r"(?<![\w./*-])/(%s)(?![\w-])(?!/|\.\S)" % NAME_ANY, re.I)
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I) NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
@@ -794,14 +848,16 @@ def _extract_sentence(sentence):
for match in ARROW_BOUNDARY.finditer(sentence): for match in ARROW_BOUNDARY.finditer(sentence):
_add(out, sentence, match.group(1), match.start(1), match.end(1), _add(out, sentence, match.group(1), match.start(1), match.end(1),
strict=True, arrow=True) strict=True, arrow=True)
# `/name` wherever it sits, in ANY sentence — not only where a route verb or
# an arrow happens to precede it, and NOT only inside a boundary sentence.
# See the EXPLICIT ROUTE NOTATION note in the header for the eight phrasings
# this recovers and for why silence was the failure mode. The sweep takes no
# follower test: _add() reads the notation first and marks it.
for match in NOTATION_SLASH.finditer(sentence):
_add(out, sentence, match.group(1), match.start(1), match.end(1))
if boundary: if boundary:
# Route notation wherever it sits in the clause, not only where a route # The arrow and backtick forms are ambiguous in ordinary prose, so they
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE # stay scoped to a sentence that carries a boundary marker.
# NOTATION note in the header for the seven phrasings this recovers and
# for why silence was the failure mode. Neither sweep takes the follower
# test: _add() reads the notation first and both forms reach it marked.
for match in NOTATION_SLASH.finditer(sentence):
_add(out, sentence, match.group(1), match.start(1), match.end(1))
for match in NOTATION_ARROW.finditer(sentence): for match in NOTATION_ARROW.finditer(sentence):
_add(out, sentence, match.group(1), match.start(1), match.end(1), _add(out, sentence, match.group(1), match.start(1), match.end(1),
strict=True, arrow=True) strict=True, arrow=True)

Some files were not shown because too many files have changed in this diff Show More