Compare commits
7
Commits
5b80f305e9
...
971e148e19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
971e148e19 | ||
|
|
4011d149bc | ||
|
|
ae791781c2 | ||
|
|
1a971ee003 | ||
|
|
a8cd5e881d | ||
|
|
00daf285ec | ||
|
|
c232e69645 |
No files matched your search
@@ -36,7 +36,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`.
|
- **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.
|
- **`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.
|
- **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 every `references/` file is unlinted — which matters because the contract's own remedy is to move prose *into* `references/`, out of the prose gate's reach. That blind spot has **two** independent causes and closing either alone changes nothing: the `Kyberforge` style is scoped `[**/SKILL.md]` (the cause #117 records), *and* the `vale-audit-prefilter-skill` hook filters on `files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$'`, so a reference file is never handed to Vale whatever the style says. Check both gates: `pre-commit run --all-files`.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ trigger clause, at most one capability clause, and a boundary clause, and nothin
|
|||||||
and the target-resolution walk: `docs/spec/gates.md`.
|
and the target-resolution walk: `docs/spec/gates.md`.
|
||||||
_Avoid_: skill budget, size limit
|
_Avoid_: skill budget, size limit
|
||||||
|
|
||||||
|
**Routing target**:
|
||||||
|
The skill or agent name a boundary clause sends work to. It **resolves** when a skill or agent of
|
||||||
|
that name is reachable from the file being checked, and **dangles** when none is — a route the router
|
||||||
|
cannot take. Dangling is a blocking ERROR in route notation (`/name`, `→ name`) and a SUGGESTION for
|
||||||
|
a bare name nothing else in the sentence corroborates. Verdicts and the resolution walk:
|
||||||
|
`docs/spec/gates.md`.
|
||||||
|
_Avoid_: route, pointer, cross-reference
|
||||||
|
|
||||||
**Dispatch body**:
|
**Dispatch body**:
|
||||||
The body pattern a skill with two or more mutually exclusive flows must use — the body carries only
|
The body pattern a skill with two or more mutually exclusive flows must use — the body carries only
|
||||||
the dispatch table and the gates common to every branch, and each flow lives in its own
|
the dispatch table and the gates common to every branch, and each flow lives in its own
|
||||||
|
|||||||
@@ -203,8 +203,14 @@ with no trigger list.
|
|||||||
|
|
||||||
Verified end-to-end rather than assumed: `plugins/bin/.apm/skills/zoom-out/SKILL.md:4` carries the
|
Verified end-to-end rather than assumed: `plugins/bin/.apm/skills/zoom-out/SKILL.md:4` carries the
|
||||||
flag, apm passes it through verbatim to both `.claude/skills/zoom-out/SKILL.md:4` and the flat mirror
|
flag, apm passes it through verbatim to both `.claude/skills/zoom-out/SKILL.md:4` and the flat mirror
|
||||||
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` is the one installed skill absent from
|
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` was — at the time of that check, when it
|
||||||
the model-visible skill listing in a live session. It remains invocable as `/zoom-out`.
|
was the only carrier — the one installed skill absent from the model-visible skill listing in a live
|
||||||
|
session. It remains invocable as `/zoom-out`. `caveman` has since taken the flag as well, so the
|
||||||
|
corpus now has **two** carriers. Do not read a carrier list off this page; re-derive it:
|
||||||
|
|
||||||
|
```
|
||||||
|
grep -l '^disable-model-invocation: true' plugins/*/.apm/skills/*/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
### Merging siblings
|
### Merging siblings
|
||||||
|
|
||||||
@@ -219,10 +225,13 @@ rather than the core job.
|
|||||||
skills still exist separately, and this change made the split deeper rather than shallower: retrofit
|
skills still exist separately, and this change made the split deeper rather than shallower: retrofit
|
||||||
to the dispatch pattern took `skill-audit` from 3 reference files to 7 and `agent-audit` from 4 to 8,
|
to the dispatch pattern took `skill-audit` from 3 reference files to 7 and `agent-audit` from 4 to 8,
|
||||||
and their two same-named `references/description-quality.md` files now differ on 100 of ~120 lines
|
and their two same-named `references/description-quality.md` files now differ on 100 of ~120 lines
|
||||||
after normalising `skill`/`agent`, where before they were closer. The merge stays the decision; it
|
after normalising `skill`/`agent`, where before they were closer. It has kept deepening since: the
|
||||||
reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call site in
|
#99 retrofit added `finding-criteria.md` to `skill-audit`, drawing it level with `agent-audit`. Both
|
||||||
`skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider on
|
figures move with the next retrofit, so measure rather than quote —
|
||||||
this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
|
`ls plugins/kyberforge/.apm/skills/<name>/references/ | grep -c '\.md$'`. The merge stays the
|
||||||
|
decision; it reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call
|
||||||
|
site in `skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider
|
||||||
|
on this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
|
||||||
and dated instead of discovered later.
|
and dated instead of discovered later.
|
||||||
|
|
||||||
### Enforcement and rollout
|
### Enforcement and rollout
|
||||||
@@ -233,11 +242,13 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
|
|||||||
|
|
||||||
| Check | Applies to | Tier | Home |
|
| Check | Applies to | Tier | Home |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| description characters (250 SUGGESTION / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
|
| description characters (250 SUGGESTION † / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
|
||||||
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
|
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
|
||||||
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
|
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
|
||||||
| boundary target resolves to a real skill or agent (ERROR when written as `/name` or `-> name`, or when its own sentence names another target that resolves; SUGGESTION otherwise) | 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 clause absent (SUGGESTION) | skills, agents | deterministic | same |
|
| boundary clause absent — `absent` (SUGGESTION) † | skills, agents | deterministic | same |
|
||||||
|
| an arrow clause is present but no target can be read out of it — `unparsed` (SUGGESTION) † | skills, agents | deterministic | same |
|
||||||
|
| one arrow clause naming two or more targets, of which only the first is resolved (SUGGESTION, issue #107) † | skills, agents | deterministic | same |
|
||||||
| Gotchas entry count over five (SUGGESTION) | skills | deterministic | same |
|
| Gotchas entry count over five (SUGGESTION) | skills | deterministic | same |
|
||||||
| Gotchas over 25% of the body (SUGGESTION) | skills | deterministic | same |
|
| Gotchas over 25% of the body (SUGGESTION) | skills | deterministic | same |
|
||||||
| every `references/<file>.md` a body names exists (ERROR) | skills | deterministic | same |
|
| every `references/<file>.md` a body names exists (ERROR) | skills | deterministic | same |
|
||||||
@@ -254,6 +265,21 @@ that guessed at them would be a worse gate than no gate, because it would be bel
|
|||||||
enforced, they are reviewed, and this table exists so that distinction is written down rather than
|
enforced, they are reviewed, and this table exists so that distinction is written down rather than
|
||||||
inferred from whether a validator happens to have been written yet.
|
inferred from whether a validator happens to have been written yet.
|
||||||
|
|
||||||
|
**† These four, and only these four, are lifted for a hand-invoked file** — one whose frontmatter
|
||||||
|
carries `disable-model-invocation: true`, read as a boolean by `hand_invoked()` in all three scripts.
|
||||||
|
No validator knew the field existed (issue **#108**), so every routing SUGGESTION above fired on
|
||||||
|
exactly the shape the *Invocation as a design axis* section mandates, and the boundary-clause
|
||||||
|
remedy — "so the router knows where NOT to send this skill" — was addressed to a router that cannot
|
||||||
|
see the skill at all. An author who took the advice made the file worse.
|
||||||
|
|
||||||
|
What does **not** lift is the point of the carve-out. Both body word tiers stand: the body is still
|
||||||
|
loaded on invocation and still competes with the caller's live conversation. The 400-character
|
||||||
|
description FAIL stands: that description is not preloaded, but it is the one line a user reads when
|
||||||
|
choosing from the `/` menu, and the ceiling is an outlier stop rather than a routing-quality budget —
|
||||||
|
which is exactly why the 250-character *target* is the tier that lifts. And a target the description
|
||||||
|
does happen to name is still resolved and can still dangle as a blocking ERROR. Mechanics, and the
|
||||||
|
reason the field is read as a boolean rather than as a mention of the key: `docs/spec/gates.md`.
|
||||||
|
|
||||||
Two of the deterministic rows are tuned for **false positives over recall**, and what they decline to
|
Two of the deterministic rows are tuned for **false positives over recall**, and what they decline to
|
||||||
see is part of the contract. On target extraction: a bare hyphenated name counts only inside a
|
see is part of the contract. On target extraction: a bare hyphenated name counts only inside a
|
||||||
boundary sentence, and a single-word name is never matchable bare — `research`, `triage`, `forge`,
|
boundary sentence, and a single-word name is never matchable bare — `research`, `triage`, `forge`,
|
||||||
@@ -264,7 +290,8 @@ raise an error: one followed by an ordinary lowercase noun is a compound **modif
|
|||||||
confirm-only — it still resolves and still counts as a route when the name exists, but it can never
|
confirm-only — it still resolves and still counts as a route when the name exists, but it can never
|
||||||
dangle. Only a *terminal* target can. The compressed arrow form `→ <name>` is exempt from that
|
dangle. Only a *terminal* target can. The compressed arrow form `→ <name>` is exempt from that
|
||||||
follower test and is always error-eligible, because nothing reads as a compound modifier after an
|
follower test and is always error-eligible, because nothing reads as a compound modifier after an
|
||||||
arrow; a `/slash` target reached through a route verb is **not** exempt and takes the same test. The
|
arrow; a `/slash` target reached through a route verb is **not** exempt and takes the same test.
|
||||||
|
*Amended 2026-08-31 — the `/slash` half is reversed: it is exempt too. See the amendment below.* The
|
||||||
simpler rule — "only marked targets may dangle" — was available and would have been wrong here: both
|
simpler rule — "only marked targets may dangle" — was available and would have been wrong here: both
|
||||||
live true positives are bare, `research`'s "(use neuledge-context)" and the `gitea-labels-` /
|
live true positives are bare, `research`'s "(use neuledge-context)" and the `gitea-labels-` /
|
||||||
`milestones` fold. On the body-shape checks: a `## Gotchas` heading must *end* in "gotchas", not
|
`milestones` fold. On the body-shape checks: a `## Gotchas` heading must *end* in "gotchas", not
|
||||||
@@ -296,6 +323,46 @@ Three pre-existing contradictions are fixed in the same change, because they are
|
|||||||
- `description-quality.md:45-50` has no FAIL condition for internal-mechanics content, which is why
|
- `description-quality.md:45-50` has no FAIL condition for internal-mechanics content, which is why
|
||||||
`skill-author/SKILL.md:102` never bit.
|
`skill-author/SKILL.md:102` never bit.
|
||||||
|
|
||||||
|
## Amendment (2026-08-31): route notation short-circuits the follower test, `/name` included
|
||||||
|
|
||||||
|
The Enforcement section above exempts the arrow form from the follower test and then withholds the
|
||||||
|
same exemption from `/name`: "a `/slash` target reached through a route verb is **not** exempt and
|
||||||
|
takes the same test." That half is reversed. **Both spellings of route notation are exempt, and the
|
||||||
|
exemption is decided before the follower test rather than weighed against it.**
|
||||||
|
|
||||||
|
Three things make the original call wrong rather than merely strict.
|
||||||
|
|
||||||
|
**It contradicted the promise the same paragraph makes.** Route notation is offered to an author as
|
||||||
|
the way to get a target checked unconditionally — the SUGGESTION text on an unpromoted target says
|
||||||
|
so in as many words: "write it as `/name` or `-> name` and it will be checked properly." Under the
|
||||||
|
original rule that was true of one of the two spellings. `-> name` reached `_add()` with
|
||||||
|
`strict=True` from both its call sites; `/name` did not, so it fell through to `_terminal()` and any
|
||||||
|
follower outside `FOLLOWER_OK` demoted it. `Do not use for Y — use /no-such-skill afterwards.` exited
|
||||||
|
0 — and, before the companion visibility fix, in total silence.
|
||||||
|
|
||||||
|
**The follower test's own justification does not reach `/name`.** That test exists for *prose*: a
|
||||||
|
bare hyphenated token followed by an ordinary lowercase noun is a compound modifier, "pre-commit
|
||||||
|
hooks" and "pull-request template". A leading slash is Claude Code's invocation syntax and occurs in
|
||||||
|
no English compound, so there is no attributive reading to protect. The exemption was withheld from
|
||||||
|
the one shape the rule it protects against cannot describe.
|
||||||
|
|
||||||
|
**`FOLLOWER_OK` is a closed whitelist of roughly eighty words, and a closed list is the wrong thing
|
||||||
|
to hang a blocking gate on.** Leaving `/name` under it made *whether a commit is blocked* depend on
|
||||||
|
whether someone had thought to enumerate the next word — the gate failing open on its own
|
||||||
|
unfamiliarity. The bare-target path keeps the follower test precisely because it needs a brake it can
|
||||||
|
justify; the notation path asked for one and was given the same brake by accident.
|
||||||
|
|
||||||
|
What is unchanged: the **corroboration** branch. A *bare* terminal name still earns its blocking
|
||||||
|
ERROR only from a resolving sibling in the same sentence, and a compound modifier still cannot
|
||||||
|
dangle at all. The conservative tuning that decision rests on is untouched — this amendment moves one
|
||||||
|
explicitly-marked spelling out from under it, not the prose path.
|
||||||
|
|
||||||
|
Verified on fixtures inside a synthetic plugin tree: `… Do not use for Y — use /no-such-skill
|
||||||
|
afterwards.` exits 1, while the same sentence with the bare `no-such-skill` exits 0 at SUGGESTION,
|
||||||
|
and rises to a blocking ERROR the moment a resolving sibling joins it. The reasoning is recorded at
|
||||||
|
the point of enforcement in `_add()`'s docstring in `scripts/skill-size-check.sh` and its two
|
||||||
|
mirrored copies, and the verdict table in `docs/spec/gates.md` states the corrected shape.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
|
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
|
||||||
@@ -342,8 +409,8 @@ and `git-*` families — where every sibling shares a keyword and boundary claus
|
|||||||
— are the ones most likely to sit at the FAIL tier permanently. If the retrofit shows that family
|
— are the ones most likely to sit at the FAIL tier permanently. If the retrofit shows that family
|
||||||
routing degrades, the tier is the first thing to revisit.
|
routing degrades, the tier is the first thing to revisit.
|
||||||
|
|
||||||
**Four broken routing targets were found; two are fixed here and two are live.** Tracked as issue
|
**Four broken routing targets were found; two were fixed here and two shortly after.** Tracked as
|
||||||
#100.
|
issue #100.
|
||||||
|
|
||||||
- `skill-audit` routed to `/skill-improve` twice in its description plus `README.md:10`, and no such
|
- `skill-audit` routed to `/skill-improve` twice in its description plus `README.md:10`, and no such
|
||||||
skill exists — the real target is `skill-author`. **Fixed here**, as a side effect of retrofitting
|
skill exists — the real target is `skill-author`. **Fixed here**, as a side effect of retrofitting
|
||||||
@@ -353,14 +420,24 @@ routing degrades, the tier is the first thing to revisit.
|
|||||||
detectable by the resolvable-target check and never will be: "examine agent files manually" names
|
detectable by the resolvable-target check and never will be: "examine agent files manually" names
|
||||||
no target, and a check that resolves names cannot see a name that is absent. A misroute to nowhere
|
no target, and a check that resolves names cannot see a name that is absent. A misroute to nowhere
|
||||||
is a review finding, not a gate finding.
|
is a review finding, not a gate finding.
|
||||||
- `research` routes to `neuledge-context`, which exists only inside that string. **Live.**
|
- `research` routes to `neuledge-context`, which exists only inside that string. Was **live**;
|
||||||
|
**fixed under #99** — the retrofitted description names no such target.
|
||||||
- `gitea-issues` carries the literal string `gitea-labels- milestones` in its folded description, a
|
- `gitea-issues` carries the literal string `gitea-labels- milestones` in its folded description, a
|
||||||
stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text.
|
stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text. Was
|
||||||
**Live** — the check reports it as a dangling `gitea-labels`.
|
**live**, reported as a dangling `gitea-labels`; **fixed under #99** — the name now folds intact.
|
||||||
|
|
||||||
So the check fires on 3 of the 4 against the base commit and on 2 at the tip of this change, and
|
So the check fired on 3 of the 4 against the base commit and on 2 at the tip of the change that
|
||||||
`tests/test-skill-size-check.sh` probes exactly those three by name rather than asserting a count, so
|
carried this ADR. **The corpus dangling set is now empty**, and that is asserted rather than
|
||||||
it degrades to SKIP as #100 lands rather than going stale.
|
observed: `tests/test-adr0020-targets.sh` pins the set as empty, so a new boundary clause naming a
|
||||||
|
non-existent skill fails the suite instead of joining a backlog. `tests/test-skill-size-check.sh`
|
||||||
|
probed the three original names rather than asserting a count; as each was retrofitted its probe was
|
||||||
|
**removed, not skipped**, because a `pass "SKIP: …"` branch is an assertion-free result counted in
|
||||||
|
the totals and makes the suite look one test stronger than it is. That file's commentary survives the
|
||||||
|
probes and states the rule. Re-derive the current set — never read it off this page:
|
||||||
|
|
||||||
|
```
|
||||||
|
bash scripts/skill-size-check.sh plugins/*/.apm/skills/*/SKILL.md | grep 'does not resolve'
|
||||||
|
```
|
||||||
|
|
||||||
**Duplication between `skill-author` and `agent-author` survives un-gated.** The merge rule
|
**Duplication between `skill-author` and `agent-author` survives un-gated.** The merge rule
|
||||||
deliberately excludes the author pair, so the commit-verification argument in four near-copies, the
|
deliberately excludes the author pair, so the commit-verification argument in four near-copies, the
|
||||||
|
|||||||
+90
-4
@@ -98,11 +98,57 @@ loudly (`Error: jq is required but not installed`).
|
|||||||
## Skill and agent context gates (ADR-0020)
|
## Skill and agent context gates (ADR-0020)
|
||||||
|
|
||||||
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
|
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
|
||||||
runs `scripts/skill-size-check.sh`. That scope means it never lints the
|
runs `scripts/skill-size-check.sh`. It is also shipped to external repos as
|
||||||
`plugins/kyberforge/docs/research/examples/` reference skills. It is also shipped to external repos
|
`kyberforge-skill-size-check` (see
|
||||||
as `kyberforge-skill-size-check` (see
|
|
||||||
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)).
|
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)).
|
||||||
|
|
||||||
|
**Two things fall outside that scope, both deliberately.** The `[^/]+/SKILL\.md$` tail admits only a
|
||||||
|
`SKILL.md` sitting directly in a skill directory under `.apm/skills/`:
|
||||||
|
|
||||||
|
- the `plugins/kyberforge/docs/research/examples/` reference skills, which are vendored upstream
|
||||||
|
corpus and not this repo's to gate;
|
||||||
|
- `plugins/kyberforge/.apm/skills/skill-author/assets/templates/SKILL.md` — inside `.apm/skills/`,
|
||||||
|
but two directories deeper. It is the `FILL IN:` scaffold `skill-author` copies, so its
|
||||||
|
`description: >` is a comment block rather than a description and every ADR-0020 measurement over
|
||||||
|
it would be meaningless. A reader adjusting the pattern needs to know it is there.
|
||||||
|
|
||||||
|
Everything else it matches exactly, with nothing over- or under-caught. Re-derive both halves:
|
||||||
|
|
||||||
|
```
|
||||||
|
git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the real skills
|
||||||
|
git ls-files | grep -E '^plugins/[^/]+/\.apm/skills/.*SKILL\.md$' \
|
||||||
|
| grep -vE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the scaffold only
|
||||||
|
```
|
||||||
|
|
||||||
|
The first count equals the number of skill directories (`ls -d plugins/*/.apm/skills/*/ | wc -l`);
|
||||||
|
the second returns exactly the template. The remaining unmatched `SKILL.md` files in the tree are the
|
||||||
|
generated flat mirror, which is excluded by the `.apm/` segment on purpose — a mirror edit is drift,
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
| Frontmatter | `skill-frontmatter` | `skill-size-check` |
|
||||||
|
|---|---|---|
|
||||||
|
| `description:` with no value, then `model: sonnet` | passes — the key is on a line | ERROR, "missing or empty" |
|
||||||
|
| `"description": …` (quoted key, valid YAML) | **fails** — `^description:` does not match | passes, description read normally |
|
||||||
|
|
||||||
|
So the grep is not a second opinion on presence. It is blind to the shape ADR-0020 was written
|
||||||
|
against, and it is the only one of the two that objects to a quoted key. Neither disagreement is
|
||||||
|
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
|
||||||
|
reason not to fold it into the size gate on the grounds of redundancy.
|
||||||
|
|
||||||
### Two independent gate families, neither replaced the other
|
### Two independent gate families, neither replaced the other
|
||||||
|
|
||||||
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
|
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
|
||||||
@@ -141,7 +187,7 @@ A boundary-clause target that resolves to no skill or agent has **three** possib
|
|||||||
| Verdict | When |
|
| Verdict | When |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **SUGGESTION** — the default | the target does not resolve and neither promotion condition below holds |
|
| **SUGGESTION** — the default | the target does not resolve and neither promotion condition below holds |
|
||||||
| **blocking ERROR** | the target is **terminal** (not a compound modifier) **and** either written in route notation (`/name` for any name; a bare `-> name` only when the name is hyphenated, a backticked `` -> `name` `` for any — see the gap below) **or** corroborated by another target in the same sentence that *does* resolve |
|
| **blocking ERROR** | the target is written in **route notation** — `/name` for any name, or any arrow form (a bare `-> name` only when the name is hyphenated, a backticked `` -> `name` `` for any — see the gap below); **or** it is a bare **terminal** name (not a compound modifier) **corroborated** by another target in the same sentence that *does* resolve |
|
||||||
| **INFO, "DID NOT RUN"** | no skill universe could be determined for the path at all — the targets are named and left unchecked, exit 0 |
|
| **INFO, "DID NOT RUN"** | no skill universe could be determined for the path at all — the targets are named and left unchecked, exit 0 |
|
||||||
|
|
||||||
The default is deliberately soft because a hyphenated word in a boundary clause is as likely to be a
|
The default is deliberately soft because a hyphenated word in a boundary clause is as likely to be a
|
||||||
@@ -149,6 +195,17 @@ tool, a file format or an English compound as a route: "pre-commit hooks" is pro
|
|||||||
never reaches the check at all, being a compound modifier rather than a terminal name. The
|
never reaches the check at all, being a compound modifier rather than a terminal name. The
|
||||||
SUGGESTION text says how to opt in — write it as `/name` or `-> name` and it gets checked properly.
|
SUGGESTION text says how to opt in — write it as `/name` or `-> name` and it gets checked properly.
|
||||||
|
|
||||||
|
**The two promotion conditions are not symmetric, and the order matters.** `_add()` decides
|
||||||
|
**notation first**: when the name is written `/name`, or reached through any arrow form, the target
|
||||||
|
is marked error-eligible there and the terminal test is never run. Terminality gates only the *bare*
|
||||||
|
path — a name in prose earns its error from corroboration, and a compound modifier can never dangle.
|
||||||
|
Reading the row as "terminal AND (notation OR corroborated)" gets the notation half backwards: it
|
||||||
|
predicts that `` … Do not use for Y — use /no-such-skill afterwards. `` is a SUGGESTION, because
|
||||||
|
`afterwards` is a follower outside `FOLLOWER_OK`. It exits 1. That was the defect — `-> name` reached
|
||||||
|
`_add()` with `strict=True` from both its call sites and `/name` did not, so the one spelling
|
||||||
|
ADR-0020 offers an author who wants a route checked unconditionally was the one spelling a stray
|
||||||
|
follower could silence.
|
||||||
|
|
||||||
**Known gap: a BARE arrow target must be hyphenated.** Target extraction is built on `NAME_HYPH` in
|
**Known gap: a BARE arrow target must be hyphenated.** Target extraction is built on `NAME_HYPH` in
|
||||||
`scripts/skill-size-check.sh`, which requires at least one hyphen, and `ARROW_BOUNDARY` inherits
|
`scripts/skill-size-check.sh`, which requires at least one hyphen, and `ARROW_BOUNDARY` inherits
|
||||||
that. So `Not X -> gitea-prs` is extracted and checked, while `Not X -> triage` yields no target.
|
that. So `Not X -> gitea-prs` is extracted and checked, while `Not X -> triage` yields no target.
|
||||||
@@ -558,6 +615,35 @@ passing one explicit file per invocation. The two manifests scope **differently
|
|||||||
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
|
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
|
||||||
and `check-vale-style-sync`'s probe set is built to catch exactly that.
|
and `check-vale-style-sync`'s probe set is built to catch exactly that.
|
||||||
|
|
||||||
|
### The blind spot: `references/` is unlinted, for two independent reasons
|
||||||
|
|
||||||
|
Every `references/*.md` file in the corpus is outside the prose gate. Count them with
|
||||||
|
`git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/references/.*\.md$'` rather than reading
|
||||||
|
a figure here; it moves with every retrofit. This is the gap that matters most, because the context
|
||||||
|
contract's own remedy for an over-long body is to move prose **into** `references/` — the gate pushes
|
||||||
|
text across its own boundary and then stops watching it.
|
||||||
|
|
||||||
|
**Closing either cause alone changes nothing.** There are two, and they are independent:
|
||||||
|
|
||||||
|
| Cause | Where | Effect on a `references/` file |
|
||||||
|
|---|---|---|
|
||||||
|
| the `Kyberforge` style is scoped `[**/SKILL.md]` | `skill-audit/assets/vale/.vale.ini` | matches no section, so Vale lints 0 files and exits 0 |
|
||||||
|
| the hook's `files:` regex is `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `vale-audit-prefilter-skill` in `.pre-commit-config.yaml` | the file is never handed to Vale at all |
|
||||||
|
|
||||||
|
Verified both ways. Handing skill-audit's `vale-wrap.sh` a reference file directly — bypassing
|
||||||
|
pre-commit entirely, so only the style scope is in play — prints `0 errors … in 0 files` and exits 0,
|
||||||
|
where the same wrapper on a `SKILL.md` reports `in 1 file`. And the hook's `files:` regex, applied to
|
||||||
|
`git ls-files`, selects only the skill-directory `SKILL.md` files scoped at the top of this page, so
|
||||||
|
pre-commit never hands Vale a reference file to begin with. Widening the glob to `[**/*.md]` would
|
||||||
|
still lint nothing through the hook; widening the hook's `files:` alone would hand Vale files its own
|
||||||
|
config declines to match, which is the [0-file NOT RUN](#a-0-file-vale-run-is-not-run) shape — a
|
||||||
|
green run that measured nothing. **Issue #117** records the style-scope half; the hook half has to
|
||||||
|
land in the same change or the fix is cosmetic.
|
||||||
|
|
||||||
|
The consumer manifest is a third axis and does not rescue this either: `.pre-commit-hooks.yaml`'s
|
||||||
|
`(^|/)SKILL\.md$` is layout-agnostic but still filename-shaped, so an external repo running
|
||||||
|
`kyberforge-vale-audit-skill` has the same gap.
|
||||||
|
|
||||||
### `vale-wrap.sh`, never bare `vale`
|
### `vale-wrap.sh`, never bare `vale`
|
||||||
|
|
||||||
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**
|
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: grill-me
|
name: grill-me
|
||||||
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
description: >
|
||||||
|
Use when the user says "grill me" or wants a plan or design stress-tested by
|
||||||
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: grill-with-docs
|
name: grill-with-docs
|
||||||
description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
<what-to-do>
|
<what-to-do>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: improve-codebase-architecture
|
name: improve-codebase-architecture
|
||||||
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Improve Codebase Architecture
|
# Improve Codebase Architecture
|
||||||
|
|||||||
@@ -109,3 +109,4 @@ Don't leave variant components or the switcher lying around. They rot fast and c
|
|||||||
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
|
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
|
||||||
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
|
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
|
||||||
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
|
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
|
||||||
|
- **Promoting the prototype directly to production.** The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in.
|
||||||
@@ -12,7 +12,7 @@ The frontmatter pins `model: sonnet` and a closed `allowed-tools` list. Notably
|
|||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
`references/file-format.md` is not optional reading before the write step: the `sources.md` field names it defines are matched literally by the downstream provenance validator. Prose written in their place parses as nothing and the check passes having verified nothing.
|
Both reference files are read on condition, never on every run — `SKILL.md` inlines the minimum each step needs (the seven default topic areas at step 1, the four `sources.md` field names and the topic-file frontmatter keys at step 6) and sends the run to the reference only for what it does not carry. Those four field names are matched literally by the downstream provenance validator, so prose written in their place parses as nothing and the check passes having verified nothing — which is why they are inlined rather than deferred.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -27,5 +27,5 @@ Name the topic and the output path — the skill will stop and ask if the path i
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | The four gotchas and the six research steps |
|
| `SKILL.md` | The four gotchas and the six research steps |
|
||||||
| `references/topics.md` | Read at Step 1 before narrowing scope: the default topic list (`overview`, `installation`, `configuration`, `cli-reference`, `api-reference`, `examples`, and more) and what each file covers |
|
| `references/topics.md` | Read at Step 1 only when what belongs in a default topic is unclear or a custom topic is needed: the per-topic coverage table and the custom-topic naming rule |
|
||||||
| `references/file-format.md` | Read at Step 6 before writing: the frontmatter schema for a topic file and the exact `sources.md` field names the provenance validator matches |
|
| `references/file-format.md` | Read at Step 6 only when the inlined field names do not settle the case: slug derivation, the Context7 slug and URL convention, and what belongs in a topic body |
|
||||||
@@ -30,7 +30,10 @@ model: sonnet
|
|||||||
|
|
||||||
Search for existing use of the topic — imports, config files, version pins, reference files already written — and narrow the research to what is missing: the version actually in use, the topics not yet documented.
|
Search for existing use of the topic — imports, config files, version pins, reference files already written — and narrow the research to what is missing: the version actually in use, the topics not yet documented.
|
||||||
|
|
||||||
Read `references/topics.md` before narrowing, for the default topic list.
|
The default topic areas are `overview`, `installation`, `configuration`, `cli-reference`,
|
||||||
|
`api-reference`, `examples` and `troubleshooting` — one file each, and only where content exists.
|
||||||
|
If what belongs in one of them is unclear, or the topic needs a file outside that set, read
|
||||||
|
`references/topics.md` for the per-topic coverage table and the custom-topic naming rule.
|
||||||
|
|
||||||
## Step 2 — Resolve against Context7
|
## Step 2 — Resolve against Context7
|
||||||
|
|
||||||
@@ -56,11 +59,20 @@ If nothing usable comes back, stop and report what was searched, then ask for st
|
|||||||
|
|
||||||
## Step 6 — Write
|
## Step 6 — Write
|
||||||
|
|
||||||
Merge every set of notes, Context7 and web alike, by topic area. Read `references/file-format.md`, then write, in the output path:
|
Merge every set of notes, Context7 and web alike, by topic area, then write, in the output path:
|
||||||
|
|
||||||
- `<topic>.md` for each topic area that has content, default or custom
|
- `<topic>.md` for each topic area that has content, default or custom. Frontmatter carries `topic:` (the filename without `.md`) and `source_keys:` (kebab-case slugs matching `sources.md`); the body is prose in `##` sections, with no inline URLs.
|
||||||
- `sources.md`, always, one section per source in the schema that file gives — URL, description, contributing files, and status — including sources that yielded nothing, marked `no content extracted`
|
- `sources.md`, always, one `##` section per source — including sources that yielded nothing — with exactly these four fields:
|
||||||
|
|
||||||
Spell the `sources.md` field names exactly as `references/file-format.md` gives them. The downstream provenance validator matches them literally; prose in their place parses as nothing, and the check passes having verified nothing.
|
```markdown
|
||||||
|
- **URL:** <full URL>
|
||||||
|
- **Description:** <one-line summary>
|
||||||
|
- **Contributing files:** <topic files this source contributed to>
|
||||||
|
- **Status:** `extracted` | `no content extracted`
|
||||||
|
```
|
||||||
|
|
||||||
|
Spell those four field names exactly as given. The downstream provenance validator matches them literally; prose in their place parses as nothing, and the check passes having verified nothing.
|
||||||
|
|
||||||
|
Read `references/file-format.md` when the four fields above do not settle the case: what a slug should be, the `context7-<library-slug>` slug and `context7:<library-id>` URL convention for a Context7 source, or what belongs in a topic body versus a verbatim copy of the source.
|
||||||
|
|
||||||
If no topic area has content, write nothing at all, `sources.md` included, and report what was searched.
|
If no topic area has content, write nothing at all, `sources.md` included, and report what was searched.
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: tdd
|
name: tdd
|
||||||
description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Test-Driven Development
|
# Test-Driven Development
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: triage
|
name: triage
|
||||||
description: Triage issues through a state machine driven by triage roles. Use when user wants to create an issue, triage issues, review incoming bugs or feature requests, prepare issues for an AFK agent, or manage issue workflow.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Triage
|
# Triage
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: write-docs
|
name: write-docs
|
||||||
description: Write documentation for X, document this module, create docs for this feature. Use when the user wants to produce or update technical documentation derived from code, spec, or existing artifacts. Do NOT use when the user wants a PRD, ADR, decision doc, or skill file — those have dedicated skills.
|
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"
|
version: "1.0"
|
||||||
updated: 2026-05-17
|
updated: 2026-05-17
|
||||||
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: grill-me
|
name: grill-me
|
||||||
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
description: >
|
||||||
|
Use when the user says "grill me" or wants a plan or design stress-tested by
|
||||||
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: grill-with-docs
|
name: grill-with-docs
|
||||||
description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
<what-to-do>
|
<what-to-do>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: improve-codebase-architecture
|
name: improve-codebase-architecture
|
||||||
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Improve Codebase Architecture
|
# Improve Codebase Architecture
|
||||||
|
|||||||
@@ -109,3 +109,4 @@ Don't leave variant components or the switcher lying around. They rot fast and c
|
|||||||
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
|
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
|
||||||
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
|
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
|
||||||
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
|
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
|
||||||
|
- **Promoting the prototype directly to production.** The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in.
|
||||||
@@ -12,7 +12,7 @@ The frontmatter pins `model: sonnet` and a closed `allowed-tools` list. Notably
|
|||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
`references/file-format.md` is not optional reading before the write step: the `sources.md` field names it defines are matched literally by the downstream provenance validator. Prose written in their place parses as nothing and the check passes having verified nothing.
|
Both reference files are read on condition, never on every run — `SKILL.md` inlines the minimum each step needs (the seven default topic areas at step 1, the four `sources.md` field names and the topic-file frontmatter keys at step 6) and sends the run to the reference only for what it does not carry. Those four field names are matched literally by the downstream provenance validator, so prose written in their place parses as nothing and the check passes having verified nothing — which is why they are inlined rather than deferred.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -27,5 +27,5 @@ Name the topic and the output path — the skill will stop and ask if the path i
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | The four gotchas and the six research steps |
|
| `SKILL.md` | The four gotchas and the six research steps |
|
||||||
| `references/topics.md` | Read at Step 1 before narrowing scope: the default topic list (`overview`, `installation`, `configuration`, `cli-reference`, `api-reference`, `examples`, and more) and what each file covers |
|
| `references/topics.md` | Read at Step 1 only when what belongs in a default topic is unclear or a custom topic is needed: the per-topic coverage table and the custom-topic naming rule |
|
||||||
| `references/file-format.md` | Read at Step 6 before writing: the frontmatter schema for a topic file and the exact `sources.md` field names the provenance validator matches |
|
| `references/file-format.md` | Read at Step 6 only when the inlined field names do not settle the case: slug derivation, the Context7 slug and URL convention, and what belongs in a topic body |
|
||||||
@@ -30,7 +30,10 @@ model: sonnet
|
|||||||
|
|
||||||
Search for existing use of the topic — imports, config files, version pins, reference files already written — and narrow the research to what is missing: the version actually in use, the topics not yet documented.
|
Search for existing use of the topic — imports, config files, version pins, reference files already written — and narrow the research to what is missing: the version actually in use, the topics not yet documented.
|
||||||
|
|
||||||
Read `references/topics.md` before narrowing, for the default topic list.
|
The default topic areas are `overview`, `installation`, `configuration`, `cli-reference`,
|
||||||
|
`api-reference`, `examples` and `troubleshooting` — one file each, and only where content exists.
|
||||||
|
If what belongs in one of them is unclear, or the topic needs a file outside that set, read
|
||||||
|
`references/topics.md` for the per-topic coverage table and the custom-topic naming rule.
|
||||||
|
|
||||||
## Step 2 — Resolve against Context7
|
## Step 2 — Resolve against Context7
|
||||||
|
|
||||||
@@ -56,11 +59,20 @@ If nothing usable comes back, stop and report what was searched, then ask for st
|
|||||||
|
|
||||||
## Step 6 — Write
|
## Step 6 — Write
|
||||||
|
|
||||||
Merge every set of notes, Context7 and web alike, by topic area. Read `references/file-format.md`, then write, in the output path:
|
Merge every set of notes, Context7 and web alike, by topic area, then write, in the output path:
|
||||||
|
|
||||||
- `<topic>.md` for each topic area that has content, default or custom
|
- `<topic>.md` for each topic area that has content, default or custom. Frontmatter carries `topic:` (the filename without `.md`) and `source_keys:` (kebab-case slugs matching `sources.md`); the body is prose in `##` sections, with no inline URLs.
|
||||||
- `sources.md`, always, one section per source in the schema that file gives — URL, description, contributing files, and status — including sources that yielded nothing, marked `no content extracted`
|
- `sources.md`, always, one `##` section per source — including sources that yielded nothing — with exactly these four fields:
|
||||||
|
|
||||||
Spell the `sources.md` field names exactly as `references/file-format.md` gives them. The downstream provenance validator matches them literally; prose in their place parses as nothing, and the check passes having verified nothing.
|
```markdown
|
||||||
|
- **URL:** <full URL>
|
||||||
|
- **Description:** <one-line summary>
|
||||||
|
- **Contributing files:** <topic files this source contributed to>
|
||||||
|
- **Status:** `extracted` | `no content extracted`
|
||||||
|
```
|
||||||
|
|
||||||
|
Spell those four field names exactly as given. The downstream provenance validator matches them literally; prose in their place parses as nothing, and the check passes having verified nothing.
|
||||||
|
|
||||||
|
Read `references/file-format.md` when the four fields above do not settle the case: what a slug should be, the `context7-<library-slug>` slug and `context7:<library-id>` URL convention for a Context7 source, or what belongs in a topic body versus a verbatim copy of the source.
|
||||||
|
|
||||||
If no topic area has content, write nothing at all, `sources.md` included, and report what was searched.
|
If no topic area has content, write nothing at all, `sources.md` included, and report what was searched.
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: tdd
|
name: tdd
|
||||||
description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Test-Driven Development
|
# Test-Driven Development
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: triage
|
name: triage
|
||||||
description: Triage issues through a state machine driven by triage roles. Use when user wants to create an issue, triage issues, review incoming bugs or feature requests, prepare issues for an AFK agent, or manage issue workflow.
|
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`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Triage
|
# Triage
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: write-docs
|
name: write-docs
|
||||||
description: Write documentation for X, document this module, create docs for this feature. Use when the user wants to produce or update technical documentation derived from code, spec, or existing artifacts. Do NOT use when the user wants a PRD, ADR, decision doc, or skill file — those have dedicated skills.
|
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"
|
version: "1.0"
|
||||||
updated: 2026-05-17
|
updated: 2026-05-17
|
||||||
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
when: invoked by explicit trigger ("write docs for X", "document this module", "create docs for this feature") or implicit request to produce technical documentation from code or spec
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Provide the path to the provider-specific file to convert (and the target repo r
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents |
|
||||||
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, and the validator flag each needs |
|
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, the validator flag each needs, and the rule that a standalone run and a run composed into by `agentsmd-author` behave identically |
|
||||||
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
|
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
|
||||||
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
|
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
|
||||||
| `scripts/README.md` | Directory documentation for `scripts/` |
|
| `scripts/README.md` | Directory documentation for `scripts/` |
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ metadata:
|
|||||||
|
|
||||||
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
|
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
|
||||||
|
|
||||||
|
- Works standalone or composed-into by `agentsmd-author` — behave identically either way; do not assume a caller skill exists. Detect the provider file, confirm `AGENTS.md`, and run the closeout validator yourself in both cases (`references/provider-matrix.md`).
|
||||||
|
|
||||||
## Step 1 — Detect
|
## Step 1 — Detect
|
||||||
|
|
||||||
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
|
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
|
||||||
@@ -28,7 +30,7 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
|
|||||||
|
|
||||||
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
|
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
|
||||||
|
|
||||||
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import line, keep the provider-specific bucket below it.
|
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it.
|
||||||
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
||||||
|
|
||||||
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
|
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
|
||||||
@@ -43,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
|
|||||||
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
|
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
|
||||||
```
|
```
|
||||||
|
|
||||||
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`.
|
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.
|
||||||
|
|
||||||
## Step 4 — Report
|
## Step 4 — Report
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,12 @@ silently drops every rule the adapter was supposed to defer to.
|
|||||||
|
|
||||||
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
|
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
|
||||||
independently against the same `AGENTS.md`.
|
independently against the same `AGENTS.md`.
|
||||||
|
|
||||||
|
## Standalone and composed runs behave identically
|
||||||
|
|
||||||
|
This skill is reached two ways: invoked directly by a user, and composed into by `agentsmd-author`
|
||||||
|
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.
|
||||||
@@ -31,6 +31,12 @@ Exit codes:
|
|||||||
0 Adapter file passes all checks
|
0 Adapter file passes all checks
|
||||||
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
||||||
excessive duplication, or file too long)
|
excessive duplication, or file too long)
|
||||||
|
2 Usage or input error — a bad or missing argument, a path that is not a
|
||||||
|
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.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +57,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--max-lines)
|
--max-lines)
|
||||||
if [[ $# -lt 2 ]]; then
|
if [[ $# -lt 2 ]]; then
|
||||||
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
MAX_LINES="$2"
|
MAX_LINES="$2"
|
||||||
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
||||||
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
@@ -71,7 +77,7 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
|
|||||||
echo "Error: adapter-file and agents-md-file are required." >&2
|
echo "Error: adapter-file and agents-md-file are required." >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
|
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
|
||||||
@@ -85,15 +91,43 @@ max_lines = int(max_lines)
|
|||||||
|
|
||||||
if not os.path.isfile(adapter_path):
|
if not os.path.isfile(adapter_path):
|
||||||
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
|
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
if not os.path.isfile(agents_md_path):
|
if not os.path.isfile(agents_md_path):
|
||||||
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
|
|
||||||
with open(adapter_path, encoding="utf-8", errors="replace") as f:
|
|
||||||
adapter_content = f.read()
|
def read_text(path):
|
||||||
with open(agents_md_path, encoding="utf-8", errors="replace") as f:
|
r"""File contents as text, UTF-8, BOM stripped.
|
||||||
agents_md_content = f.read()
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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,
|
||||||
|
the same policy the ADR-0020 validators' read_text() uses.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
adapter_content = read_text(adapter_path)
|
||||||
|
agents_md_content = read_text(agents_md_path)
|
||||||
|
|
||||||
has_fail = False
|
has_fail = False
|
||||||
|
|
||||||
@@ -124,8 +158,8 @@ if not has_reference:
|
|||||||
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.")
|
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.")
|
||||||
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
||||||
else:
|
else:
|
||||||
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line; merely naming the file in prose defers nothing.")
|
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: Add an `@AGENTS.md` (or equivalent relative path) import line, or pass --no-import-syntax if this provider resolves no imports.")
|
print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line, or pass --no-import-syntax if this provider resolves no imports.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# --- Duplication check ---
|
# --- Duplication check ---
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ EOF
|
|||||||
ADAPTER="$TMPDIR/CLAUDE.md"
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
echo "@AGENTS.md" > "$ADAPTER"
|
echo "@AGENTS.md" > "$ADAPTER"
|
||||||
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" --max-lines
|
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" --max-lines
|
||||||
assert_failure
|
assert_failure 2
|
||||||
assert_output --partial "--max-lines requires a value"
|
assert_output --partial "--max-lines requires a value"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ EOF
|
|||||||
ADAPTER="$TMPDIR/CLAUDE.md"
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
echo "@AGENTS.md" > "$ADAPTER"
|
echo "@AGENTS.md" > "$ADAPTER"
|
||||||
run bash "$SCRIPT" --max-lines abc "$ADAPTER" "$AGENTS_MD"
|
run bash "$SCRIPT" --max-lines abc "$ADAPTER" "$AGENTS_MD"
|
||||||
assert_failure
|
assert_failure 2
|
||||||
assert_output --partial "non-negative integer"
|
assert_output --partial "non-negative integer"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +168,47 @@ EOF
|
|||||||
|
|
||||||
@test "fails with a clear error when the adapter file argument is missing" {
|
@test "fails with a clear error when the adapter file argument is missing" {
|
||||||
run bash "$SCRIPT"
|
run bash "$SCRIPT"
|
||||||
assert_failure
|
assert_failure 2
|
||||||
assert_output --partial "required"
|
assert_output --partial "required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "a UTF-8 BOM before the @import line does not hide it" {
|
||||||
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
|
python3 -c "import sys; open(sys.argv[1], 'w', encoding='utf-8-sig').write('@AGENTS.md\n')" "$ADAPTER"
|
||||||
|
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
|
||||||
|
assert_success
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a usage error exits 2, a genuine finding exits 1" {
|
||||||
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
|
echo "@AGENTS.md" > "$ADAPTER"
|
||||||
|
|
||||||
|
run bash "$SCRIPT" --max-lines -3 "$ADAPTER" "$AGENTS_MD"
|
||||||
|
assert_failure 2
|
||||||
|
refute_output --partial "FAIL"
|
||||||
|
|
||||||
|
: > "$ADAPTER"
|
||||||
|
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
|
||||||
|
assert_failure 1
|
||||||
|
assert_output --partial "FAIL"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a non-UTF-8 adapter is reported as an encoding error, not as a missing reference" {
|
||||||
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
|
python3 -c "import sys; open(sys.argv[1], 'wb').write('@AGENTS.md\n'.encode('utf-16'))" "$ADAPTER"
|
||||||
|
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
|
||||||
|
assert_failure 2
|
||||||
|
assert_output --partial "not valid UTF-8"
|
||||||
|
refute_output --partial "no reference"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "an @AGENTS.md folded into a sentence fails, and the message says the import needs its own line" {
|
||||||
|
ADAPTER="$TMPDIR/CLAUDE.md"
|
||||||
|
cat > "$ADAPTER" <<'EOF'
|
||||||
|
See @AGENTS.md for shared conventions.
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
|
||||||
|
assert_failure 1
|
||||||
|
assert_output --partial "no reference"
|
||||||
|
assert_output --partial "line of its own"
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ Provide the path to the provider-specific file to convert (and the target repo r
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents |
|
||||||
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, and the validator flag each needs |
|
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, the validator flag each needs, and the rule that a standalone run and a run composed into by `agentsmd-author` behave identically |
|
||||||
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
|
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
|
||||||
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
|
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
|
||||||
| `scripts/README.md` | Directory documentation for `scripts/` |
|
| `scripts/README.md` | Directory documentation for `scripts/` |
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ metadata:
|
|||||||
|
|
||||||
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
|
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
|
||||||
|
|
||||||
|
- Works standalone or composed-into by `agentsmd-author` — behave identically either way; do not assume a caller skill exists. Detect the provider file, confirm `AGENTS.md`, and run the closeout validator yourself in both cases (`references/provider-matrix.md`).
|
||||||
|
|
||||||
## Step 1 — Detect
|
## Step 1 — Detect
|
||||||
|
|
||||||
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
|
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
|
||||||
@@ -28,7 +30,7 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
|
|||||||
|
|
||||||
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
|
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
|
||||||
|
|
||||||
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import line, keep the provider-specific bucket below it.
|
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it.
|
||||||
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
|
||||||
|
|
||||||
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
|
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
|
||||||
@@ -43,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
|
|||||||
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
|
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
|
||||||
```
|
```
|
||||||
|
|
||||||
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`.
|
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.
|
||||||
|
|
||||||
## Step 4 — Report
|
## Step 4 — Report
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,12 @@ silently drops every rule the adapter was supposed to defer to.
|
|||||||
|
|
||||||
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
|
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
|
||||||
independently against the same `AGENTS.md`.
|
independently against the same `AGENTS.md`.
|
||||||
|
|
||||||
|
## Standalone and composed runs behave identically
|
||||||
|
|
||||||
|
This skill is reached two ways: invoked directly by a user, and composed into by `agentsmd-author`
|
||||||
|
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.
|
||||||
@@ -31,6 +31,12 @@ Exit codes:
|
|||||||
0 Adapter file passes all checks
|
0 Adapter file passes all checks
|
||||||
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
||||||
excessive duplication, or file too long)
|
excessive duplication, or file too long)
|
||||||
|
2 Usage or input error — a bad or missing argument, a path that is not a
|
||||||
|
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.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +57,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--max-lines)
|
--max-lines)
|
||||||
if [[ $# -lt 2 ]]; then
|
if [[ $# -lt 2 ]]; then
|
||||||
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
MAX_LINES="$2"
|
MAX_LINES="$2"
|
||||||
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
||||||
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
@@ -71,7 +77,7 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
|
|||||||
echo "Error: adapter-file and agents-md-file are required." >&2
|
echo "Error: adapter-file and agents-md-file are required." >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 1
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
|
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
|
||||||
@@ -85,15 +91,43 @@ max_lines = int(max_lines)
|
|||||||
|
|
||||||
if not os.path.isfile(adapter_path):
|
if not os.path.isfile(adapter_path):
|
||||||
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
|
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
if not os.path.isfile(agents_md_path):
|
if not os.path.isfile(agents_md_path):
|
||||||
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
|
|
||||||
with open(adapter_path, encoding="utf-8", errors="replace") as f:
|
|
||||||
adapter_content = f.read()
|
def read_text(path):
|
||||||
with open(agents_md_path, encoding="utf-8", errors="replace") as f:
|
r"""File contents as text, UTF-8, BOM stripped.
|
||||||
agents_md_content = f.read()
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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,
|
||||||
|
the same policy the ADR-0020 validators' read_text() uses.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
adapter_content = read_text(adapter_path)
|
||||||
|
agents_md_content = read_text(agents_md_path)
|
||||||
|
|
||||||
has_fail = False
|
has_fail = False
|
||||||
|
|
||||||
@@ -124,8 +158,8 @@ if not has_reference:
|
|||||||
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.")
|
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.")
|
||||||
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
||||||
else:
|
else:
|
||||||
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line; merely naming the file in prose defers nothing.")
|
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: Add an `@AGENTS.md` (or equivalent relative path) import line, or pass --no-import-syntax if this provider resolves no imports.")
|
print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line, or pass --no-import-syntax if this provider resolves no imports.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# --- Duplication check ---
|
# --- Duplication check ---
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ metadata:
|
|||||||
## Gotchas
|
## 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.
|
- **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.** 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>` 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>`.
|
||||||
- **`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.
|
- **`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
|
## Step 1 — Determine the branching pattern
|
||||||
|
|||||||
@@ -57,8 +57,17 @@ For an agent caller, return:
|
|||||||
"semver_impact": "MAJOR|MINOR|PATCH|none",
|
"semver_impact": "MAJOR|MINOR|PATCH|none",
|
||||||
"breaking_change": false,
|
"breaking_change": false,
|
||||||
"confirmation_required": false,
|
"confirmation_required": false,
|
||||||
"details": { "type": "feat", "scope": "api", "description": "add user authentication" }
|
"details": {
|
||||||
|
"type": "feat",
|
||||||
|
"scope": "api",
|
||||||
|
"description": "add user authentication",
|
||||||
|
"body": "optional body text, or null",
|
||||||
|
"footers": ["Fixes: #123", "Refs: #456", "Co-authored-by: Bob <[email protected]>"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`details.footers` is an array of the resolved trailer lines, empty when there are none — never a
|
||||||
|
single joined string, and never omitted. Downstream agents index it.
|
||||||
|
|
||||||
For a human caller, show the same fields as a prose preview with a confirmation prompt.
|
For a human caller, show the same fields as a prose preview with a confirmation prompt.
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
name: git-history
|
name: git-history
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when investigating git history — querying logs, tracing when a change
|
Use when investigating git history — pickaxe (`-S`/`-G`) or `-L` line-range log
|
||||||
landed, bisecting the commit that broke something, or locating one to
|
queries, tracing when a change landed, bisecting what broke something, or
|
||||||
revert or backport. Not authoring or rebasing commits -> `git-commits`.
|
locating a commit to revert or backport. Not authoring or rebasing commits ->
|
||||||
Not history on a Gitea server -> `gitea-branches`.
|
`git-commits`. Not a Gitea server's history -> `gitea-branches`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
category: git
|
category: git
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ description: >
|
|||||||
remote, even when the user does not name it.
|
remote, even when the user does not name it.
|
||||||
Not local commits -> `git-commits`.
|
Not local commits -> `git-commits`.
|
||||||
Not local branches -> `git-branches`.
|
Not local branches -> `git-branches`.
|
||||||
|
Not log or bisect queries -> `git-history`.
|
||||||
Not submodule pointers -> `git-submodules`.
|
Not submodule pointers -> `git-submodules`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ conflicts, and a recovery `next_step` when applicable).
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents — gotchas, shared working rules, and the task dispatch table |
|
| `SKILL.md` | Skill instructions for agents — gotchas, shared working rules, and the task dispatch table |
|
||||||
| `references/README.md` | Describes contents of references/ |
|
| `references/README.md` | Describes contents of references/ |
|
||||||
| `references/setup-and-update.md` | Loaded when cloning a superproject, adding a submodule, or initializing, updating, or re-pinning one — includes the full `add` and `update` flag tables and the pinning workflows |
|
| `references/setup-and-update.md` | Loaded when cloning a superproject, adding a submodule, initializing, updating, or re-pinning one, or running a command across all of them — includes the full `add` and `update` flag tables, the pinning workflows, and the `foreach` shell-variable table |
|
||||||
| `references/urls-and-config.md` | Loaded when changing where a submodule points or how it is configured — `.gitmodules` vs `.git/config` anatomy, both key tables, `sync`/`set-url`/`set-branch`, local mirror overrides, relative URLs, the custom-`update` security gate, and `absorbgitdirs` |
|
| `references/urls-and-config.md` | Loaded when changing where a submodule points or how it is configured — `.gitmodules` vs `.git/config` anatomy, both key tables, `sync`/`set-url`/`set-branch`, local mirror overrides, relative URLs, the custom-`update` security gate, and `absorbgitdirs` |
|
||||||
| `references/removal.md` | Loaded when removing or deinitializing a submodule — why `deinit` is not removal, and the four-step removal sequence |
|
| `references/removal.md` | Loaded when removing or deinitializing a submodule — why `deinit` is not removal, and the four-step removal sequence |
|
||||||
| `references/sources.md` | Research sources and provenance |
|
| `references/sources.md` | Research sources and provenance |
|
||||||
@@ -34,7 +34,9 @@ them pins a state nobody else can reproduce.
|
|||||||
|
|
||||||
To run one command across every submodule: `rtk git submodule foreach --recursive '<cmd>'`. Inside
|
To run one command across every submodule: `rtk git submodule foreach --recursive '<cmd>'`. Inside
|
||||||
`<cmd>`, Git sets `$name`, `$sm_path`, `$displaypath`, `$sha1` and `$toplevel`; append `|| :` to
|
`<cmd>`, Git sets `$name`, `$sm_path`, `$displaypath`, `$sha1` and `$toplevel`; append `|| :` to
|
||||||
continue past a failure instead of aborting the traversal.
|
continue past a failure instead of aborting the traversal. `$sm_path` and `$displaypath` name the
|
||||||
|
same directory from different vantage points — if which one you want is not obvious, read the
|
||||||
|
variable table in `references/setup-and-update.md` before writing the command.
|
||||||
|
|
||||||
## Dispatch
|
## Dispatch
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ Read only the row that matches the request.
|
|||||||
|
|
||||||
| Task | Reference |
|
| Task | Reference |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Clone a superproject with submodules, or add, initialize, update or re-pin one | `references/setup-and-update.md` |
|
| Clone a superproject with submodules; add, initialize, update or re-pin one; run a command across all of them with `foreach` | `references/setup-and-update.md` |
|
||||||
| Change where a submodule points — `sync`, `set-url`, `set-branch`, a local mirror override, `absorbgitdirs`, or any `.gitmodules` / `.git/config` key | `references/urls-and-config.md` |
|
| Change where a submodule points — `sync`, `set-url`, `set-branch`, a local mirror override, `absorbgitdirs`, or any `.gitmodules` / `.git/config` key | `references/urls-and-config.md` |
|
||||||
| Remove a submodule, or `deinit` one without removing it | `references/removal.md` |
|
| Remove a submodule, or `deinit` one without removing it | `references/removal.md` |
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ One file per task branch in SKILL.md's dispatch table. Load only the one that ma
|
|||||||
## setup-and-update.md
|
## setup-and-update.md
|
||||||
|
|
||||||
Cloning a superproject that has submodules, adding a dependency as a submodule, initializing
|
Cloning a superproject that has submodules, adding a dependency as a submodule, initializing
|
||||||
without cloning, and updating or re-pinning. Carries the `add` and `update` flag tables and the
|
without cloning, updating or re-pinning, and running one command across every submodule. Carries
|
||||||
keep-pinned and move-the-pin-forward workflows.
|
the `add` and `update` flag tables, the keep-pinned and move-the-pin-forward workflows, and the
|
||||||
|
`foreach` shell-variable table (`$name`, `$sm_path`, `$displaypath`, `$sha1`, `$toplevel`).
|
||||||
|
|
||||||
## urls-and-config.md
|
## urls-and-config.md
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Both operations are destructive. Confirm with the user before executing either.
|
|||||||
## `deinit` is not removal
|
## `deinit` is not removal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule deinit <path> # --all for every submodule, -f if locally modified
|
rtk git submodule deinit <path> # --all for every submodule, -f if locally modified
|
||||||
```
|
```
|
||||||
|
|
||||||
`deinit` clears the submodule's section from `.git/config` and empties its working tree. The
|
`deinit` clears the submodule's section from `.git/config` and empties its working tree. The
|
||||||
@@ -22,11 +22,11 @@ or to reset a broken checkout, not to delete a dependency.
|
|||||||
## Full removal, in order
|
## Full removal, in order
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule deinit -f <path> # unregister from .git/config
|
rtk git submodule deinit -f <path> # unregister from .git/config
|
||||||
git rm <path> # drop the .gitmodules entry and the gitlink from the index
|
rtk git rm <path> # drop the .gitmodules entry and the gitlink from the index
|
||||||
rm -rf .git/modules/<name>/ # stale git dir: not tracked, not cleaned up by git
|
rm -rf .git/modules/<name>/ # stale git dir: not tracked, not cleaned up by git
|
||||||
git commit -m "chore: remove <name> submodule"
|
rtk git commit -m "chore: remove <name> submodule"
|
||||||
```
|
```
|
||||||
|
|
||||||
The third step is the one that gets skipped. `.git/modules/<name>/` survives `git rm`, and while it
|
The third step is the one that gets skipped. `.git/modules/<name>/` survives `rtk git rm`, and while it
|
||||||
is present Git refuses to add a submodule at the same path again.
|
is present Git refuses to add a submodule at the same path again.
|
||||||
@@ -9,16 +9,16 @@ source_keys:
|
|||||||
## Clone a superproject that already has submodules
|
## Clone a superproject that already has submodules
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recurse-submodules <url> # Git 2.13+, one step
|
rtk git clone --recurse-submodules <url> # Git 2.13+, one step
|
||||||
# or, against an existing clone
|
# or, against an existing clone
|
||||||
git submodule update --init --recursive
|
rtk git submodule update --init --recursive
|
||||||
```
|
```
|
||||||
|
|
||||||
## Add a dependency as a submodule
|
## Add a dependency as a submodule
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule add <url> <path>
|
rtk git submodule add <url> <path>
|
||||||
git commit -m "chore: add <name> as submodule"
|
rtk git commit -m "chore: add <name> as submodule"
|
||||||
```
|
```
|
||||||
|
|
||||||
`add` stages a `.gitmodules` entry and a gitlink — the commit is still required. Flags:
|
`add` stages a `.gitmodules` entry and a gitlink — the commit is still required. Flags:
|
||||||
@@ -32,14 +32,14 @@ git commit -m "chore: add <name> as submodule"
|
|||||||
|
|
||||||
## Initialize without cloning
|
## Initialize without cloning
|
||||||
|
|
||||||
`git submodule init [<path>...]` copies submodule URLs from `.gitmodules` into `.git/config` and
|
`rtk git submodule init [<path>...]` copies submodule URLs from `.gitmodules` into `.git/config` and
|
||||||
does nothing else. This is the point at which a local URL override can be edited before any fetch
|
does nothing else. This is the point at which a local URL override can be edited before any fetch
|
||||||
happens. If a local mirror override is wanted, read `references/urls-and-config.md` before running
|
happens. If a local mirror override is wanted, read `references/urls-and-config.md` before running
|
||||||
`update`. Use `update --init` to run both steps at once.
|
`update`. Use `update --init` to run both steps at once.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
`git submodule update --init --recursive` is the common case: it clones what is missing and checks
|
`rtk git submodule update --init --recursive` is the common case: it clones what is missing and checks
|
||||||
out the commit the superproject recorded, in detached HEAD.
|
out the commit the superproject recorded, in detached HEAD.
|
||||||
|
|
||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
@@ -59,16 +59,38 @@ out the commit the superproject recorded, in detached HEAD.
|
|||||||
## Keep submodules pinned to the recorded commit
|
## Keep submodules pinned to the recorded commit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --recursive # after every git pull
|
rtk git submodule update --recursive # after every rtk git pull
|
||||||
git config submodule.recurse true # or do it automatically on pull/push/checkout
|
rtk git config submodule.recurse true # or do it automatically on pull/push/checkout
|
||||||
```
|
```
|
||||||
|
|
||||||
## Move the pin forward to the tracked branch tip
|
## Move the pin forward to the tracked branch tip
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --remote --merge --recursive
|
rtk git submodule update --remote --merge --recursive
|
||||||
git commit -am "chore: update submodules to latest"
|
rtk git commit -am "chore: update submodules to latest"
|
||||||
```
|
```
|
||||||
|
|
||||||
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default
|
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default
|
||||||
branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
|
branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
|
||||||
|
|
||||||
|
## Run one command across every submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk git submodule foreach --recursive '<command>'
|
||||||
|
rtk git submodule foreach 'git pull origin main || :' # || : continues past a failure
|
||||||
|
```
|
||||||
|
|
||||||
|
`<command>` runs inside each submodule's own working tree, so the git calls in it are the
|
||||||
|
submodule's own — that is the one place a bare `git` is correct. Append `|| :` to keep the
|
||||||
|
traversal going instead of aborting at the first failure.
|
||||||
|
|
||||||
|
Git exports five shell variables into `<command>`. `$sm_path` and `$displaypath` name the same
|
||||||
|
directory from different vantage points and are not interchangeable:
|
||||||
|
|
||||||
|
| Variable | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `$name` | Logical submodule name (the `.gitmodules` section name, which need not match the path) |
|
||||||
|
| `$sm_path` | Path relative to the superproject root |
|
||||||
|
| `$displaypath` | Path relative to the current working directory |
|
||||||
|
| `$sha1` | Commit SHA the superproject has recorded for this submodule |
|
||||||
|
| `$toplevel` | Absolute path of the superproject's root |
|
||||||
@@ -10,7 +10,7 @@ source_keys:
|
|||||||
|
|
||||||
- **`.gitmodules`** — version-controlled, shared with collaborators. Defines each submodule's
|
- **`.gitmodules`** — version-controlled, shared with collaborators. Defines each submodule's
|
||||||
logical name, path, and canonical URL.
|
logical name, path, and canonical URL.
|
||||||
- **`.git/config`** — local only, populated by `git submodule init`. Local URL overrides live here
|
- **`.git/config`** — local only, populated by `rtk git submodule init`. Local URL overrides live here
|
||||||
and never propagate to another clone.
|
and never propagate to another clone.
|
||||||
|
|
||||||
The submodule's own `.git` directory lives at `.git/modules/<name>/` in the superproject and is
|
The submodule's own `.git` directory lives at `.git/modules/<name>/` in the superproject and is
|
||||||
@@ -38,9 +38,9 @@ linked to the submodule's working tree by a `.git` pointer file.
|
|||||||
## Rebind a URL or branch
|
## Rebind a URL or branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule sync --recursive # push .gitmodules URLs into .git/config
|
rtk git submodule sync --recursive # push .gitmodules URLs into .git/config
|
||||||
git submodule set-url <path> <url> # change the canonical URL
|
rtk git submodule set-url <path> <url> # change the canonical URL
|
||||||
git submodule set-branch -b <branch> <path> # set the branch used by update --remote
|
rtk git submodule set-branch -b <branch> <path> # set the branch used by update --remote
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `sync` after an upstream rename: existing clones keep the stale URL in `.git/config` until
|
Run `sync` after an upstream rename: existing clones keep the stale URL in `.git/config` until
|
||||||
@@ -49,9 +49,9 @@ they do.
|
|||||||
## Override a URL locally (private mirror)
|
## Override a URL locally (private mirror)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule init
|
rtk git submodule init
|
||||||
# edit .git/config: submodule.<name>.url = <mirror-url>
|
# edit .git/config: submodule.<name>.url = <mirror-url>
|
||||||
git submodule update
|
rtk git submodule update
|
||||||
```
|
```
|
||||||
|
|
||||||
Local-only, invisible to collaborators, and overwritten by the next `sync`.
|
Local-only, invisible to collaborators, and overwritten by the next `sync`.
|
||||||
@@ -65,15 +65,15 @@ everywhere else.
|
|||||||
## Custom `update` commands are security-gated
|
## Custom `update` commands are security-gated
|
||||||
|
|
||||||
A `.gitmodules` entry of `update = !some-command` is never copied into `.git/config` by
|
A `.gitmodules` entry of `update = !some-command` is never copied into `.git/config` by
|
||||||
`git submodule init`. That is deliberate: it stops a hostile clone from silently executing
|
`rtk git submodule init`. That is deliberate: it stops a hostile clone from silently executing
|
||||||
arbitrary code. Setting it locally in `.git/config` is the only way to enable it.
|
arbitrary code. Setting it locally in `.git/config` is the only way to enable it.
|
||||||
|
|
||||||
## Relocate an embedded `.git` directory
|
## Relocate an embedded `.git` directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule absorbgitdirs [<path>...]
|
rtk git submodule absorbgitdirs [<path>...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Moves a submodule's own `.git` directory into `.git/modules/<name>/` and leaves a `.git` pointer
|
Moves a submodule's own `.git` directory into `.git/modules/<name>/` and leaves a `.git` pointer
|
||||||
file behind. Needed when a nested repository was created or copied in without going through
|
file behind. Needed when a nested repository was created or copied in without going through
|
||||||
`git submodule add`.
|
`rtk git submodule add`.
|
||||||
@@ -4,7 +4,7 @@ Human-friendly interface for interactive git workflows with conversational promp
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
This skill wraps the `git-orchestrate` agent to provide an interactive, educational interface for humans performing git workflows. It handles commits, branch management, history inspection, submodules, worktrees, and remotes. The skill parses user intent, gathers session context, invokes the orchestrator, and presents results in plain language with inline help, progress updates, and explanations of what's happening. It enforces confirmation gates for destructive operations (force-push, branch deletion, rebasing with history loss, force-checkout) and provides best-practices guidance throughout. The org's non-negotiable git rules live in `references/hard-rules.md` and are loaded only when a request could conflict with one.
|
This skill wraps the `git-orchestrate` agent to provide an interactive, educational interface for humans performing git workflows. It is the router for the six local-git domain skills — `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules` and `git-worktrees` — and `SKILL.md` carries a table mapping each of them to the requests it owns, so an ambiguous request resolves to exactly one domain before anything runs. The skill parses user intent, gathers session context, invokes the orchestrator, and presents results in plain language with inline help, progress updates, and explanations of what's happening. It enforces confirmation gates for destructive operations (force-push, branch deletion, rebasing with history loss, force-checkout) and provides best-practices guidance throughout. The org's non-negotiable git rules live in `references/hard-rules.md` and are loaded only when a request could conflict with one.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Describe your git workflow: commit, create a branch, rebase, inspect history, ma
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents — the six-domain routing table, the workflow steps, and the interaction style |
|
||||||
| `README.md` | This file |
|
| `README.md` | This file |
|
||||||
| `references/hard-rules.md` | The org's non-negotiable git rules; read when a request creates, amends, or rewrites a commit, pushes, or touches hooks, config, or credentials |
|
| `references/hard-rules.md` | The org's non-negotiable git rules; read when a request creates, amends, or rewrites a commit, pushes, or touches hooks, config, or credentials |
|
||||||
| `references/README.md` | Describes the references directory contents |
|
| `references/README.md` | Describes the references directory contents |
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ name: git-workflow
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when a human's local git request is general or ambiguous — it routes to the owning
|
Use when a human's local git request is general or ambiguous — it routes to the owning
|
||||||
domain skill. Not an unambiguous commit -> `git-commits`. Not an unambiguous branch ->
|
domain skill: `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules`
|
||||||
`git-branches`. Not an agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`.
|
or `git-worktrees`. An unambiguous request goes straight to its domain skill instead. Not an
|
||||||
|
agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
category: git
|
category: git
|
||||||
@@ -25,10 +26,27 @@ metadata:
|
|||||||
mandated org wrapper, not a style preference. Submodule-specific commands run from inside the
|
mandated org wrapper, not a style preference. Submodule-specific commands run from inside the
|
||||||
submodule's own directory instead.
|
submodule's own directory instead.
|
||||||
|
|
||||||
|
## Domains
|
||||||
|
|
||||||
|
Every request resolves to exactly one of these six. An unambiguous one should have gone straight
|
||||||
|
to the domain skill; this skill exists for the ones that did not.
|
||||||
|
|
||||||
|
| The request is about | Domain |
|
||||||
|
|---|---|
|
||||||
|
| Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` |
|
||||||
|
| Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` |
|
||||||
|
| When a change landed, which commit broke something, what to revert or backport | `git-history` |
|
||||||
|
| Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` |
|
||||||
|
| A nested repository pinned inside this one by a recorded commit | `git-submodules` |
|
||||||
|
| Several branches checked out at once, in separate directories, without stashing | `git-worktrees` |
|
||||||
|
|
||||||
|
`git-orchestrate` executes whatever this resolves to (step 5); the table only decides which domain
|
||||||
|
owns the request.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. **Parse intent** — extract the operation (commit, create branch, rebase, inspect history, …)
|
1. **Parse intent** — extract the operation and, from the table above, the domain that owns it,
|
||||||
and any options the user named.
|
plus any options the user named.
|
||||||
2. **Check the hard rules** — if the request creates, amends, or rewrites a commit, pushes, or
|
2. **Check the hard rules** — if the request creates, amends, or rewrites a commit, pushes, or
|
||||||
touches hooks, config, or credentials, read `references/hard-rules.md`. Raise the relevant rule
|
touches hooks, config, or credentials, read `references/hard-rules.md`. Raise the relevant rule
|
||||||
before acting, not after.
|
before acting, not after.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ metadata:
|
|||||||
- **A branch can be checked out in only one worktree at a time.** `git worktree add` on an already-checked-out branch fails; `--force` is the only override, so use it only deliberately.
|
- **A branch can be checked out in only one worktree at a time.** `git worktree add` on an already-checked-out branch fails; `--force` is the only override, so use it only deliberately.
|
||||||
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `git worktree remove`, or `git worktree prune` afterwards.
|
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `git worktree remove`, or `git worktree prune` afterwards.
|
||||||
- **Submodules break worktree support.** A worktree containing submodules cannot be moved at all, and needs `--force` to remove.
|
- **Submodules break worktree support.** A worktree containing submodules cannot be moved at all, and needs `--force` to remove.
|
||||||
- **`extensions.worktreeConfig = true` is a one-way door.** It costs compatibility with older Git and forces `core.bare`/`core.worktree` into `config.worktree`. Leave it off unless per-worktree config is needed.
|
- **`extensions.worktreeConfig = true` is a one-way door.** Without it, `git config --worktree` errors; with it, that flag writes to the worktree's own `config.worktree` file, and `core.bare`/`core.worktree` are forced there too. It also breaks older Git. Leave it off unless per-worktree config is needed.
|
||||||
|
|
||||||
## Step 1 — Dispatch
|
## Step 1 — Dispatch
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ Determine intent from the user's request, then execute the matching operation. W
|
|||||||
| "clean", "wipe cache", "rebuild from scratch" | `pre-commit clean` — read `references/clean.md` |
|
| "clean", "wipe cache", "rebuild from scratch" | `pre-commit clean` — read `references/clean.md` |
|
||||||
| "hooks aren't running", "hook never fires", "why did a hook fail", a hook failure whose cause is unclear | Diagnose — read `references/failure-patterns.md` |
|
| "hooks aren't running", "hook never fires", "why did a hook fail", a hook failure whose cause is unclear | Diagnose — read `references/failure-patterns.md` |
|
||||||
|
|
||||||
|
If the intent is ambiguous, default to `pre-commit run --all-files` — do not stop to ask, and do
|
||||||
|
not fall through to a narrower row on a guess.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Default to `pre-commit run --all-files`; never silently narrow to staged files. Run `pre-commit run` (staged only) or `pre-commit run <hook-id>` (one named hook) when the user asks for it.
|
Default to `pre-commit run --all-files`; never silently narrow to staged files. Run `pre-commit run` (staged only) or `pre-commit run <hook-id>` (one named hook) when the user asks for it.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ metadata:
|
|||||||
## Gotchas
|
## 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.
|
- **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.** 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>` 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>`.
|
||||||
- **`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.
|
- **`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
|
## Step 1 — Determine the branching pattern
|
||||||
|
|||||||
@@ -57,8 +57,17 @@ For an agent caller, return:
|
|||||||
"semver_impact": "MAJOR|MINOR|PATCH|none",
|
"semver_impact": "MAJOR|MINOR|PATCH|none",
|
||||||
"breaking_change": false,
|
"breaking_change": false,
|
||||||
"confirmation_required": false,
|
"confirmation_required": false,
|
||||||
"details": { "type": "feat", "scope": "api", "description": "add user authentication" }
|
"details": {
|
||||||
|
"type": "feat",
|
||||||
|
"scope": "api",
|
||||||
|
"description": "add user authentication",
|
||||||
|
"body": "optional body text, or null",
|
||||||
|
"footers": ["Fixes: #123", "Refs: #456", "Co-authored-by: Bob <[email protected]>"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`details.footers` is an array of the resolved trailer lines, empty when there are none — never a
|
||||||
|
single joined string, and never omitted. Downstream agents index it.
|
||||||
|
|
||||||
For a human caller, show the same fields as a prose preview with a confirmation prompt.
|
For a human caller, show the same fields as a prose preview with a confirmation prompt.
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
name: git-history
|
name: git-history
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when investigating git history — querying logs, tracing when a change
|
Use when investigating git history — pickaxe (`-S`/`-G`) or `-L` line-range log
|
||||||
landed, bisecting the commit that broke something, or locating one to
|
queries, tracing when a change landed, bisecting what broke something, or
|
||||||
revert or backport. Not authoring or rebasing commits -> `git-commits`.
|
locating a commit to revert or backport. Not authoring or rebasing commits ->
|
||||||
Not history on a Gitea server -> `gitea-branches`.
|
`git-commits`. Not a Gitea server's history -> `gitea-branches`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
category: git
|
category: git
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ description: >
|
|||||||
remote, even when the user does not name it.
|
remote, even when the user does not name it.
|
||||||
Not local commits -> `git-commits`.
|
Not local commits -> `git-commits`.
|
||||||
Not local branches -> `git-branches`.
|
Not local branches -> `git-branches`.
|
||||||
|
Not log or bisect queries -> `git-history`.
|
||||||
Not submodule pointers -> `git-submodules`.
|
Not submodule pointers -> `git-submodules`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ conflicts, and a recovery `next_step` when applicable).
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents — gotchas, shared working rules, and the task dispatch table |
|
| `SKILL.md` | Skill instructions for agents — gotchas, shared working rules, and the task dispatch table |
|
||||||
| `references/README.md` | Describes contents of references/ |
|
| `references/README.md` | Describes contents of references/ |
|
||||||
| `references/setup-and-update.md` | Loaded when cloning a superproject, adding a submodule, or initializing, updating, or re-pinning one — includes the full `add` and `update` flag tables and the pinning workflows |
|
| `references/setup-and-update.md` | Loaded when cloning a superproject, adding a submodule, initializing, updating, or re-pinning one, or running a command across all of them — includes the full `add` and `update` flag tables, the pinning workflows, and the `foreach` shell-variable table |
|
||||||
| `references/urls-and-config.md` | Loaded when changing where a submodule points or how it is configured — `.gitmodules` vs `.git/config` anatomy, both key tables, `sync`/`set-url`/`set-branch`, local mirror overrides, relative URLs, the custom-`update` security gate, and `absorbgitdirs` |
|
| `references/urls-and-config.md` | Loaded when changing where a submodule points or how it is configured — `.gitmodules` vs `.git/config` anatomy, both key tables, `sync`/`set-url`/`set-branch`, local mirror overrides, relative URLs, the custom-`update` security gate, and `absorbgitdirs` |
|
||||||
| `references/removal.md` | Loaded when removing or deinitializing a submodule — why `deinit` is not removal, and the four-step removal sequence |
|
| `references/removal.md` | Loaded when removing or deinitializing a submodule — why `deinit` is not removal, and the four-step removal sequence |
|
||||||
| `references/sources.md` | Research sources and provenance |
|
| `references/sources.md` | Research sources and provenance |
|
||||||
@@ -34,7 +34,9 @@ them pins a state nobody else can reproduce.
|
|||||||
|
|
||||||
To run one command across every submodule: `rtk git submodule foreach --recursive '<cmd>'`. Inside
|
To run one command across every submodule: `rtk git submodule foreach --recursive '<cmd>'`. Inside
|
||||||
`<cmd>`, Git sets `$name`, `$sm_path`, `$displaypath`, `$sha1` and `$toplevel`; append `|| :` to
|
`<cmd>`, Git sets `$name`, `$sm_path`, `$displaypath`, `$sha1` and `$toplevel`; append `|| :` to
|
||||||
continue past a failure instead of aborting the traversal.
|
continue past a failure instead of aborting the traversal. `$sm_path` and `$displaypath` name the
|
||||||
|
same directory from different vantage points — if which one you want is not obvious, read the
|
||||||
|
variable table in `references/setup-and-update.md` before writing the command.
|
||||||
|
|
||||||
## Dispatch
|
## Dispatch
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ Read only the row that matches the request.
|
|||||||
|
|
||||||
| Task | Reference |
|
| Task | Reference |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Clone a superproject with submodules, or add, initialize, update or re-pin one | `references/setup-and-update.md` |
|
| Clone a superproject with submodules; add, initialize, update or re-pin one; run a command across all of them with `foreach` | `references/setup-and-update.md` |
|
||||||
| Change where a submodule points — `sync`, `set-url`, `set-branch`, a local mirror override, `absorbgitdirs`, or any `.gitmodules` / `.git/config` key | `references/urls-and-config.md` |
|
| Change where a submodule points — `sync`, `set-url`, `set-branch`, a local mirror override, `absorbgitdirs`, or any `.gitmodules` / `.git/config` key | `references/urls-and-config.md` |
|
||||||
| Remove a submodule, or `deinit` one without removing it | `references/removal.md` |
|
| Remove a submodule, or `deinit` one without removing it | `references/removal.md` |
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ One file per task branch in SKILL.md's dispatch table. Load only the one that ma
|
|||||||
## setup-and-update.md
|
## setup-and-update.md
|
||||||
|
|
||||||
Cloning a superproject that has submodules, adding a dependency as a submodule, initializing
|
Cloning a superproject that has submodules, adding a dependency as a submodule, initializing
|
||||||
without cloning, and updating or re-pinning. Carries the `add` and `update` flag tables and the
|
without cloning, updating or re-pinning, and running one command across every submodule. Carries
|
||||||
keep-pinned and move-the-pin-forward workflows.
|
the `add` and `update` flag tables, the keep-pinned and move-the-pin-forward workflows, and the
|
||||||
|
`foreach` shell-variable table (`$name`, `$sm_path`, `$displaypath`, `$sha1`, `$toplevel`).
|
||||||
|
|
||||||
## urls-and-config.md
|
## urls-and-config.md
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Both operations are destructive. Confirm with the user before executing either.
|
|||||||
## `deinit` is not removal
|
## `deinit` is not removal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule deinit <path> # --all for every submodule, -f if locally modified
|
rtk git submodule deinit <path> # --all for every submodule, -f if locally modified
|
||||||
```
|
```
|
||||||
|
|
||||||
`deinit` clears the submodule's section from `.git/config` and empties its working tree. The
|
`deinit` clears the submodule's section from `.git/config` and empties its working tree. The
|
||||||
@@ -22,11 +22,11 @@ or to reset a broken checkout, not to delete a dependency.
|
|||||||
## Full removal, in order
|
## Full removal, in order
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule deinit -f <path> # unregister from .git/config
|
rtk git submodule deinit -f <path> # unregister from .git/config
|
||||||
git rm <path> # drop the .gitmodules entry and the gitlink from the index
|
rtk git rm <path> # drop the .gitmodules entry and the gitlink from the index
|
||||||
rm -rf .git/modules/<name>/ # stale git dir: not tracked, not cleaned up by git
|
rm -rf .git/modules/<name>/ # stale git dir: not tracked, not cleaned up by git
|
||||||
git commit -m "chore: remove <name> submodule"
|
rtk git commit -m "chore: remove <name> submodule"
|
||||||
```
|
```
|
||||||
|
|
||||||
The third step is the one that gets skipped. `.git/modules/<name>/` survives `git rm`, and while it
|
The third step is the one that gets skipped. `.git/modules/<name>/` survives `rtk git rm`, and while it
|
||||||
is present Git refuses to add a submodule at the same path again.
|
is present Git refuses to add a submodule at the same path again.
|
||||||
@@ -9,16 +9,16 @@ source_keys:
|
|||||||
## Clone a superproject that already has submodules
|
## Clone a superproject that already has submodules
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recurse-submodules <url> # Git 2.13+, one step
|
rtk git clone --recurse-submodules <url> # Git 2.13+, one step
|
||||||
# or, against an existing clone
|
# or, against an existing clone
|
||||||
git submodule update --init --recursive
|
rtk git submodule update --init --recursive
|
||||||
```
|
```
|
||||||
|
|
||||||
## Add a dependency as a submodule
|
## Add a dependency as a submodule
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule add <url> <path>
|
rtk git submodule add <url> <path>
|
||||||
git commit -m "chore: add <name> as submodule"
|
rtk git commit -m "chore: add <name> as submodule"
|
||||||
```
|
```
|
||||||
|
|
||||||
`add` stages a `.gitmodules` entry and a gitlink — the commit is still required. Flags:
|
`add` stages a `.gitmodules` entry and a gitlink — the commit is still required. Flags:
|
||||||
@@ -32,14 +32,14 @@ git commit -m "chore: add <name> as submodule"
|
|||||||
|
|
||||||
## Initialize without cloning
|
## Initialize without cloning
|
||||||
|
|
||||||
`git submodule init [<path>...]` copies submodule URLs from `.gitmodules` into `.git/config` and
|
`rtk git submodule init [<path>...]` copies submodule URLs from `.gitmodules` into `.git/config` and
|
||||||
does nothing else. This is the point at which a local URL override can be edited before any fetch
|
does nothing else. This is the point at which a local URL override can be edited before any fetch
|
||||||
happens. If a local mirror override is wanted, read `references/urls-and-config.md` before running
|
happens. If a local mirror override is wanted, read `references/urls-and-config.md` before running
|
||||||
`update`. Use `update --init` to run both steps at once.
|
`update`. Use `update --init` to run both steps at once.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
`git submodule update --init --recursive` is the common case: it clones what is missing and checks
|
`rtk git submodule update --init --recursive` is the common case: it clones what is missing and checks
|
||||||
out the commit the superproject recorded, in detached HEAD.
|
out the commit the superproject recorded, in detached HEAD.
|
||||||
|
|
||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
@@ -59,16 +59,38 @@ out the commit the superproject recorded, in detached HEAD.
|
|||||||
## Keep submodules pinned to the recorded commit
|
## Keep submodules pinned to the recorded commit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --recursive # after every git pull
|
rtk git submodule update --recursive # after every rtk git pull
|
||||||
git config submodule.recurse true # or do it automatically on pull/push/checkout
|
rtk git config submodule.recurse true # or do it automatically on pull/push/checkout
|
||||||
```
|
```
|
||||||
|
|
||||||
## Move the pin forward to the tracked branch tip
|
## Move the pin forward to the tracked branch tip
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --remote --merge --recursive
|
rtk git submodule update --remote --merge --recursive
|
||||||
git commit -am "chore: update submodules to latest"
|
rtk git commit -am "chore: update submodules to latest"
|
||||||
```
|
```
|
||||||
|
|
||||||
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default
|
`--remote` requires `submodule.<name>.branch`; without it Git falls back to the remote's default
|
||||||
branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
|
branch. Commit the superproject afterwards or the new pin is lost on the next `update`.
|
||||||
|
|
||||||
|
## Run one command across every submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk git submodule foreach --recursive '<command>'
|
||||||
|
rtk git submodule foreach 'git pull origin main || :' # || : continues past a failure
|
||||||
|
```
|
||||||
|
|
||||||
|
`<command>` runs inside each submodule's own working tree, so the git calls in it are the
|
||||||
|
submodule's own — that is the one place a bare `git` is correct. Append `|| :` to keep the
|
||||||
|
traversal going instead of aborting at the first failure.
|
||||||
|
|
||||||
|
Git exports five shell variables into `<command>`. `$sm_path` and `$displaypath` name the same
|
||||||
|
directory from different vantage points and are not interchangeable:
|
||||||
|
|
||||||
|
| Variable | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `$name` | Logical submodule name (the `.gitmodules` section name, which need not match the path) |
|
||||||
|
| `$sm_path` | Path relative to the superproject root |
|
||||||
|
| `$displaypath` | Path relative to the current working directory |
|
||||||
|
| `$sha1` | Commit SHA the superproject has recorded for this submodule |
|
||||||
|
| `$toplevel` | Absolute path of the superproject's root |
|
||||||
@@ -10,7 +10,7 @@ source_keys:
|
|||||||
|
|
||||||
- **`.gitmodules`** — version-controlled, shared with collaborators. Defines each submodule's
|
- **`.gitmodules`** — version-controlled, shared with collaborators. Defines each submodule's
|
||||||
logical name, path, and canonical URL.
|
logical name, path, and canonical URL.
|
||||||
- **`.git/config`** — local only, populated by `git submodule init`. Local URL overrides live here
|
- **`.git/config`** — local only, populated by `rtk git submodule init`. Local URL overrides live here
|
||||||
and never propagate to another clone.
|
and never propagate to another clone.
|
||||||
|
|
||||||
The submodule's own `.git` directory lives at `.git/modules/<name>/` in the superproject and is
|
The submodule's own `.git` directory lives at `.git/modules/<name>/` in the superproject and is
|
||||||
@@ -38,9 +38,9 @@ linked to the submodule's working tree by a `.git` pointer file.
|
|||||||
## Rebind a URL or branch
|
## Rebind a URL or branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule sync --recursive # push .gitmodules URLs into .git/config
|
rtk git submodule sync --recursive # push .gitmodules URLs into .git/config
|
||||||
git submodule set-url <path> <url> # change the canonical URL
|
rtk git submodule set-url <path> <url> # change the canonical URL
|
||||||
git submodule set-branch -b <branch> <path> # set the branch used by update --remote
|
rtk git submodule set-branch -b <branch> <path> # set the branch used by update --remote
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `sync` after an upstream rename: existing clones keep the stale URL in `.git/config` until
|
Run `sync` after an upstream rename: existing clones keep the stale URL in `.git/config` until
|
||||||
@@ -49,9 +49,9 @@ they do.
|
|||||||
## Override a URL locally (private mirror)
|
## Override a URL locally (private mirror)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule init
|
rtk git submodule init
|
||||||
# edit .git/config: submodule.<name>.url = <mirror-url>
|
# edit .git/config: submodule.<name>.url = <mirror-url>
|
||||||
git submodule update
|
rtk git submodule update
|
||||||
```
|
```
|
||||||
|
|
||||||
Local-only, invisible to collaborators, and overwritten by the next `sync`.
|
Local-only, invisible to collaborators, and overwritten by the next `sync`.
|
||||||
@@ -65,15 +65,15 @@ everywhere else.
|
|||||||
## Custom `update` commands are security-gated
|
## Custom `update` commands are security-gated
|
||||||
|
|
||||||
A `.gitmodules` entry of `update = !some-command` is never copied into `.git/config` by
|
A `.gitmodules` entry of `update = !some-command` is never copied into `.git/config` by
|
||||||
`git submodule init`. That is deliberate: it stops a hostile clone from silently executing
|
`rtk git submodule init`. That is deliberate: it stops a hostile clone from silently executing
|
||||||
arbitrary code. Setting it locally in `.git/config` is the only way to enable it.
|
arbitrary code. Setting it locally in `.git/config` is the only way to enable it.
|
||||||
|
|
||||||
## Relocate an embedded `.git` directory
|
## Relocate an embedded `.git` directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule absorbgitdirs [<path>...]
|
rtk git submodule absorbgitdirs [<path>...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Moves a submodule's own `.git` directory into `.git/modules/<name>/` and leaves a `.git` pointer
|
Moves a submodule's own `.git` directory into `.git/modules/<name>/` and leaves a `.git` pointer
|
||||||
file behind. Needed when a nested repository was created or copied in without going through
|
file behind. Needed when a nested repository was created or copied in without going through
|
||||||
`git submodule add`.
|
`rtk git submodule add`.
|
||||||
@@ -4,7 +4,7 @@ Human-friendly interface for interactive git workflows with conversational promp
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
This skill wraps the `git-orchestrate` agent to provide an interactive, educational interface for humans performing git workflows. It handles commits, branch management, history inspection, submodules, worktrees, and remotes. The skill parses user intent, gathers session context, invokes the orchestrator, and presents results in plain language with inline help, progress updates, and explanations of what's happening. It enforces confirmation gates for destructive operations (force-push, branch deletion, rebasing with history loss, force-checkout) and provides best-practices guidance throughout. The org's non-negotiable git rules live in `references/hard-rules.md` and are loaded only when a request could conflict with one.
|
This skill wraps the `git-orchestrate` agent to provide an interactive, educational interface for humans performing git workflows. It is the router for the six local-git domain skills — `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules` and `git-worktrees` — and `SKILL.md` carries a table mapping each of them to the requests it owns, so an ambiguous request resolves to exactly one domain before anything runs. The skill parses user intent, gathers session context, invokes the orchestrator, and presents results in plain language with inline help, progress updates, and explanations of what's happening. It enforces confirmation gates for destructive operations (force-push, branch deletion, rebasing with history loss, force-checkout) and provides best-practices guidance throughout. The org's non-negotiable git rules live in `references/hard-rules.md` and are loaded only when a request could conflict with one.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Describe your git workflow: commit, create a branch, rebase, inspect history, ma
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents — the six-domain routing table, the workflow steps, and the interaction style |
|
||||||
| `README.md` | This file |
|
| `README.md` | This file |
|
||||||
| `references/hard-rules.md` | The org's non-negotiable git rules; read when a request creates, amends, or rewrites a commit, pushes, or touches hooks, config, or credentials |
|
| `references/hard-rules.md` | The org's non-negotiable git rules; read when a request creates, amends, or rewrites a commit, pushes, or touches hooks, config, or credentials |
|
||||||
| `references/README.md` | Describes the references directory contents |
|
| `references/README.md` | Describes the references directory contents |
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ name: git-workflow
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when a human's local git request is general or ambiguous — it routes to the owning
|
Use when a human's local git request is general or ambiguous — it routes to the owning
|
||||||
domain skill. Not an unambiguous commit -> `git-commits`. Not an unambiguous branch ->
|
domain skill: `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules`
|
||||||
`git-branches`. Not an agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`.
|
or `git-worktrees`. An unambiguous request goes straight to its domain skill instead. Not an
|
||||||
|
agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
category: git
|
category: git
|
||||||
@@ -25,10 +26,27 @@ metadata:
|
|||||||
mandated org wrapper, not a style preference. Submodule-specific commands run from inside the
|
mandated org wrapper, not a style preference. Submodule-specific commands run from inside the
|
||||||
submodule's own directory instead.
|
submodule's own directory instead.
|
||||||
|
|
||||||
|
## Domains
|
||||||
|
|
||||||
|
Every request resolves to exactly one of these six. An unambiguous one should have gone straight
|
||||||
|
to the domain skill; this skill exists for the ones that did not.
|
||||||
|
|
||||||
|
| The request is about | Domain |
|
||||||
|
|---|---|
|
||||||
|
| Writing, amending, squashing, or cherry-picking a commit, and its message | `git-commits` |
|
||||||
|
| Creating, switching, deleting, renaming, tracking, or merging a local branch | `git-branches` |
|
||||||
|
| When a change landed, which commit broke something, what to revert or backport | `git-history` |
|
||||||
|
| Anything touching a remote — remote config, fetch, push, pull — even unnamed | `git-remotes` |
|
||||||
|
| A nested repository pinned inside this one by a recorded commit | `git-submodules` |
|
||||||
|
| Several branches checked out at once, in separate directories, without stashing | `git-worktrees` |
|
||||||
|
|
||||||
|
`git-orchestrate` executes whatever this resolves to (step 5); the table only decides which domain
|
||||||
|
owns the request.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. **Parse intent** — extract the operation (commit, create branch, rebase, inspect history, …)
|
1. **Parse intent** — extract the operation and, from the table above, the domain that owns it,
|
||||||
and any options the user named.
|
plus any options the user named.
|
||||||
2. **Check the hard rules** — if the request creates, amends, or rewrites a commit, pushes, or
|
2. **Check the hard rules** — if the request creates, amends, or rewrites a commit, pushes, or
|
||||||
touches hooks, config, or credentials, read `references/hard-rules.md`. Raise the relevant rule
|
touches hooks, config, or credentials, read `references/hard-rules.md`. Raise the relevant rule
|
||||||
before acting, not after.
|
before acting, not after.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ metadata:
|
|||||||
- **A branch can be checked out in only one worktree at a time.** `git worktree add` on an already-checked-out branch fails; `--force` is the only override, so use it only deliberately.
|
- **A branch can be checked out in only one worktree at a time.** `git worktree add` on an already-checked-out branch fails; `--force` is the only override, so use it only deliberately.
|
||||||
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `git worktree remove`, or `git worktree prune` afterwards.
|
- **Never `rm -rf` a worktree directory.** That strands metadata in `$GIT_DIR/worktrees/`. Use `git worktree remove`, or `git worktree prune` afterwards.
|
||||||
- **Submodules break worktree support.** A worktree containing submodules cannot be moved at all, and needs `--force` to remove.
|
- **Submodules break worktree support.** A worktree containing submodules cannot be moved at all, and needs `--force` to remove.
|
||||||
- **`extensions.worktreeConfig = true` is a one-way door.** It costs compatibility with older Git and forces `core.bare`/`core.worktree` into `config.worktree`. Leave it off unless per-worktree config is needed.
|
- **`extensions.worktreeConfig = true` is a one-way door.** Without it, `git config --worktree` errors; with it, that flag writes to the worktree's own `config.worktree` file, and `core.bare`/`core.worktree` are forced there too. It also breaks older Git. Leave it off unless per-worktree config is needed.
|
||||||
|
|
||||||
## Step 1 — Dispatch
|
## Step 1 — Dispatch
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ Determine intent from the user's request, then execute the matching operation. W
|
|||||||
| "clean", "wipe cache", "rebuild from scratch" | `pre-commit clean` — read `references/clean.md` |
|
| "clean", "wipe cache", "rebuild from scratch" | `pre-commit clean` — read `references/clean.md` |
|
||||||
| "hooks aren't running", "hook never fires", "why did a hook fail", a hook failure whose cause is unclear | Diagnose — read `references/failure-patterns.md` |
|
| "hooks aren't running", "hook never fires", "why did a hook fail", a hook failure whose cause is unclear | Diagnose — read `references/failure-patterns.md` |
|
||||||
|
|
||||||
|
If the intent is ambiguous, default to `pre-commit run --all-files` — do not stop to ask, and do
|
||||||
|
not fall through to a narrower row on a guess.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Default to `pre-commit run --all-files`; never silently narrow to staged files. Run `pre-commit run` (staged only) or `pre-commit run <hook-id>` (one named hook) when the user asks for it.
|
Default to `pre-commit run --all-files`; never silently narrow to staged files. Run `pre-commit run` (staged only) or `pre-commit run <hook-id>` (one named hook) when the user asks for it.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools: Bash mcp__gitea__list_branches mcp__gitea__create_branch mcp__git
|
|||||||
|
|
||||||
- **404 often means 403.** Gitea masks permission errors as not-found; on an unexpected one, check token scope before reporting a branch or commit missing.
|
- **404 often means 403.** Gitea masks permission errors as not-found; on an unexpected one, check token scope before reporting a branch or commit missing.
|
||||||
- **Nothing auto-paginates.** `list_branches` and `list_commits` return one page; iterate `page` until the returned count is below `per_page`.
|
- **Nothing auto-paginates.** `list_branches` and `list_commits` return one page; iterate `page` until the returned count is below `per_page`.
|
||||||
- **`delete_branch` has no force-push guard.** Check `protected` from `list_branches` first and require explicit confirmation — a protected branch need not be named `main`.
|
- **`delete_branch` has no force-push guard.** Treat deleting a protected branch as a hard refusal unless the user explicitly confirms it in the conversation. Check `protected` from `list_branches` first — a protected branch need not be named `main`.
|
||||||
|
|
||||||
## Step 1 — Resolve owner and repo
|
## Step 1 — Resolve owner and repo
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ name: gitea-files
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing files or directories in a Gitea repository via the MCP server,
|
Use when reading or writing files or directories in a Gitea repository via the MCP server,
|
||||||
rather than the local filesystem — even when the user does not say "Gitea". Not commit
|
rather than the local filesystem (for a local path use Read/Write/Edit) — even when the user
|
||||||
history -> `gitea-branches`. Not pull requests -> `gitea-prs`.
|
does not say "Gitea". Not commit history -> `gitea-branches`. Not pull requests -> `gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires the Gitea MCP server configured with a token scoped to at least
|
compatibility: Requires the Gitea MCP server configured with a token scoped to at least
|
||||||
write:repository. Tested with a token holding write:issue + write:repository; write:issue
|
write:repository. Tested with a token holding write:issue + write:repository; write:issue
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## gitea-mcp-repo
|
## gitea-mcp-repo
|
||||||
|
|
||||||
**Description:** Official gitea-mcp repository (v1.3.0); operation/*.go source files documenting all 55 MCP tools, their parameters, and CLI flags.
|
**Description:** Official gitea-mcp repository (v1.3.0); operation/*.go source files documenting all 55 MCP tools, their parameters, and CLI flags. Tool parameters and SHA/concurrency behavior were cross-checked live against the deployed MCP tool schemas via `ToolSearch`, per this repo's process for resolving schema-vs-docs drift, rather than copied from the derived research doc.
|
||||||
|
|
||||||
**Source:** https://gitea.com/gitea/gitea-mcp
|
**Source:** https://gitea.com/gitea/gitea-mcp
|
||||||
|
|
||||||
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
||||||
|
|
||||||
**Contributing files:** (tool parameters and SHA/concurrency behavior cross-checked live against the deployed MCP tool schemas via ToolSearch)
|
**Contributing files:**
|
||||||
- SKILL.md (Gotchas — cross-flow parameter and encoding traps; Dispatch)
|
- SKILL.md (Gotchas — cross-flow parameter and encoding traps; Dispatch)
|
||||||
- references/reading.md (read-tool parameters, `ref`/`tree_sha` selection, tree pagination)
|
- references/reading.md (read-tool parameters, `ref`/`tree_sha` selection, tree pagination)
|
||||||
- references/writing.md (write-tool parameters, SHA/concurrency behavior, canonical call sequences, failed-write triage)
|
- references/writing.md (write-tool parameters, SHA/concurrency behavior, canonical call sequences, failed-write triage)
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
|
|
||||||
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
||||||
|
|
||||||
**Contributing files:** (none)
|
- **Contributing files:** (none)
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
name: gitea-issues
|
name: gitea-issues
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing Gitea issues — even when the user does not say "Gitea".
|
Use when reading or writing Gitea issues — "create an issue", "what issues are open",
|
||||||
Not pull requests -> `gitea-prs`.
|
"close issue #N", "comment on issue #N", "search issues for X" — even when the user does not
|
||||||
|
say "Gitea". Not pull requests -> `gitea-prs`.
|
||||||
Not label or milestone definitions -> `gitea-labels-milestones`.
|
Not label or milestone definitions -> `gitea-labels-milestones`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
||||||
@@ -25,14 +26,14 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`, never on a list item.
|
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`.
|
||||||
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
|
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
|
||||||
- **A merged PR leaves its issue open.** Gitea does not auto-close on merge the way GitHub does. Re-read the issue's state after a merge before closing it manually.
|
- **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually.
|
||||||
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.
|
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.
|
||||||
|
|
||||||
## Step 1 — Resolve owner and repo
|
## Step 1 — Resolve owner and repo
|
||||||
|
|
||||||
An orchestrating caller may pass `owner` and `repo` in already; if so, skip this. The `search` row is cross-repository and needs only a query, so it skips this too. Otherwise, before any tool call:
|
An orchestrating caller may pass `owner` and `repo` in already, and the `search` row is cross-repository and needs only a query — both skip this step. Otherwise, before any tool call:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git remote get-url origin
|
git remote get-url origin
|
||||||
@@ -58,7 +59,7 @@ One invocation takes one row. Read only the reference(s) that row names — the
|
|||||||
|
|
||||||
## Step 3 — Create
|
## Step 3 — Create
|
||||||
|
|
||||||
Only the create flow reaches this step; every other row goes straight to its reference.
|
Only the create flow reaches this step.
|
||||||
|
|
||||||
1. Take `title` and `body` from conversation context — the most recent task, bug report, or explicit statement. An empty body is an acceptable fallback, an invented one is not.
|
1. Take `title` and `body` from conversation context — the most recent task, bug report, or explicit statement. An empty body is an acceptable fallback, an invented one is not.
|
||||||
2. Run the enrichments in `references/enrichments.md`, then create with the resolved IDs per `references/issues.md`. Omitting a parameter always beats guessing its value — a wrong milestone or assignee is harder to notice than a missing one.
|
2. Run the enrichments in `references/enrichments.md`, then create with the resolved IDs per `references/issues.md`. Omitting a parameter always beats guessing its value — a wrong milestone or assignee is harder to notice than a missing one.
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
name: gitea-labels-milestones
|
name: gitea-labels-milestones
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing Gitea labels or milestones — resolve names to IDs, or infer
|
Use when reading or writing Gitea labels or milestones — "create a label", "what labels does
|
||||||
labels — even when the user does not say "Gitea".
|
this repo have", "close the milestone" — or to resolve label names to IDs, or infer a
|
||||||
|
Kind/Priority/Status label from context, even when the user does not say "Gitea".
|
||||||
Not applying them to an issue -> `gitea-issues`. Not to a PR -> `gitea-prs`.
|
Not applying them to an issue -> `gitea-issues`. Not to a PR -> `gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token scopes.
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token scopes.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ name: gitea-prs
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when listing, reading, creating, updating, merging, or reviewing Gitea pull requests — even
|
Use when listing, reading, creating, updating, merging, or reviewing Gitea pull requests — even
|
||||||
when the user does not say "Gitea". Not issues -> `gitea-issues`.
|
when the user does not say "Gitea". A number the user names may be an issue or a PR — they share
|
||||||
|
one number space — so confirm which domain applies before dispatching. Not issues ->
|
||||||
|
`gitea-issues`. Not branch or commit operations -> `gitea-branches`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
||||||
scopes. Requires git remote "origin" pointing to the Gitea instance for owner/repo resolution when
|
scopes. Requires git remote "origin" pointing to the Gitea instance for owner/repo resolution when
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
name: gitea-releases
|
name: gitea-releases
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when managing Gitea releases or the git tags underneath them — list, get,
|
Use when managing Gitea releases or the git tags underneath them — list, get, create, or
|
||||||
create, or delete either — even when the user does not say "release" or
|
delete either — even when the user does not say "release" or "Gitea": "cut a v1.2.0",
|
||||||
"Gitea". Not branches or commit history -> `gitea-branches`.
|
"publish a prerelease", "tag this commit", "what's the latest release". Not branches or
|
||||||
|
commit history -> `gitea-branches`. Not issues -> `gitea-issues`. Not pull requests ->
|
||||||
|
`gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with a token with write:repository scope, which
|
compatibility: Requires Gitea MCP server configured with a token with write:repository scope, which
|
||||||
gates every release and tag tool here. Requires git remote "origin" pointing to the Gitea instance
|
gates every release and tag tool here. Requires git remote "origin" pointing to the Gitea instance
|
||||||
@@ -55,7 +57,7 @@ If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea r
|
|||||||
|
|
||||||
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
|
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
|
||||||
|
|
||||||
Pass a caller-supplied `tag_name` through verbatim — the API accepts any string, and semver with a `v` prefix is a tooling convention rather than a Gitea constraint.
|
Pass a caller-supplied `tag_name` through verbatim — the API accepts any string; semver is convention, not constraint.
|
||||||
|
|
||||||
## Step 3 — Procedure for the scenario in hand
|
## Step 3 — Procedure for the scenario in hand
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ These four are mutually exclusive — pick the one row the request lands on.
|
|||||||
|
|
||||||
| Scenario | Procedure |
|
| Scenario | Procedure |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Create a release | Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. This surface carries no update or edit tool, so a wrong flag is repairable only by delete-and-recreate (`references/conventions.md`). A separate `create_tag` is only needed to tag a commit without wrapping it in a release. |
|
| Create a release | Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. This surface carries no update or edit tool, so a wrong flag is repairable only by delete-and-recreate (`references/conventions.md`). A separate `create_tag` is only needed to tag a commit without wrapping it in a release — whether `create_release` creates a missing tag is unconfirmed, so verify with `get_tag`. |
|
||||||
| Delete a release | Resolve the numeric `id` per the first Gotcha, confirm intent, then call `delete_release`. The tag survives. |
|
| Delete a release | Resolve the numeric `id` per the first Gotcha, confirm intent, then call `delete_release`. The tag survives. |
|
||||||
| Delete a tag along with its release | Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. |
|
| Delete a tag along with its release | Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. |
|
||||||
| List every page | Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. |
|
| List every page | Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. |
|
||||||
|
|||||||
@@ -12,11 +12,20 @@ additional tool schemas.
|
|||||||
|
|
||||||
## Release wraps a tag, not the reverse
|
## Release wraps a tag, not the reverse
|
||||||
|
|
||||||
A release is a title, body (notes), and draft/prerelease flags layered on top of an existing or
|
A release is a title, body (notes), and draft/prerelease flags layered on top of a tag. The tag is
|
||||||
newly-created tag. The tag is the git-level object (a name pointing at a commit); the release is a
|
the git-level object (a name pointing at a commit); the release is a Gitea-level metadata wrapper
|
||||||
Gitea-level metadata wrapper around it. This is why `delete_release` and `delete_tag` are separate
|
around it. This is why `delete_release` and `delete_tag` are separate calls with separate
|
||||||
calls with separate identifiers (numeric id vs. tag name) — removing the wrapper never implies
|
identifiers (numeric id vs. tag name) — removing the wrapper never implies removing the underlying
|
||||||
removing the underlying pointer, and vice versa.
|
pointer, and vice versa.
|
||||||
|
|
||||||
|
### Whether `create_release` creates a missing tag is unconfirmed
|
||||||
|
|
||||||
|
`create_release` takes both `tag_name` and `target` (a commitish), and that shape *suggests* Gitea
|
||||||
|
creates the tag at `target` when `tag_name` does not already exist. That is inferred from the API
|
||||||
|
shape, not confirmed by any source this skill was built from (`references/sources.md`) — so treat it
|
||||||
|
as an assumption, not behaviour. When the caller depends on the tag existing, verify it afterward
|
||||||
|
with `get_tag` (or `list_tags`) rather than reporting it as created. `create_tag` is the only call
|
||||||
|
confirmed to create one.
|
||||||
|
|
||||||
## Semver tag naming
|
## Semver tag naming
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ description: >
|
|||||||
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that
|
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that
|
||||||
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which
|
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which
|
||||||
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an
|
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an
|
||||||
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component.
|
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component ->
|
||||||
|
`git-workflow`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
|
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
|
||||||
domain skills, which in turn require write:issue and write:repository scopes at minimum.
|
domain skills, which in turn require write:issue and write:repository scopes at minimum.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools: Bash mcp__gitea__list_branches mcp__gitea__create_branch mcp__git
|
|||||||
|
|
||||||
- **404 often means 403.** Gitea masks permission errors as not-found; on an unexpected one, check token scope before reporting a branch or commit missing.
|
- **404 often means 403.** Gitea masks permission errors as not-found; on an unexpected one, check token scope before reporting a branch or commit missing.
|
||||||
- **Nothing auto-paginates.** `list_branches` and `list_commits` return one page; iterate `page` until the returned count is below `per_page`.
|
- **Nothing auto-paginates.** `list_branches` and `list_commits` return one page; iterate `page` until the returned count is below `per_page`.
|
||||||
- **`delete_branch` has no force-push guard.** Check `protected` from `list_branches` first and require explicit confirmation — a protected branch need not be named `main`.
|
- **`delete_branch` has no force-push guard.** Treat deleting a protected branch as a hard refusal unless the user explicitly confirms it in the conversation. Check `protected` from `list_branches` first — a protected branch need not be named `main`.
|
||||||
|
|
||||||
## Step 1 — Resolve owner and repo
|
## Step 1 — Resolve owner and repo
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ name: gitea-files
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing files or directories in a Gitea repository via the MCP server,
|
Use when reading or writing files or directories in a Gitea repository via the MCP server,
|
||||||
rather than the local filesystem — even when the user does not say "Gitea". Not commit
|
rather than the local filesystem (for a local path use Read/Write/Edit) — even when the user
|
||||||
history -> `gitea-branches`. Not pull requests -> `gitea-prs`.
|
does not say "Gitea". Not commit history -> `gitea-branches`. Not pull requests -> `gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires the Gitea MCP server configured with a token scoped to at least
|
compatibility: Requires the Gitea MCP server configured with a token scoped to at least
|
||||||
write:repository. Tested with a token holding write:issue + write:repository; write:issue
|
write:repository. Tested with a token holding write:issue + write:repository; write:issue
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## gitea-mcp-repo
|
## gitea-mcp-repo
|
||||||
|
|
||||||
**Description:** Official gitea-mcp repository (v1.3.0); operation/*.go source files documenting all 55 MCP tools, their parameters, and CLI flags.
|
**Description:** Official gitea-mcp repository (v1.3.0); operation/*.go source files documenting all 55 MCP tools, their parameters, and CLI flags. Tool parameters and SHA/concurrency behavior were cross-checked live against the deployed MCP tool schemas via `ToolSearch`, per this repo's process for resolving schema-vs-docs drift, rather than copied from the derived research doc.
|
||||||
|
|
||||||
**Source:** https://gitea.com/gitea/gitea-mcp
|
**Source:** https://gitea.com/gitea/gitea-mcp
|
||||||
|
|
||||||
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
||||||
|
|
||||||
**Contributing files:** (tool parameters and SHA/concurrency behavior cross-checked live against the deployed MCP tool schemas via ToolSearch)
|
**Contributing files:**
|
||||||
- SKILL.md (Gotchas — cross-flow parameter and encoding traps; Dispatch)
|
- SKILL.md (Gotchas — cross-flow parameter and encoding traps; Dispatch)
|
||||||
- references/reading.md (read-tool parameters, `ref`/`tree_sha` selection, tree pagination)
|
- references/reading.md (read-tool parameters, `ref`/`tree_sha` selection, tree pagination)
|
||||||
- references/writing.md (write-tool parameters, SHA/concurrency behavior, canonical call sequences, failed-write triage)
|
- references/writing.md (write-tool parameters, SHA/concurrency behavior, canonical call sequences, failed-write triage)
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
|
|
||||||
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
|
||||||
|
|
||||||
**Contributing files:** (none)
|
- **Contributing files:** (none)
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
name: gitea-issues
|
name: gitea-issues
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing Gitea issues — even when the user does not say "Gitea".
|
Use when reading or writing Gitea issues — "create an issue", "what issues are open",
|
||||||
Not pull requests -> `gitea-prs`.
|
"close issue #N", "comment on issue #N", "search issues for X" — even when the user does not
|
||||||
|
say "Gitea". Not pull requests -> `gitea-prs`.
|
||||||
Not label or milestone definitions -> `gitea-labels-milestones`.
|
Not label or milestone definitions -> `gitea-labels-milestones`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
||||||
@@ -25,14 +26,14 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`, never on a list item.
|
- **`list_issues` mixes in PRs unless you filter.** Issues and PRs share one repo number space; pass `type: "issues"` to exclude PRs (or `"pulls"` for only PRs). Nothing on a list item flags which is which — `is_pull` appears only on `issue_read method: "get"`.
|
||||||
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
|
- **Label IDs and names are not interchangeable.** `issue_write` takes numeric IDs only; `list_issues` and `search_issues` filter by name; `issue_read "get"` returns names but `"get_labels"` returns full objects with IDs. Resolve via `gitea-labels-milestones` unless the caller named exact labels.
|
||||||
- **A merged PR leaves its issue open.** Gitea does not auto-close on merge the way GitHub does. Re-read the issue's state after a merge before closing it manually.
|
- **A merge does not itself close the issue.** Gitea has no close-on-merge event, but a `Fixes #N` in the merged commits can, depending on merge style (`gitea-prs`). Re-read its state after a merge before closing it manually.
|
||||||
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.
|
- **A 404 may really be a 403.** Gitea hides permission errors as not-found — check the token's `write:issue` scope before concluding the issue does not exist.
|
||||||
|
|
||||||
## Step 1 — Resolve owner and repo
|
## Step 1 — Resolve owner and repo
|
||||||
|
|
||||||
An orchestrating caller may pass `owner` and `repo` in already; if so, skip this. The `search` row is cross-repository and needs only a query, so it skips this too. Otherwise, before any tool call:
|
An orchestrating caller may pass `owner` and `repo` in already, and the `search` row is cross-repository and needs only a query — both skip this step. Otherwise, before any tool call:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git remote get-url origin
|
git remote get-url origin
|
||||||
@@ -58,7 +59,7 @@ One invocation takes one row. Read only the reference(s) that row names — the
|
|||||||
|
|
||||||
## Step 3 — Create
|
## Step 3 — Create
|
||||||
|
|
||||||
Only the create flow reaches this step; every other row goes straight to its reference.
|
Only the create flow reaches this step.
|
||||||
|
|
||||||
1. Take `title` and `body` from conversation context — the most recent task, bug report, or explicit statement. An empty body is an acceptable fallback, an invented one is not.
|
1. Take `title` and `body` from conversation context — the most recent task, bug report, or explicit statement. An empty body is an acceptable fallback, an invented one is not.
|
||||||
2. Run the enrichments in `references/enrichments.md`, then create with the resolved IDs per `references/issues.md`. Omitting a parameter always beats guessing its value — a wrong milestone or assignee is harder to notice than a missing one.
|
2. Run the enrichments in `references/enrichments.md`, then create with the resolved IDs per `references/issues.md`. Omitting a parameter always beats guessing its value — a wrong milestone or assignee is harder to notice than a missing one.
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
name: gitea-labels-milestones
|
name: gitea-labels-milestones
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when reading or writing Gitea labels or milestones — resolve names to IDs, or infer
|
Use when reading or writing Gitea labels or milestones — "create a label", "what labels does
|
||||||
labels — even when the user does not say "Gitea".
|
this repo have", "close the milestone" — or to resolve label names to IDs, or infer a
|
||||||
|
Kind/Priority/Status label from context, even when the user does not say "Gitea".
|
||||||
Not applying them to an issue -> `gitea-issues`. Not to a PR -> `gitea-prs`.
|
Not applying them to an issue -> `gitea-issues`. Not to a PR -> `gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token scopes.
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token scopes.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ name: gitea-prs
|
|||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when listing, reading, creating, updating, merging, or reviewing Gitea pull requests — even
|
Use when listing, reading, creating, updating, merging, or reviewing Gitea pull requests — even
|
||||||
when the user does not say "Gitea". Not issues -> `gitea-issues`.
|
when the user does not say "Gitea". A number the user names may be an issue or a PR — they share
|
||||||
|
one number space — so confirm which domain applies before dispatching. Not issues ->
|
||||||
|
`gitea-issues`. Not branch or commit operations -> `gitea-branches`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
compatibility: Requires Gitea MCP server configured with write:issue and write:repository token
|
||||||
scopes. Requires git remote "origin" pointing to the Gitea instance for owner/repo resolution when
|
scopes. Requires git remote "origin" pointing to the Gitea instance for owner/repo resolution when
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
name: gitea-releases
|
name: gitea-releases
|
||||||
|
|
||||||
description: >
|
description: >
|
||||||
Use when managing Gitea releases or the git tags underneath them — list, get,
|
Use when managing Gitea releases or the git tags underneath them — list, get, create, or
|
||||||
create, or delete either — even when the user does not say "release" or
|
delete either — even when the user does not say "release" or "Gitea": "cut a v1.2.0",
|
||||||
"Gitea". Not branches or commit history -> `gitea-branches`.
|
"publish a prerelease", "tag this commit", "what's the latest release". Not branches or
|
||||||
|
commit history -> `gitea-branches`. Not issues -> `gitea-issues`. Not pull requests ->
|
||||||
|
`gitea-prs`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with a token with write:repository scope, which
|
compatibility: Requires Gitea MCP server configured with a token with write:repository scope, which
|
||||||
gates every release and tag tool here. Requires git remote "origin" pointing to the Gitea instance
|
gates every release and tag tool here. Requires git remote "origin" pointing to the Gitea instance
|
||||||
@@ -55,7 +57,7 @@ If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea r
|
|||||||
|
|
||||||
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
|
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
|
||||||
|
|
||||||
Pass a caller-supplied `tag_name` through verbatim — the API accepts any string, and semver with a `v` prefix is a tooling convention rather than a Gitea constraint.
|
Pass a caller-supplied `tag_name` through verbatim — the API accepts any string; semver is convention, not constraint.
|
||||||
|
|
||||||
## Step 3 — Procedure for the scenario in hand
|
## Step 3 — Procedure for the scenario in hand
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ These four are mutually exclusive — pick the one row the request lands on.
|
|||||||
|
|
||||||
| Scenario | Procedure |
|
| Scenario | Procedure |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Create a release | Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. This surface carries no update or edit tool, so a wrong flag is repairable only by delete-and-recreate (`references/conventions.md`). A separate `create_tag` is only needed to tag a commit without wrapping it in a release. |
|
| Create a release | Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. This surface carries no update or edit tool, so a wrong flag is repairable only by delete-and-recreate (`references/conventions.md`). A separate `create_tag` is only needed to tag a commit without wrapping it in a release — whether `create_release` creates a missing tag is unconfirmed, so verify with `get_tag`. |
|
||||||
| Delete a release | Resolve the numeric `id` per the first Gotcha, confirm intent, then call `delete_release`. The tag survives. |
|
| Delete a release | Resolve the numeric `id` per the first Gotcha, confirm intent, then call `delete_release`. The tag survives. |
|
||||||
| Delete a tag along with its release | Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. |
|
| Delete a tag along with its release | Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. |
|
||||||
| List every page | Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. |
|
| List every page | Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. |
|
||||||
|
|||||||
@@ -12,11 +12,20 @@ additional tool schemas.
|
|||||||
|
|
||||||
## Release wraps a tag, not the reverse
|
## Release wraps a tag, not the reverse
|
||||||
|
|
||||||
A release is a title, body (notes), and draft/prerelease flags layered on top of an existing or
|
A release is a title, body (notes), and draft/prerelease flags layered on top of a tag. The tag is
|
||||||
newly-created tag. The tag is the git-level object (a name pointing at a commit); the release is a
|
the git-level object (a name pointing at a commit); the release is a Gitea-level metadata wrapper
|
||||||
Gitea-level metadata wrapper around it. This is why `delete_release` and `delete_tag` are separate
|
around it. This is why `delete_release` and `delete_tag` are separate calls with separate
|
||||||
calls with separate identifiers (numeric id vs. tag name) — removing the wrapper never implies
|
identifiers (numeric id vs. tag name) — removing the wrapper never implies removing the underlying
|
||||||
removing the underlying pointer, and vice versa.
|
pointer, and vice versa.
|
||||||
|
|
||||||
|
### Whether `create_release` creates a missing tag is unconfirmed
|
||||||
|
|
||||||
|
`create_release` takes both `tag_name` and `target` (a commitish), and that shape *suggests* Gitea
|
||||||
|
creates the tag at `target` when `tag_name` does not already exist. That is inferred from the API
|
||||||
|
shape, not confirmed by any source this skill was built from (`references/sources.md`) — so treat it
|
||||||
|
as an assumption, not behaviour. When the caller depends on the tag existing, verify it afterward
|
||||||
|
with `get_tag` (or `list_tags`) rather than reporting it as created. `create_tag` is the only call
|
||||||
|
confirmed to create one.
|
||||||
|
|
||||||
## Semver tag naming
|
## Semver tag naming
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ description: >
|
|||||||
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that
|
Use when a Gitea request is general or ambiguous — a no-args repo check-in, a bare number that
|
||||||
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which
|
could be an issue or a PR, or a capability whose owning skill is unclear. Resolves which
|
||||||
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an
|
domain skill applies. Not an unambiguous issue request -> `gitea-issues`. Not an
|
||||||
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component.
|
unambiguous PR request -> `gitea-prs`. Not local git work with no Gitea component ->
|
||||||
|
`git-workflow`.
|
||||||
|
|
||||||
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
|
compatibility: Requires Gitea MCP server configured with a token; delegates all calls to the six
|
||||||
domain skills, which in turn require write:issue and write:repository scopes at minimum.
|
domain skills, which in turn require write:issue and write:repository scopes at minimum.
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ Checks performed:
|
|||||||
0 source_keys present in agent pair but sources.md absent
|
0 source_keys present in agent pair but sources.md absent
|
||||||
1 FILL IN: placeholders in sources.md
|
1 FILL IN: placeholders in sources.md
|
||||||
2 source_keys in agent files → slug exists in sources.md
|
2 source_keys in agent files → slug exists in sources.md
|
||||||
3 Contributing files listed in sources.md exist on disk (plugin-root relative)
|
3 Contributing files listed in sources.md exist on disk (plugin-root
|
||||||
|
relative). An explicit '(none)' skips silently; a Contributing files block
|
||||||
|
this parser cannot read is reported as an INFO saying checks 3 and 4 did
|
||||||
|
not run, never skipped silently.
|
||||||
4 Contributing files back-reference the parent slug in their source_keys
|
4 Contributing files back-reference the parent slug in their source_keys
|
||||||
5 Research doc field present and not placeholder
|
5 Research doc field present and not placeholder
|
||||||
EOF
|
EOF
|
||||||
@@ -244,14 +247,31 @@ has_fail = False
|
|||||||
def emit_fail(desc, fpath, why, fix):
|
def emit_fail(desc, fpath, why, fix):
|
||||||
global has_fail
|
global has_fail
|
||||||
has_fail = True
|
has_fail = True
|
||||||
findings.append(("FAIL", desc, fpath, why, fix))
|
findings.append(("FAIL", desc, fpath, why, fix, None))
|
||||||
|
|
||||||
|
# INFO does not set has_fail and does not change the exit code. It is for a
|
||||||
|
# check that could not RUN — an unverified entry, not a broken one — and it
|
||||||
|
# exists so that "did not run" is never spelled the same way as "passed".
|
||||||
|
def emit_info(desc, fpath, note):
|
||||||
|
findings.append(("INFO", desc, fpath, None, None, note))
|
||||||
|
|
||||||
def print_findings():
|
def print_findings():
|
||||||
for kind, desc, fpath, why, fix in findings:
|
for entry in findings:
|
||||||
|
kind = entry[0]
|
||||||
|
desc = entry[1]
|
||||||
|
fpath = entry[2]
|
||||||
|
why = entry[3]
|
||||||
|
fix = entry[4]
|
||||||
|
note = entry[5]
|
||||||
|
if kind == "FAIL":
|
||||||
print(f"FAIL {desc} — {fpath}")
|
print(f"FAIL {desc} — {fpath}")
|
||||||
print(f" Why: {why}")
|
print(f" Why: {why}")
|
||||||
print(f" Fix: {fix}")
|
print(f" Fix: {fix}")
|
||||||
print()
|
print()
|
||||||
|
else:
|
||||||
|
print(f"INFO {desc} — {fpath}")
|
||||||
|
print(f" Note: {note}")
|
||||||
|
print()
|
||||||
|
|
||||||
# --- Collect source_keys from agent pair ---
|
# --- Collect source_keys from agent pair ---
|
||||||
def get_source_keys_from_file(fpath):
|
def get_source_keys_from_file(fpath):
|
||||||
@@ -321,9 +341,24 @@ for fpath, keys in [(agent_file, given_keys)]:
|
|||||||
|
|
||||||
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
|
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
|
||||||
for slug in parse_h2_slugs(sources_content):
|
for slug in parse_h2_slugs(sources_content):
|
||||||
# Check 3: Contributing files exist (paths relative to plugin root)
|
# Checks 3 and 4: Contributing files exist (paths relative to plugin root),
|
||||||
|
# and back-reference the slug. `[]` and None are NOT the same answer here.
|
||||||
|
# `[]` is the author writing "(none)" — there is nothing to check and the
|
||||||
|
# skip is correct. None is a Contributing-files block this parser cannot
|
||||||
|
# read, and skipping THAT silently disables both checks on the one entry
|
||||||
|
# least likely to be right, which is the failure mode
|
||||||
|
# parse_contributing_files' own docstring warns about. Say so out loud.
|
||||||
cf_files = parse_contributing_files(sources_content, slug)
|
cf_files = parse_contributing_files(sources_content, slug)
|
||||||
if cf_files:
|
if cf_files is None:
|
||||||
|
emit_info(
|
||||||
|
f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed",
|
||||||
|
f"sources.md (## {slug})",
|
||||||
|
f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. "
|
||||||
|
f"Checks 3 and 4 did not run for this slug, so nothing verified that its contributing files exist or name it back. "
|
||||||
|
f"Write the value as '- **Contributing files:** <comma-separated paths>', or as a '**Contributing files:**' heading followed by '- ' bullets — "
|
||||||
|
f"or record '(none)' if this source contributed no files."
|
||||||
|
)
|
||||||
|
elif cf_files:
|
||||||
for cf_rel in cf_files:
|
for cf_rel in cf_files:
|
||||||
cf_abs = os.path.join(plugin_root, cf_rel)
|
cf_abs = os.path.join(plugin_root, cf_rel)
|
||||||
if not os.path.isfile(cf_abs):
|
if not os.path.isfile(cf_abs):
|
||||||
|
|||||||
@@ -264,6 +264,16 @@ def _collect_package(pkg_dir, names):
|
|||||||
names.add(os.path.basename(path.rstrip('/')).lower())
|
names.add(os.path.basename(path.rstrip('/')).lower())
|
||||||
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
||||||
for path in glob.glob(os.path.join(safe_dir, sub)):
|
for path in glob.glob(os.path.join(safe_dir, sub)):
|
||||||
|
# The same rule one directory over, which until now had no
|
||||||
|
# counterpart here at all: the skills branch above tests for a
|
||||||
|
# SKILL.md, the agents branch took every glob hit on trust. A
|
||||||
|
# DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not
|
||||||
|
# tell the two apart, so a leftover of that shape resolved a routing
|
||||||
|
# target on the machine holding it and dangled everywhere else —
|
||||||
|
# identical install-dependence, arriving through the one door
|
||||||
|
# nobody guarded.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
if base.endswith('.agent.md'):
|
if base.endswith('.agent.md'):
|
||||||
base = base[:-len('.agent.md')]
|
base = base[:-len('.agent.md')]
|
||||||
@@ -542,6 +552,34 @@ def known_targets(start_dir):
|
|||||||
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
||||||
# has two ways to say so.
|
# has two ways to say so.
|
||||||
#
|
#
|
||||||
|
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is
|
||||||
|
# a repair of the promise above rather than a widening of it. Until the sweeps
|
||||||
|
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use
|
||||||
|
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
|
||||||
|
# between. Every one of these therefore exited 0 in total silence — no ERROR, no
|
||||||
|
# SUGGESTION, not even the target's name:
|
||||||
|
# Do not use for Y — /no-such-skill instead.
|
||||||
|
# Do not use for Y; /no-such-skill handles that.
|
||||||
|
# Do not use for Y (/no-such-skill covers it).
|
||||||
|
# Do not use for Y — that is /no-such-skill's job.
|
||||||
|
# Do not use for Y — defer to /no-such-skill.
|
||||||
|
# Do not use for Y — /no-such-skill.
|
||||||
|
# Do not use for Y; -> no-such-skill covers it.
|
||||||
|
# The target was never EXTRACTED, so the notation-first rule in _add() had
|
||||||
|
# nothing to apply itself to and the "always blocks" promise was false for the
|
||||||
|
# ordinary way an author writes the thing. The SUGGESTION tier made it worse
|
||||||
|
# than a gap: its printed remedy tells the author to "write it as `/name` or
|
||||||
|
# `-> name` and it will be checked properly", and taking that advice turned a
|
||||||
|
# visible SUGGESTION into silence — the gate teaching the one edit that blinds
|
||||||
|
# it.
|
||||||
|
#
|
||||||
|
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate
|
||||||
|
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a
|
||||||
|
# PATH: a following `/`, or a `.` followed by a non-space, means
|
||||||
|
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's
|
||||||
|
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still
|
||||||
|
# counts.
|
||||||
|
#
|
||||||
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
||||||
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
||||||
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
||||||
@@ -565,6 +603,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
|
|||||||
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
||||||
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
||||||
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
||||||
|
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their
|
||||||
|
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name`
|
||||||
|
# alternative: that one only ever runs behind a route verb or an arrow, and the
|
||||||
|
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe.
|
||||||
|
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
|
||||||
|
# what made `Do not use for Y; -> no-such-skill covers it.` invisible:
|
||||||
|
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
|
||||||
|
# check. Dropping that prefix costs the one false positive the bare-arrow bullet
|
||||||
|
# above names — a process chain ending in a hyphenated word, `Instead, reproduce
|
||||||
|
# -> minimise -> regression-test.` — and costs it only in a sentence that already
|
||||||
|
# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same
|
||||||
|
# chain written `Do not use for X — reproduce -> regression-test.` was already a
|
||||||
|
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
|
||||||
|
# arrow, not whether prose can. An author who means the chain and not a route
|
||||||
|
# writes it in its own sentence, where neither pattern looks.
|
||||||
|
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||||
|
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
|
||||||
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
||||||
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
||||||
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
||||||
@@ -740,6 +795,16 @@ def _extract_sentence(sentence):
|
|||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
strict=True, arrow=True)
|
strict=True, arrow=True)
|
||||||
if boundary:
|
if boundary:
|
||||||
|
# Route notation wherever it sits in the clause, not only where a route
|
||||||
|
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE
|
||||||
|
# NOTATION note in the header for the seven phrasings this recovers and
|
||||||
|
# for why silence was the failure mode. Neither sweep takes the follower
|
||||||
|
# test: _add() reads the notation first and both forms reach it marked.
|
||||||
|
for match in NOTATION_SLASH.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
|
for match in NOTATION_ARROW.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
|
strict=True, arrow=True)
|
||||||
for match in BACKTICK.finditer(sentence):
|
for match in BACKTICK.finditer(sentence):
|
||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
return out
|
return out
|
||||||
@@ -1110,7 +1175,15 @@ def missing_reference_pointers(body, skill_dir):
|
|||||||
end = masked.find('\n', match.end())
|
end = masked.find('\n', match.end())
|
||||||
if end < 0:
|
if end < 0:
|
||||||
end = len(masked)
|
end = len(masked)
|
||||||
if REFERENCE_PAST.search(masked[start:end]):
|
# The pointer's OWN SPAN is excised before the sweep. Run over the
|
||||||
|
# whole line, the past-tense test matched the very path it was judging,
|
||||||
|
# so a file exempted itself by its NAME: `references/deprecated-api.md`,
|
||||||
|
# `references/removed-flags.md` and `references/gone.md` produced no
|
||||||
|
# ERROR at all, while `references/missing.md` — an identical break —
|
||||||
|
# errored. The exemption is about what the SENTENCE says about the
|
||||||
|
# pointer, never about what the pointer is called.
|
||||||
|
line = masked[start:match.start()] + masked[match.end():end]
|
||||||
|
if REFERENCE_PAST.search(line):
|
||||||
continue
|
continue
|
||||||
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -433,3 +433,140 @@ EOF
|
|||||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Checks 3 and 4: None ("could not parse") is NOT [] ("explicitly (none)")
|
||||||
|
#
|
||||||
|
# parse_contributing_files returns three distinguishable answers and checks 3
|
||||||
|
# and 4 have to honour all three. `[]` is the author writing "(none)" — the
|
||||||
|
# skip is correct and silent. None is a Contributing files block the parser
|
||||||
|
# cannot read, and skipping THAT silently disables both checks on the one entry
|
||||||
|
# least likely to be right, which is the failure mode the parser's own
|
||||||
|
# docstring warns about. The assertions below are therefore about the INFO
|
||||||
|
# appearing; a silent exit 0 is exactly the bug.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@test "INFO: an unparsable Contributing files block names the slug instead of skipping checks 3 and 4 silently" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
cat > "$root/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
**Contributing files:**
|
||||||
|
* .apm/agents/ghost.agent.md (asterisk bullets are not the bullet form)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "INFO: an entry with no Contributing files field at all is reported, not skipped silently" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
cat > "$root/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "checks 3 and 4 skipped silently: an explicit '(none)' emits no INFO" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
make_sources_md "$root" "my-source" "(none — not used directly)"
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_success
|
||||||
|
assert_output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "checks 3 and 4 still run: a parseable Contributing files list is not diverted to the INFO" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
make_sources_md "$root" "my-source" ".apm/agents/nonexistent.agent.md"
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial "FAIL"
|
||||||
|
assert_output --partial "Contributing file '.apm/agents/nonexistent.agent.md' does not exist"
|
||||||
|
refute_output --partial "could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# The INFO tier itself: kind-aware printing and a kind-aware exit code
|
||||||
|
#
|
||||||
|
# INFO is new here — before it, findings was a 5-tuple and print_findings
|
||||||
|
# stamped every entry FAIL. The two cases below pin the tier rather than any
|
||||||
|
# one check: an INFO must print under the INFO prefix and leave the exit code
|
||||||
|
# at 0, and a real FAIL must keep printing under the FAIL prefix and still exit
|
||||||
|
# non-zero even when an INFO is sitting in the same findings list.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@test "INFO tier: an INFO alone prints as INFO with a Note and does not set a failing exit code" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
cat > "$root/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO Contributing-file checks skipped for 'my-source'"
|
||||||
|
assert_output --partial "Note:"
|
||||||
|
refute_output --partial "FAIL"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "FAIL tier: a genuine FAIL alongside an INFO still prints as FAIL and exits non-zero" {
|
||||||
|
local root="$TMPDIR/package"
|
||||||
|
make_package "$root"
|
||||||
|
make_agent_with_source_keys "$root"
|
||||||
|
cat > "$root/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Contributing files:** .apm/agents/nonexistent.agent.md
|
||||||
|
- **Research doc:** (none)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
|
||||||
|
## ghost-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/ghost-source
|
||||||
|
- **Description:** Another test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial "FAIL Contributing file '.apm/agents/nonexistent.agent.md' does not exist"
|
||||||
|
assert_output --partial "Why:"
|
||||||
|
assert_output --partial "INFO Contributing-file checks skipped for 'ghost-source'"
|
||||||
|
assert_output --partial "Note:"
|
||||||
|
}
|
||||||
@@ -21,7 +21,10 @@ Checks performed:
|
|||||||
3 source_keys in references/*.md → slug exists in sources.md (INFO if no
|
3 source_keys in references/*.md → slug exists in sources.md (INFO if no
|
||||||
source_keys; an explicit 'source_keys: []' declares the file house-authored
|
source_keys; an explicit 'source_keys: []' declares the file house-authored
|
||||||
and passes silently)
|
and passes silently)
|
||||||
4 Contributing files listed in sources.md exist on disk
|
4 Contributing files listed in sources.md exist on disk. An explicit
|
||||||
|
'(none)' skips silently; a Contributing files block this parser cannot
|
||||||
|
read is reported as an INFO saying checks 4 and 5 did not run, never
|
||||||
|
skipped silently.
|
||||||
5 Contributing files back-reference the parent slug in their source_keys
|
5 Contributing files back-reference the parent slug in their source_keys
|
||||||
6 Research doc field present and not placeholder
|
6 Research doc field present and not placeholder
|
||||||
7 Slug in sources.md present in upstream research doc (INFO only). A section
|
7 Slug in sources.md present in upstream research doc (INFO only). A section
|
||||||
@@ -104,7 +107,13 @@ def parse_source_keys(fm):
|
|||||||
# claims that then have to be maintained in sources.md as well. A bare
|
# claims that then have to be maintained in sources.md as well. A bare
|
||||||
# `source_keys:` with nothing after it is NOT accepted here — that reads as a
|
# `source_keys:` with nothing after it is NOT accepted here — that reads as a
|
||||||
# truncated or half-written entry, not a decision.
|
# truncated or half-written entry, not a decision.
|
||||||
EMPTY_SOURCE_KEYS_RE = re.compile(r'^\s*source_keys:\s*\[\s*\]\s*$')
|
#
|
||||||
|
# The indent is pinned to the two positions parse_source_keys() actually reads
|
||||||
|
# — column 0, or two spaces under `metadata:`. A permissive `^\s*` matched a
|
||||||
|
# `source_keys: []` nested at ANY depth under an unrelated key, which
|
||||||
|
# parse_source_keys() never reads, so a stray nested key silenced the check-3
|
||||||
|
# INFO for a file that had declared nothing.
|
||||||
|
EMPTY_SOURCE_KEYS_RE = re.compile(r'^(?: )?source_keys:\s*\[\s*\]\s*$')
|
||||||
|
|
||||||
def declares_empty_source_keys(fm):
|
def declares_empty_source_keys(fm):
|
||||||
"""True when frontmatter carries an explicit, empty `source_keys: []`."""
|
"""True when frontmatter carries an explicit, empty `source_keys: []`."""
|
||||||
@@ -436,9 +445,25 @@ repo_root = find_repo_root(skill_dir)
|
|||||||
research_docs_seen = {} # abs_path → set of slugs in sources.md that reference it
|
research_docs_seen = {} # abs_path → set of slugs in sources.md that reference it
|
||||||
|
|
||||||
for slug in parse_h2_slugs(sources_content):
|
for slug in parse_h2_slugs(sources_content):
|
||||||
# Check 4: Contributing files exist
|
# Checks 4 and 5: Contributing files exist, and back-reference the slug.
|
||||||
|
# `[]` and None are NOT the same answer here. `[]` is the author writing
|
||||||
|
# "(none)" — there is nothing to check and the skip is correct. None is a
|
||||||
|
# Contributing-files block this parser cannot read, and skipping THAT
|
||||||
|
# silently disables both checks on the one entry least likely to be right,
|
||||||
|
# which is the failure mode parse_contributing_files' own docstring warns
|
||||||
|
# about. Say so out loud instead, the same way an unresolvable Research doc
|
||||||
|
# value does.
|
||||||
cf_files = parse_contributing_files(sources_content, slug)
|
cf_files = parse_contributing_files(sources_content, slug)
|
||||||
if cf_files:
|
if cf_files is None:
|
||||||
|
emit_info(
|
||||||
|
f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed",
|
||||||
|
f"references/sources.md (## {slug})",
|
||||||
|
f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. "
|
||||||
|
f"Checks 4 and 5 did not run for this slug, so nothing verified that its contributing files exist or name it back. "
|
||||||
|
f"Write the value as '- **Contributing files:** <comma-separated paths>', or as a '**Contributing files:**' heading followed by '- ' bullets — "
|
||||||
|
f"or record '(none)' if this source contributed no files."
|
||||||
|
)
|
||||||
|
elif cf_files:
|
||||||
for cf_rel in cf_files:
|
for cf_rel in cf_files:
|
||||||
cf_abs = os.path.join(skill_dir, cf_rel)
|
cf_abs = os.path.join(skill_dir, cf_rel)
|
||||||
if not os.path.isfile(cf_abs):
|
if not os.path.isfile(cf_abs):
|
||||||
|
|||||||
@@ -190,6 +190,16 @@ def _collect_package(pkg_dir, names):
|
|||||||
names.add(os.path.basename(path.rstrip('/')).lower())
|
names.add(os.path.basename(path.rstrip('/')).lower())
|
||||||
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
||||||
for path in glob.glob(os.path.join(safe_dir, sub)):
|
for path in glob.glob(os.path.join(safe_dir, sub)):
|
||||||
|
# The same rule one directory over, which until now had no
|
||||||
|
# counterpart here at all: the skills branch above tests for a
|
||||||
|
# SKILL.md, the agents branch took every glob hit on trust. A
|
||||||
|
# DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not
|
||||||
|
# tell the two apart, so a leftover of that shape resolved a routing
|
||||||
|
# target on the machine holding it and dangled everywhere else —
|
||||||
|
# identical install-dependence, arriving through the one door
|
||||||
|
# nobody guarded.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
if base.endswith('.agent.md'):
|
if base.endswith('.agent.md'):
|
||||||
base = base[:-len('.agent.md')]
|
base = base[:-len('.agent.md')]
|
||||||
@@ -468,6 +478,34 @@ def known_targets(start_dir):
|
|||||||
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
||||||
# has two ways to say so.
|
# has two ways to say so.
|
||||||
#
|
#
|
||||||
|
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is
|
||||||
|
# a repair of the promise above rather than a widening of it. Until the sweeps
|
||||||
|
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use
|
||||||
|
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
|
||||||
|
# between. Every one of these therefore exited 0 in total silence — no ERROR, no
|
||||||
|
# SUGGESTION, not even the target's name:
|
||||||
|
# Do not use for Y — /no-such-skill instead.
|
||||||
|
# Do not use for Y; /no-such-skill handles that.
|
||||||
|
# Do not use for Y (/no-such-skill covers it).
|
||||||
|
# Do not use for Y — that is /no-such-skill's job.
|
||||||
|
# Do not use for Y — defer to /no-such-skill.
|
||||||
|
# Do not use for Y — /no-such-skill.
|
||||||
|
# Do not use for Y; -> no-such-skill covers it.
|
||||||
|
# The target was never EXTRACTED, so the notation-first rule in _add() had
|
||||||
|
# nothing to apply itself to and the "always blocks" promise was false for the
|
||||||
|
# ordinary way an author writes the thing. The SUGGESTION tier made it worse
|
||||||
|
# than a gap: its printed remedy tells the author to "write it as `/name` or
|
||||||
|
# `-> name` and it will be checked properly", and taking that advice turned a
|
||||||
|
# visible SUGGESTION into silence — the gate teaching the one edit that blinds
|
||||||
|
# it.
|
||||||
|
#
|
||||||
|
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate
|
||||||
|
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a
|
||||||
|
# PATH: a following `/`, or a `.` followed by a non-space, means
|
||||||
|
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's
|
||||||
|
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still
|
||||||
|
# counts.
|
||||||
|
#
|
||||||
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
||||||
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
||||||
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
||||||
@@ -491,6 +529,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
|
|||||||
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
||||||
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
||||||
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
||||||
|
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their
|
||||||
|
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name`
|
||||||
|
# alternative: that one only ever runs behind a route verb or an arrow, and the
|
||||||
|
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe.
|
||||||
|
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
|
||||||
|
# what made `Do not use for Y; -> no-such-skill covers it.` invisible:
|
||||||
|
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
|
||||||
|
# check. Dropping that prefix costs the one false positive the bare-arrow bullet
|
||||||
|
# above names — a process chain ending in a hyphenated word, `Instead, reproduce
|
||||||
|
# -> minimise -> regression-test.` — and costs it only in a sentence that already
|
||||||
|
# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same
|
||||||
|
# chain written `Do not use for X — reproduce -> regression-test.` was already a
|
||||||
|
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
|
||||||
|
# arrow, not whether prose can. An author who means the chain and not a route
|
||||||
|
# writes it in its own sentence, where neither pattern looks.
|
||||||
|
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||||
|
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
|
||||||
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
||||||
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
||||||
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
||||||
@@ -666,6 +721,16 @@ def _extract_sentence(sentence):
|
|||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
strict=True, arrow=True)
|
strict=True, arrow=True)
|
||||||
if boundary:
|
if boundary:
|
||||||
|
# Route notation wherever it sits in the clause, not only where a route
|
||||||
|
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE
|
||||||
|
# NOTATION note in the header for the seven phrasings this recovers and
|
||||||
|
# for why silence was the failure mode. Neither sweep takes the follower
|
||||||
|
# test: _add() reads the notation first and both forms reach it marked.
|
||||||
|
for match in NOTATION_SLASH.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
|
for match in NOTATION_ARROW.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
|
strict=True, arrow=True)
|
||||||
for match in BACKTICK.finditer(sentence):
|
for match in BACKTICK.finditer(sentence):
|
||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
return out
|
return out
|
||||||
@@ -1036,7 +1101,15 @@ def missing_reference_pointers(body, skill_dir):
|
|||||||
end = masked.find('\n', match.end())
|
end = masked.find('\n', match.end())
|
||||||
if end < 0:
|
if end < 0:
|
||||||
end = len(masked)
|
end = len(masked)
|
||||||
if REFERENCE_PAST.search(masked[start:end]):
|
# The pointer's OWN SPAN is excised before the sweep. Run over the
|
||||||
|
# whole line, the past-tense test matched the very path it was judging,
|
||||||
|
# so a file exempted itself by its NAME: `references/deprecated-api.md`,
|
||||||
|
# `references/removed-flags.md` and `references/gone.md` produced no
|
||||||
|
# ERROR at all, while `references/missing.md` — an identical break —
|
||||||
|
# errored. The exemption is about what the SENTENCE says about the
|
||||||
|
# pointer, never about what the pointer is called.
|
||||||
|
line = masked[start:match.start()] + masked[match.end():end]
|
||||||
|
if REFERENCE_PAST.search(line):
|
||||||
continue
|
continue
|
||||||
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -985,3 +985,132 @@ EOF
|
|||||||
assert_output --partial "INFO"
|
assert_output --partial "INFO"
|
||||||
assert_output --partial "Upstream checks skipped for 'my-source' — no repo root above the skill directory"
|
assert_output --partial "Upstream checks skipped for 'my-source' — no repo root above the skill directory"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cycle 16 — Checks 4 and 5: None ("could not parse") is NOT [] ("explicitly
|
||||||
|
# (none)"), on the sources.md side this time
|
||||||
|
#
|
||||||
|
# Cycle 13 pinned the distinction for check 8, which reads the parser's output
|
||||||
|
# against a RESEARCH doc. Checks 4 and 5 read it against the skill's own
|
||||||
|
# sources.md and honoured neither half: a truthiness test collapsed None into
|
||||||
|
# [], so an unreadable Contributing files block disabled both checks and the
|
||||||
|
# script still exited 0 with no output — the failure mode
|
||||||
|
# parse_contributing_files' docstring names in as many words. The assertions
|
||||||
|
# below are therefore about the INFO appearing; a silent exit 0 is exactly the
|
||||||
|
# bug.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@test "INFO: an unparsable Contributing files block names the slug instead of skipping checks 4 and 5 silently" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
mkdir -p "$skill/references"
|
||||||
|
cat > "$skill/references/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
**Contributing files:**
|
||||||
|
* references/ghost.md (asterisk bullets are not the bullet form)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "INFO: an entry with no Contributing files field at all is reported, not skipped silently" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
mkdir -p "$skill/references"
|
||||||
|
cat > "$skill/references/sources.md" <<EOF
|
||||||
|
# Sources
|
||||||
|
|
||||||
|
## my-source
|
||||||
|
|
||||||
|
- **URL:** https://example.com/my-source
|
||||||
|
- **Description:** A test source.
|
||||||
|
- **Research doc:** (none)
|
||||||
|
- **Status:** \`extracted\`
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "checks 4 and 5 skipped silently: an explicit '(none)' emits no INFO" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
make_sources_md "$skill" "my-source" "(none — not used directly)"
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_success
|
||||||
|
assert_output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "checks 4 and 5 still run: a parseable Contributing files list is not diverted to the INFO" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
make_sources_md "$skill" "my-source" "references/nonexistent.md"
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial "FAIL"
|
||||||
|
assert_output --partial "Contributing file 'references/nonexistent.md' does not exist"
|
||||||
|
refute_output --partial "could not be parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cycle 17 — Check 3 (#111): 'source_keys: []' only declares anything in the
|
||||||
|
# two positions parse_source_keys() actually reads
|
||||||
|
#
|
||||||
|
# The declaration and the parse have to agree on WHERE the key lives. They did
|
||||||
|
# not: the declaration regex accepted any indent, so a `source_keys: []` buried
|
||||||
|
# under an unrelated key — a position parse_source_keys() never reads — passed
|
||||||
|
# as a house-authored declaration and silenced the INFO for a file that had
|
||||||
|
# declared nothing.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@test "INFO: 'source_keys: []' nested under an unrelated key is not a declaration" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
make_sources_md "$skill"
|
||||||
|
cat > "$skill/references/extra.md" <<EOF
|
||||||
|
---
|
||||||
|
title: Extra Reference
|
||||||
|
unrelated:
|
||||||
|
nested:
|
||||||
|
source_keys: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extra Reference
|
||||||
|
|
||||||
|
The empty list is nested where parse_source_keys never looks.
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "No source_keys frontmatter"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "INFO: a four-space-indented 'source_keys: []' is not a declaration either" {
|
||||||
|
local skill="$TMPDIR/my-skill"
|
||||||
|
make_skill_with_source_keys "$skill"
|
||||||
|
make_sources_md "$skill"
|
||||||
|
cat > "$skill/references/extra.md" <<EOF
|
||||||
|
---
|
||||||
|
metadata:
|
||||||
|
source_keys: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extra Reference
|
||||||
|
|
||||||
|
Two spaces is the position parse_source_keys reads under metadata:, not four.
|
||||||
|
EOF
|
||||||
|
run bash "$SCRIPT" "$skill"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "INFO"
|
||||||
|
assert_output --partial "No source_keys frontmatter"
|
||||||
|
}
|
||||||
@@ -22,7 +22,10 @@ Checks performed:
|
|||||||
0 source_keys present in agent pair but sources.md absent
|
0 source_keys present in agent pair but sources.md absent
|
||||||
1 FILL IN: placeholders in sources.md
|
1 FILL IN: placeholders in sources.md
|
||||||
2 source_keys in agent files → slug exists in sources.md
|
2 source_keys in agent files → slug exists in sources.md
|
||||||
3 Contributing files listed in sources.md exist on disk (plugin-root relative)
|
3 Contributing files listed in sources.md exist on disk (plugin-root
|
||||||
|
relative). An explicit '(none)' skips silently; a Contributing files block
|
||||||
|
this parser cannot read is reported as an INFO saying checks 3 and 4 did
|
||||||
|
not run, never skipped silently.
|
||||||
4 Contributing files back-reference the parent slug in their source_keys
|
4 Contributing files back-reference the parent slug in their source_keys
|
||||||
5 Research doc field present and not placeholder
|
5 Research doc field present and not placeholder
|
||||||
EOF
|
EOF
|
||||||
@@ -244,14 +247,31 @@ has_fail = False
|
|||||||
def emit_fail(desc, fpath, why, fix):
|
def emit_fail(desc, fpath, why, fix):
|
||||||
global has_fail
|
global has_fail
|
||||||
has_fail = True
|
has_fail = True
|
||||||
findings.append(("FAIL", desc, fpath, why, fix))
|
findings.append(("FAIL", desc, fpath, why, fix, None))
|
||||||
|
|
||||||
|
# INFO does not set has_fail and does not change the exit code. It is for a
|
||||||
|
# check that could not RUN — an unverified entry, not a broken one — and it
|
||||||
|
# exists so that "did not run" is never spelled the same way as "passed".
|
||||||
|
def emit_info(desc, fpath, note):
|
||||||
|
findings.append(("INFO", desc, fpath, None, None, note))
|
||||||
|
|
||||||
def print_findings():
|
def print_findings():
|
||||||
for kind, desc, fpath, why, fix in findings:
|
for entry in findings:
|
||||||
|
kind = entry[0]
|
||||||
|
desc = entry[1]
|
||||||
|
fpath = entry[2]
|
||||||
|
why = entry[3]
|
||||||
|
fix = entry[4]
|
||||||
|
note = entry[5]
|
||||||
|
if kind == "FAIL":
|
||||||
print(f"FAIL {desc} — {fpath}")
|
print(f"FAIL {desc} — {fpath}")
|
||||||
print(f" Why: {why}")
|
print(f" Why: {why}")
|
||||||
print(f" Fix: {fix}")
|
print(f" Fix: {fix}")
|
||||||
print()
|
print()
|
||||||
|
else:
|
||||||
|
print(f"INFO {desc} — {fpath}")
|
||||||
|
print(f" Note: {note}")
|
||||||
|
print()
|
||||||
|
|
||||||
# --- Collect source_keys from agent pair ---
|
# --- Collect source_keys from agent pair ---
|
||||||
def get_source_keys_from_file(fpath):
|
def get_source_keys_from_file(fpath):
|
||||||
@@ -321,9 +341,24 @@ for fpath, keys in [(agent_file, given_keys)]:
|
|||||||
|
|
||||||
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
|
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
|
||||||
for slug in parse_h2_slugs(sources_content):
|
for slug in parse_h2_slugs(sources_content):
|
||||||
# Check 3: Contributing files exist (paths relative to plugin root)
|
# Checks 3 and 4: Contributing files exist (paths relative to plugin root),
|
||||||
|
# and back-reference the slug. `[]` and None are NOT the same answer here.
|
||||||
|
# `[]` is the author writing "(none)" — there is nothing to check and the
|
||||||
|
# skip is correct. None is a Contributing-files block this parser cannot
|
||||||
|
# read, and skipping THAT silently disables both checks on the one entry
|
||||||
|
# least likely to be right, which is the failure mode
|
||||||
|
# parse_contributing_files' own docstring warns about. Say so out loud.
|
||||||
cf_files = parse_contributing_files(sources_content, slug)
|
cf_files = parse_contributing_files(sources_content, slug)
|
||||||
if cf_files:
|
if cf_files is None:
|
||||||
|
emit_info(
|
||||||
|
f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed",
|
||||||
|
f"sources.md (## {slug})",
|
||||||
|
f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. "
|
||||||
|
f"Checks 3 and 4 did not run for this slug, so nothing verified that its contributing files exist or name it back. "
|
||||||
|
f"Write the value as '- **Contributing files:** <comma-separated paths>', or as a '**Contributing files:**' heading followed by '- ' bullets — "
|
||||||
|
f"or record '(none)' if this source contributed no files."
|
||||||
|
)
|
||||||
|
elif cf_files:
|
||||||
for cf_rel in cf_files:
|
for cf_rel in cf_files:
|
||||||
cf_abs = os.path.join(plugin_root, cf_rel)
|
cf_abs = os.path.join(plugin_root, cf_rel)
|
||||||
if not os.path.isfile(cf_abs):
|
if not os.path.isfile(cf_abs):
|
||||||
|
|||||||
@@ -264,6 +264,16 @@ def _collect_package(pkg_dir, names):
|
|||||||
names.add(os.path.basename(path.rstrip('/')).lower())
|
names.add(os.path.basename(path.rstrip('/')).lower())
|
||||||
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
||||||
for path in glob.glob(os.path.join(safe_dir, sub)):
|
for path in glob.glob(os.path.join(safe_dir, sub)):
|
||||||
|
# The same rule one directory over, which until now had no
|
||||||
|
# counterpart here at all: the skills branch above tests for a
|
||||||
|
# SKILL.md, the agents branch took every glob hit on trust. A
|
||||||
|
# DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not
|
||||||
|
# tell the two apart, so a leftover of that shape resolved a routing
|
||||||
|
# target on the machine holding it and dangled everywhere else —
|
||||||
|
# identical install-dependence, arriving through the one door
|
||||||
|
# nobody guarded.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
if base.endswith('.agent.md'):
|
if base.endswith('.agent.md'):
|
||||||
base = base[:-len('.agent.md')]
|
base = base[:-len('.agent.md')]
|
||||||
@@ -542,6 +552,34 @@ def known_targets(start_dir):
|
|||||||
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
||||||
# has two ways to say so.
|
# has two ways to say so.
|
||||||
#
|
#
|
||||||
|
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is
|
||||||
|
# a repair of the promise above rather than a widening of it. Until the sweeps
|
||||||
|
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use
|
||||||
|
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
|
||||||
|
# between. Every one of these therefore exited 0 in total silence — no ERROR, no
|
||||||
|
# SUGGESTION, not even the target's name:
|
||||||
|
# Do not use for Y — /no-such-skill instead.
|
||||||
|
# Do not use for Y; /no-such-skill handles that.
|
||||||
|
# Do not use for Y (/no-such-skill covers it).
|
||||||
|
# Do not use for Y — that is /no-such-skill's job.
|
||||||
|
# Do not use for Y — defer to /no-such-skill.
|
||||||
|
# Do not use for Y — /no-such-skill.
|
||||||
|
# Do not use for Y; -> no-such-skill covers it.
|
||||||
|
# The target was never EXTRACTED, so the notation-first rule in _add() had
|
||||||
|
# nothing to apply itself to and the "always blocks" promise was false for the
|
||||||
|
# ordinary way an author writes the thing. The SUGGESTION tier made it worse
|
||||||
|
# than a gap: its printed remedy tells the author to "write it as `/name` or
|
||||||
|
# `-> name` and it will be checked properly", and taking that advice turned a
|
||||||
|
# visible SUGGESTION into silence — the gate teaching the one edit that blinds
|
||||||
|
# it.
|
||||||
|
#
|
||||||
|
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate
|
||||||
|
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a
|
||||||
|
# PATH: a following `/`, or a `.` followed by a non-space, means
|
||||||
|
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's
|
||||||
|
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still
|
||||||
|
# counts.
|
||||||
|
#
|
||||||
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
||||||
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
||||||
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
||||||
@@ -565,6 +603,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
|
|||||||
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
||||||
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
||||||
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
||||||
|
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their
|
||||||
|
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name`
|
||||||
|
# alternative: that one only ever runs behind a route verb or an arrow, and the
|
||||||
|
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe.
|
||||||
|
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
|
||||||
|
# what made `Do not use for Y; -> no-such-skill covers it.` invisible:
|
||||||
|
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
|
||||||
|
# check. Dropping that prefix costs the one false positive the bare-arrow bullet
|
||||||
|
# above names — a process chain ending in a hyphenated word, `Instead, reproduce
|
||||||
|
# -> minimise -> regression-test.` — and costs it only in a sentence that already
|
||||||
|
# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same
|
||||||
|
# chain written `Do not use for X — reproduce -> regression-test.` was already a
|
||||||
|
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
|
||||||
|
# arrow, not whether prose can. An author who means the chain and not a route
|
||||||
|
# writes it in its own sentence, where neither pattern looks.
|
||||||
|
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||||
|
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
|
||||||
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
||||||
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
||||||
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
||||||
@@ -740,6 +795,16 @@ def _extract_sentence(sentence):
|
|||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
strict=True, arrow=True)
|
strict=True, arrow=True)
|
||||||
if boundary:
|
if boundary:
|
||||||
|
# Route notation wherever it sits in the clause, not only where a route
|
||||||
|
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE
|
||||||
|
# NOTATION note in the header for the seven phrasings this recovers and
|
||||||
|
# for why silence was the failure mode. Neither sweep takes the follower
|
||||||
|
# test: _add() reads the notation first and both forms reach it marked.
|
||||||
|
for match in NOTATION_SLASH.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
|
for match in NOTATION_ARROW.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
|
strict=True, arrow=True)
|
||||||
for match in BACKTICK.finditer(sentence):
|
for match in BACKTICK.finditer(sentence):
|
||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
return out
|
return out
|
||||||
@@ -1110,7 +1175,15 @@ def missing_reference_pointers(body, skill_dir):
|
|||||||
end = masked.find('\n', match.end())
|
end = masked.find('\n', match.end())
|
||||||
if end < 0:
|
if end < 0:
|
||||||
end = len(masked)
|
end = len(masked)
|
||||||
if REFERENCE_PAST.search(masked[start:end]):
|
# The pointer's OWN SPAN is excised before the sweep. Run over the
|
||||||
|
# whole line, the past-tense test matched the very path it was judging,
|
||||||
|
# so a file exempted itself by its NAME: `references/deprecated-api.md`,
|
||||||
|
# `references/removed-flags.md` and `references/gone.md` produced no
|
||||||
|
# ERROR at all, while `references/missing.md` — an identical break —
|
||||||
|
# errored. The exemption is about what the SENTENCE says about the
|
||||||
|
# pointer, never about what the pointer is called.
|
||||||
|
line = masked[start:match.start()] + masked[match.end():end]
|
||||||
|
if REFERENCE_PAST.search(line):
|
||||||
continue
|
continue
|
||||||
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ Checks performed:
|
|||||||
3 source_keys in references/*.md → slug exists in sources.md (INFO if no
|
3 source_keys in references/*.md → slug exists in sources.md (INFO if no
|
||||||
source_keys; an explicit 'source_keys: []' declares the file house-authored
|
source_keys; an explicit 'source_keys: []' declares the file house-authored
|
||||||
and passes silently)
|
and passes silently)
|
||||||
4 Contributing files listed in sources.md exist on disk
|
4 Contributing files listed in sources.md exist on disk. An explicit
|
||||||
|
'(none)' skips silently; a Contributing files block this parser cannot
|
||||||
|
read is reported as an INFO saying checks 4 and 5 did not run, never
|
||||||
|
skipped silently.
|
||||||
5 Contributing files back-reference the parent slug in their source_keys
|
5 Contributing files back-reference the parent slug in their source_keys
|
||||||
6 Research doc field present and not placeholder
|
6 Research doc field present and not placeholder
|
||||||
7 Slug in sources.md present in upstream research doc (INFO only). A section
|
7 Slug in sources.md present in upstream research doc (INFO only). A section
|
||||||
@@ -104,7 +107,13 @@ def parse_source_keys(fm):
|
|||||||
# claims that then have to be maintained in sources.md as well. A bare
|
# claims that then have to be maintained in sources.md as well. A bare
|
||||||
# `source_keys:` with nothing after it is NOT accepted here — that reads as a
|
# `source_keys:` with nothing after it is NOT accepted here — that reads as a
|
||||||
# truncated or half-written entry, not a decision.
|
# truncated or half-written entry, not a decision.
|
||||||
EMPTY_SOURCE_KEYS_RE = re.compile(r'^\s*source_keys:\s*\[\s*\]\s*$')
|
#
|
||||||
|
# The indent is pinned to the two positions parse_source_keys() actually reads
|
||||||
|
# — column 0, or two spaces under `metadata:`. A permissive `^\s*` matched a
|
||||||
|
# `source_keys: []` nested at ANY depth under an unrelated key, which
|
||||||
|
# parse_source_keys() never reads, so a stray nested key silenced the check-3
|
||||||
|
# INFO for a file that had declared nothing.
|
||||||
|
EMPTY_SOURCE_KEYS_RE = re.compile(r'^(?: )?source_keys:\s*\[\s*\]\s*$')
|
||||||
|
|
||||||
def declares_empty_source_keys(fm):
|
def declares_empty_source_keys(fm):
|
||||||
"""True when frontmatter carries an explicit, empty `source_keys: []`."""
|
"""True when frontmatter carries an explicit, empty `source_keys: []`."""
|
||||||
@@ -436,9 +445,25 @@ repo_root = find_repo_root(skill_dir)
|
|||||||
research_docs_seen = {} # abs_path → set of slugs in sources.md that reference it
|
research_docs_seen = {} # abs_path → set of slugs in sources.md that reference it
|
||||||
|
|
||||||
for slug in parse_h2_slugs(sources_content):
|
for slug in parse_h2_slugs(sources_content):
|
||||||
# Check 4: Contributing files exist
|
# Checks 4 and 5: Contributing files exist, and back-reference the slug.
|
||||||
|
# `[]` and None are NOT the same answer here. `[]` is the author writing
|
||||||
|
# "(none)" — there is nothing to check and the skip is correct. None is a
|
||||||
|
# Contributing-files block this parser cannot read, and skipping THAT
|
||||||
|
# silently disables both checks on the one entry least likely to be right,
|
||||||
|
# which is the failure mode parse_contributing_files' own docstring warns
|
||||||
|
# about. Say so out loud instead, the same way an unresolvable Research doc
|
||||||
|
# value does.
|
||||||
cf_files = parse_contributing_files(sources_content, slug)
|
cf_files = parse_contributing_files(sources_content, slug)
|
||||||
if cf_files:
|
if cf_files is None:
|
||||||
|
emit_info(
|
||||||
|
f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed",
|
||||||
|
f"references/sources.md (## {slug})",
|
||||||
|
f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. "
|
||||||
|
f"Checks 4 and 5 did not run for this slug, so nothing verified that its contributing files exist or name it back. "
|
||||||
|
f"Write the value as '- **Contributing files:** <comma-separated paths>', or as a '**Contributing files:**' heading followed by '- ' bullets — "
|
||||||
|
f"or record '(none)' if this source contributed no files."
|
||||||
|
)
|
||||||
|
elif cf_files:
|
||||||
for cf_rel in cf_files:
|
for cf_rel in cf_files:
|
||||||
cf_abs = os.path.join(skill_dir, cf_rel)
|
cf_abs = os.path.join(skill_dir, cf_rel)
|
||||||
if not os.path.isfile(cf_abs):
|
if not os.path.isfile(cf_abs):
|
||||||
|
|||||||
@@ -190,6 +190,16 @@ def _collect_package(pkg_dir, names):
|
|||||||
names.add(os.path.basename(path.rstrip('/')).lower())
|
names.add(os.path.basename(path.rstrip('/')).lower())
|
||||||
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
||||||
for path in glob.glob(os.path.join(safe_dir, sub)):
|
for path in glob.glob(os.path.join(safe_dir, sub)):
|
||||||
|
# The same rule one directory over, which until now had no
|
||||||
|
# counterpart here at all: the skills branch above tests for a
|
||||||
|
# SKILL.md, the agents branch took every glob hit on trust. A
|
||||||
|
# DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not
|
||||||
|
# tell the two apart, so a leftover of that shape resolved a routing
|
||||||
|
# target on the machine holding it and dangled everywhere else —
|
||||||
|
# identical install-dependence, arriving through the one door
|
||||||
|
# nobody guarded.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
if base.endswith('.agent.md'):
|
if base.endswith('.agent.md'):
|
||||||
base = base[:-len('.agent.md')]
|
base = base[:-len('.agent.md')]
|
||||||
@@ -468,6 +478,34 @@ def known_targets(start_dir):
|
|||||||
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
||||||
# has two ways to say so.
|
# has two ways to say so.
|
||||||
#
|
#
|
||||||
|
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is
|
||||||
|
# a repair of the promise above rather than a widening of it. Until the sweeps
|
||||||
|
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use
|
||||||
|
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
|
||||||
|
# between. Every one of these therefore exited 0 in total silence — no ERROR, no
|
||||||
|
# SUGGESTION, not even the target's name:
|
||||||
|
# Do not use for Y — /no-such-skill instead.
|
||||||
|
# Do not use for Y; /no-such-skill handles that.
|
||||||
|
# Do not use for Y (/no-such-skill covers it).
|
||||||
|
# Do not use for Y — that is /no-such-skill's job.
|
||||||
|
# Do not use for Y — defer to /no-such-skill.
|
||||||
|
# Do not use for Y — /no-such-skill.
|
||||||
|
# Do not use for Y; -> no-such-skill covers it.
|
||||||
|
# The target was never EXTRACTED, so the notation-first rule in _add() had
|
||||||
|
# nothing to apply itself to and the "always blocks" promise was false for the
|
||||||
|
# ordinary way an author writes the thing. The SUGGESTION tier made it worse
|
||||||
|
# than a gap: its printed remedy tells the author to "write it as `/name` or
|
||||||
|
# `-> name` and it will be checked properly", and taking that advice turned a
|
||||||
|
# visible SUGGESTION into silence — the gate teaching the one edit that blinds
|
||||||
|
# it.
|
||||||
|
#
|
||||||
|
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate
|
||||||
|
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a
|
||||||
|
# PATH: a following `/`, or a `.` followed by a non-space, means
|
||||||
|
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's
|
||||||
|
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still
|
||||||
|
# counts.
|
||||||
|
#
|
||||||
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
||||||
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
||||||
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
||||||
@@ -491,6 +529,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
|
|||||||
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
||||||
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
||||||
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
||||||
|
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their
|
||||||
|
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name`
|
||||||
|
# alternative: that one only ever runs behind a route verb or an arrow, and the
|
||||||
|
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe.
|
||||||
|
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
|
||||||
|
# what made `Do not use for Y; -> no-such-skill covers it.` invisible:
|
||||||
|
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
|
||||||
|
# check. Dropping that prefix costs the one false positive the bare-arrow bullet
|
||||||
|
# above names — a process chain ending in a hyphenated word, `Instead, reproduce
|
||||||
|
# -> minimise -> regression-test.` — and costs it only in a sentence that already
|
||||||
|
# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same
|
||||||
|
# chain written `Do not use for X — reproduce -> regression-test.` was already a
|
||||||
|
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
|
||||||
|
# arrow, not whether prose can. An author who means the chain and not a route
|
||||||
|
# writes it in its own sentence, where neither pattern looks.
|
||||||
|
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||||
|
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
|
||||||
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
||||||
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
||||||
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
||||||
@@ -666,6 +721,16 @@ def _extract_sentence(sentence):
|
|||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
strict=True, arrow=True)
|
strict=True, arrow=True)
|
||||||
if boundary:
|
if boundary:
|
||||||
|
# Route notation wherever it sits in the clause, not only where a route
|
||||||
|
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE
|
||||||
|
# NOTATION note in the header for the seven phrasings this recovers and
|
||||||
|
# for why silence was the failure mode. Neither sweep takes the follower
|
||||||
|
# test: _add() reads the notation first and both forms reach it marked.
|
||||||
|
for match in NOTATION_SLASH.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
|
for match in NOTATION_ARROW.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
|
strict=True, arrow=True)
|
||||||
for match in BACKTICK.finditer(sentence):
|
for match in BACKTICK.finditer(sentence):
|
||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
return out
|
return out
|
||||||
@@ -1036,7 +1101,15 @@ def missing_reference_pointers(body, skill_dir):
|
|||||||
end = masked.find('\n', match.end())
|
end = masked.find('\n', match.end())
|
||||||
if end < 0:
|
if end < 0:
|
||||||
end = len(masked)
|
end = len(masked)
|
||||||
if REFERENCE_PAST.search(masked[start:end]):
|
# The pointer's OWN SPAN is excised before the sweep. Run over the
|
||||||
|
# whole line, the past-tense test matched the very path it was judging,
|
||||||
|
# so a file exempted itself by its NAME: `references/deprecated-api.md`,
|
||||||
|
# `references/removed-flags.md` and `references/gone.md` produced no
|
||||||
|
# ERROR at all, while `references/missing.md` — an identical break —
|
||||||
|
# errored. The exemption is about what the SENTENCE says about the
|
||||||
|
# pointer, never about what the pointer is called.
|
||||||
|
line = masked[start:match.start()] + masked[match.end():end]
|
||||||
|
if REFERENCE_PAST.search(line):
|
||||||
continue
|
continue
|
||||||
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -292,6 +292,16 @@ def _collect_package(pkg_dir, names):
|
|||||||
names.add(os.path.basename(path.rstrip('/')).lower())
|
names.add(os.path.basename(path.rstrip('/')).lower())
|
||||||
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
for sub in ('.apm/agents/*.md', 'agents/*.md'):
|
||||||
for path in glob.glob(os.path.join(safe_dir, sub)):
|
for path in glob.glob(os.path.join(safe_dir, sub)):
|
||||||
|
# The same rule one directory over, which until now had no
|
||||||
|
# counterpart here at all: the skills branch above tests for a
|
||||||
|
# SKILL.md, the agents branch took every glob hit on trust. A
|
||||||
|
# DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not
|
||||||
|
# tell the two apart, so a leftover of that shape resolved a routing
|
||||||
|
# target on the machine holding it and dangled everywhere else —
|
||||||
|
# identical install-dependence, arriving through the one door
|
||||||
|
# nobody guarded.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
if base.endswith('.agent.md'):
|
if base.endswith('.agent.md'):
|
||||||
base = base[:-len('.agent.md')]
|
base = base[:-len('.agent.md')]
|
||||||
@@ -570,6 +580,34 @@ def known_targets(start_dir):
|
|||||||
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
# ambiguity to resolve, and an author who wants a route checked unconditionally
|
||||||
# has two ways to say so.
|
# has two ways to say so.
|
||||||
#
|
#
|
||||||
|
# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is
|
||||||
|
# a repair of the promise above rather than a widening of it. Until the sweeps
|
||||||
|
# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use
|
||||||
|
# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in
|
||||||
|
# between. Every one of these therefore exited 0 in total silence — no ERROR, no
|
||||||
|
# SUGGESTION, not even the target's name:
|
||||||
|
# Do not use for Y — /no-such-skill instead.
|
||||||
|
# Do not use for Y; /no-such-skill handles that.
|
||||||
|
# Do not use for Y (/no-such-skill covers it).
|
||||||
|
# Do not use for Y — that is /no-such-skill's job.
|
||||||
|
# Do not use for Y — defer to /no-such-skill.
|
||||||
|
# Do not use for Y — /no-such-skill.
|
||||||
|
# Do not use for Y; -> no-such-skill covers it.
|
||||||
|
# The target was never EXTRACTED, so the notation-first rule in _add() had
|
||||||
|
# nothing to apply itself to and the "always blocks" promise was false for the
|
||||||
|
# ordinary way an author writes the thing. The SUGGESTION tier made it worse
|
||||||
|
# than a gap: its printed remedy tells the author to "write it as `/name` or
|
||||||
|
# `-> name` and it will be checked properly", and taking that advice turned a
|
||||||
|
# visible SUGGESTION into silence — the gate teaching the one edit that blinds
|
||||||
|
# it.
|
||||||
|
#
|
||||||
|
# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate
|
||||||
|
# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a
|
||||||
|
# PATH: a following `/`, or a `.` followed by a non-space, means
|
||||||
|
# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's
|
||||||
|
# closing `.` is not followed by a non-space, so `— /no-such-skill.` still
|
||||||
|
# counts.
|
||||||
|
#
|
||||||
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
# NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs
|
||||||
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
# still resolve `gitea:gitea-prs`), so the patterns admit an optional
|
||||||
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
# `<plugin>:` prefix and normalize_target() strips it before resolution.
|
||||||
@@ -593,6 +631,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET)
|
|||||||
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I)
|
||||||
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I)
|
||||||
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I)
|
||||||
|
# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their
|
||||||
|
# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name`
|
||||||
|
# alternative: that one only ever runs behind a route verb or an arrow, and the
|
||||||
|
# trailing lookahead here is the part that makes a FREE-STANDING sweep safe.
|
||||||
|
# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is
|
||||||
|
# what made `Do not use for Y; -> no-such-skill covers it.` invisible:
|
||||||
|
# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the
|
||||||
|
# check. Dropping that prefix costs the one false positive the bare-arrow bullet
|
||||||
|
# above names — a process chain ending in a hyphenated word, `Instead, reproduce
|
||||||
|
# -> minimise -> regression-test.` — and costs it only in a sentence that already
|
||||||
|
# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same
|
||||||
|
# chain written `Do not use for X — reproduce -> regression-test.` was already a
|
||||||
|
# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the
|
||||||
|
# arrow, not whether prose can. An author who means the chain and not a route
|
||||||
|
# writes it in its own sentence, where neither pattern looks.
|
||||||
|
NOTATION_SLASH = re.compile(r"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||||
|
NOTATION_ARROW = re.compile(r"(?:->|→)\s*(%s)\b" % NAME_HYPH, re.I)
|
||||||
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
# CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT
|
||||||
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
# `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a
|
||||||
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
# DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`,
|
||||||
@@ -768,6 +823,16 @@ def _extract_sentence(sentence):
|
|||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
strict=True, arrow=True)
|
strict=True, arrow=True)
|
||||||
if boundary:
|
if boundary:
|
||||||
|
# Route notation wherever it sits in the clause, not only where a route
|
||||||
|
# verb or an arrow happens to precede it. See the EXPLICIT ROUTE
|
||||||
|
# NOTATION note in the header for the seven phrasings this recovers and
|
||||||
|
# for why silence was the failure mode. Neither sweep takes the follower
|
||||||
|
# test: _add() reads the notation first and both forms reach it marked.
|
||||||
|
for match in NOTATION_SLASH.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
|
for match in NOTATION_ARROW.finditer(sentence):
|
||||||
|
_add(out, sentence, match.group(1), match.start(1), match.end(1),
|
||||||
|
strict=True, arrow=True)
|
||||||
for match in BACKTICK.finditer(sentence):
|
for match in BACKTICK.finditer(sentence):
|
||||||
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
_add(out, sentence, match.group(1), match.start(1), match.end(1))
|
||||||
return out
|
return out
|
||||||
@@ -1138,7 +1203,15 @@ def missing_reference_pointers(body, skill_dir):
|
|||||||
end = masked.find('\n', match.end())
|
end = masked.find('\n', match.end())
|
||||||
if end < 0:
|
if end < 0:
|
||||||
end = len(masked)
|
end = len(masked)
|
||||||
if REFERENCE_PAST.search(masked[start:end]):
|
# The pointer's OWN SPAN is excised before the sweep. Run over the
|
||||||
|
# whole line, the past-tense test matched the very path it was judging,
|
||||||
|
# so a file exempted itself by its NAME: `references/deprecated-api.md`,
|
||||||
|
# `references/removed-flags.md` and `references/gone.md` produced no
|
||||||
|
# ERROR at all, while `references/missing.md` — an identical break —
|
||||||
|
# errored. The exemption is about what the SENTENCE says about the
|
||||||
|
# pointer, never about what the pointer is called.
|
||||||
|
line = masked[start:match.start()] + masked[match.end():end]
|
||||||
|
if REFERENCE_PAST.search(line):
|
||||||
continue
|
continue
|
||||||
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
if REFERENCE_QUALIFIER.search(masked[start:match.start()]):
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in new issue
Block a user