Compare commits
29 Commits
971e148e19
...
feat/66-wi
| Author | SHA1 | Date | |
|---|---|---|---|
| 644a77b0cb | |||
| f1fa65ebc2 | |||
| dda7f8d3dd | |||
| a712f2c186 | |||
| 2fb1036329 | |||
| 5d7c76d797 | |||
| c613927fb4 | |||
| c75e4ef4f3 | |||
| 4f49b2a249 | |||
| df575129a1 | |||
| 058fb5b748 | |||
| 8516305b91 | |||
| 29eefe7f70 | |||
| 6ba29b696c | |||
| ac41326ba8 | |||
| b7bec71b8f | |||
| 0f2bb242ad | |||
| a3e721e937 | |||
| 3811f5481b | |||
| 175ea89c0a | |||
| ed8c99efbd | |||
| a6eedacfd8 | |||
| 4f4b55b0be | |||
| af8b46cd57 | |||
| 09eea5e7ab | |||
| 2c6ce438b6 | |||
| ffaa3afb41 | |||
| 60be7b3232 | |||
|
|
598a7c326a |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"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": {
|
||||
"name": "Defame1297",
|
||||
"email": "defame1297@rkdr.net",
|
||||
@@ -11,35 +11,35 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"version": "1.7.0",
|
||||
"version": "1.6.2",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"category": "Utilities",
|
||||
"source": "./plugins/bin"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.7",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/git"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.8",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/gitea"
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"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",
|
||||
"source": "./plugins/core"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"name": "lint",
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/lint"
|
||||
}
|
||||
|
||||
14
.github/plugin/marketplace.json
vendored
14
.github/plugin/marketplace.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"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": {
|
||||
"name": "Defame1297",
|
||||
"email": "defame1297@rkdr.net",
|
||||
@@ -11,35 +11,35 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"version": "1.7.0",
|
||||
"version": "1.6.2",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"category": "Utilities",
|
||||
"source": "./plugins/bin"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.7",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/git"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.8",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/gitea"
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"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",
|
||||
"source": "./plugins/core"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"name": "lint",
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/lint"
|
||||
}
|
||||
|
||||
@@ -254,17 +254,79 @@ repos:
|
||||
entry: bash
|
||||
language: system
|
||||
files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$'
|
||||
# Pinned by tests/test-skill-frontmatter.sh, which drives this exact
|
||||
# `bash -c <script> <arg0> <files...>` call shape rather than a copy of
|
||||
# the script -- the bug below was invisible to any test that did not.
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
# Every check reads the FRONTMATTER only, never the whole file. A
|
||||
# `metadata:` / `name:` / `description:` line inside a body code
|
||||
# fence is documentation (skill-author quotes exactly such a block)
|
||||
# and used to satisfy these greps.
|
||||
for f in "$@"; do
|
||||
if [[ -f "$f" ]]; then
|
||||
if ! grep -q "^name:" "$f" || ! grep -q "^description:" "$f"; then
|
||||
echo "ERROR: $f is missing required frontmatter fields (name: and description:)"
|
||||
exit 1
|
||||
fi
|
||||
[[ -f "$f" ]] || continue
|
||||
|
||||
fm="$(awk '
|
||||
{ sub(/\r$/, "") }
|
||||
NR == 1 { sub(/^\357\273\277/, "") }
|
||||
!opened && /^[[:blank:]]*$/ { next }
|
||||
!opened {
|
||||
if ($0 ~ /^---[[:blank:]]*$/) { opened = 1; next }
|
||||
exit
|
||||
}
|
||||
/^---[[:blank:]]*$/ { closed = 1; exit }
|
||||
{ print }
|
||||
END { if (!opened || !closed) exit 3 }
|
||||
' "$f")" || {
|
||||
echo "ERROR: $f has no closing YAML frontmatter block (expected --- ... --- at the top of the file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
missing=""
|
||||
printf '%s\n' "$fm" | grep -q "^name:" || missing="${missing}name: "
|
||||
printf '%s\n' "$fm" | grep -q "^description:" || missing="${missing}description: "
|
||||
|
||||
# Scoped to the `metadata:` block and stopped at the next
|
||||
# top-level key, so a `version:` under a following `source:` list
|
||||
# cannot stand in for it; the `^ version:` anchor is exact, so a
|
||||
# deeper-nested ` version:` cannot either. No line budget, so a
|
||||
# long `metadata:` block does not hide the key.
|
||||
ver="$(printf '%s\n' "$fm" | awk '
|
||||
/^metadata:/ { inm = 1; next }
|
||||
inm && /^[A-Za-z]/ { exit }
|
||||
inm && /^ version:/ {
|
||||
v = $0
|
||||
sub(/^ version:[[:blank:]]*/, "", v)
|
||||
sub(/[[:blank:]]+#.*$/, "", v)
|
||||
sub(/[[:blank:]]+$/, "", v)
|
||||
print "found:" v
|
||||
exit
|
||||
}
|
||||
')"
|
||||
[[ -n "$ver" ]] || missing="${missing}metadata.version "
|
||||
|
||||
if [[ -n "$missing" ]]; then
|
||||
echo "ERROR: $f is missing required frontmatter fields (${missing})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
raw="${ver#found:}"
|
||||
v="$raw"
|
||||
case "$v" in
|
||||
\"*\") v="${v#\"}"; v="${v%\"}" ;;
|
||||
\'*\') v="${v#\'}"; v="${v%\'}" ;;
|
||||
esac
|
||||
if [[ ! "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "ERROR: $f has a malformed frontmatter metadata.version (${raw:-<empty>}) -- expected a three-part semver, e.g. \"1.0.0\""
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# arg0 for `bash -c`. WITHOUT it pre-commit's first filename lands in
|
||||
# $0 and is dropped from "$@" -- so a single-file commit, the normal
|
||||
# case, ran the loop zero times and reported Passed having checked
|
||||
# nothing. Do not remove; tests/test-skill-frontmatter.sh pins it.
|
||||
- skill-frontmatter
|
||||
|
||||
- id: skill-size-check
|
||||
stages: ['pre-commit']
|
||||
@@ -284,6 +346,21 @@ repos:
|
||||
# records for Vale warnings. Costs nothing on a clean file: the script
|
||||
# prints only findings.
|
||||
|
||||
- id: check-rtk-prefix
|
||||
stages: ['pre-commit']
|
||||
name: ADR-0023 rtk prefix on executable git commands
|
||||
description: Enforce ADR-0023 clause 1 -- an executable, instructed git command in a shell code fence or a dispatch-table Run cell is written `rtk git`. Clauses 2 and 3 are not machine-decidable; a deliberately bare command opts out with the literal string ADR-0023 on its own line
|
||||
entry: scripts/check-rtk-prefix.sh
|
||||
language: script
|
||||
files: '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$'
|
||||
# README.md is excluded on purpose, not by oversight. A skill-directory
|
||||
# README is consumer-facing prose that no agent ever loads, and the
|
||||
# `git clone` lines in the seven tests/README.md files are setup
|
||||
# instructions for a third party who has no rtk installed. Prefixing
|
||||
# those would be actively wrong -- see ADR-0023's consumer section.
|
||||
exclude: '(^|/)README\.md$'
|
||||
pass_filenames: true
|
||||
|
||||
- id: vale-audit-prefilter-skill
|
||||
stages: ['pre-commit']
|
||||
name: Vale audit prefilter (SKILL.md)
|
||||
|
||||
@@ -15,6 +15,8 @@ This file carries only what applies to **every** session. Setup, prerequisites,
|
||||
|
||||
Not everything in a plugin root is generated. `README.md`, `docs/`, `bin/`, `sources.md`, `.mcp.json` and per-plugin extras are hand-authored there with no `.apm/` source — edit those in place. The rule is per-path, not per-directory. But a file placed *inside* a mirrored directory is deleted on the next sync (`sync_dir` runs `rm -rf` before every copy), so plugin-root documentation goes in `docs/`, never in `hooks/` or `skills/`.
|
||||
|
||||
`.mcp.json` is hand-authored but it is **not** outside apm. MCP is a first-class apm primitive, and a plugin's `.mcp.json` is how this repo declares one: apm reads the `mcpServers` pointer in the generated `.github/plugin/plugin.json`, resolves it to `.mcp.json`, and injects the result into that package's `dependencies.mcp` when a consumer installs it. Declare MCP servers there and **never** in the plugin's own `apm.yml` — that arms a per-package gate this repo cannot satisfy (`LESSONS.md`, 2026-09-12).
|
||||
|
||||
Full model: `docs/spec/architecture.md`.
|
||||
|
||||
## Prefer plugin skills over raw shell
|
||||
@@ -36,11 +38,7 @@ Fall back to raw shell only when no skill covers it.
|
||||
- **Do not add repo-owned keys to `.claude/settings.json`.** apm treats it as its own deployed artifact and `apm audit --ci` replays the install and diffs, so anything apm would not have written is permanent drift that fails the `apm-audit-ci` pre-push hook. A hook you want here is authored in `plugins/<name>/.apm/hooks/` and deployed by apm, never hand-written into that file. The `SessionStart` entry already in it is exactly that: kyberforge authors it in `plugins/kyberforge/.apm/hooks/hooks.json` and apm merges it in, so it is apm's own output, it is what the replay expects, and it belongs in the commit — do not strip it (ADR-0019). Machine-specific settings go in the gitignored `.claude/settings.local.json`; shared enforcement goes in `.pre-commit-config.yaml`.
|
||||
- **`apm.lock.yaml` turning up modified is expected, not a bug.** kyberforge's `SessionStart` hook runs `apm outdated` at startup and `apm update --yes` when something is behind, which rewrites the lock. Commit or discard it deliberately.
|
||||
- **A `.apm/` edit is not live in this session until it is pushed.** The six dependencies resolve from the holocron remote, unpinned against the default branch. `apm install` deploys from the lock; `apm update` is what re-resolves refs.
|
||||
- **The ADR-0020 skill gates ship hot, with no baseline — and the corpus is now clean.** All 39 skills clear both FAIL tiers: no description over 400 characters, no body over 900 words (counted body-only). Retrofitted plugin by plugin under #99 (see `docs/spec/gates.md`). Because nothing is grandfathered, the gates now bite on first commit — a new skill, or an edit that pushes a description past 400, is blocked until it complies. **No routing target dangles**, and `tests/test-adr0020-targets.sh` pins that set as empty, so a new boundary clause naming a non-existent skill fails the suite rather than joining a backlog. Two blind spots survive: `skill-size-check` does not cover the Vale half, so `Kyberforge.CompositionNote` fires nowhere today but any new description can reintroduce it; and no `references/` file is linted by anything, so prose relocated out of a body to satisfy the word gate lands outside the prose gate. It has two independent causes and closing either alone changes nothing — `docs/spec/gates.md` has both, issue #117 tracks it. Check both gates: `pre-commit run --all-files`.
|
||||
- **Run `bash tests/run-tests.sh --strict` before considering any change done.** Keep the flag: without it a suite whose dependency is missing exits 77 and is counted SKIPPED rather than failed, so the run goes green having verified less than it claims.
|
||||
- **Before pushing, rehearse the gate locally:** `pre-commit run --hook-stage pre-push --all-files`. It runs the 14 pre-push hooks this repo authors itself plus pre-commit's 2 `meta` hooks, so it prints 16; `check-release-needed` passes without checking anything, because it needs a real push to `main`. `docs/spec/gates.md` reconciles both.
|
||||
- **Pushing without a network** needs `SKIP=apm-marketplace-check,apm-pack-check-clean git push` — those two resolve a remote marketplace entry via `git ls-remote`. Skip only those two; the rest are real local checks, and adding one to `SKIP` disarms it silently.
|
||||
- **Author commits with `git-commits`** — it validates Conventional Commits, which `commit-msg` enforces.
|
||||
- **This repo and Gitea are the only source of truth.** All project state, decisions, and working conventions live here. Do not use an external memory system for this project — cached state diverges from the repo and you get a split brain. Before answering any design or architecture question, check `docs/adr/` for an existing decision.
|
||||
|
||||
## Key documents
|
||||
|
||||
10
CONTEXT.md
10
CONTEXT.md
@@ -16,8 +16,12 @@ decisions.
|
||||
|
||||
**Preload tax**:
|
||||
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
|
||||
figure: ADR-0020.
|
||||
first token of every session whether the skill is invoked or not. Measurement method: ADR-0020. Its
|
||||
**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
|
||||
|
||||
**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
|
||||
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
|
||||
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`.
|
||||
_Avoid_: manual skill, disabled skill
|
||||
|
||||
|
||||
24
LESSONS.md
24
LESSONS.md
@@ -10,6 +10,14 @@ Patterns observed during development of this repo. Three or more entries on the
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-12 — "Not an `.apm/` primitive" was read as "not an apm primitive", and the review that followed was wrong
|
||||
|
||||
`AGENTS.md` and `docs/spec/architecture.md` both listed `.mcp.json` alongside `README.md`, `docs/` and `bin/` as plugin-root material "hand-authored with no `.apm/` source". That is literally true — nothing under `.apm/` produces MCP config — but it reads as "apm has no MCP concept", and a review of PR #132 drew exactly that conclusion and recommended moving the declaration into the plugin's `apm.yml` under `dependencies.mcp`. The recommendation was wrong twice over. It arms `lockfile-exists` in the per-package `apm audit --ci` that the `apm-audit-ci` hook runs in every `plugins/*/`, which then demands the package's whole deployed tree inside the package directory: 93 missing files and 79 drifted paths on `plugins/gitea`. And it was unnecessary, because the `.mcp.json` route already reaches `dependencies.mcp` through the `mcpServers` pointer in the generated Copilot manifest, env references intact.
|
||||
|
||||
Two process lessons, not one. First, when a doc says a file is not a primitive **of a specific subsystem**, say which subsystem and what the file actually is instead — the negative claim alone invites the wrong generalisation. Second, the three scratch installs that produced the wrong conclusion all used local `./path` dependencies, where apm skips the plugin-normalisation step that injects `.mcp.json`. The repo consumes its plugins as `git:` + `path:` objects. A scratch test that does not reproduce the real dependency form can invert the result, so reproduce the form, not just the shape.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-17 — Workflow documents should prescribe sub-agent usage, not just allow it
|
||||
|
||||
When writing workflow documents (like `docs/notes/skill-implementation-workflow.md`), the natural tendency is to describe steps at a high level and leave sub-agent usage as an implementation detail. But if the workflow doesn't explicitly prescribe "spawn a sub-agent here," practitioners default to doing everything in the main context — accumulating token cost and losing the isolation benefit. Fix: make sub-agent usage a named step in the workflow, specifying what the agent receives, what it returns, and why it's isolated. This makes the workflow reproducible rather than dependent on the practitioner remembering to use agents.
|
||||
@@ -60,29 +68,21 @@ write-eval's process requires presenting the full test plan and waiting for user
|
||||
|
||||
The write-skill authoring standard required 8 body sections including Role and When/When not. These were assumed to be agentskills.io requirements. Checking the actual spec revealed the body has no format restrictions at all — recommended sections are step-by-step instructions, examples, and edge cases. Role and When/When not were added by convention without verifying the standard. Fix: before encoding any requirement as part of an authoring standard, check the upstream spec directly. The agentskills.io spec also confirmed that negative triggers belong in the description field — not in a separate body section — which eliminates a persistent duplication pattern across all skills.
|
||||
|
||||
## 2026-05-18 — Provenance fields in frontmatter are loaded on every skill scan
|
||||
|
||||
Fields like `source:`, `references:`, `version:`, `updated:`, and `when:` in SKILL.md frontmatter are loaded at agent startup alongside `name` and `description` for every installed skill. None of these are used for routing or runtime execution — they are audit and upgrade-cycle records. Loading them at startup violates progressive disclosure and wastes tokens proportional to the number of installed skills. Fix: move all non-routing frontmatter to a separate `META.md` file in the skill directory. Frontmatter keeps only `name`, `description`, `metadata.category`, and `allowed-tools` (when applicable) — the four fields the spec actually uses for routing and discovery.
|
||||
|
||||
## 2026-05-18 — Copy-fill is more deterministic than generate for structured skill artifacts
|
||||
|
||||
When a skill produces a structured artifact like SKILL.md, the natural approach is to generate it from internalized rules in the Process section. But this means section structure is only as reliable as the agent's instruction-following under token pressure. Copy-fill (copy the template to the target path, then fill in content) separates structure from content: the template mechanically enforces section order and presence, freeing the Process section to focus only on sequencing constraints (what order to decide things) rather than also policing structure. Side benefit: the template is a human-usable artifact that can be adopted independently of the skill. Fix applied in write-skill refactor: SKILL-TEMPLATE.md and META-TEMPLATE.md are the authoritative structure sources; the Process section no longer contains a body structure constraint — the template handles it.
|
||||
When a skill produces a structured artifact like SKILL.md, the natural approach is to generate it from internalized rules in the Process section. But this means section structure is only as reliable as the agent's instruction-following under token pressure. Copy-fill (copy the template to the target path, then fill in content) separates structure from content: the template mechanically enforces section order and presence, freeing the Process section to focus only on sequencing constraints (what order to decide things) rather than also policing structure. Side benefit: the template is a human-usable artifact that can be adopted independently of the skill. Fix applied in write-skill refactor: SKILL-TEMPLATE.md is the authoritative structure source; the Process section no longer contains a body structure constraint — the template handles it.
|
||||
|
||||
## 2026-05-17 — HITL gap: agent delegates confirmation to permission system
|
||||
|
||||
The agent-level HITL rule ("require explicit confirmation before irreversible shared-state operations") is being bypassed: the agent calls the tool and lets the permission dialog catch it. This means the rule is not firing in agent reasoning — it's the permission system acting as a safety net. If a user selects "don't ask again," the net disappears. Fix: the HITL rule needs to be framed as "do not call the tool" rather than "ask before proceeding" — the agent must ask first, then act only after explicit confirmation.
|
||||
|
||||
## 2026-05-26 — META-TEMPLATE uses YAML comments; META.md output retains them
|
||||
|
||||
META-TEMPLATE.md uses YAML `#` comments to explain fields inline. SKILL-TEMPLATE.md uses HTML comments inside XML tags, which the agent strips on fill. The structural difference means SKILL.md output is clean but META.md output retains the explanatory `#` lines — an inconsistency. Fix (deferred): restructure META-TEMPLATE.md so all explanatory guidance is prose above the code block (markdown, never copied into the output YAML), and the code block itself uses `<placeholder>` syntax with no `#` comment lines. This makes META.md fill behaviour deterministic for the same reason SKILL.md fill is: `<...>` markers are unambiguously replaceable; prose above the block is not part of the template. Do not apply until the human/copy-fill tradeoff is resolved — see 2026-05-26 session discussion.
|
||||
|
||||
## 2026-05-26 — Overlap checks must scan the deployed directory, not just the source repo
|
||||
|
||||
`write-a-skill` existed only in `~/.agents/skills/` (installed from a pre-refactor source) and was invisible during a repo-level scan of `.agents/skills/`. Governance reviews and overlap checks that only look at the source repo will miss skills added by install.sh from other sources or prior runs. Fix: overlap checks must scan the deployed `~/.agents/skills/` directory, not just the repo's `.agents/skills/`.
|
||||
|
||||
## 2026-05-26 — `model:` field belongs in SKILL.md frontmatter, not META.md
|
||||
## 2026-05-26 — `model:` field belongs in SKILL.md frontmatter, not a sidecar file
|
||||
|
||||
Claude Code supports `model:` as a provider extension in SKILL.md frontmatter — it overrides the session model for the skill's turn and reverts after. Attempting to put it in META.md was wrong: META.md is provenance/audit metadata, not runtime config. The boundary: if a field affects agent behaviour at invocation time, it belongs in SKILL.md frontmatter; if it serves upgrade reviews and audit trails, it belongs in META.md.
|
||||
Claude Code supports `model:` as a provider extension in SKILL.md frontmatter — it overrides the session model for the skill's turn and reverts after. Attempting to move it out to a provenance sidecar was wrong: a sidecar is audit metadata, not runtime config. The boundary: if a field affects agent behaviour at invocation time, it belongs in SKILL.md frontmatter.
|
||||
|
||||
## 2026-05-26 — Research agents present synthesis as spec fact
|
||||
|
||||
@@ -126,7 +126,7 @@ Two forks independently fixed `references/sources.md` with different approaches
|
||||
|
||||
## 2026-06-28 — Implementation agents must invoke /skill-author, not write skill files directly
|
||||
|
||||
When briefing an agent to implement a new skill, the instinct is to tell it to write the SKILL.md and supporting files directly. This bypasses Step 5 of the skill-author process (provenance), which requires reading all research `sources.md` files and recording every `extracted` slug in META.md. The `validate-provenance.sh` script catches the gap — but only after the commit, requiring a fix round. This pattern recurred twice in one session (plugin-author and marketplace-author initial implementation, then again in the first round of fix agents). Fix: briefs for implementation agents must explicitly say "invoke `/skill-author` (read and follow `plugins/kyberforge/.apm/skills/skill-author/SKILL.md`)" — not "write the skill files." Invoking the skill is the only reliable way to ensure all process gates, including provenance, run.
|
||||
When briefing an agent to implement a new skill, the instinct is to tell it to write the SKILL.md and supporting files directly. This bypasses Step 5 of the skill-author process (provenance), which requires reading all research `sources.md` files and recording every `extracted` slug in the skill's own `references/sources.md`. The `validate-provenance.sh` script catches the gap — but only after the commit, requiring a fix round. This pattern recurred twice in one session (plugin-author and marketplace-author initial implementation, then again in the first round of fix agents). Fix: briefs for implementation agents must explicitly say "invoke `/skill-author` (read and follow `plugins/kyberforge/.apm/skills/skill-author/SKILL.md`)" — not "write the skill files." Invoking the skill is the only reliable way to ensure all process gates, including provenance, run.
|
||||
|
||||
## 2026-07-05 — Repo root is a bare checkout; work happens in worktrees only
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -35,6 +35,17 @@ Install all of these before setting up. Each one is a hard dependency of a git h
|
||||
| `python3` + PyYAML | Required by `scripts/skill-size-check.sh` (the `skill-size-check` pre-commit hook), which reads folded YAML frontmatter | `python3` is usually present — pre-commit is itself a Python application. `pip install pyyaml` if the hook reports PyYAML missing |
|
||||
| `vale` | Required by the `vale-audit-prefilter-skill` / `-agent` pre-commit hooks and the `check-vale-style-sync` pre-push hook | `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or https://vale.sh/docs/vale-cli/installation/ |
|
||||
| `claude` CLI | Required by the `validate-plugins` and `validate-marketplace` pre-push hooks | Claude Code |
|
||||
| `go` toolchain | The gitea MCP server runs as `go run gitea.com/gitea/gitea-mcp@v1.7.0`, resolved from `PATH`. Without it the server fails to start and every `gitea-*` skill loses its tools | https://go.dev/dl/ — verify with `go version` |
|
||||
|
||||
The gitea MCP server additionally needs two environment variables in the shell that launches your agent — referenced as `${GITEA_ACCESS_TOKEN}` and `${GITEA_HOST}` in `plugins/gitea/.mcp.json`, with apm passing those references through to the deployed config unexpanded so the values are resolved at server startup and never committed. Copy `plugins/gitea/.env.example` to `.env` at the repo root, fill in real values, then export it — nothing in this repo auto-loads a `.env` file:
|
||||
|
||||
```bash
|
||||
cp plugins/gitea/.env.example .env
|
||||
$EDITOR .env
|
||||
set -a; source .env; set +a
|
||||
```
|
||||
|
||||
Generate the token in Gitea under Settings, Applications. Scope it to the repositories you want the agent to reach. If the server starts but every call returns an authorization error, that token is the first thing to check.
|
||||
|
||||
Two notes worth reading before you skip one:
|
||||
|
||||
|
||||
1142
apm.lock.yaml
1142
apm.lock.yaml
File diff suppressed because it is too large
Load Diff
18
apm.yml
18
apm.yml
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
license: MIT
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies:
|
||||
# after a kyberforge release, check this first.
|
||||
executables:
|
||||
allow:
|
||||
kyberforge#1.7.0:
|
||||
kyberforge#1.6.2:
|
||||
hooks: true
|
||||
bin: true
|
||||
|
||||
@@ -52,7 +52,7 @@ marketplace:
|
||||
# top-level apm.yml description:/version: above are NOT inherited into the
|
||||
# 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.
|
||||
version: 0.5.0
|
||||
version: 0.4.6
|
||||
owner:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
@@ -79,31 +79,31 @@ marketplace:
|
||||
- name: kyberforge
|
||||
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
||||
source: ./plugins/kyberforge
|
||||
version: 1.7.0
|
||||
version: 1.6.2
|
||||
category: Developer Tools
|
||||
|
||||
- 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.
|
||||
source: ./plugins/bin
|
||||
version: 1.2.0
|
||||
version: 1.1.7
|
||||
category: Utilities
|
||||
|
||||
- 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.
|
||||
source: ./plugins/git
|
||||
version: 1.4.0
|
||||
version: 1.3.7
|
||||
category: Version Control
|
||||
|
||||
- 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.
|
||||
source: ./plugins/gitea
|
||||
version: 1.4.0
|
||||
version: 1.3.8
|
||||
category: Version Control
|
||||
|
||||
- name: core
|
||||
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
|
||||
source: ./plugins/core
|
||||
version: 1.2.0
|
||||
version: 1.1.2
|
||||
category: Productivity
|
||||
|
||||
- name: mattpocock-skills
|
||||
@@ -115,5 +115,5 @@ marketplace:
|
||||
- name: lint
|
||||
description: Skills and agents for configuring and running linters.
|
||||
source: ./plugins/lint
|
||||
version: 1.2.0
|
||||
version: 1.1.7
|
||||
category: Developer Tools
|
||||
|
||||
@@ -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
|
||||
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`,
|
||||
with no `.claude/` or `.agents/` anywhere, now produces findings identical to the working tree —
|
||||
26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs.
|
||||
with no `.claude/` or `.agents/` anywhere, produced findings identical to the working tree. The
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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 —
|
||||
running it directly over `plugins/*/.apm/agents/*.agent.md` today reports 900-word body FAILs on
|
||||
`git-orchestrate` (933), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080). Agents escape by
|
||||
running it directly over `plugins/*/.apm/agents/*.agent.md` exits 1 with 900-word body FAILs on
|
||||
`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
|
||||
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` |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and `gitea-workflow`
|
||||
independently of anything `skill-size-check` reports. Someone scoping the #99 retrofit off the size
|
||||
findings alone will fix those and still be blocked. The two gates want fixing together.
|
||||
`level: error` like every other rule in that style, so at decision time `pre-commit run --all-files`
|
||||
was red on 10 alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and
|
||||
`gitea-workflow` independently of anything `skill-size-check` reports. Someone scoping the #99
|
||||
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
|
||||
preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to
|
||||
|
||||
78
docs/adr/0022-skill-metadata-version-is-mandatory.md
Normal file
78
docs/adr/0022-skill-metadata-version-is-mandatory.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Every skill's `metadata.version` is mandatory, not a per-plugin option
|
||||
|
||||
**Status: accepted (2026-09-07).**
|
||||
|
||||
## Context
|
||||
|
||||
`metadata.version` is optional SKILL.md frontmatter (`create.md`'s "Optional frontmatter" list:
|
||||
"uncomment and fill in, or remove entirely"). `skill-author`'s own bump logic was written
|
||||
conditionally — "with `metadata.version` present, bump the minor version on create... and the
|
||||
patch version on improve" — which only makes sense if presence is a real per-skill choice.
|
||||
|
||||
Adoption never followed a rule; it followed the plugin. Of 39 skills, 12 carry a version:
|
||||
|
||||
| Plugin | Has it | Total |
|
||||
|---|---|---|
|
||||
| `core` | 3 | 3 |
|
||||
| `gitea` | 6 | 7 |
|
||||
| `lint` | 2 | 2 |
|
||||
| `git` | 1 | 9 |
|
||||
| `bin` | 0 | 11 |
|
||||
| `kyberforge` | 0 | 7 |
|
||||
|
||||
`core`, `gitea` and `lint` are consistent adopters (`gitea-files` the one gap); `bin` and
|
||||
`kyberforge` are consistent non-adopters; `git` has one outlier (`git-commits`, versioned for no
|
||||
plugin-specific reason found on inspection — no comment, no cross-reference, nothing distinguishing
|
||||
it from its eight siblings). Issue #127 raised this as an undocumented split: two house norms
|
||||
coexisting with no stated rule for which applies where, the same class of defect as an unstated
|
||||
`rtk`/bare-`git` convention (#113) found in the same audit pass.
|
||||
|
||||
## Decision
|
||||
|
||||
**Every skill's frontmatter carries `metadata.version`.** It is no longer optional, and no longer a
|
||||
per-plugin choice.
|
||||
|
||||
- **The 27 skills that never carried one are seeded at `1.0.0`**, not `0.1.0`. `0.1.0` is
|
||||
`skill-author`'s existing new-skill starting point, chosen for a skill with no revision history to
|
||||
its name yet. These 27 have all been through the ADR-0020 retrofit and repeated audit passes
|
||||
without ever tracking a version; crediting them with `0.1.0` would understate that, and there is
|
||||
no real history to justify seeding higher than a first stable release. `1.0.0` marks "versioned as
|
||||
of this retrofit," `0.1.0` keeps meaning "created and never yet revised."
|
||||
- **New skills still start at `0.1.0`.** `skill-author`'s create/improve bump convention is
|
||||
unchanged; only the presence of the field stops being conditional.
|
||||
- **The one outlier in the other direction, `git-commits`, keeps its existing value** (`0.1.3`) —
|
||||
it already had real tracked history under the old conditional rule, and this decision does not
|
||||
reset skills that were already compliant.
|
||||
- **`bin/write-docs`'s top-level `version:` moves into `metadata:`, normalized to `1.0.0`.** It is
|
||||
the one skill that carried a version outside the `metadata:` block, which is why the table above
|
||||
counts `bin` as 0 — a top-level `version:` is not `metadata.version`, and nothing reads it. #127
|
||||
raised it alongside the split because "does a skill carry a version" and "where does it live" are
|
||||
the same question. Its value (`1.0`) is not semver and carries no more real history than the 27
|
||||
unversioned skills, so it is relocated and reset to the same `1.0.0` seed rather than preserved
|
||||
like `git-commits`'s tracked `0.1.3`.
|
||||
- **`skill-frontmatter`'s pre-commit hook gains the check.** It already fails a SKILL.md missing
|
||||
`name:` or `description:`; a missing `metadata.version` is now the same class of failure, not a
|
||||
style nit an audit might or might not catch.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Leave it per-plugin, document the split.** This was the initial framing of #127 and is coherent —
|
||||
`core`/`gitea`/`lint` keep it, `bin`/`kyberforge` don't, two outliers get normalized to match their
|
||||
plugin. Rejected on reconsideration: a rule that says "some plugins track this and some don't" is
|
||||
strictly harder to state, audit and onboard against than "every skill does," for a field whose entire
|
||||
job is answering "did this change since I last read it" — a question with the same shape everywhere
|
||||
it's asked, not one that varies by plugin domain.
|
||||
|
||||
**Drop the field corpus-wide.** Rejected: `skill-author` already depends on it to decide whether a
|
||||
create/improve pass owes a bump, so the 12 skills carrying it are not tracking dead weight — removing
|
||||
it discards real revision signal for no gain.
|
||||
|
||||
## Consequences
|
||||
|
||||
27 SKILL.md files gain `metadata.version: "1.0.0"`, and a 28th — `bin/write-docs` — reaches the same
|
||||
value by relocating its top-level `version: "1.0"` into `metadata:`. `skill-author`'s `create.md`
|
||||
moves the field from "Optional frontmatter" to the required list, citing this ADR. `skill-author`'s
|
||||
own SKILL.md drops the "with `metadata.version` present" conditional in its bump-rule line, since
|
||||
presence is no longer in question. `.pre-commit-config.yaml`'s `skill-frontmatter` hook is extended
|
||||
to require the field, closing the gap #113 and #118 both named in the same audit pass: a stated rule
|
||||
with nothing enforcing it drifts the same way an unstated one does.
|
||||
168
docs/adr/0023-rtk-prefix-marks-executable-commands-only.md
Normal file
168
docs/adr/0023-rtk-prefix-marks-executable-commands-only.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# The `rtk` prefix marks executable commands only, and is repo-wide
|
||||
|
||||
**Status: accepted (2026-09-08).**
|
||||
|
||||
## Context
|
||||
|
||||
`CLAUDE.md` states the org convention as a golden rule: "Always prefix commands with `rtk`. If RTK
|
||||
has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always
|
||||
safe to use." Issue #113 observed that the rule had never been written down for skill *prose*, where
|
||||
a `git <subcommand>` mention can be either an instruction to execute or a reference to the concept,
|
||||
and that the corpus had drifted into carrying both spellings with no stated rule. PR #130 swept the
|
||||
`git` plugin and recorded a two-way split in `plugins/git/README.md`.
|
||||
|
||||
Review found two defects in that sweep, and both are in the premise rather than the execution.
|
||||
|
||||
**RTK is not output-transparent.** `rtk git --help` enumerates twelve filtered subcommands — `diff`,
|
||||
`log`, `status`, `show`, `add`, `commit`, `push`, `pull`, `branch`, `fetch`, `stash`, `worktree`.
|
||||
Everything else is a true passthrough. Inside that set the filter is not a formatting preference; it
|
||||
changes what the command *reports*. Measured against rtk 0.42.4:
|
||||
|
||||
| Command | What rtk does to it |
|
||||
|---|---|
|
||||
| `worktree list --porcelain -z` | discards both flags; no NUL separators, no `locked`/`lock_reason` field at all |
|
||||
| `worktree list -v` | abbreviates `/root/…` to `~/…`, collapses column alignment |
|
||||
| `branch --list <name>` | emits a phantom `* ` line even on no match |
|
||||
| `diff --name-only` / `--name-status` | appends a blank line and a `Changes:` trailer |
|
||||
| `diff --word-diff[=color\|=porcelain]` | emits no `[-removed-] {+added+}` markers; substitutes a diffstat |
|
||||
| `log -L` | truncates each diff body line at ~72 characters with an ellipsis |
|
||||
| `stash pop` (on conflict) | prints only `FAILED: git stash pop`, swallowing `CONFLICT`, `Unmerged paths` and the retained-entry notice |
|
||||
| `stash list` (empty) | prints `No stashes` where git prints nothing |
|
||||
|
||||
Every one of those falsified a skill that was written against the bare output. `git-worktrees`'s
|
||||
Step 2 required `locked` and `lock_reason` from a command whose rtk rendering has never carried
|
||||
them; `git-log-format.md` documented `[-removed-] {+added+}` markers beside a command that no longer
|
||||
produces them. The two-way split could not see any of this, because both halves of it are about what
|
||||
a *sentence* is doing and none of it is about what the *command* does.
|
||||
|
||||
**The rule is not `git`-plugin-scoped.** `plugins/git/README.md` claimed the `gitea-*` skills
|
||||
"contain no `git`/`rtk` mentions at all". Five `gitea-*` SKILL.md files run `git remote get-url
|
||||
origin` in a fenced ```bash Step block — the README's own canonical example of "executable,
|
||||
instructed" — plus `git branch --show-current` in a reference file and three `git remote -v` in
|
||||
`gitea-orchestrate.agent.md`. A convention stated inside one plugin's README is invisible from the
|
||||
plugin next door, which is how those eight sites stayed bare through the sweep that existed to find
|
||||
them.
|
||||
|
||||
## Decision
|
||||
|
||||
**One rule, three clauses, repo-wide** — every `plugins/*/.apm/skills/**` and
|
||||
`plugins/*/.apm/agents/**` file, not the `git` plugin alone.
|
||||
|
||||
1. **Executable and instructed → `rtk git`.** Anything telling the agent to run a command now: an
|
||||
imperative step, a dispatch-table "Run" cell, a fenced code-block procedure.
|
||||
`rtk git push -u origin <branch>`.
|
||||
2. **Illustrative or referential → bare `git`.** Naming a flag's behaviour, quoting a doc's own
|
||||
heading, describing a command in the abstract, warning against an anti-pattern. "`git switch`
|
||||
refuses rather than clobbering conflicting local edits."
|
||||
3. **Machine-parsed or interactive → bare `git`, and say why inline.** A command whose output the
|
||||
skill parses, where rtk is in the filtered set above; or a command that hands control to an
|
||||
interactive child process.
|
||||
|
||||
Clause 3 is the new one and it looks arbitrary without the table in Context, which is why the
|
||||
measurements are recorded here rather than left in a PR thread. It is applied per subcommand and per
|
||||
flag, not per skill: `tag --list` stays prefixed because rtk passes it through byte-identically,
|
||||
while `branch --list` two words away goes bare because it does not. `git remote get-url origin`,
|
||||
`git remote -v`, `git branch --show-current`, `git log --oneline -1` and `git add -u` were all
|
||||
re-measured as byte-identical passthroughs and are therefore prefixed, parsing notwithstanding.
|
||||
|
||||
Two consequences of that per-subcommand basis are worth stating, because both are load-bearing and
|
||||
neither is comfortable:
|
||||
|
||||
- **rtk's filtered set is a moving target.** `git rebase` and `git mergetool` are passthroughs on
|
||||
0.42.4 — verified under `script(1)`, both inherit a real TTY, contradicting an earlier report that
|
||||
they did not. They stay bare anyway, on the interactive limb: a token filter has nothing to offer a
|
||||
command that hands control to an editor, and the prefix would only buy exposure to whatever a later
|
||||
rtk version decides to do with those subcommands. The same reasoning makes the *inner* call in
|
||||
`` `rtk git remote add origin-push $(git config remote.origin.url)` `` bare while the outer stays
|
||||
prefixed — `config` passes through cleanly today, but its stdout becomes a remote URL that is then
|
||||
force-pushed to, and that is not a blast radius to lend to a future filter change.
|
||||
- **`branch --show-current` sits on the sharp edge.** It is in the filtered set, it is parsed, and it
|
||||
is prefixed — on a measurement, in a subcommand whose sibling `--list` is exactly the defect clause
|
||||
3 exists for. If rtk's `branch` filter is ever extended, that is the first site to break. It is
|
||||
called out rather than hedged, because a rule whose exceptions are unrecorded is the state this ADR
|
||||
is replacing.
|
||||
|
||||
**A clause-3 site says so inline, in a few words.** "bare, not `rtk`: rtk prints a phantom `* ` line
|
||||
even on no match". Without it the next sweep re-prefixes the command, which is how #113 recurs.
|
||||
|
||||
**The rule lives here, and `docs/spec/gates.md` carries the gate.** `plugins/git/README.md` is
|
||||
reduced to a pointer. It had also cited `git-workflow/references/hard-rules.md` as a place the rule
|
||||
was written down; that file contains no occurrence of "rtk", and the citation is removed rather than
|
||||
repaired.
|
||||
|
||||
**Clause 1 is enforced by a `check-rtk-prefix` pre-commit hook; clauses 2 and 3 are not enforceable
|
||||
and are not gated.** The hook checks the two places a `git` mention is unambiguously an instruction —
|
||||
a line in a shell-tagged code fence, and the opening backticked span of a "Run" column cell — and a
|
||||
deliberately-bare command opts out with the literal string `ADR-0023` on its own line. Its coverage
|
||||
limits are recorded in `docs/spec/gates.md`, not smoothed over.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Add `compatibility:` frontmatter to every skill.** These six plugins are installable by third
|
||||
parties, and a consumer who installs `git` from the marketplace has no `rtk` on their PATH. Every
|
||||
prefixed command in the corpus is a plain `git` invocation with a word in front of it, so the prefix
|
||||
is *droppable*: delete `rtk ` and the command is correct. A `compatibility:` line per skill would
|
||||
state that in a machine-readable field. Rejected on cost. It is 39 lines of frontmatter restating one
|
||||
sentence, it is preloaded into every agent's context every session under ADR-0020's budget — the
|
||||
field is not free the way a line in a doc is — and it has no consumer: nothing reads
|
||||
`compatibility:`, so the field would be a comment with a colon in it. The consumer situation is
|
||||
documented here and in `plugins/git/README.md` instead, which is where a human installing a plugin
|
||||
actually looks. The same two-line note is owed to the other five plugin READMEs and is not yet
|
||||
written.
|
||||
|
||||
**Move rtk to the execution layer entirely.** Skills instruct bare `git` throughout; `CLAUDE.md`'s
|
||||
session rule handles prefixing at the point of execution. This is the strongest rejected option and
|
||||
it deserves the space: it closes the consumer gap and all eight output defects at once, because the
|
||||
executing agent knows what it is about to parse and the skill does not have to predict it. It also
|
||||
removes clause 3 entirely — there is nothing to except. Rejected because the prefix is lost wherever
|
||||
an agent copies a command literally, which is the common case for a fenced procedure block and the
|
||||
whole reason dispatch tables exist. The org convention's value is that the prefix is *already there*
|
||||
in the text the agent lifts; a rule that relies on the agent remembering to add it is the rule that
|
||||
produced the drift in the first place. Worth revisiting if rtk ever ships a shell shim, which would
|
||||
make the execution layer transparent and this trade different.
|
||||
|
||||
**Keep the two-way split and fix the eight sites by hand.** Rejected: the split has no vocabulary for
|
||||
"this command is executable, instructed, and must still be bare", so the eight sites would be
|
||||
unexplained exceptions and the next sweep re-prefixes them. That is the failure this ADR exists to
|
||||
stop, not a smaller version of it.
|
||||
|
||||
**Gate clauses 2 and 3 as well.** Rejected as undecidable. "Run `git switch <branch>`" and "`git
|
||||
switch` refuses rather than clobbering local edits" are the same token sequence; separating them is a
|
||||
judgement about what a sentence is doing. A gate that guessed would fire on correct content, and a
|
||||
gate that fires on correct content gets added to `SKIP`, which disarms clause 1 along with it.
|
||||
|
||||
## The boundary the rule does not decide
|
||||
|
||||
Two shapes in the corpus resisted the two-way split. The three-clause rule resolves one and does not
|
||||
resolve the other; both are recorded so an author meeting a third one knows which kind it is.
|
||||
|
||||
**`git-worktrees/SKILL.md`'s tracking row carries both spellings in one Run cell** — `rtk git
|
||||
worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add
|
||||
<path> <branch>` expands to exactly this. **Resolved: the clauses apply per mention, not per row,
|
||||
per cell or per file.** The first is the instruction (clause 1), the second names what the first
|
||||
expands to (clause 2), and one table cell can hold one of each. The rule needed no change; the
|
||||
*gate* did, and it checks only a Run cell's opening span for exactly this reason.
|
||||
|
||||
**`git-submodules/references/setup-and-update.md:80` has a git command inside a quoted argument to
|
||||
another command** — `rtk git submodule foreach 'git pull origin main || :'`. **Not resolved: all
|
||||
three clauses describe a command the reading agent executes, and the inner `git pull` is not one.**
|
||||
It is the literal text of an argument that `git submodule foreach` hands to a subshell running inside
|
||||
each submodule's own working tree, where the local convention does not reach. The file already gets
|
||||
this right and already justifies it in prose two lines below ("the git calls in it are the
|
||||
submodule's own — that is the one place a bare `git` is correct"). **An author meeting this shape
|
||||
should do the same: leave the inner command bare and justify it inline.** It is deliberately not
|
||||
promoted to a fourth clause on one instance. The gate does not decide it either — it happens to pass
|
||||
this line, because the segment containing the inner command begins with `rtk`, and that is an
|
||||
accident of the split rather than an understanding of quoting.
|
||||
|
||||
## Consequences
|
||||
|
||||
Eleven sites in `plugins/git/.apm/skills/**` revert to bare `git` under clause 3, each carrying a
|
||||
short inline reason. Eight sites across `plugins/gitea/.apm/skills/**` and
|
||||
`plugins/gitea/.apm/agents/gitea-orchestrate.agent.md` gain the prefix under clause 1, and one in
|
||||
`pc-run/SKILL.md` that the #130 sweep's grep missed because the backtick opens with `SKIP=` rather
|
||||
than `git `. `plugins/git/README.md`'s Conventions section becomes a pointer here, minus a paragraph
|
||||
that was false about the `gitea-*` skills and a citation to a file that does not carry the rule.
|
||||
A `check-rtk-prefix` pre-commit hook and `tests/test-check-rtk-prefix.sh` land with it; the test runs
|
||||
the gate against the pre-sweep corpus on `main` and asserts it fails there, because a gate that only
|
||||
passes on the fixed tree proves nothing about the drift it was written for.
|
||||
@@ -53,8 +53,7 @@ All skills — new and rebuilt — must follow this standard:
|
||||
- `name:` — matches directory name
|
||||
- `description:` — trigger-tested before writing the body (explicit, implicit, negative cases)
|
||||
- `metadata: category:` — from the category table above
|
||||
|
||||
`version:`, `updated:`, `when:`, `source:`, and `references:` are provenance/audit fields — they live in `META.md` alongside the SKILL.md (not in frontmatter). See `META-TEMPLATE.md` in `.agents/skills/write-skill/` for the META.md schema.
|
||||
- `metadata: version:` — mandatory for every skill (ADR-0022)
|
||||
|
||||
**Body required sections:**
|
||||
- Constraints (highest-ROI element — prevents overengineering)
|
||||
|
||||
@@ -103,21 +103,21 @@ Do not write the SKILL.md until the human has confirmed every section. The synth
|
||||
**c. SKILL.md** (sub-agent)
|
||||
Once all sections are confirmed, spawn a write agent to produce the SKILL.md using `write-skill` (or hand-write for bootstrap skills). The agent receives: trigger description, per-section decisions from step b, upstream content to incorporate, authoring standard (see below).
|
||||
|
||||
**c. META.md — `source:` and `references:` fields**
|
||||
Populate `META.md` after upstream review. Two distinct fields:
|
||||
- `source:` — upstream provenance tracking (repo slug, commit SHA, files adopted with inline comments, updated date). Present only if content was adopted. Absence = self-authored.
|
||||
- `references:` — general citations (research papers, documentation, standard specifications). Present only if the skill cites external research.
|
||||
**d. Provenance — source and reference records**
|
||||
Record provenance after upstream review. Two distinct kinds:
|
||||
- Upstream provenance (repo slug, commit SHA, files adopted with inline comments, updated date). Present only if content was adopted. Absence = self-authored.
|
||||
- General citations (research papers, documentation, standard specifications). Present only if the skill cites external research.
|
||||
|
||||
Both fields live in `META.md` alongside the SKILL.md — not in frontmatter. See `META-TEMPLATE.md` in `.agents/skills/write-skill/` for the full schema.
|
||||
Both are recorded in the skill's own `references/sources.md`, keyed by the `source_keys:` its SKILL.md and reference files declare. `validate-provenance.sh` checks that chain.
|
||||
|
||||
**d. eval.yaml** (sub-agent)
|
||||
**e. eval.yaml** (sub-agent)
|
||||
Invoke `write-eval` in two steps to preserve its confirmation gate:
|
||||
1. Sub-agent proposes test cases and returns the plan to the main conversation.
|
||||
2. Human confirms the plan; then sub-agent writes the file.
|
||||
|
||||
Do not pass pre-designed test cases directly to a write agent — that collapses the plan-then-confirm gate into a single step, bypassing write-eval's own constraint. Co-located at `.agents/evals/<category>/<skill-name>/eval.yaml`. Must contain all five required test types (see Eval schema below).
|
||||
|
||||
**e. HITL behavioral test**
|
||||
**f. HITL behavioral test**
|
||||
Human opens a fresh Claude session, invokes the skill with its trigger phrase, and verifies output. Do not batch more than 2–3 skills before running behavioral tests — output volume must stay within genuine human review capacity. An approval that cannot be meaningfully evaluated is not an approval.
|
||||
|
||||
### Step 6 — Session handoff
|
||||
@@ -157,12 +157,11 @@ name: skill-name
|
||||
description: <trigger description — routing only; written and tested first; max 1024 chars>
|
||||
metadata:
|
||||
category: <design|factory|implement|test|review|deploy|operate|cross-cutting|iac>
|
||||
version: <semver — mandatory for every skill; see ADR-0022>
|
||||
# allowed-tools: <add only when the skill has a narrow, well-defined tool surface; omit otherwise>
|
||||
---
|
||||
```
|
||||
|
||||
Frontmatter contains only these fields. `version`, `updated`, `when`, `source`, and `references` are provenance/audit fields — they are not used for routing or runtime execution. They live in `META.md` alongside the SKILL.md, loaded only when needed. See `META-TEMPLATE.md` in `.agents/skills/write-skill/` for the META.md schema.
|
||||
|
||||
### Body sections
|
||||
|
||||
Use `.agents/skills/write-skill/SKILL-TEMPLATE.md` as the authoritative structure reference. The template defines the required sections, correct order, XML grouping, and placeholder comments for each section.
|
||||
@@ -230,6 +229,6 @@ Upstream review happens per-skill during step 2, not once at chunk start.
|
||||
|
||||
## Open decisions carried forward
|
||||
|
||||
- **Bidirectional reference convention** — Chunk 4 (reference scanner tooling; reverse map "what files point to X?"). The `when:` field itself is resolved — it lives in `META.md` alongside every skill.
|
||||
- **Bidirectional reference convention** — Chunk 4 (reference scanner tooling; reverse map "what files point to X?").
|
||||
- **PRD/issue template scope** — refined during `write-prd` (0020) and `write-issue-spec` (0019) implementation
|
||||
- **Merging `zoom-out` into architect role** — revisit at Chunk 5 grill
|
||||
|
||||
@@ -49,6 +49,13 @@ Two compilers produce the plugin roots you see in the tree:
|
||||
|
||||
`.apm/` is the sole hand-edited authoring source for plugin content. An edit made in the flat mirror is discarded by the next sync and is reported as drift by the `check-plugin-content-sync` pre-push hook. Hand-authored material that is not an `.apm/` primitive — `README.md`, `docs/`, `bin/`, `sources.md`, `.mcp.json`, and per-plugin extras such as `plugins/git/config.example.json`, `plugins/gitea/references/` and `plugins/bin/evals/` — lives at the plugin **root** and is untouched by either compiler.
|
||||
|
||||
`.mcp.json` is the one entry in that list that is still load-bearing for apm rather than merely ignored by it. MCP is a first-class apm primitive — `dependencies.mcp` sits beside `dependencies.apm` in the manifest schema, and apm tracks deployed servers in `apm.lock.yaml` under `mcp_servers`, `mcp_configs` and `mcp_config_provenance`. A plugin reaches that primitive indirectly. `apm pack` writes the string `".mcp.json"` into the generated `.github/plugin/plugin.json` as its `mcpServers` value, and on install apm resolves the plugin manifest in the order `plugin.json`, `.github/plugin/plugin.json`, `.claude-plugin/plugin.json` — so the Copilot manifest wins, the pointer is followed, and `.mcp.json` is injected into the package's `dependencies.mcp` with any `${VAR}` env references intact. Verified against the real remote: a git-sourced install of `plugins/gitea` deploys the gitea server with both env references unexpanded.
|
||||
|
||||
Two consequences follow, and both have bitten already:
|
||||
|
||||
- **Do not declare `dependencies.mcp` in a plugin's own `apm.yml`.** It is the schema-correct place and it breaks the build. The `apm-audit-ci` pre-push hook runs `apm audit --ci` inside every `plugins/*/`, so a declared dependency arms `lockfile-exists` there, which then demands an `apm.lock.yaml` in the package plus every file of that package's own deployed tree present inside the package directory. Measured on `plugins/gitea`: 93 missing deployed files and 79 drifted paths.
|
||||
- **`.claude-plugin/plugin.json` carries an env-stripped copy.** `apm pack` inlines `.mcp.json` there, and its sanitiser drops `env` and `headers` blocks unconditionally at any depth, `${VAR}` indirection included. That copy is inert under apm, which never reaches it, but a native Claude Code plugin install reads exactly that file and would launch the server with no credentials. Anything installed natively rather than through apm needs its MCP env supplied by the host.
|
||||
|
||||
That immunity is positional, not by filename. Anything placed *inside* a mirrored directory is destroyed regardless of what it is: `sync_dir` runs `rm -rf "$dst"` before every copy, and `sync_hooks_json` does the same to `hooks/`. A hand-written `README.md` under `plugins/<name>/hooks/` or `plugins/<name>/skills/` is deleted by the next sync with no drift report, because a file with no `.apm/` counterpart is simply absent from the regenerated tree. This has already cost the repo one document — `plugins/kyberforge/hooks/README.md`, since restored to `plugins/kyberforge/docs/hooks.md`. Plugin-root documentation belongs in `docs/`.
|
||||
|
||||
## Governance layer
|
||||
|
||||
@@ -128,12 +128,43 @@ not an authoring change.
|
||||
### `skill-frontmatter`, the other hook on that scope
|
||||
|
||||
A second `repo: local` pre-commit hook, `skill-frontmatter`, runs on the **same** `files:` pattern at
|
||||
the same stage. It is a short shell loop: for each file, `grep -q "^name:"` and
|
||||
`grep -q "^description:"`, failing with "missing required frontmatter fields" if either is absent.
|
||||
the same stage. It is a shell loop that, **for the YAML frontmatter block only** — everything between
|
||||
the opening `---` and the next `---` — asserts four things per file:
|
||||
|
||||
**It overlaps ADR-0020's "description present and non-empty" FAIL, and the overlap is not clean.**
|
||||
The ADR (`:95-101`) requires that question be decided on the **YAML-folded value** and nowhere else,
|
||||
precisely because a line regex gets it wrong in both directions. Measured on fixtures:
|
||||
| Check | Rejects with |
|
||||
|---|---|
|
||||
| a `^name:` line is present | "missing required frontmatter fields (name: …)" |
|
||||
| a `^description:` line is present | "missing required frontmatter fields (description: …)" |
|
||||
| `metadata:` contains a `^ version:` key, anchored, scanning to the next top-level key | "missing required frontmatter fields (metadata.version)" |
|
||||
| that version's value is three-part semver (`1.0.0`, quoted or not) | "has a malformed frontmatter metadata.version (…)" |
|
||||
|
||||
Every one of those qualifiers is load-bearing, and each replaced a defect that let the hook report
|
||||
Passed having measured nothing. `tests/test-skill-frontmatter.sh` pins all of them:
|
||||
|
||||
- **Frontmatter-scoped, not whole-file.** The checks used to `grep` the entire file, so a `metadata:`
|
||||
or `name:` block quoted in a **body code fence** satisfied them — `skill-author`'s own docs quote
|
||||
exactly such a block.
|
||||
- **Bounded by the next top-level key, not by `-A10`.** The version check was
|
||||
`grep -A10 "^metadata:" | grep -q " version:"`, which ran ten lines past the end of the block: a
|
||||
`version:` belonging to a following `source:` list entry counted (`write-docs` and `research` both
|
||||
have a `source:` list immediately after `metadata:`), while a `metadata:` block with more than ten
|
||||
lines before its `version:` was reported missing.
|
||||
- **`^ version:` anchored.** `" version:"` was an unanchored substring, so a deeper-nested
|
||||
` version:` matched too.
|
||||
- **The value is asserted, not just the key.** `plugins/bin/.apm/skills/write-docs/SKILL.md` carried
|
||||
`version: "1.0"` — present, correctly nested, and not a version — through an entire PR under a
|
||||
presence-only check. Two-part `1.0` is a YAML float, not a version string.
|
||||
- **The call shape is pinned.** `entry: bash` with `args: ['-c', <script>, …]` needs an explicit
|
||||
arg0 placeholder after the script: without it `bash -c` puts pre-commit's **first** filename in
|
||||
`$0`, where `for f in "$@"` never sees it. A single-file commit — the normal case — therefore ran
|
||||
the loop body zero times and exited 0. The third `args` entry (`skill-frontmatter`) exists solely
|
||||
to absorb `$0`; do not remove it.
|
||||
- **An unreadable file is an error, not a pass.** A file with no closing `---` fails with "no closing
|
||||
YAML frontmatter block" rather than falling through to a green.
|
||||
|
||||
**It still overlaps ADR-0020's "description present and non-empty" FAIL, and the overlap is not
|
||||
clean.** The ADR (`:95-101`) requires that question be decided on the **YAML-folded value** and
|
||||
nowhere else, precisely because a line regex gets it wrong in both directions. Measured on fixtures:
|
||||
|
||||
| Frontmatter | `skill-frontmatter` | `skill-size-check` |
|
||||
|---|---|---|
|
||||
@@ -145,10 +176,34 @@ against, and it is the only one of the two that objects to a quoted key. Neither
|
||||
currently live in the corpus, and the honest reading is that presence is `skill-size-check`'s
|
||||
question — the grep's contribution to it is noise on one shape and silence on the other.
|
||||
|
||||
What the grep does add is the `name:` key, which **no** ADR-0020 check reads: a `SKILL.md` with no
|
||||
`name:` passes `skill-size-check` at exit 0. That is its real and only unique coverage, and the
|
||||
What the hook adds that **no** ADR-0020 check reads is two keys: `name:` and `metadata.version`. A
|
||||
`SKILL.md` missing either passes `skill-size-check` at exit 0. That is its unique coverage, and the
|
||||
reason not to fold it into the size gate on the grounds of redundancy.
|
||||
|
||||
#### Why this one stays a shell parser
|
||||
|
||||
[`python3` and PyYAML are hard requirements](#python3-and-pyyaml-are-hard-requirements) below records
|
||||
that a hand-rolled frontmatter reader on this exact `files:` scope was **deliberately deleted**,
|
||||
because "a reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never
|
||||
measured." That reasoning is about `skill-size-check` and does **not** transfer here. Do not delete
|
||||
this hook citing it. Three differences:
|
||||
|
||||
1. **It answers a strictly narrower question.** `skill-size-check` must know the *folded value* of a
|
||||
`>`-block scalar to count its characters, which is where a line reader diverges from a parser —
|
||||
one corpus description measured 270 characters parsed and 412 unparsed. This hook asks only
|
||||
whether a key is on a line and whether one short **plain scalar** matches `N.N.N`. There is no
|
||||
folding, no multi-line value, and no measurement to get subtly wrong.
|
||||
2. **It is frontmatter-scoped.** The failure mode that killed the old fallback was silently reading
|
||||
past or short of the block. This one extracts the block explicitly and errors out when it cannot
|
||||
find a closing marker, so "could not parse" is a red, never a green.
|
||||
3. **It is pinned by tests.** `tests/test-skill-frontmatter.sh` drives the hook through pre-commit's
|
||||
real `bash -c <script> <arg0> <files…>` invocation and asserts each defect class above. The
|
||||
deleted fallback had no such suite; that is how its disagreement with a real parser survived.
|
||||
|
||||
The trade it buys is that the hook stays repo-local. Moving it to a script would change the
|
||||
externally exposed `.pre-commit-hooks.yaml` contract for consumers, for a check that has no need of a
|
||||
YAML parser.
|
||||
|
||||
### Two independent gate families, neither replaced the other
|
||||
|
||||
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
|
||||
@@ -413,6 +468,15 @@ reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file
|
||||
which is the exact vacuous-green failure the `python3` check exists to avoid. `pip install pyyaml`
|
||||
(or `python3 -m pip install PyYAML`, or the distro's `python3-yaml`) if the hook reports it missing.
|
||||
|
||||
**Neither requirement generalises to every hook on this scope, and one deliberate exception sits
|
||||
right next to it.** [`skill-frontmatter`](#skill-frontmatter-the-other-hook-on-that-scope) runs on the
|
||||
same `files:` pattern as a **shell** parser, on purpose — it asks only whether a key is on a line and
|
||||
whether one short plain scalar matches `N.N.N`, with no folding to get wrong, and moving it to a
|
||||
script would change the externally exposed `.pre-commit-hooks.yaml` contract for consumers. That
|
||||
section carries the full argument. A reader arriving here first should not read this one as
|
||||
condemning it. `check-rtk-prefix` needs `python3` but **not** PyYAML: it reads the markdown body and
|
||||
never touches frontmatter, so it has no scalar to fold.
|
||||
|
||||
## Agent files take the description gates, not the body gate
|
||||
|
||||
`check-apm-agents-valid` runs agent-audit's `validate.sh` over every real
|
||||
@@ -497,6 +561,91 @@ pre-commit run --all-files # size AND Vale
|
||||
|
||||
Scoping a retrofit off `skill-size-check` output alone leaves you blocked at the second gate.
|
||||
|
||||
## The `rtk` prefix gate (ADR-0023)
|
||||
|
||||
`check-rtk-prefix` is a `repo: local` pre-commit hook running `scripts/check-rtk-prefix.sh` over
|
||||
`^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$`, with `README.md` excluded. It enforces
|
||||
**ADR-0023 clause 1 and nothing else**: an executable, instructed local git command in plugin skill
|
||||
or agent content is written `rtk git`.
|
||||
|
||||
It is wider in file scope than the ADR-0020 hooks — every markdown file under a plugin's
|
||||
`.apm/skills/` and `.apm/agents/`, not `SKILL.md` alone — because the rule it enforces is about
|
||||
commands an agent runs, and most of those live in `references/`, which the ADR-0020 gates do not
|
||||
reach ([the `references/` blind spot](#the-blind-spot-references-is-unlinted-for-two-independent-reasons)).
|
||||
|
||||
### What it can decide, and what it declines to
|
||||
|
||||
ADR-0023 has three clauses and only the first is a pattern:
|
||||
|
||||
| Clause | Rule | Gated |
|
||||
|---|---|---|
|
||||
| 1 | executable + instructed → `rtk git` | yes |
|
||||
| 2 | illustrative / referential → bare `git` | no — undecidable |
|
||||
| 3 | machine-parsed or interactive → bare `git` | no — opt-out marker |
|
||||
|
||||
Clause 2 is a judgement about what a sentence is *doing*. "Run `git switch <branch>`" and "`git
|
||||
switch` refuses rather than clobbering local edits" are the same token sequence. A gate that guessed
|
||||
would fire on correct prose, and **a gate that fires on correct content gets added to `SKIP`** —
|
||||
which disarms clause 1 along with it. So the hook looks only at the two contexts where a `git`
|
||||
mention is unambiguously an instruction to execute:
|
||||
|
||||
- a line inside a fenced code block whose info string names a shell — `bash`, `sh`, `shell`, `zsh`,
|
||||
`console`, `shell-session`. Fences tagged `text`, `yaml`, `json`, or tagged with nothing, are **not**
|
||||
checked;
|
||||
- the **opening** backticked span of a "Run" column cell in a markdown dispatch table, and only the
|
||||
opening span.
|
||||
|
||||
That last narrowing is not fussiness. A Run cell routinely carries a command followed by prose about
|
||||
it, and the prose is clause 2. `git-worktrees/SKILL.md` has both shapes on adjacent rows — one cell
|
||||
reading `` `rtk git worktree add --track …` `` — always correct. `` `git worktree add <path>
|
||||
<branch>` `` expands to exactly this (instruction, then reference), and a `**Never** …` row whose Run
|
||||
cell is entirely explanation containing a bare `git push`. Checking every backticked span flags both;
|
||||
checking only a leading span flags neither, and still catches the ordinary
|
||||
`` | List | `git worktree list -v` | `` case the gate exists for.
|
||||
|
||||
### The clause-3 opt-out
|
||||
|
||||
A command that is deliberately bare — because rtk rewrites the output the skill parses, or because
|
||||
the command is interactive — is exempted by putting the literal string `ADR-0023` **on the same
|
||||
line**: in a shell comment for a code line, in the cell text for a table row.
|
||||
|
||||
Per line, never per block. A fenced procedure routinely mixes `rtk git` steps with one deliberately
|
||||
bare command (`git-remotes/references/push.md` does exactly that), and a block-level marker would
|
||||
silently disarm every checked line around the marked one. The cost is a repeated `# bare per
|
||||
ADR-0023` in the three blocks of `git-log-format.md` where every line is deliberately bare; that
|
||||
repetition is the price of the marked line being the only line the marker speaks for.
|
||||
|
||||
The marker is a plain substring match, so a line that mentions `ADR-0023` for an unrelated reason is
|
||||
also exempt. Accepted deliberately: the marker records an author's opt-out, it is not a security
|
||||
boundary, and a stricter form would only move the same trust to a different string.
|
||||
|
||||
### What it deliberately does not cover
|
||||
|
||||
- **Clause 2.** Nothing checks that an illustrative mention stayed bare. A sweep that re-prefixes a
|
||||
referential `git` passes this gate. The inline reasons ADR-0023 requires on clause-3 sites are the
|
||||
only defence, and they are prose.
|
||||
- **Prose bullets.** Most of `branch-operations.md`, `merging.md` and `rewrite-history.md` instruct
|
||||
in list items, not fences. Those are clause-1 sites the gate cannot see, because it cannot
|
||||
distinguish them from clause-2 mentions in the same list.
|
||||
- **`README.md`, excluded by pattern.** A skill-directory README is consumer-facing prose no agent
|
||||
loads, and the `git clone https://github.com/bats-core/…` lines in the seven `tests/README.md`
|
||||
files are setup instructions for a third party who has no `rtk`. Prefixing those would be actively
|
||||
wrong, not merely noisy — see ADR-0023's consumer section.
|
||||
- **Quoting.** The line splitter breaks on `;`, `|`, `&&`, `||`, `$(` and backticks without tracking
|
||||
quotes, so a git command inside a quoted argument is decided by accident.
|
||||
`rtk git submodule foreach 'git pull origin main || :'` passes because the segment holding the
|
||||
inner command begins with `rtk` — the right answer for the wrong reason. Write
|
||||
`foreach 'git a; git b'` and the second inner command is a false positive needing the marker.
|
||||
ADR-0023 records this shape as one the rule itself does not decide.
|
||||
- **Non-git commands.** Only `git` is checked. `rtk` fronts `gh`, `docker`, `kubectl` and others; no
|
||||
gate covers those, and the corpus does not currently instruct them.
|
||||
|
||||
`tests/test-check-rtk-prefix.sh` pins all of it, including the false-positive cases. Its first case
|
||||
reconstructs the plugin corpus as it stood on `main` before the #113 sweep and asserts the gate
|
||||
fails there with at least 20 findings, one of them the `gitea-*` `git remote get-url origin` drift
|
||||
the sweep missed — a gate that only passes on the already-fixed tree proves nothing about the drift
|
||||
it was written for.
|
||||
|
||||
## Vale
|
||||
|
||||
Install the `vale` binary — `brew install vale` (macOS), `snap install vale` (Linux),
|
||||
|
||||
@@ -4,6 +4,8 @@ disable-model-invocation: true
|
||||
description: >
|
||||
Ultra-compressed output mode that drops articles, filler and pleasantries while
|
||||
keeping technical substance exact, cutting token usage by roughly 75%.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user says "diagnose this" or "debug this", reports something
|
||||
broken, throwing, or failing, or says something got slow. Not filing or
|
||||
triaging a reported bug -> `triage`. Not test-first feature work -> `tdd`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Diagnose
|
||||
|
||||
@@ -5,6 +5,8 @@ description: >
|
||||
relentless interview — one question at a time, down each branch of the
|
||||
decision tree. Not a plan to challenge against `CONTEXT.md` and ADRs ->
|
||||
`grill-with-docs`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
@@ -4,10 +4,11 @@ The grilling interview, run against the project's domain model — and writing d
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **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 |
|
||||
|------|---------|
|
||||
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the four 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 |
|
||||
| `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |
|
||||
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the five during-session behaviours, and the three-part ADR test |
|
||||
| `references/context-format.md` | Cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry |
|
||||
| `references/adr-format.md` | Cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when a plan should be stress-tested against the project's domain model —
|
||||
the interview challenges terms against `CONTEXT.md` and writes decisions into
|
||||
it and into ADRs as they land. Not a plain interview -> `grill-me`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
<what-to-do>
|
||||
@@ -74,7 +76,7 @@ When the user states how something works, check whether the code agrees. If you
|
||||
|
||||
### Update CONTEXT.md inline
|
||||
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [context-format.md](references/context-format.md).
|
||||
|
||||
Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts.
|
||||
|
||||
@@ -86,6 +88,6 @@ Only offer to create an ADR when all three are true:
|
||||
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
|
||||
If any of the three is missing, skip the ADR. Use the format in [adr-format.md](references/adr-format.md).
|
||||
|
||||
</supporting-info>
|
||||
|
||||
@@ -10,7 +10,7 @@ Looks for places where a codebase is hard to understand, hard to test, or hard f
|
||||
2. **Present candidates.** A numbered list, each with files, problem, solution and benefits — benefits stated in terms of *locality* and *leverage* and of how tests would improve. No interfaces are proposed yet; the user picks one.
|
||||
3. **Grilling loop.** Walks the design tree for the chosen candidate, with documentation side effects landing inline as decisions crystallise.
|
||||
|
||||
The skill is opinionated about vocabulary, and that is the point: **module, interface, implementation, depth, seam, adapter, leverage, locality**, used exactly, with no drift into "component", "service", "API" or "boundary". Domain nouns come from `CONTEXT.md`, architecture nouns from `LANGUAGE.md` — so a proposal reads as "the Order intake module", never "the FooBarHandler".
|
||||
The skill is opinionated about vocabulary, and that is the point: **module, interface, implementation, depth, seam, adapter, leverage, locality**, used exactly, with no drift into "component", "service", "API" or "boundary". Domain nouns come from `CONTEXT.md`, architecture nouns from `references/language.md` — so a proposal reads as "the Order intake module", never "the FooBarHandler".
|
||||
|
||||
ADRs are treated as decisions not to be re-litigated. A candidate that contradicts one is surfaced only when the friction is real enough to warrant reopening it, and is marked as such.
|
||||
|
||||
@@ -31,6 +31,6 @@ Point at a codebase or an area of one. Expect a numbered candidate list and a "w
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Condensed glossary, key principles, and the three-stage process |
|
||||
| `LANGUAGE.md` | Skill-root document, cited throughout `SKILL.md`: full definitions of every term, the words each one replaces, and the full principle list |
|
||||
| `INTERFACE-DESIGN.md` | Skill-root document, read at stage 3 when the user wants alternative interfaces explored: the parallel sub-agent "Design It Twice" pattern, framing the problem space, and the per-agent design constraints |
|
||||
| `DEEPENING.md` | Skill-root document, cited from `INTERFACE-DESIGN.md`: how to deepen a cluster of shallow modules safely, the four dependency categories (in-process, local-substitutable, remote-but-owned, true external), seam discipline, and the replace-don't-layer testing strategy |
|
||||
| `references/language.md` | Cited throughout `SKILL.md`: full definitions of every term, the words each one replaces, and the full principle list |
|
||||
| `references/interface-design.md` | Read at stage 3 when the user wants alternative interfaces explored: the parallel sub-agent "Design It Twice" pattern, framing the problem space, and the per-agent design constraints |
|
||||
| `references/deepening.md` | Cited from `references/interface-design.md`: how to deepen a cluster of shallow modules safely, the four dependency categories (in-process, local-substitutable, remote-but-owned, true external), seam discipline, and the replace-don't-layer testing strategy |
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
---
|
||||
name: improve-codebase-architecture
|
||||
description: >
|
||||
Use when the user wants a codebase's architecture improved — deepening
|
||||
opportunities that turn shallow modules into deep ones, informed by
|
||||
`CONTEXT.md` and `docs/adr/`. Not debugging a failure -> `diagnose`. Not
|
||||
test-first feature work -> `tdd`.
|
||||
Use when the user wants to improve architecture, find refactoring
|
||||
opportunities, consolidate tightly-coupled modules, or make a codebase more
|
||||
testable and AI-navigable — deepening opportunities that turn shallow modules
|
||||
into deep ones, informed by `CONTEXT.md` and `docs/adr/`. Not debugging a
|
||||
failure -> `diagnose`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Improve Codebase Architecture
|
||||
@@ -13,7 +16,7 @@ Surface architectural friction and propose **deepening opportunities** — refac
|
||||
|
||||
## 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).
|
||||
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
|
||||
@@ -24,12 +27,14 @@ Use these terms exactly in every suggestion. Consistent language is the point
|
||||
- **Leverage** — what callers get from depth.
|
||||
- **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.
|
||||
- **The interface is the test surface.**
|
||||
- **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 `references/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.
|
||||
|
||||
## Process
|
||||
@@ -57,7 +62,7 @@ Present a numbered list of deepening opportunities. For each candidate:
|
||||
- **Solution** — plain English description of what would change
|
||||
- **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.
|
||||
|
||||
@@ -69,7 +74,7 @@ Once the user picks a candidate, drop into a grilling conversation. Walk the des
|
||||
|
||||
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 `references/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.
|
||||
- **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).
|
||||
- **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 `references/adr-format.md`.
|
||||
- **Want to explore alternative interfaces for the deepened module?** Read `references/interface-design.md`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deepening
|
||||
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [language.md](language.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
|
||||
## Dependency categories
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
||||
|
||||
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
Uses the vocabulary in [language.md](language.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
|
||||
## Process
|
||||
|
||||
@@ -11,7 +11,7 @@ Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**,
|
||||
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
||||
|
||||
- The constraints any new interface would need to satisfy
|
||||
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
|
||||
- The dependencies it would rely on, and which category they fall into (see [deepening.md](deepening.md))
|
||||
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
|
||||
|
||||
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
||||
@@ -20,21 +20,21 @@ Show this to the user, then immediately proceed to Step 2. The user reads and th
|
||||
|
||||
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
|
||||
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [deepening.md](deepening.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
|
||||
- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point."
|
||||
- Agent 2: "Maximise flexibility — support many use cases and extension."
|
||||
- Agent 3: "Optimise for the most common caller — make the default case trivial."
|
||||
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
||||
|
||||
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
Include both [language.md](language.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
|
||||
Each sub-agent outputs:
|
||||
|
||||
1. Interface (types, methods, params — plus invariants, ordering, error modes)
|
||||
2. Usage example showing how callers use it
|
||||
3. What the implementation hides behind the seam
|
||||
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
|
||||
4. Dependency strategy and adapters (see [deepening.md](deepening.md))
|
||||
5. Trade-offs — where leverage is high, where it's thin
|
||||
|
||||
### 3. Present and compare
|
||||
@@ -5,6 +5,8 @@ description: >
|
||||
a data model, state machine or business logic, or to mock up a UI in several
|
||||
variations. Not production code -> `tdd`. Not talking a design through ->
|
||||
`grill-me`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Prototype
|
||||
|
||||
@@ -72,5 +72,8 @@ When the prototype has done its job, the answer to the question is the only thin
|
||||
|
||||
## 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 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.
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
documentation written from existing code or specs -> `write-docs`. Not a bug
|
||||
or incident -> `diagnose`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: research
|
||||
allowed-tools:
|
||||
- Grep
|
||||
|
||||
@@ -25,8 +25,8 @@ Describe the feature or bug. Expect the skill to ask what the public interface s
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Philosophy, the horizontal-slicing anti-pattern, the four-stage workflow, and the per-cycle checklist |
|
||||
| `tests.md` | Skill-root document, cited from Philosophy: worked good and bad test examples |
|
||||
| `mocking.md` | Skill-root document, cited from Philosophy: mock at system boundaries only, and what not to mock |
|
||||
| `deep-modules.md` | Skill-root document, cited from stage 1: what a deep module is (small interface, large implementation) and why it is the design to aim for |
|
||||
| `interface-design.md` | Skill-root document, cited from stage 1: designing interfaces for testability, starting with accepting dependencies rather than creating them |
|
||||
| `refactoring.md` | Skill-root document, cited from stage 4: the refactor-candidate checklist — duplication, long methods, shallow modules, feature envy, primitive obsession |
|
||||
| `references/tests.md` | Cited from Philosophy: worked good and bad test examples |
|
||||
| `references/mocking.md` | Cited from Philosophy: mock at system boundaries only, and what not to mock |
|
||||
| `references/deep-modules.md` | Cited from stage 1: what a deep module is (small interface, large implementation) and why it is the design to aim for |
|
||||
| `references/interface-design.md` | Cited from stage 1: designing interfaces for testability, starting with accepting dependencies rather than creating them |
|
||||
| `references/refactoring.md` | Cited from stage 4: the refactor-candidate checklist — duplication, long methods, shallow modules, feature envy, primitive obsession |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user wants a feature built or a bug fixed test-first, in a strict
|
||||
red-green-refactor loop, one behaviour at a time. Not diagnosing an existing
|
||||
bug -> `diagnose`. Not throwaway exploratory code -> `prototype`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Test-Driven Development
|
||||
@@ -16,7 +18,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.
|
||||
|
||||
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 `references/tests.md`. If a test needs a collaborator faked, read `references/mocking.md` before reaching for a mock.
|
||||
|
||||
## Anti-Pattern: Horizontal Slices
|
||||
|
||||
@@ -53,8 +55,8 @@ Before writing any code:
|
||||
|
||||
- [ ] Confirm with user what interface changes are needed
|
||||
- [ ] Confirm with user which behaviors to test (prioritize)
|
||||
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
|
||||
- [ ] Design interfaces for [testability](interface-design.md)
|
||||
- [ ] Identify opportunities for [deep modules](references/deep-modules.md) (small interface, deep implementation)
|
||||
- [ ] Design interfaces for [testability](references/interface-design.md)
|
||||
- [ ] List the behaviors to test (not implementation steps)
|
||||
- [ ] Get user approval on the plan
|
||||
|
||||
@@ -91,7 +93,7 @@ Rules:
|
||||
|
||||
### 4. Refactor
|
||||
|
||||
After all tests pass, look for [refactor candidates](refactoring.md):
|
||||
After all tests pass, look for [refactor candidates](references/refactoring.md):
|
||||
|
||||
- [ ] Extract duplication
|
||||
- [ ] Deepen modules (move complexity behind simple interfaces)
|
||||
|
||||
@@ -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.
|
||||
- **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
|
||||
|
||||
@@ -31,5 +31,5 @@ Then describe what you want in natural language — "show me anything that needs
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | The roles and state machine, the three invocation modes, the needs-info template, and how to resume a prior session |
|
||||
| `AGENT-BRIEF.md` | Skill-root document, cited when an issue moves to `ready-for-agent` (and reused for `ready-for-human`): how to write a brief that stays durable for weeks while the codebase moves under it — describe interfaces and behavioural contracts, not line numbers |
|
||||
| `OUT-OF-SCOPE.md` | Skill-root document, cited when an enhancement is closed `wontfix` and when checking for prior rejections: how the `.out-of-scope/` knowledge base is laid out and what it is for — institutional memory, and deduplication against re-litigated requests |
|
||||
| `references/agent-brief.md` | Cited when an issue moves to `ready-for-agent` (and reused for `ready-for-human`): how to write a brief that stays durable for weeks while the codebase moves under it — describe interfaces and behavioural contracts, not line numbers |
|
||||
| `references/out-of-scope.md` | Cited when an enhancement is closed `wontfix` and when checking for prior rejections: how the `.out-of-scope/` knowledge base is laid out and what it is for — institutional memory, and deduplication against re-litigated requests |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user wants an issue created, triaged, or moved through the
|
||||
tracker's triage states, or an issue prepared for an AFK agent. Not debugging
|
||||
the bug itself -> `diagnose`. Not fleshing out a design -> `grill-with-docs`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Triage
|
||||
@@ -18,8 +20,8 @@ Every comment or issue posted to the issue tracker during triage **must** start
|
||||
|
||||
## Reference docs
|
||||
|
||||
- [AGENT-BRIEF.md](AGENT-BRIEF.md) — how to write durable agent briefs
|
||||
- [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md) — how the `.out-of-scope/` knowledge base works
|
||||
- [agent-brief.md](references/agent-brief.md) — how to write durable agent briefs
|
||||
- [out-of-scope.md](references/out-of-scope.md) — how the `.out-of-scope/` knowledge base works
|
||||
|
||||
## Roles
|
||||
|
||||
@@ -72,11 +74,11 @@ Show counts and a one-line summary per issue. Let the maintainer pick.
|
||||
4. **Grill (if needed).** If the issue needs fleshing out, run a `/grill-with-docs` session.
|
||||
|
||||
5. **Apply the outcome:**
|
||||
- `ready-for-agent` — post an agent brief comment ([AGENT-BRIEF.md](AGENT-BRIEF.md)).
|
||||
- `ready-for-agent` — post an agent brief comment ([agent-brief.md](references/agent-brief.md)).
|
||||
- `ready-for-human` — same structure as an agent brief, but note why it can't be delegated (judgment calls, external access, design decisions, manual testing).
|
||||
- `needs-info` — post triage notes (template below).
|
||||
- `wontfix` (bug) — polite explanation, then close.
|
||||
- `wontfix` (enhancement) — write to `.out-of-scope/`, link to it from a comment, then close ([OUT-OF-SCOPE.md](OUT-OF-SCOPE.md)).
|
||||
- `wontfix` (enhancement) — write to `.out-of-scope/`, link to it from a comment, then close ([out-of-scope.md](references/out-of-scope.md)).
|
||||
- `needs-triage` — apply the role. Optional comment if there's partial progress.
|
||||
|
||||
## Quick state override
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
name: write-docs
|
||||
description: >
|
||||
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 ->
|
||||
`grill-with-docs`. Not an external tool researched from its docs -> `research`.
|
||||
version: "1.0"
|
||||
or spec, every claim traced to a source — "write docs for X", "document this
|
||||
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`.
|
||||
updated: 2026-05-17
|
||||
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: implement
|
||||
source:
|
||||
- repo: anthropics/skills
|
||||
@@ -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"
|
||||
|
||||
**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 marketing or blog copy
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
name: zoom-out
|
||||
description: Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.
|
||||
disable-model-invocation: true
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
2
plugins/bin/.github/plugin/plugin.json
vendored
2
plugins/bin/.github/plugin/plugin.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.7",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: bin
|
||||
version: 1.2.0
|
||||
version: 1.1.7
|
||||
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
|
||||
author:
|
||||
name: Defame1297
|
||||
|
||||
@@ -4,6 +4,8 @@ disable-model-invocation: true
|
||||
description: >
|
||||
Ultra-compressed output mode that drops articles, filler and pleasantries while
|
||||
keeping technical substance exact, cutting token usage by roughly 75%.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user says "diagnose this" or "debug this", reports something
|
||||
broken, throwing, or failing, or says something got slow. Not filing or
|
||||
triaging a reported bug -> `triage`. Not test-first feature work -> `tdd`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Diagnose
|
||||
|
||||
@@ -5,6 +5,8 @@ description: >
|
||||
relentless interview — one question at a time, down each branch of the
|
||||
decision tree. Not a plan to challenge against `CONTEXT.md` and ADRs ->
|
||||
`grill-with-docs`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
@@ -4,10 +4,11 @@ The grilling interview, run against the project's domain model — and writing d
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **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 |
|
||||
|------|---------|
|
||||
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the four 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 |
|
||||
| `ADR-FORMAT.md` | Skill-root document, cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |
|
||||
| `SKILL.md` | The interview instruction plus the domain-awareness rules: file layout discovery, the five during-session behaviours, and the three-part ADR test |
|
||||
| `references/context-format.md` | Cited when a term is resolved: the structure of a `CONTEXT.md` and how to write a Language entry |
|
||||
| `references/adr-format.md` | Cited when an ADR is offered: `docs/adr/` naming, sequential numbering, and the ADR template |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when a plan should be stress-tested against the project's domain model —
|
||||
the interview challenges terms against `CONTEXT.md` and writes decisions into
|
||||
it and into ADRs as they land. Not a plain interview -> `grill-me`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
<what-to-do>
|
||||
@@ -74,7 +76,7 @@ When the user states how something works, check whether the code agrees. If you
|
||||
|
||||
### Update CONTEXT.md inline
|
||||
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [context-format.md](references/context-format.md).
|
||||
|
||||
Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts.
|
||||
|
||||
@@ -86,6 +88,6 @@ Only offer to create an ADR when all three are true:
|
||||
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
|
||||
If any of the three is missing, skip the ADR. Use the format in [adr-format.md](references/adr-format.md).
|
||||
|
||||
</supporting-info>
|
||||
|
||||
@@ -10,7 +10,7 @@ Looks for places where a codebase is hard to understand, hard to test, or hard f
|
||||
2. **Present candidates.** A numbered list, each with files, problem, solution and benefits — benefits stated in terms of *locality* and *leverage* and of how tests would improve. No interfaces are proposed yet; the user picks one.
|
||||
3. **Grilling loop.** Walks the design tree for the chosen candidate, with documentation side effects landing inline as decisions crystallise.
|
||||
|
||||
The skill is opinionated about vocabulary, and that is the point: **module, interface, implementation, depth, seam, adapter, leverage, locality**, used exactly, with no drift into "component", "service", "API" or "boundary". Domain nouns come from `CONTEXT.md`, architecture nouns from `LANGUAGE.md` — so a proposal reads as "the Order intake module", never "the FooBarHandler".
|
||||
The skill is opinionated about vocabulary, and that is the point: **module, interface, implementation, depth, seam, adapter, leverage, locality**, used exactly, with no drift into "component", "service", "API" or "boundary". Domain nouns come from `CONTEXT.md`, architecture nouns from `references/language.md` — so a proposal reads as "the Order intake module", never "the FooBarHandler".
|
||||
|
||||
ADRs are treated as decisions not to be re-litigated. A candidate that contradicts one is surfaced only when the friction is real enough to warrant reopening it, and is marked as such.
|
||||
|
||||
@@ -31,6 +31,6 @@ Point at a codebase or an area of one. Expect a numbered candidate list and a "w
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Condensed glossary, key principles, and the three-stage process |
|
||||
| `LANGUAGE.md` | Skill-root document, cited throughout `SKILL.md`: full definitions of every term, the words each one replaces, and the full principle list |
|
||||
| `INTERFACE-DESIGN.md` | Skill-root document, read at stage 3 when the user wants alternative interfaces explored: the parallel sub-agent "Design It Twice" pattern, framing the problem space, and the per-agent design constraints |
|
||||
| `DEEPENING.md` | Skill-root document, cited from `INTERFACE-DESIGN.md`: how to deepen a cluster of shallow modules safely, the four dependency categories (in-process, local-substitutable, remote-but-owned, true external), seam discipline, and the replace-don't-layer testing strategy |
|
||||
| `references/language.md` | Cited throughout `SKILL.md`: full definitions of every term, the words each one replaces, and the full principle list |
|
||||
| `references/interface-design.md` | Read at stage 3 when the user wants alternative interfaces explored: the parallel sub-agent "Design It Twice" pattern, framing the problem space, and the per-agent design constraints |
|
||||
| `references/deepening.md` | Cited from `references/interface-design.md`: how to deepen a cluster of shallow modules safely, the four dependency categories (in-process, local-substitutable, remote-but-owned, true external), seam discipline, and the replace-don't-layer testing strategy |
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
---
|
||||
name: improve-codebase-architecture
|
||||
description: >
|
||||
Use when the user wants a codebase's architecture improved — deepening
|
||||
opportunities that turn shallow modules into deep ones, informed by
|
||||
`CONTEXT.md` and `docs/adr/`. Not debugging a failure -> `diagnose`. Not
|
||||
test-first feature work -> `tdd`.
|
||||
Use when the user wants to improve architecture, find refactoring
|
||||
opportunities, consolidate tightly-coupled modules, or make a codebase more
|
||||
testable and AI-navigable — deepening opportunities that turn shallow modules
|
||||
into deep ones, informed by `CONTEXT.md` and `docs/adr/`. Not debugging a
|
||||
failure -> `diagnose`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Improve Codebase Architecture
|
||||
@@ -13,7 +16,7 @@ Surface architectural friction and propose **deepening opportunities** — refac
|
||||
|
||||
## 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).
|
||||
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
|
||||
@@ -24,12 +27,14 @@ Use these terms exactly in every suggestion. Consistent language is the point
|
||||
- **Leverage** — what callers get from depth.
|
||||
- **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.
|
||||
- **The interface is the test surface.**
|
||||
- **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 `references/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.
|
||||
|
||||
## Process
|
||||
@@ -57,7 +62,7 @@ Present a numbered list of deepening opportunities. For each candidate:
|
||||
- **Solution** — plain English description of what would change
|
||||
- **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.
|
||||
|
||||
@@ -69,7 +74,7 @@ Once the user picks a candidate, drop into a grilling conversation. Walk the des
|
||||
|
||||
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 `references/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.
|
||||
- **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).
|
||||
- **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 `references/adr-format.md`.
|
||||
- **Want to explore alternative interfaces for the deepened module?** Read `references/interface-design.md`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deepening
|
||||
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [language.md](language.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
|
||||
## Dependency categories
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
||||
|
||||
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
Uses the vocabulary in [language.md](language.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
|
||||
## Process
|
||||
|
||||
@@ -11,7 +11,7 @@ Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**,
|
||||
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
||||
|
||||
- The constraints any new interface would need to satisfy
|
||||
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
|
||||
- The dependencies it would rely on, and which category they fall into (see [deepening.md](deepening.md))
|
||||
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
|
||||
|
||||
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
||||
@@ -20,21 +20,21 @@ Show this to the user, then immediately proceed to Step 2. The user reads and th
|
||||
|
||||
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
|
||||
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [deepening.md](deepening.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
|
||||
- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point."
|
||||
- Agent 2: "Maximise flexibility — support many use cases and extension."
|
||||
- Agent 3: "Optimise for the most common caller — make the default case trivial."
|
||||
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
||||
|
||||
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
Include both [language.md](language.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
|
||||
Each sub-agent outputs:
|
||||
|
||||
1. Interface (types, methods, params — plus invariants, ordering, error modes)
|
||||
2. Usage example showing how callers use it
|
||||
3. What the implementation hides behind the seam
|
||||
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
|
||||
4. Dependency strategy and adapters (see [deepening.md](deepening.md))
|
||||
5. Trade-offs — where leverage is high, where it's thin
|
||||
|
||||
### 3. Present and compare
|
||||
@@ -5,6 +5,8 @@ description: >
|
||||
a data model, state machine or business logic, or to mock up a UI in several
|
||||
variations. Not production code -> `tdd`. Not talking a design through ->
|
||||
`grill-me`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Prototype
|
||||
|
||||
@@ -72,5 +72,8 @@ When the prototype has done its job, the answer to the question is the only thin
|
||||
|
||||
## 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 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.
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
documentation written from existing code or specs -> `write-docs`. Not a bug
|
||||
or incident -> `diagnose`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: research
|
||||
allowed-tools:
|
||||
- Grep
|
||||
|
||||
@@ -25,8 +25,8 @@ Describe the feature or bug. Expect the skill to ask what the public interface s
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Philosophy, the horizontal-slicing anti-pattern, the four-stage workflow, and the per-cycle checklist |
|
||||
| `tests.md` | Skill-root document, cited from Philosophy: worked good and bad test examples |
|
||||
| `mocking.md` | Skill-root document, cited from Philosophy: mock at system boundaries only, and what not to mock |
|
||||
| `deep-modules.md` | Skill-root document, cited from stage 1: what a deep module is (small interface, large implementation) and why it is the design to aim for |
|
||||
| `interface-design.md` | Skill-root document, cited from stage 1: designing interfaces for testability, starting with accepting dependencies rather than creating them |
|
||||
| `refactoring.md` | Skill-root document, cited from stage 4: the refactor-candidate checklist — duplication, long methods, shallow modules, feature envy, primitive obsession |
|
||||
| `references/tests.md` | Cited from Philosophy: worked good and bad test examples |
|
||||
| `references/mocking.md` | Cited from Philosophy: mock at system boundaries only, and what not to mock |
|
||||
| `references/deep-modules.md` | Cited from stage 1: what a deep module is (small interface, large implementation) and why it is the design to aim for |
|
||||
| `references/interface-design.md` | Cited from stage 1: designing interfaces for testability, starting with accepting dependencies rather than creating them |
|
||||
| `references/refactoring.md` | Cited from stage 4: the refactor-candidate checklist — duplication, long methods, shallow modules, feature envy, primitive obsession |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user wants a feature built or a bug fixed test-first, in a strict
|
||||
red-green-refactor loop, one behaviour at a time. Not diagnosing an existing
|
||||
bug -> `diagnose`. Not throwaway exploratory code -> `prototype`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Test-Driven Development
|
||||
@@ -16,7 +18,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.
|
||||
|
||||
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 `references/tests.md`. If a test needs a collaborator faked, read `references/mocking.md` before reaching for a mock.
|
||||
|
||||
## Anti-Pattern: Horizontal Slices
|
||||
|
||||
@@ -53,8 +55,8 @@ Before writing any code:
|
||||
|
||||
- [ ] Confirm with user what interface changes are needed
|
||||
- [ ] Confirm with user which behaviors to test (prioritize)
|
||||
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
|
||||
- [ ] Design interfaces for [testability](interface-design.md)
|
||||
- [ ] Identify opportunities for [deep modules](references/deep-modules.md) (small interface, deep implementation)
|
||||
- [ ] Design interfaces for [testability](references/interface-design.md)
|
||||
- [ ] List the behaviors to test (not implementation steps)
|
||||
- [ ] Get user approval on the plan
|
||||
|
||||
@@ -91,7 +93,7 @@ Rules:
|
||||
|
||||
### 4. Refactor
|
||||
|
||||
After all tests pass, look for [refactor candidates](refactoring.md):
|
||||
After all tests pass, look for [refactor candidates](references/refactoring.md):
|
||||
|
||||
- [ ] Extract duplication
|
||||
- [ ] Deepen modules (move complexity behind simple interfaces)
|
||||
|
||||
@@ -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.
|
||||
- **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
|
||||
|
||||
@@ -31,5 +31,5 @@ Then describe what you want in natural language — "show me anything that needs
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | The roles and state machine, the three invocation modes, the needs-info template, and how to resume a prior session |
|
||||
| `AGENT-BRIEF.md` | Skill-root document, cited when an issue moves to `ready-for-agent` (and reused for `ready-for-human`): how to write a brief that stays durable for weeks while the codebase moves under it — describe interfaces and behavioural contracts, not line numbers |
|
||||
| `OUT-OF-SCOPE.md` | Skill-root document, cited when an enhancement is closed `wontfix` and when checking for prior rejections: how the `.out-of-scope/` knowledge base is laid out and what it is for — institutional memory, and deduplication against re-litigated requests |
|
||||
| `references/agent-brief.md` | Cited when an issue moves to `ready-for-agent` (and reused for `ready-for-human`): how to write a brief that stays durable for weeks while the codebase moves under it — describe interfaces and behavioural contracts, not line numbers |
|
||||
| `references/out-of-scope.md` | Cited when an enhancement is closed `wontfix` and when checking for prior rejections: how the `.out-of-scope/` knowledge base is laid out and what it is for — institutional memory, and deduplication against re-litigated requests |
|
||||
|
||||
@@ -4,6 +4,8 @@ description: >
|
||||
Use when the user wants an issue created, triaged, or moved through the
|
||||
tracker's triage states, or an issue prepared for an AFK agent. Not debugging
|
||||
the bug itself -> `diagnose`. Not fleshing out a design -> `grill-with-docs`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Triage
|
||||
@@ -18,8 +20,8 @@ Every comment or issue posted to the issue tracker during triage **must** start
|
||||
|
||||
## Reference docs
|
||||
|
||||
- [AGENT-BRIEF.md](AGENT-BRIEF.md) — how to write durable agent briefs
|
||||
- [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md) — how the `.out-of-scope/` knowledge base works
|
||||
- [agent-brief.md](references/agent-brief.md) — how to write durable agent briefs
|
||||
- [out-of-scope.md](references/out-of-scope.md) — how the `.out-of-scope/` knowledge base works
|
||||
|
||||
## Roles
|
||||
|
||||
@@ -72,11 +74,11 @@ Show counts and a one-line summary per issue. Let the maintainer pick.
|
||||
4. **Grill (if needed).** If the issue needs fleshing out, run a `/grill-with-docs` session.
|
||||
|
||||
5. **Apply the outcome:**
|
||||
- `ready-for-agent` — post an agent brief comment ([AGENT-BRIEF.md](AGENT-BRIEF.md)).
|
||||
- `ready-for-agent` — post an agent brief comment ([agent-brief.md](references/agent-brief.md)).
|
||||
- `ready-for-human` — same structure as an agent brief, but note why it can't be delegated (judgment calls, external access, design decisions, manual testing).
|
||||
- `needs-info` — post triage notes (template below).
|
||||
- `wontfix` (bug) — polite explanation, then close.
|
||||
- `wontfix` (enhancement) — write to `.out-of-scope/`, link to it from a comment, then close ([OUT-OF-SCOPE.md](OUT-OF-SCOPE.md)).
|
||||
- `wontfix` (enhancement) — write to `.out-of-scope/`, link to it from a comment, then close ([out-of-scope.md](references/out-of-scope.md)).
|
||||
- `needs-triage` — apply the role. Optional comment if there's partial progress.
|
||||
|
||||
## Quick state override
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
name: write-docs
|
||||
description: >
|
||||
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 ->
|
||||
`grill-with-docs`. Not an external tool researched from its docs -> `research`.
|
||||
version: "1.0"
|
||||
or spec, every claim traced to a source — "write docs for X", "document this
|
||||
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`.
|
||||
updated: 2026-05-17
|
||||
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: implement
|
||||
source:
|
||||
- repo: anthropics/skills
|
||||
@@ -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"
|
||||
|
||||
**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 marketing or blog copy
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
name: zoom-out
|
||||
description: Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.
|
||||
disable-model-invocation: true
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary.
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **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 without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
||||
- **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 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.
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ This skill is reached two ways: invoked directly by a user, and composed into by
|
||||
once it has written or updated the repo's `AGENTS.md`. Behave identically either way — do not
|
||||
assume a caller skill exists. Detect the provider file yourself, confirm `AGENTS.md` yourself, and
|
||||
run the closeout validator yourself, rather than treating any step as already done by the caller or
|
||||
as something the caller will do afterwards. There is no handshake to rely on and no state passed
|
||||
in beyond the file paths.
|
||||
as something the caller will do afterwards. No handshake exists to rely on, and no state is
|
||||
passed in beyond the file paths.
|
||||
|
||||
@@ -4,6 +4,25 @@ Deterministic self-check this skill shells out to instead of relying on LLM judg
|
||||
|
||||
| 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 |
|
||||
|
||||
@@ -14,6 +14,10 @@ Arguments:
|
||||
adapter-file Path to the provider-specific file to check.
|
||||
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:
|
||||
--no-import-syntax The target provider has no native cross-file import
|
||||
mechanism. Require a plain-text pointer line naming
|
||||
@@ -26,26 +30,69 @@ Options:
|
||||
it's considered no longer "thin". Must be a
|
||||
non-negative integer. Default: 60.
|
||||
--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:
|
||||
0 Adapter file passes all checks
|
||||
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
||||
excessive duplication, or file too long)
|
||||
2 Usage or input error — a bad or missing argument, a path that is not a
|
||||
file, or a file that is not UTF-8. Nothing was graded, so there is no
|
||||
FAIL line and no adapter edit to make: fix the invocation or the file's
|
||||
encoding and re-run. Kept distinct from 1 because the skill's own
|
||||
closeout tells the agent to fix every non-zero exit by editing the
|
||||
provider file, which for a mistyped flag edits the wrong file forever.
|
||||
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 and no adapter edit to make: fix
|
||||
the invocation or the file's encoding and re-run. Kept distinct from 1
|
||||
because the skill's own closeout tells the agent to fix every non-zero
|
||||
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
|
||||
}
|
||||
|
||||
NO_IMPORT_SYNTAX=0
|
||||
MAX_LINES=60
|
||||
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
|
||||
if [[ $END_OF_OPTS -eq 1 ]]; then
|
||||
ARGS+=("$1")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
case "$1" in
|
||||
--)
|
||||
END_OF_OPTS=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
@@ -54,17 +101,37 @@ while [[ $# -gt 0 ]]; do
|
||||
NO_IMPORT_SYNTAX=1
|
||||
shift
|
||||
;;
|
||||
--no-import-syntax=*)
|
||||
echo "Error: --no-import-syntax is a flag and takes no value (got '$1')." >&2
|
||||
exit 2
|
||||
;;
|
||||
--max-lines)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
||||
exit 2
|
||||
fi
|
||||
MAX_LINES="$2"
|
||||
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
||||
require_int "$MAX_LINES"
|
||||
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
|
||||
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")
|
||||
@@ -80,6 +147,13 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
|
||||
exit 2
|
||||
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'
|
||||
import sys
|
||||
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"
|
||||
max_lines = int(max_lines)
|
||||
|
||||
EXIT_FAIL = 1
|
||||
EXIT_USAGE = 2
|
||||
EXIT_UNREADABLE = 3
|
||||
|
||||
if not os.path.isfile(adapter_path):
|
||||
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):
|
||||
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(EXIT_USAGE)
|
||||
|
||||
|
||||
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
|
||||
not `\s` in Python, so a CLAUDE.md saved by an editor that emits one had
|
||||
its first line — the `@AGENTS.md` import, which is the whole adapter —
|
||||
silently treated as prose. The check then said "no reference to AGENTS.md"
|
||||
told the author to add the line already sitting in front of them. Same
|
||||
class of silent BOM miss recorded in scripts/skill-size-check.sh; strip it
|
||||
at the reader so no later check has to know about it.
|
||||
The BOM strip is not cosmetic. IMPORT_RE anchors on `^ {0,3}@`, and a BOM
|
||||
is not whitespace in Python, so a CLAUDE.md saved by an editor that emits
|
||||
one had its first line — the `@AGENTS.md` import, which is the whole
|
||||
adapter — silently treated as prose. The check then said "no reference to
|
||||
AGENTS.md" and told the author to add the line already sitting in front of
|
||||
them. Same class of silent BOM miss recorded in scripts/skill-size-check.sh;
|
||||
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
|
||||
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",
|
||||
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.
|
||||
|
||||
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:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
with open(path, "rb") as fh:
|
||||
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:
|
||||
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 "
|
||||
"at other encodings.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
return text[1:] if text.startswith("\ufeff") else text
|
||||
sys.exit(EXIT_USAGE)
|
||||
return text.replace("\ufeff", "")
|
||||
|
||||
|
||||
adapter_content = read_text(adapter_path)
|
||||
agents_md_content = read_text(agents_md_path)
|
||||
adapter_dir = os.path.dirname(os.path.abspath(adapter_path))
|
||||
|
||||
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(" Fix: Add at least an import (or text pointer) to AGENTS.md.")
|
||||
print()
|
||||
sys.exit(1)
|
||||
sys.exit(EXIT_FAIL)
|
||||
|
||||
IMPORT_RE = re.compile(r'(?m)^\s*@\S*AGENTS\.md\s*$')
|
||||
lines = adapter_content.splitlines()
|
||||
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
|
||||
# line — an inert `@AGENTS.md` in a provider that resolves no imports points
|
||||
# a reader at nothing.
|
||||
pointer_lines = [ln for ln in lines if not IMPORT_RE.match(ln) and "AGENTS.md" in ln]
|
||||
|
||||
# --- Inert regions -----------------------------------------------------------
|
||||
#
|
||||
# A reference only counts where something would actually resolve it. Fenced
|
||||
# code blocks, indented code blocks and HTML comments are shown to the reader
|
||||
# (or hidden from them) as literal text; Claude Code resolves an @import in
|
||||
# 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:
|
||||
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:
|
||||
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:
|
||||
has_fail = True
|
||||
print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}")
|
||||
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(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
||||
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.\", outside any fence, naming a path that exists relative to this file.")
|
||||
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(" 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(" 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 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()
|
||||
|
||||
# --- 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()]
|
||||
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()
|
||||
|
||||
if has_fail:
|
||||
sys.exit(1)
|
||||
sys.exit(EXIT_FAIL)
|
||||
sys.exit(0)
|
||||
PYTHON
|
||||
|
||||
@@ -212,3 +212,308 @@ EOF
|
||||
assert_output --partial "no reference"
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
2
plugins/core/.github/plugin/plugin.json
vendored
2
plugins/core/.github/plugin/plugin.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
author:
|
||||
name: Defame1297
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **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 without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
||||
- **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 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.
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ This skill is reached two ways: invoked directly by a user, and composed into by
|
||||
once it has written or updated the repo's `AGENTS.md`. Behave identically either way — do not
|
||||
assume a caller skill exists. Detect the provider file yourself, confirm `AGENTS.md` yourself, and
|
||||
run the closeout validator yourself, rather than treating any step as already done by the caller or
|
||||
as something the caller will do afterwards. There is no handshake to rely on and no state passed
|
||||
in beyond the file paths.
|
||||
as something the caller will do afterwards. No handshake exists to rely on, and no state is
|
||||
passed in beyond the file paths.
|
||||
|
||||
@@ -4,6 +4,25 @@ Deterministic self-check this skill shells out to instead of relying on LLM judg
|
||||
|
||||
| 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 |
|
||||
|
||||
@@ -14,6 +14,10 @@ Arguments:
|
||||
adapter-file Path to the provider-specific file to check.
|
||||
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:
|
||||
--no-import-syntax The target provider has no native cross-file import
|
||||
mechanism. Require a plain-text pointer line naming
|
||||
@@ -26,26 +30,69 @@ Options:
|
||||
it's considered no longer "thin". Must be a
|
||||
non-negative integer. Default: 60.
|
||||
--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:
|
||||
0 Adapter file passes all checks
|
||||
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
||||
excessive duplication, or file too long)
|
||||
2 Usage or input error — a bad or missing argument, a path that is not a
|
||||
file, or a file that is not UTF-8. Nothing was graded, so there is no
|
||||
FAIL line and no adapter edit to make: fix the invocation or the file's
|
||||
encoding and re-run. Kept distinct from 1 because the skill's own
|
||||
closeout tells the agent to fix every non-zero exit by editing the
|
||||
provider file, which for a mistyped flag edits the wrong file forever.
|
||||
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 and no adapter edit to make: fix
|
||||
the invocation or the file's encoding and re-run. Kept distinct from 1
|
||||
because the skill's own closeout tells the agent to fix every non-zero
|
||||
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
|
||||
}
|
||||
|
||||
NO_IMPORT_SYNTAX=0
|
||||
MAX_LINES=60
|
||||
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
|
||||
if [[ $END_OF_OPTS -eq 1 ]]; then
|
||||
ARGS+=("$1")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
case "$1" in
|
||||
--)
|
||||
END_OF_OPTS=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
@@ -54,17 +101,37 @@ while [[ $# -gt 0 ]]; do
|
||||
NO_IMPORT_SYNTAX=1
|
||||
shift
|
||||
;;
|
||||
--no-import-syntax=*)
|
||||
echo "Error: --no-import-syntax is a flag and takes no value (got '$1')." >&2
|
||||
exit 2
|
||||
;;
|
||||
--max-lines)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
||||
exit 2
|
||||
fi
|
||||
MAX_LINES="$2"
|
||||
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
||||
require_int "$MAX_LINES"
|
||||
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
|
||||
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")
|
||||
@@ -80,6 +147,13 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
|
||||
exit 2
|
||||
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'
|
||||
import sys
|
||||
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"
|
||||
max_lines = int(max_lines)
|
||||
|
||||
EXIT_FAIL = 1
|
||||
EXIT_USAGE = 2
|
||||
EXIT_UNREADABLE = 3
|
||||
|
||||
if not os.path.isfile(adapter_path):
|
||||
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):
|
||||
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(EXIT_USAGE)
|
||||
|
||||
|
||||
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
|
||||
not `\s` in Python, so a CLAUDE.md saved by an editor that emits one had
|
||||
its first line — the `@AGENTS.md` import, which is the whole adapter —
|
||||
silently treated as prose. The check then said "no reference to AGENTS.md"
|
||||
told the author to add the line already sitting in front of them. Same
|
||||
class of silent BOM miss recorded in scripts/skill-size-check.sh; strip it
|
||||
at the reader so no later check has to know about it.
|
||||
The BOM strip is not cosmetic. IMPORT_RE anchors on `^ {0,3}@`, and a BOM
|
||||
is not whitespace in Python, so a CLAUDE.md saved by an editor that emits
|
||||
one had its first line — the `@AGENTS.md` import, which is the whole
|
||||
adapter — silently treated as prose. The check then said "no reference to
|
||||
AGENTS.md" and told the author to add the line already sitting in front of
|
||||
them. Same class of silent BOM miss recorded in scripts/skill-size-check.sh;
|
||||
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
|
||||
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",
|
||||
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.
|
||||
|
||||
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:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
with open(path, "rb") as fh:
|
||||
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:
|
||||
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 "
|
||||
"at other encodings.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
return text[1:] if text.startswith("\ufeff") else text
|
||||
sys.exit(EXIT_USAGE)
|
||||
return text.replace("\ufeff", "")
|
||||
|
||||
|
||||
adapter_content = read_text(adapter_path)
|
||||
agents_md_content = read_text(agents_md_path)
|
||||
adapter_dir = os.path.dirname(os.path.abspath(adapter_path))
|
||||
|
||||
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(" Fix: Add at least an import (or text pointer) to AGENTS.md.")
|
||||
print()
|
||||
sys.exit(1)
|
||||
sys.exit(EXIT_FAIL)
|
||||
|
||||
IMPORT_RE = re.compile(r'(?m)^\s*@\S*AGENTS\.md\s*$')
|
||||
lines = adapter_content.splitlines()
|
||||
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
|
||||
# line — an inert `@AGENTS.md` in a provider that resolves no imports points
|
||||
# a reader at nothing.
|
||||
pointer_lines = [ln for ln in lines if not IMPORT_RE.match(ln) and "AGENTS.md" in ln]
|
||||
|
||||
# --- Inert regions -----------------------------------------------------------
|
||||
#
|
||||
# A reference only counts where something would actually resolve it. Fenced
|
||||
# code blocks, indented code blocks and HTML comments are shown to the reader
|
||||
# (or hidden from them) as literal text; Claude Code resolves an @import in
|
||||
# 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:
|
||||
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:
|
||||
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:
|
||||
has_fail = True
|
||||
print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}")
|
||||
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(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
||||
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.\", outside any fence, naming a path that exists relative to this file.")
|
||||
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(" 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(" 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 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()
|
||||
|
||||
# --- 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()]
|
||||
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()
|
||||
|
||||
if has_fail:
|
||||
sys.exit(1)
|
||||
sys.exit(EXIT_FAIL)
|
||||
sys.exit(0)
|
||||
PYTHON
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
|
||||
|------|---------|
|
||||
| `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-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/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 |
|
||||
@@ -29,5 +29,6 @@ Describe your branch task: create a feature/hotfix/release branch, switch, delet
|
||||
## Composition
|
||||
|
||||
`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
|
||||
`git-commits`'; branch operations against a Gitea-hosted remote are `gitea-branches`'.
|
||||
structured result. Revert is `git-history`'s; commit authoring, rebase, reset and cherry-pick are
|
||||
`git-commits`'; deleting a remote branch is `git-remotes`'; branch operations against a
|
||||
Gitea-hosted remote are `gitea-branches`'.
|
||||
|
||||
@@ -9,6 +9,7 @@ description: >
|
||||
Not a Gitea remote's branches -> `gitea-branches`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- context7-git-htmldocs
|
||||
@@ -20,7 +21,7 @@ metadata:
|
||||
## Gotchas
|
||||
|
||||
- **Uncommitted changes abort a switch.** `git switch` refuses rather than clobbering conflicting local edits. Offer to stash and retry — forcing the checkout past it is how work disappears.
|
||||
- **A branch and a tag can carry the same name.** Detect it before acting — `git branch --list <name>` and `git tag --list <name>`; output from both means the name is ambiguous. Prefer `git switch` over `git checkout`, and where a command accepts either ref, disambiguate with `refs/heads/<name>` or `refs/tags/<name>`.
|
||||
- **A branch and a tag can carry the same name.** Detect it before acting — `git branch --list <name>` (bare, not `rtk`: rtk prints a phantom `* ` line even on no match, which reports every name as ambiguous — ADR-0023) and `rtk git tag --list <name>`; output from both means the name is ambiguous. Prefer `git switch` over `git checkout`, and where a command accepts either ref, disambiguate with `refs/heads/<name>` or `refs/tags/<name>`.
|
||||
- **`main` and `master` are a refusal, not a gate.** Force-pushing, force-deleting, or renaming them is rejected even when the caller passes `confirm: true` — no flag makes the remote's history recoverable. Offer a new branch instead.
|
||||
|
||||
## Step 1 — Determine the branching pattern
|
||||
@@ -33,7 +34,7 @@ The two patterns are not mixable, and the wrong merge rule silently damages hist
|
||||
|
||||
| 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` |
|
||||
| compare two branches, find their divergence | `references/comparing-branches.md` |
|
||||
|
||||
@@ -51,7 +52,7 @@ These gates are passable. The `main`/`master` refusal in Gotchas is not.
|
||||
|
||||
## 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 `rtk git push -u origin <branch>`. The push itself is `git-remotes`' — every remote-side gate lives there, which is why Step 2's remote-delete row hands off the same way — but the upstream requirement originates here, so carry it in the handoff. Without an upstream, later pushes and pulls either fail or silently target the wrong remote branch, and the caller has no way to tell which happened.
|
||||
|
||||
## Step 5 — Return a structured result
|
||||
|
||||
|
||||
@@ -8,19 +8,21 @@ source_keys:
|
||||
One command per action. Where two forms exist, the first is the default and the second the escape
|
||||
hatch.
|
||||
|
||||
- **create** — `git switch -c <branch> <base>`. Base comes from the config's `base_branch`
|
||||
- **create** — `rtk git switch -c <branch> <base>`. Base comes from the config's `base_branch`
|
||||
(`main` under GitHub Flow, usually `develop` under Gitflow).
|
||||
- **switch** — `git switch <branch>` moves to an existing local branch; it aborts rather than
|
||||
clobbering conflicting local changes. `git switch -` returns to the previous branch.
|
||||
- **delete (local)** — `git branch -d <branch>` refuses when the branch holds unmerged commits,
|
||||
which is why it is the default. `git branch -D <branch>` forces the deletion and discards that
|
||||
- **switch** — `rtk git switch <branch>` moves to an existing local branch; it aborts rather than
|
||||
clobbering conflicting local changes. `rtk git switch -` returns to the previous branch.
|
||||
- **delete (local)** — `rtk git branch -d <branch>` refuses when the branch holds unmerged commits,
|
||||
which is why it is the default. `rtk git branch -D <branch>` forces the deletion and discards that
|
||||
work — only after the destructive-operation gates pass and `confirm: true` is set.
|
||||
- **delete (remote)** — `git push origin --delete <branch>`.
|
||||
- **rename** — `git branch -m <old> <new>`.
|
||||
- **list** — `git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
|
||||
- **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** — `rtk git branch -m <old> <new>`.
|
||||
- **list** — `rtk git branch` (local), `-a` (local plus remote-tracking), `-r` (remote-tracking only),
|
||||
`--merged` / `--no-merged` (filter by merge status into the current branch).
|
||||
- **track** — `git branch --set-upstream-to=origin/<branch>` sets an upstream without pushing.
|
||||
`git branch -vv` shows the tracking state of every local branch.
|
||||
- **track** — `rtk git branch --set-upstream-to=origin/<branch>` sets an upstream without pushing.
|
||||
`rtk git branch -vv` shows the tracking state of every local branch.
|
||||
|
||||
## get-intent
|
||||
|
||||
@@ -32,3 +34,26 @@ 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
|
||||
create-time value. Never fabricate an intent: a downstream commit message built on a guessed
|
||||
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** — `rtk git stash push -m "<message>"`. Add `-u` to include untracked files; verified on Git
|
||||
2.39.5, a plain `push` leaves them in place, and a plain `push` with *only* untracked changes
|
||||
reports `No local changes to save` and stashes nothing. Bare `git stash` is `push` with no message.
|
||||
- **restore** — `git stash pop` applies the newest entry and deletes it. Bare, not `rtk`: on a
|
||||
conflict rtk prints only `FAILED: git stash pop` and swallows the conflict report the paragraph
|
||||
below tells you to read (ADR-0023). `rtk git stash apply stash@{n}`
|
||||
applies without deleting, for replaying one shelf onto more than one branch.
|
||||
- **list** — `git stash list` — bare, not `rtk`: rtk prints `No stashes` where git prints nothing,
|
||||
so an empty-output test misfires (ADR-0023). `rtk git stash show -p stash@{n}` prints that entry's diff.
|
||||
- **drop** — `rtk git stash drop stash@{n}` deletes one entry. `rtk git stash clear` deletes all of them
|
||||
and nothing recovers them — confirm before running it.
|
||||
- **branch from a stash** — `rtk git stash branch <branch> stash@{n}` creates a branch at the commit the
|
||||
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, `rtk git add`, then `rtk git stash drop` the entry by hand — otherwise it silently accumulates.
|
||||
|
||||
@@ -8,9 +8,9 @@ source_keys:
|
||||
The two-dot and three-dot forms mean different things and are easy to swap by accident — check the
|
||||
direction before reporting a result.
|
||||
|
||||
- `git log main..feature` — commits on `feature` that are not on `main`.
|
||||
- `git log feature..main` — the reverse direction: commits on `main` not on `feature`.
|
||||
- `git log --left-right main...feature` — both diverging sets at once (symmetric difference).
|
||||
- `git diff main...feature` — the diff from the common ancestor to `feature`'s tip, which is what
|
||||
- `rtk git log main..feature` — commits on `feature` that are not on `main`.
|
||||
- `rtk git log feature..main` — the reverse direction: commits on `main` not on `feature`.
|
||||
- `rtk git log --left-right main...feature` — both diverging sets at once (symmetric difference).
|
||||
- `rtk git diff main...feature` — the diff from the common ancestor to `feature`'s tip, which is what
|
||||
a reviewer sees, rather than the diff between the two tips.
|
||||
- `git merge-base main feature` — print the common ancestor commit.
|
||||
- `rtk git merge-base main feature` — print the common ancestor commit.
|
||||
|
||||
@@ -6,25 +6,26 @@ source_keys:
|
||||
|
||||
# Merging one branch into another
|
||||
|
||||
Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase belongs to
|
||||
`git-commits`; cherry-pick and revert to `git-history`.
|
||||
Scope is fast-forward and merge-commit mechanics plus conflict resolution. Rebase and cherry-pick
|
||||
belong to `git-commits`; revert to `git-history`.
|
||||
|
||||
- **Fast-forward** — `git merge <branch>` advances the pointer with no merge commit when the
|
||||
- **Fast-forward** — `rtk git merge <branch>` advances the pointer with no merge commit when the
|
||||
target has not diverged.
|
||||
- **True merge** — `git merge --no-ff <branch>` forces a merge commit even when a fast-forward is
|
||||
- **True merge** — `rtk git merge --no-ff <branch>` forces a merge commit even when a fast-forward is
|
||||
possible. Gitflow requires it on every supporting-branch merge.
|
||||
- **Squash merge** — `git merge --squash <branch>` stages the combined diff without committing.
|
||||
Follow it with a `git commit`.
|
||||
- **Octopus merge** — `git merge branch-a branch-b branch-c` merges more than two branches at
|
||||
- **Squash merge** — `rtk git merge --squash <branch>` stages the combined diff without committing.
|
||||
Follow it with a `rtk git commit`.
|
||||
- **Octopus merge** — `rtk git merge branch-a branch-b branch-c` merges more than two branches at
|
||||
once, but fails outright on any conflict. Use sequential two-way merges when conflicts are
|
||||
likely.
|
||||
|
||||
## Conflict resolution
|
||||
|
||||
When Git cannot auto-merge it writes conflict markers and stops mid-merge. Run `git status` to
|
||||
list the conflicted files, edit each to resolve its markers, then `git add <file>` and
|
||||
`git merge --continue`.
|
||||
When Git cannot auto-merge it writes conflict markers and stops mid-merge. Run `rtk git status` to
|
||||
list the conflicted files, edit each to resolve its markers, then `rtk git add <file>` and
|
||||
`rtk git merge --continue`.
|
||||
|
||||
- `git merge --abort` restores the pre-merge state.
|
||||
- `git mergetool` opens the configured merge tool.
|
||||
- `git diff --diff-filter=U` shows only the still-conflicted files.
|
||||
- `rtk git merge --abort` restores the pre-merge state.
|
||||
- `git mergetool` opens the configured merge tool — bare, not `rtk`: it hands control to an
|
||||
interactive child process, and a token filter has nothing to offer there (ADR-0023).
|
||||
- `rtk git diff --diff-filter=U` shows only the still-conflicted files.
|
||||
|
||||
@@ -8,6 +8,7 @@ description: >
|
||||
Not branch lifecycle -> `git-branches`.
|
||||
|
||||
metadata:
|
||||
version: "0.1.4"
|
||||
category: git
|
||||
source_keys:
|
||||
- conventional-commits-spec
|
||||
@@ -20,8 +21,9 @@ allowed-tools: Bash
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Run git as `rtk git <subcommand>`, never bare `git`** — org convention, in `&&` chains too.
|
||||
- **Run git as `rtk git <subcommand>`, never bare `git`** — org convention, in `&&` chains too. Exceptions: ADR-0023 clause 3.
|
||||
- **Refuse to force-push `main`/`master`** — a rewrite leaves the branch diverged and the reflex is to force it back; safe only where nobody else has based work on it.
|
||||
- **`reset --hard` is a confirmation gate, not a default.** It overwrites the working tree, and uncommitted edits it discards were never in git, so no reflog recovers them. Name what will be lost and offer a stash first.
|
||||
- **Never add `--no-verify`** — using it when a hook fails bypasses the QA gate the pipeline depends on. Only on the user's explicit demand, with a warning.
|
||||
|
||||
## Dispatch
|
||||
@@ -31,18 +33,19 @@ Read exactly one flow file. Each is self-contained.
|
||||
| Condition | Flow | Read |
|
||||
|---|---|---|
|
||||
| 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` |
|
||||
|
||||
## 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.
|
||||
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
|
||||
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. **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.
|
||||
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.
|
||||
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.
|
||||
6. **Conflicts.** If a rebase or cherry-pick halts, offer resolution or an abort. Do not resolve automatically without confirmation.
|
||||
|
||||
## Output
|
||||
|
||||
|
||||
@@ -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.
|
||||
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 `git rebase -i --autosquash HEAD~N` — bare, not `rtk`: `-i` opens an interactive sequence editor (ADR-0023). Git pre-fills the todo list with the tagged commits already reordered against their targets; save it unchanged to apply.
|
||||
|
||||
**`-i` is not optional here.** On Git 2.39.5, `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)
|
||||
|
||||
@@ -29,9 +31,46 @@ Use this when the commits were not tagged at commit time. **Interactive rebase h
|
||||
|
||||
1. Identify the commits to squash — typically the last N on the current branch.
|
||||
2. Get explicit approval.
|
||||
3. Run `rtk git rebase -i HEAD~N`, marking the older commits `squash` to keep their messages for editing, or `fixup` to discard them.
|
||||
3. Run `git rebase -i HEAD~N` — bare, not `rtk`, for the same interactive-editor reason — marking the older commits `squash` to keep their messages for editing, or `fixup` to discard them.
|
||||
4. Compose the combined message when the rebase stops to ask. For a non-trivial combined message, follow the structure in `references/commit-template.md`.
|
||||
|
||||
## When a rebase halts on a conflict
|
||||
|
||||
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.
|
||||
|
||||
@@ -8,7 +8,7 @@ This skill handles history inspection within the git workflow suite. It queries
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ description: >
|
||||
`git-commits`. Not a Gitea server's history -> `gitea-branches`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- git-scm-bisect-docs
|
||||
@@ -34,13 +35,13 @@ allowed-tools: Bash
|
||||
|
||||
## Step 2 — Query the log
|
||||
|
||||
Default to `git log --oneline`, then narrow by whatever is known:
|
||||
Default to `rtk git log --oneline`, then narrow by whatever is known:
|
||||
|
||||
- **Content**: `git log -S"string"`, or `-G"regex"` to match any diff line. `--pickaxe-regex` makes the `-S` argument a POSIX ERE; `--pickaxe-all` shows every file in a matching changeset.
|
||||
- **A line or function**: `git log -L <start>,<end>:<file>` or `git log -L :<function>:<file>`. Confirm the range resolves before reporting on it — an off-by-one silently omits the target.
|
||||
- **A file across renames**: `git log --follow -- <file>`. Without `--follow` the history stops at the rename boundary.
|
||||
- **Content**: `rtk git log -S"string"`, or `-G"regex"` to match any diff line. `--pickaxe-regex` makes the `-S` argument a POSIX ERE; `--pickaxe-all` shows every file in a matching changeset.
|
||||
- **A line or function**: `git log -L <start>,<end>:<file>` or `git log -L :<function>:<file>` — bare, not `rtk`: rtk truncates each diff line at ~72 characters (ADR-0023). Confirm the range resolves before reporting on it — an off-by-one silently omits the target.
|
||||
- **A file across renames**: `rtk git log --follow -- <file>`. Without `--follow` the history stops at the rename boundary.
|
||||
- **Mainline only**: `--first-parent` follows the integration branch and skips commits merged in from side branches.
|
||||
- **Structured output**: `git log --format="%h | %s | %an (%ar)"`.
|
||||
- **Structured output**: `rtk git log --format="%h | %s | %an (%ar)"`.
|
||||
|
||||
If you need the placeholder catalogue, format presets, `--diff-filter` letters, full `-L` syntax, ancestry filters, pickaxe binary-file behaviour, or `git diff` output-control flags such as `--stat`, `--word-diff` and the whitespace options, read `references/git-log-format.md`.
|
||||
|
||||
@@ -48,9 +49,9 @@ 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.
|
||||
|
||||
- `git cherry-pick <commit>` copies the commit's changes onto the current HEAD — for backporting a fix to another branch.
|
||||
- `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.
|
||||
- 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.
|
||||
- `rtk git revert <commit>` adds a new commit undoing it — for un-applying merged work without rewriting history.
|
||||
- `rtk git blame <file>` attributes each line to the commit that last touched it, when the question is which commit introduced one specific line.
|
||||
|
||||
For diff output control on the located commit, read `references/git-log-format.md`.
|
||||
|
||||
|
||||
@@ -12,43 +12,43 @@ or line range to search the log for. Binary search reduces the trials from O(N)
|
||||
## Manual flow
|
||||
|
||||
```bash
|
||||
git bisect start
|
||||
git bisect bad [HEAD] # mark current (or specified) as broken
|
||||
git bisect good <commit> # mark known-good baseline
|
||||
rtk git bisect start
|
||||
rtk git bisect bad [HEAD] # mark current (or specified) as broken
|
||||
rtk git bisect good <commit> # mark known-good baseline
|
||||
# Git checks out the midpoint; test it
|
||||
git bisect good # test passes
|
||||
git bisect bad # test fails
|
||||
rtk git bisect good # test passes
|
||||
rtk git bisect bad # test fails
|
||||
# Repeat until git reports "X is the first bad commit"
|
||||
git bisect reset # return to the original HEAD
|
||||
rtk git bisect reset # return to the original HEAD
|
||||
```
|
||||
|
||||
## Automated
|
||||
|
||||
With a test command available, use `git bisect run <cmd>`. Git reads the exit code: `0` good,
|
||||
With a test command available, use `rtk git bisect run <cmd>`. Git reads the exit code: `0` good,
|
||||
`1`–`124` bad, `125` skip (build broken), `126`–`127` POSIX shell errors, treated as bad, and
|
||||
`128` or above aborts the session outright rather than marking the commit bad.
|
||||
|
||||
## Untestable commits
|
||||
|
||||
`git bisect skip` excludes a commit that cannot be built or tested without deciding good or bad
|
||||
`rtk git bisect skip` excludes a commit that cannot be built or tested without deciding good or bad
|
||||
for it. When the first bad commit is adjacent to a skipped range, bisect reports that it cannot
|
||||
pinpoint the culprit and lists the candidates — that is the precise answer the skip range allows,
|
||||
not a failure.
|
||||
|
||||
## Undoing a wrong good/bad call
|
||||
|
||||
`git bisect log` prints the session's decision history. Save it, edit out the mistaken entry, and
|
||||
`rtk git bisect log` prints the session's decision history. Save it, edit out the mistaken entry, and
|
||||
resume from the corrected log rather than restarting the search:
|
||||
|
||||
```bash
|
||||
git bisect log > bisect.log
|
||||
rtk git bisect log > bisect.log
|
||||
# edit bisect.log, removing the wrong decision
|
||||
git bisect reset && git bisect replay bisect.log
|
||||
rtk git bisect reset && rtk git bisect replay bisect.log
|
||||
```
|
||||
|
||||
## Narrowing and speeding up
|
||||
|
||||
- `git bisect start HEAD v1.2 -- src/` restricts bisection to a path, cutting the trial count.
|
||||
- `rtk git bisect start HEAD v1.2 -- src/` restricts bisection to a path, cutting the trial count.
|
||||
- `--no-checkout` updates the `BISECT_HEAD` ref instead of checking out a working tree — useful
|
||||
for tests that do not need one, and automatic in bare repos.
|
||||
- `--first-parent` follows only first parents at merges, finding the integration commit that
|
||||
@@ -56,12 +56,12 @@ git bisect reset && git bisect replay bisect.log
|
||||
|
||||
## Inspecting the remaining candidates
|
||||
|
||||
`git bisect visualize` (alias `view`) opens the suspects in gitk, falling back to `git log` when
|
||||
`rtk git bisect visualize` (alias `view`) opens the suspects in gitk, falling back to `git log` when
|
||||
no graphical display is detected. Add `--stat` or `-p` for a diffstat or full patches.
|
||||
|
||||
## Hunting a non-bug property change
|
||||
|
||||
`git bisect start --term-new <new> --term-old <old>` searches for any property change — a
|
||||
`rtk git bisect start --term-new <new> --term-old <old>` searches for any property change — a
|
||||
performance regression, say — instead of a bug. Use the custom terms in place of `good` and `bad`
|
||||
for the rest of the session.
|
||||
|
||||
|
||||
@@ -116,15 +116,15 @@ source_keys:
|
||||
**`-S<string>`** — finds commits where the **count** of `<string>` changed (i.e. the string was added or removed net). Does not match commits where the string merely appears in a diff hunk without a count change.
|
||||
|
||||
```bash
|
||||
git log -S"my_function"
|
||||
git log -S"my_function" --pickaxe-regex # treat as POSIX ERE
|
||||
git log -S"my_function" --pickaxe-all # show all files in matching changesets
|
||||
rtk git log -S"my_function"
|
||||
rtk git log -S"my_function" --pickaxe-regex # treat as POSIX ERE
|
||||
rtk git log -S"my_function" --pickaxe-all # show all files in matching changesets
|
||||
```
|
||||
|
||||
**`-G<regex>`** — finds commits where any added or removed **line** in the patch matches `<regex>`. Broader than `-S`: matches whenever the pattern appears in diff text regardless of count.
|
||||
|
||||
```bash
|
||||
git log -G"frotz\(nitfol"
|
||||
rtk git log -G"frotz\(nitfol"
|
||||
```
|
||||
|
||||
**Critical distinction:** given a diff that removes one occurrence of `foo` and adds one occurrence of `foo` (net change = 0):
|
||||
@@ -151,8 +151,8 @@ Selects commits (in `git log`) or files (in `git diff`) by change type:
|
||||
|
||||
Lowercase letters **exclude** that type:
|
||||
```bash
|
||||
git log --diff-filter=ad # exclude added and deleted files
|
||||
git log --diff-filter=M # only show commits with modified files
|
||||
rtk git log --diff-filter=ad # exclude added and deleted files
|
||||
rtk git log --diff-filter=M # only show commits with modified files
|
||||
```
|
||||
|
||||
`C` and `R` only appear when copy/rename detection is enabled (`-C`, `-M` flags or `diff.renames` config).
|
||||
@@ -161,11 +161,15 @@ git log --diff-filter=M # only show commits with modified files
|
||||
|
||||
Traces the evolution of a specific range of lines or a named function through commits. Implies `--patch`.
|
||||
|
||||
Bare `git`, not `rtk git`, on every `-L` form below: rtk truncates each diff body
|
||||
line at roughly 72 characters with an ellipsis, on the one query whose whole point
|
||||
is showing line content.
|
||||
|
||||
```bash
|
||||
git log -L 10,20:file.txt
|
||||
git log -L /start_pattern/,/end_pattern/:file.txt
|
||||
git log -L :myfunction:src/app.c
|
||||
git log -L /init/,+15:config.py # 15 lines after first match of /init/
|
||||
git log -L 10,20:file.txt # bare per ADR-0023
|
||||
git log -L /start_pattern/,/end_pattern/:file.txt # bare per ADR-0023
|
||||
git log -L :myfunction:src/app.c # bare per ADR-0023
|
||||
git log -L /init/,+15:config.py # bare per ADR-0023; 15 lines after first /init/ match
|
||||
```
|
||||
|
||||
Range formats:
|
||||
@@ -182,12 +186,12 @@ Limitations: incompatible with `--raw`, `--numstat`, `--shortstat`, `--name-only
|
||||
## Graph and Ancestry Filters
|
||||
|
||||
```bash
|
||||
git log --first-parent # at merges, follow only first parent (mainline evolution)
|
||||
git log --merges # only merge commits (≥2 parents); equivalent to --min-parents=2
|
||||
git log --no-merges # only non-merge commits; equivalent to --max-parents=1
|
||||
git log --ancestry-path D..M # only commits actually on the path from D to M
|
||||
git log --min-parents=<n> # include only commits with ≥ n parents
|
||||
git log --max-parents=<n> # include only commits with ≤ n parents
|
||||
rtk git log --first-parent # at merges, follow only first parent (mainline evolution)
|
||||
rtk git log --merges # only merge commits (≥2 parents); equivalent to --min-parents=2
|
||||
rtk git log --no-merges # only non-merge commits; equivalent to --max-parents=1
|
||||
rtk git log --ancestry-path D..M # only commits actually on the path from D to M
|
||||
rtk git log --min-parents=<n> # include only commits with ≥ n parents
|
||||
rtk git log --max-parents=<n> # include only commits with ≤ n parents
|
||||
```
|
||||
|
||||
`--ancestry-path` is significant: without it, `D..M` includes all commits reachable from M but not D — including side branches that merged into the path. With it, only commits directly between D and M are shown.
|
||||
@@ -197,28 +201,34 @@ git log --max-parents=<n> # include only commits with ≤ n parents
|
||||
### --stat
|
||||
|
||||
```bash
|
||||
git diff --stat # diffstat: file names + ± bar
|
||||
git diff --stat=<width>,<name-width>,<count>
|
||||
git diff --compact-summary # alongside --stat: shows new/gone, +x/-x (executable), +l (symlink)
|
||||
git diff --numstat # machine-readable: <added>\t<deleted>\t<path>; - for binary
|
||||
rtk git diff --stat # diffstat: file names + ± bar
|
||||
rtk git diff --stat=<width>,<name-width>,<count>
|
||||
rtk git diff --compact-summary # alongside --stat: shows new/gone, +x/-x (executable), +l (symlink)
|
||||
rtk git diff --numstat # machine-readable: <added>\t<deleted>\t<path>; - for binary
|
||||
```
|
||||
|
||||
### --name-only / --name-status
|
||||
|
||||
Bare `git`, not `rtk git`: rtk appends a blank line and a `Changes:` trailer, so
|
||||
the output is no longer one record per line.
|
||||
|
||||
```bash
|
||||
git diff --name-only # only filenames, one per line
|
||||
git diff --name-status # status letter + filename per line
|
||||
git diff --name-only # bare per ADR-0023; only filenames, one per line
|
||||
git diff --name-status # bare per ADR-0023; status letter + filename per line
|
||||
```
|
||||
|
||||
`--name-status` uses the same status letters as `--diff-filter`.
|
||||
|
||||
### --word-diff
|
||||
|
||||
Bare `git`, not `rtk git`: rtk replaces the word-diff with its own diffstat
|
||||
renderer and emits none of the `[-removed-] {+added+}` markers.
|
||||
|
||||
```bash
|
||||
git diff --word-diff # inline word-level diff with [-removed-] {+added+} markers
|
||||
git diff --word-diff=color # color only, no markers
|
||||
git diff --word-diff=porcelain # machine-readable: +/- prefixed lines, ~ for newlines
|
||||
git diff --word-diff-regex=<re> # define what counts as a "word"
|
||||
git diff --word-diff # bare per ADR-0023; inline word-level diff, [-removed-] {+added+} markers
|
||||
git diff --word-diff=color # bare per ADR-0023; color only, no markers
|
||||
git diff --word-diff=porcelain # bare per ADR-0023; machine-readable: +/- prefixed lines, ~ for newlines
|
||||
git diff --word-diff-regex=<re> # bare per ADR-0023; define what counts as a "word"
|
||||
```
|
||||
|
||||
### Whitespace Flags
|
||||
|
||||
@@ -10,6 +10,7 @@ description: >
|
||||
Not submodule pointers -> `git-submodules`.
|
||||
|
||||
metadata:
|
||||
version: "1.0.1"
|
||||
category: git
|
||||
source_keys:
|
||||
- git-scm-remote-docs
|
||||
@@ -27,7 +28,7 @@ metadata:
|
||||
|
||||
## Step 1 — Clear the force-push gate
|
||||
|
||||
`main` and `master` are a hard refusal: decline a force-push targeting either, whatever confirmation accompanies it, because no local approval can restore what the remote loses. On any other branch, `git push --force` and `-f` run only after the caller passes `confirm: true` for that specific push — for a human caller, prompt instead of failing.
|
||||
`main` and `master` are a hard refusal: decline a force-push targeting either, whatever confirmation accompanies it, because no local approval can restore what the remote loses. On any other branch, `rtk git push --force` and `-f` run only after the caller passes `confirm: true` for that specific push — for a human caller, prompt instead of failing.
|
||||
|
||||
## Step 2 — Dispatch
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user