11 Commits

Author SHA1 Message Date
095929142f chore(release): bump all six plugin versions for the ADR-0020 retrofit
No wave on this branch bumped a version across 56 commits, though
apm-workflow's own configure.md states the policy: bump a package's
apm.yml version: whenever anything reaching its compiled output changes.
All six local packages have substantive .apm/ edits here.

Minor rather than patch. The retrofit rewrote every skill description,
which is the routing surface a caller matches against, and redistributed
bodies into references/. Behaviour is preserved but discovery changes, so
this is more than a fix.

The same file is explicit that versions are per package — editing
plugins/foo/.apm/ never bumps plugins/bar — so this is six independent
bumps that happen to land together, not one release number. Under the
per_package strategy the catalog carries a second copy in
marketplace.packages[], and apm-marketplace-check fails the push when the
two disagree; it cannot see a bump skipped in both, which is the state the
branch was in.

executables.allow is version-pinned by apm's design and moves with
kyberforge, or the SessionStart hook silently stops deploying (ADR-0019).
The comment above that block predicted exactly this; the pre-push suite
caught it when the first bump orphaned the key.

Refs #99
2026-08-30 21:00:58 +00:00
c7c9311d80 docs(gates): refresh the retrofit status to measured state
docs/spec/gates.md still described both ADR-0020 gates as "currently red"
and tabled the pre-retrofit figures: 26 of 39 descriptions and 9 of 39
bodies over their FAIL tier, 2 dangling targets, 58 SUGGESTIONs, and 10
Kyberforge.CompositionNote errors across four gitea-* skills. Measured
now: 0, 0, 0, 33 and 0.

The branch correctly left ADR-0020 itself untouched, since it self-pins
every citation to base commit f9b919d. gates.md carries no such pin, and
AGENTS.md names it three times as the authoritative reasoning layer — so
the shallow doc and the deep doc it defers to asserted opposite facts
about the same two gates, with the stale one telling a reader that an
unrelated one-line fix to a skill is blocked pending a retrofit that is
already done.

Also corrects the apm-orchestrate agent body figure, which drifted from
1,080 to 1,113 across this branch. Its point is that the numbers are live
evidence for leaving that hook's files: pattern alone, so a reader who
re-measures and gets a third value loses the argument.

AGENTS.md gains the second cause of the references/ blind spot: besides
the Kyberforge style being scoped [**/SKILL.md], the
vale-audit-prefilter-skill hook filters on a SKILL.md-only files: pattern,
so widening .vale.ini alone would change nothing. It also no longer
implies the kyberforge wave was the end of the work.

Refs #99 #117
2026-08-30 20:52:06 +00:00
484357a3b9 fix(gates): parse the bullet form of Contributing files
validate-provenance.sh matched Contributing files only as a single inline
line beginning "- **Contributing files:**". Seven skills write it as a
bare "**Contributing files:**" heading above a bullet list, so
parse_contributing_files returned None and checks 4 (contributing file
exists) and 5 (bidirectional source_keys) silently verified nothing on
git-branches, git-remotes, git-submodules, git-workflow, git-worktrees,
gitea-files and gitea-releases.

Those are among the skills this branch changed most — git-branches alone
gained five reference files — and the retrofit's mandatory sources.md
collateral went in unchecked. Demonstrated rather than argued: planting a
nonexistent contributing path in git-remotes yields 0 findings under the
old parser and 1 FAIL under the new one.

Both forms are now accepted. The bullet form is parsed per bullet rather
than by splitting a joined value, because its per-file notes contain
commas that would otherwise be read as path separators. The return type
becomes a list of note-stripped paths, with "(none)" as an empty list and
an absent entry as None, so the two callers no longer re-split a string.

Applied to agent-audit's copy as well. No agent ships a sources.md today,
so it is latent there, but it is the same defect.

This is a third gate blind spot alongside #117 and #118, and was unfiled.
One real defect surfaced immediately and is fixed separately.

Refs #99
2026-08-30 20:51:56 +00:00
164948a0bc fix(apm-workflow): type: selects processing, it does not validate content
configure.md said apm.yml's `type:` field "constrains what .apm/ may
contain" and that changing it later "does not retroactively validate what
is already on disk" — both implying a validation step that does not exist.
Read against the installed apm-cli 0.28.0: PackageContentType controls how
a package is processed during install/compile, apm_package.py only
enum-checks the declared string, and validate_apm_package() branches on the
structural type derived from files on disk, never on the declared field.
There is no content-vs-type mismatch check anywhere.

The hazard is therefore the opposite of what the wording primed for:
silent omission. A package declaring type: instructions while shipping
.apm/skills/ installs no skill and compiles AGENTS.md only, exits 0, and
reports success having shipped none of its primitives. The rule is now to
verify deployed output rather than the exit code. apm-orchestrate carried
the same wording as a Hard Rule and is corrected in step; its separate
defects stay with #120.

Also refreshes the exemplar figures this branch had re-staled. 264a5db set
them to 3,222 words of references; 6cb47f8 then added 63 words and
invalidated them, and the correction above adds more. Re-measured after
all edits: body 237 and whole-file 304 both still hold, references total
3,416. body-discipline.md's "roughly 3,200" moves with it.

ADR-0020 is deliberately untouched — it self-pins its citations to
f9b919d — as is the git-commits negative example pinned to 5e23250.

Refs #99
2026-08-30 20:51:45 +00:00
ca744d5d6c fix(vale-config): correct the missing-style failure mode
Gotcha 1 said a style in BasedOnStyles that is not built-in and not
already under StylesPath "finds nothing until vale sync fetches it — a
clean run is not proof anything linted". Reproduced against vale 3.15.2:
that case is a hard `E100 [loadStyles] style '<name>' does not exist on
StylesPath`, exit 2. Nothing is linted and nothing is silent.

Worse, the same commit deleted the Gotcha that was the actual diagnostic —
that Packages and BasedOnStyles are separate keys and a style lints only
once it is in both. So the surviving rule sent a reader staring at E100 to
run `vale sync`, which fetches only what Packages declares and reports
"Synced 0 package(s)" against a BasedOnStyles-only name. The remediation
loop did not terminate. Reproduced end to end.

The genuinely silent case is the reverse — declared in Packages and
synced, but absent from BasedOnStyles — and it is now the one labelled as
such. Testing also turned up that StylesPath must exist as a directory
even when Vale is the only style (E201, exit 2), which was documented
nowhere.

references/configuration-reference.md gains a seven-row resolution matrix,
each row backed by a fixture. Its Frontmatter Scopes section claimed
provenance from the vale.sh research corpus, which contains no frontmatter
material at all; it is house-verified and now says so under its own slug.

Issue #99's wave-3 comment recorded this defect as found and repaired. It
was not — the file was byte-identical to the commit that introduced it, so
nothing here was treated as already correct.

Refs #99
2026-08-30 20:51:32 +00:00
d2da45f78c fix(gitea-releases): restore the prerelease imperative to the create path
The retrofit left only a descriptive sentence on the loaded path — "Gitea
never infers a prerelease from a -beta/-rc tag name" — and moved the
imperative into references/conventions.md behind a trigger listing semver
naming, release-notes sourcing and release-to-tag relationships. Draft and
prerelease are not in that list, and "cut a v2.0.0-beta.1" is exactly the
request where the caller does not raise the topic, so the rule was
unreachable from the flow that needs it.

Severity comes from the repair path: the MCP surface has create, get,
get_latest, list and delete only — there is no update or edit tool. A
release published without is_pre_release can only be corrected by
delete_release plus a fresh create, and get_latest_release points
consumers at the beta meanwhile. Neither SKILL.md nor call-signatures.md
said so anywhere.

The flags are now set explicitly on every create, the missing update tool
and its delete-and-recreate consequence are stated in the body, and the
semver pass-through rule stranded behind the same trigger is promoted
alongside it. call-signatures.md now cites the deployed tool description
as direct evidence that get_latest_release excludes drafts, while keeping
the prerelease half hedged — that remains unconfirmed.

Verified live against gitea-mcp v1.7.0, read-only calls.

Refs #99
2026-08-30 20:51:22 +00:00
8982ac58b7 fix(gitea-labels-milestones): read exclusive per label, never infer it
SKILL.md called `exclusive` "an org-labels-only flag" and concluded that
applying a Kind/*, Priority/* or Status/* label "must replace the one
already there, not stack on it". Live `list_repo_labels` on this repo
returns `exclusive` on every REPO label: all seven Kind/* plus
Compat/Breaking are false, while Priority/*, Reviewed/* and Status/* are
true. So the field is not org-only, and the replace rule would strip a
valid Kind/* label — a destructive write from a false premise.

The nuance kept: label_write's `exclusive` parameter genuinely is
annotated org-only, so that row was schema-accurate. The error was
generalising a write-parameter restriction into a claim about where the
field exists. The row is qualified rather than deleted.

The rule is now per-label: where exclusive is true the server drops the
sibling itself, so do not pre-remove; where it is false the label is
legitimately stackable.

Also fixes the org-label fallback, which treated any list_org_labels
failure as proof the owner is a user account with no org pool and said so
was "an answer, not an error to report". Under the token scopes this skill
declares the call fails with required=[read:organization] before any
org-vs-user determination is made, so a capability gap was being reported
as an absent label. Scope errors are now distinguished and reported.

Verified live against gitea-mcp v1.7.0, read-only calls.

Refs #99
2026-08-30 20:51:12 +00:00
dd1981db80 fix(gitea-issues): list_issues does have type and milestones on v1.7.0
SKILL.md's headline Gotcha said `list_issues` "has no `type` filter", and
references/issues.md stated in bold that neither `type` nor `milestones`
exists, "despite both appearing in api-reference.md". Both parameters are
present on the deployed gitea-mcp v1.7.0 and both work: `type: "issues"`
returns only issues, `type: "pulls"` only PRs, and `milestones` filters by
name. Unfiltered, the same window returns them interleaved, so the mixing
the Gotcha describes is real — only the stated remedy was wrong.

This mattered most in gitea-workflow's no-args check-in, which lists open
issues through this skill and so reported PRs under "Open Issues" while
the skill forbade the one-parameter fix. The list flow now passes
`type: "issues"`.

references/sources.md recorded the absence as a live-verification win over
stale research docs; it now records that the earlier check was superseded
by v1.7.0, since drift runs in both directions. gitea-prs cited the same
parameter as its canonical drift example and no longer does — no
replacement example was substituted, because the obvious candidate was not
verified in this pass.

Also defaults label writes to `add_labels`: `replace_labels` clears every
label not in the array, and per-label exclusivity makes blanket replacement
destructive for a non-exclusive scope.

Verified live against gitea-mcp v1.7.0, read-only calls.

Refs #99
2026-08-30 20:51:02 +00:00
c5b207aee8 fix(git-branches): restore merging.md's provenance back-reference
references/sources.md credits atlassian-gitflow-tutorial with contributing
the `--no-ff` requirement on Gitflow supporting-branch merges to
references/merging.md, and merging.md:14 does carry that claim — but the
file's own source_keys listed only context7-git-htmldocs, so the chain was
one-directional.

Surfaced by repairing validate-provenance.sh's Contributing-files parser
in the same series; this skill was one of seven whose sources.md the old
parser could not read, so checks 4 and 5 had never run against it.

Refs #99
2026-08-30 20:50:51 +00:00
93d3263f8c fix(pc-run): rebind the clean gate and fixer-hook rule to every branch
SKILL.md tells a dispatching agent to read the matching reference file
"and no other". The retrofit moved the `pre-commit clean` confirmation
gate out of the always-loaded body into references/clean.md, but
references/failure-patterns.md — loaded by the diagnosis route, not the
clean route — prescribes `pre-commit clean` with no gate at all. So "why
is this hook failing" could wipe the machine-wide cache at
~/.cache/pre-commit for every repo without asking.

The gate returns to the body, where every branch loads it, and is
restated at the point of use in failure-patterns.md. It is its own
section rather than a Gotchas bullet because folding it in pushed the
Gotchas ratio to 41%, whose only suggested remedy is moving it back to
references/ — the move that caused this.

The fixer-hook rule had the same shape: reachable only behind "if the
cause is not obvious from the output", which is false precisely when
pre-commit prints `- files were modified by this hook`. The fix
(`git add -u && git commit`) and the prohibition on `pre-commit install -f`
are now unconditional, and the two weakened pointers that stranded them
are restored.

Found by an independent review of this branch.

Refs #99
2026-08-30 20:50:45 +00:00
ddf85518c7 fix(git-worktrees): correct the remote-tracking and repair claims
The dispatch row "Create tracking a remote branch" prescribed
`git worktree add <path> <remote>/<branch>`. Per git-worktree(1) the
tracking DWIM fires only when <commit-ish> is a bare branch name that is
NOT found locally, no -b/-B/--detach is given, and exactly one remote has
a matching name; only then is it equivalent to
`git worktree add --track -b <branch> <path> <remote>/<branch>`.

An explicit <remote>/<branch> is found, so that precondition fails and no
branch is created: the result is a detached HEAD with no upstream. Commits
made in it become unreachable once the worktree is removed or HEAD moves,
and push needs an explicit refspec. Only the -d row was flagged detached.

The table now names --track -b as the always-correct form, keeps the bare
name shortcut with its precondition stated, and adds a Never row for the
<remote>/<branch> spelling. The single-remote and checkout.defaultRemote
preconditions move into the body, since a reader who trusts the table
never follows the reference pointer.

Also corrects `git worktree repair`: the no-argument form is the only
cwd-dependent one, and `repair <path>...` runs from any worktree. The
claim that running it from the wrong directory "reports nothing and fixes
nothing" has no basis in the manual and is removed.

Found by an independent review of this branch.

Refs #99
2026-08-30 20:50:36 +00:00
73 changed files with 625 additions and 231 deletions

View File

@@ -11,35 +11,35 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"version": "1.6.0", "version": "1.7.0",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/kyberforge" "source": "./plugins/kyberforge"
}, },
{ {
"name": "bin", "name": "bin",
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
"version": "1.1.5", "version": "1.2.0",
"category": "Utilities", "category": "Utilities",
"source": "./plugins/bin" "source": "./plugins/bin"
}, },
{ {
"name": "git", "name": "git",
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
"version": "1.3.5", "version": "1.4.0",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/git" "source": "./plugins/git"
}, },
{ {
"name": "gitea", "name": "gitea",
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
"version": "1.3.6", "version": "1.4.0",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/gitea" "source": "./plugins/gitea"
}, },
{ {
"name": "core", "name": "core",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"version": "1.1.1", "version": "1.2.0",
"category": "Productivity", "category": "Productivity",
"source": "./plugins/core" "source": "./plugins/core"
}, },
@@ -59,7 +59,7 @@
{ {
"name": "lint", "name": "lint",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"version": "1.1.6", "version": "1.2.0",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/lint" "source": "./plugins/lint"
} }

View File

@@ -11,35 +11,35 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"version": "1.6.0", "version": "1.7.0",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/kyberforge" "source": "./plugins/kyberforge"
}, },
{ {
"name": "bin", "name": "bin",
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
"version": "1.1.5", "version": "1.2.0",
"category": "Utilities", "category": "Utilities",
"source": "./plugins/bin" "source": "./plugins/bin"
}, },
{ {
"name": "git", "name": "git",
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
"version": "1.3.5", "version": "1.4.0",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/git" "source": "./plugins/git"
}, },
{ {
"name": "gitea", "name": "gitea",
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
"version": "1.3.6", "version": "1.4.0",
"category": "Version Control", "category": "Version Control",
"source": "./plugins/gitea" "source": "./plugins/gitea"
}, },
{ {
"name": "core", "name": "core",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"version": "1.1.1", "version": "1.2.0",
"category": "Productivity", "category": "Productivity",
"source": "./plugins/core" "source": "./plugins/core"
}, },
@@ -59,7 +59,7 @@
{ {
"name": "lint", "name": "lint",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"version": "1.1.6", "version": "1.2.0",
"category": "Developer Tools", "category": "Developer Tools",
"source": "./plugins/lint" "source": "./plugins/lint"
} }

View File

@@ -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). Issue #99 retrofitted them plugin by plugin and `kyberforge` was the last wave. 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 the `Kyberforge` style is scoped `[**/SKILL.md]`, so 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. Check both: `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). Issue #99 retrofitted them plugin by plugin — `kyberforge` was the last plugin wave, followed by corpus-wide passes and two rounds of independent audit fixes. 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]`, *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 (#117). 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.

14
apm.yml
View File

@@ -42,7 +42,7 @@ dependencies:
# after a kyberforge release, check this first. # after a kyberforge release, check this first.
executables: executables:
allow: allow:
kyberforge#1.6.0: kyberforge#1.7.0:
hooks: true hooks: true
bin: true bin: true
@@ -79,31 +79,31 @@ marketplace:
- name: kyberforge - name: kyberforge
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace. description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
source: ./plugins/kyberforge source: ./plugins/kyberforge
version: 1.6.0 version: 1.7.0
category: Developer Tools category: Developer Tools
- name: bin - name: bin
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin. description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
source: ./plugins/bin source: ./plugins/bin
version: 1.1.5 version: 1.2.0
category: Utilities category: Utilities
- name: git - name: git
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it. description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
source: ./plugins/git source: ./plugins/git
version: 1.3.5 version: 1.4.0
category: Version Control category: Version Control
- name: gitea - name: gitea
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone. description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
source: ./plugins/gitea source: ./plugins/gitea
version: 1.3.6 version: 1.4.0
category: Version Control category: Version Control
- name: core - name: core
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it. description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
source: ./plugins/core source: ./plugins/core
version: 1.1.1 version: 1.2.0
category: Productivity category: Productivity
- name: mattpocock-skills - name: mattpocock-skills
@@ -115,5 +115,5 @@ marketplace:
- name: lint - name: lint
description: Skills and agents for configuring and running linters. description: Skills and agents for configuring and running linters.
source: ./plugins/lint source: ./plugins/lint
version: 1.1.6 version: 1.2.0
category: Developer Tools category: Developer Tools

View File

@@ -282,7 +282,7 @@ bash scripts/skill-size-check.sh plugins/*/.apm/agents/*.agent.md
``` ```
exits 1 today with 900-word body FAILs on `git-orchestrate` (933), `gitea-orchestrate` (1,199) and exits 1 today with 900-word body FAILs on `git-orchestrate` (933), `gitea-orchestrate` (1,199) and
`apm-orchestrate` (1,080). Agent files escape only because the hook definitions filter on `SKILL.md` `apm-orchestrate` (1,113). Agent files escape only because the hook definitions filter on `SKILL.md`
— a file-pattern accident that happens to implement the design, not the design itself. **Do not — a file-pattern accident that happens to implement the design, not the design itself. **Do not
"extend" that hook's `files:` pattern to cover agents** on the assumption that the script already "extend" that hook's `files:` pattern to cover agents** on the assumption that the script already
knows the difference; doing so silently enforces a gate ADR-0020 declines to set. knows the difference; doing so silently enforces a gate ADR-0020 declines to set.
@@ -292,20 +292,22 @@ knows the difference; doing so silently enforces a gate ADR-0020 declines to set
**The ADR-0020 gates ship hot, with no baseline file.** A shrinking baseline recording each **The ADR-0020 gates ship hot, with no baseline file.** A shrinking baseline recording each
non-compliant skill's current numbers was considered and rejected in favour of hot gates. non-compliant skill's current numbers was considered and rejected in favour of hot gates.
Two independent hot gates are currently red, and the first will not warn you about the second. **The corpus is now clean on both gates.** Issue **#99** retrofitted all 39 skills plugin by plugin;
`kyberforge` was the last wave, followed by two corpus-wide passes.
| Gate | Current findings | | Gate | Current findings |
|---|---| |---|---|
| `skill-size-check` | **26 of 39** descriptions and **9 of 39** bodies exceed their FAIL tier; 2 dangling targets; 58 SUGGESTIONs | | `skill-size-check` | **0 of 39** descriptions and **0 of 39** bodies exceed their FAIL tier; 0 dangling targets; 31 SUGGESTIONs |
| `Kyberforge.CompositionNote` (Vale) | **10 errors across four skills**: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-workflow` | | `Kyberforge.CompositionNote` (Vale) | **0 errors** — the four `gitea-*` carriers were all retrofitted |
`Kyberforge.CompositionNote` is the ADR-0020 Vale rule banning composition and architecture prose `Kyberforge.CompositionNote` is the ADR-0020 Vale rule banning composition and architecture prose
from a description. Every Vale rule here is `level: error` with no ignorable tier, so touching any of from a description. Every Vale rule here is `level: error` with no ignorable tier, so a description
those four skills means fixing its prose findings as well as its size findings. that reintroduces one blocks the commit even though no skill carries one today.
Consequence: editing a non-compliant skill *for any reason* means retrofitting it to the contract Because nothing is grandfathered, the gates now bite on **first commit**: a new skill, or an edit
first — a one-line fix to `gitea-prs` cannot be committed until that skill complies. This is that pushes a description past 400 characters or a body past 900 words, is blocked until it
deliberate; it guarantees convergence and avoids a half-state. Tracked as Gitea issue **#99**. complies. That is the steady state the retrofit was for — it is no longer true that an unrelated
one-line fix to a skill requires retrofitting that skill first.
Check where a skill stands before starting, and check **both** gates: Check where a skill stands before starting, and check **both** gates:

View File

@@ -1,6 +1,6 @@
{ {
"name": "bin", "name": "bin",
"version": "1.1.5", "version": "1.2.0",
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "bin", "name": "bin",
"version": "1.1.5", "version": "1.2.0",
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,5 +1,5 @@
name: bin name: bin
version: 1.1.5 version: 1.2.0
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin. description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
author: author:
name: Defame1297 name: Defame1297

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
--- ---
source_keys: source_keys:
- context7-git-htmldocs - context7-git-htmldocs
- atlassian-gitflow-tutorial
--- ---
# Merging one branch into another # Merging one branch into another

View File

@@ -24,18 +24,19 @@ metadata:
| Operation | Run | | Operation | Run |
|---|---| |---|---|
| Create on an existing branch | `git worktree add <path> <branch>` | | Create on a branch that already exists locally | `git worktree add <path> <branch>` |
| Create on a new branch | `git worktree add -b <branch> <path>` | | Create on a new branch | `git worktree add -b <branch> <path>` |
| Create on the branch named after the path basename | `git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD | | Create on the branch named after the path basename | `git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD |
| Create and reset an existing branch to HEAD — discards its commits | `git worktree add -B <branch> <path>` | | Create and reset an existing branch to HEAD — discards its commits | `git worktree add -B <branch> <path>` |
| Create tracking a remote branch | `git worktree add <path> <remote>/<branch>` | | Create a local branch tracking a remote one | `git worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add <path> <branch>` expands to exactly this, but **only** when `<branch>` has no local copy (gate below) |
| Throwaway experiment, no branch | `git worktree add -d <path>` | | Throwaway experiment, no branch | `git worktree add -d <path>` — detached HEAD |
| **Never** `git worktree add <path> <remote>/<branch>` | That ref resolves, so the shortcut never fires and you get **a detached HEAD, no branch, no upstream**. Commits there go unreachable once HEAD moves, and `git push` needs an explicit refspec. Use the tracking row above |
| List | `git worktree list -v`, or `--porcelain -z` to parse | | List | `git worktree list -v`, or `--porcelain -z` to parse |
| Lock or unlock | `git worktree lock [--reason <str>] <path>` / `git worktree unlock <path>` | | Lock or unlock | `git worktree lock [--reason <str>] <path>` / `git worktree unlock <path>` |
| Move | `git worktree move <from> <to>` | | Move | `git worktree move <from> <to>` |
| Remove | `git worktree remove <path>` | | Remove | `git worktree remove <path>` |
| Prune stale metadata | `git worktree prune --dry-run`, then without the flag | | Prune stale metadata | `git worktree prune --dry-run`, then without the flag |
| Repair after a manual move | `git worktree repair [<path>]` — from the main worktree to fix all links, or from the moved worktree itself | | Repair after a manual move | `git worktree repair` — in the main worktree if *it* moved, or inside a linked worktree that moved. `git worktree repair <path>...` — from any worktree, naming each moved linked worktree's new path |
If the operation needs anything the table does not carry — the full `add` flag If the operation needs anything the table does not carry — the full `add` flag
table, orphan branches, sparse-checkout, locking for removable media, remote table, orphan branches, sparse-checkout, locking for removable media, remote
@@ -47,6 +48,7 @@ Gates:
- **`move`, `remove` — the main worktree cannot be moved or removed.** Only linked worktrees, the ones `git worktree add` created, are candidates. - **`move`, `remove` — the main worktree cannot be moved or removed.** Only linked worktrees, the ones `git worktree add` created, are candidates.
- **`add`, `move`, `remove` — escalate force flags one step at a time.** `-f` overrides a safeguard such as an unclean tree; `move` and `remove` need `-ff` on top of that when the worktree is locked. Confirm with the user before either — both discard state. - **`add`, `move`, `remove` — escalate force flags one step at a time.** `-f` overrides a safeguard such as an unclean tree; `move` and `remove` need `-ff` on top of that when the worktree is locked. Confirm with the user before either — both discard state.
- **`lock`, `move`, `remove`, `repair` — identify a worktree by full path, unique basename, or unique partial path.** An ambiguous name errors rather than picking; `git worktree list` shows the usable identifiers. - **`lock`, `move`, `remove`, `repair` — identify a worktree by full path, unique basename, or unique partial path.** An ambiguous name errors rather than picking; `git worktree list` shows the usable identifiers.
- **`add` — the bare-name tracking shortcut needs exactly one remote.** `git worktree add <path> <branch>` sets up tracking only when `<branch>` is absent locally, no `-b`/`-B`/`-d` is given, and exactly one remote carries the name. With several, it fires only if `checkout.defaultRemote` names one. When the remote is ambiguous or unknown, use `--track -b`.
- **`add` — lock at creation, not after.** `git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected. - **`add` — lock at creation, not after.** `git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected.
## Step 2 — Report ## Step 2 — Report

View File

@@ -15,16 +15,29 @@ or moved. Every other worktree is a **linked worktree** created by `git worktree
## `add` forms ## `add` forms
```bash ```bash
git worktree add <path> <branch> # check out an existing branch — non-destructive git worktree add <path> <branch> # <branch> exists locally: check it out — non-destructive
git worktree add -b <branch> <path> # create a new branch; fails if it exists git worktree add -b <branch> <path> # create a new branch; fails if it exists
git worktree add <path> # branch named after $(basename <path>): checked out git worktree add <path> # branch named after $(basename <path>): checked out
# if it exists, else created from HEAD # if it exists, else created from HEAD
git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD, git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD,
# discarding the commits it carried # discarding the commits it carried
git worktree add <path> <remote>/<branch> # track a remote branch git worktree add --track -b <branch> <path> <remote>/<branch>
# new local branch tracking the remote — always works
git worktree add <path> <branch> # <branch> absent locally and in exactly one remote:
# Git expands this to the --track -b form above
git worktree add -d <path> # detached HEAD, no branch git worktree add -d <path> # detached HEAD, no branch
``` ```
The same `git worktree add <path> <branch>` spelling appears twice above and does two
different things: it checks out a local branch when one exists, and only otherwise falls
through to the remote-tracking shortcut. Read the local branch list before relying on either.
**Do not write `git worktree add <path> <remote>/<branch>`.** A remote-tracking ref resolves as a
commit-ish, so the tracking shortcut never fires and the worktree lands on a **detached HEAD with
no local branch and no upstream** — commits there go unreachable once HEAD moves or the worktree is
removed, and `git push` fails without an explicit refspec. That spelling is correct only as the
final argument of the `--track -b` form.
## Full `add` flag table ## Full `add` flag table
| Flag | Meaning | | Flag | Meaning |
@@ -69,19 +82,40 @@ git worktree unlock <path> # when reconnected
## Remote-branch disambiguation ## Remote-branch disambiguation
```bash ```bash
git worktree add <path> <remote>/<branch> git worktree add --track -b <branch> <path> <remote>/<branch> # explicit: no guessing at all
git worktree add <path> <branch> # shortcut: needs one clear remote
``` ```
For ambiguous names across remotes, `checkout.defaultRemote` config disambiguates explicitly, or `--guess-remote` auto-matches by path basename (default controlled by `worktree.guessRemote` config). If a branch name matches multiple remotes during `worktree add` and neither is set, Git refuses rather than guessing. The shortcut fires only when `<branch>` is not found locally, none of `-b`/`-B`/`--detach` were
given, and a tracking branch of that name exists in exactly one remote. When several remotes carry
the name, `checkout.defaultRemote` picks one for disambiguation purposes; with no such setting the
shortcut has no single remote to resolve against and does not apply.
`--guess-remote` covers the *other* spelling — `git worktree add <path>` with no `<commit-ish>` at
all. It bases the new branch on the remote-tracking branch matching `$(basename <path>)` when
exactly one remote has it, and marks that branch as upstream. Its default comes from the
`worktree.guessRemote` config.
## Repair after a manual move ## Repair after a manual move
```bash ```bash
git worktree repair # run from the main worktree: fixes the links to every linked worktree git worktree repair # the MAIN worktree moved: run it there to reconnect every linked
git worktree repair <path> # run from a moved linked worktree: fixes its own pointer back to main # worktree back to the main worktree
git worktree repair # a LINKED worktree moved: run it inside that recently-moved worktree
git worktree repair <path>... # reconnect a specific linked worktree — runnable from any worktree,
# naming each moved tree's new path
``` ```
`repair` reestablishes the bidirectional pointers a manual move breaks, but only for the side it is Which form applies depends on what moved:
run from. Run it from the wrong directory and it reports nothing and fixes nothing.
| What moved | Remedy |
|---|---|
| The main worktree (or bare repo) | `git worktree repair` in the main worktree |
| One linked worktree | `git worktree repair` inside that worktree |
| Several linked worktrees | `git worktree repair <path>...` from any worktree, listing each new path |
| Both main and linked worktrees | `git worktree repair <path>...` in the main worktree, naming each linked worktree's new path — this restores the connections in both directions |
Only the no-argument form is tied to the current directory. The `<path>...` form is not — it
reestablishes the connection to every path you name, run from any worktree.
## Configuration ## Configuration
@@ -111,6 +145,10 @@ git worktree remove ../temp
context switch: context switch:
```bash ```bash
git worktree add ../review-pr-123 origin/feature-xyz git worktree add ../review-pr-123 origin/feature-xyz # detached HEAD — read-only review
git worktree add --track -b feature-xyz ../review-pr-123 origin/feature-xyz # if you will commit
# open ../review-pr-123 in a second editor window or terminal # open ../review-pr-123 in a second editor window or terminal
``` ```
Pick the second form the moment you intend to push anything back: the first leaves no branch to
push and no upstream to push to.

View File

@@ -20,6 +20,15 @@ allowed-tools: Bash Read
- The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks git commit -m "msg"`. A space after a comma silently skips nothing instead of erroring. - The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks git commit -m "msg"`. A space after a comma silently skips nothing instead of erroring.
- Never bypass a failing hook with `git commit --no-verify` (or `-n`). Hooks are the automated QA gate, so a bypassed commit pushes the failure downstream where it costs more — diagnose it instead. - Never bypass a failing hook with `git commit --no-verify` (or `-n`). Hooks are the automated QA gate, so a bypassed commit pushes the failure downstream where it costs more — diagnose it instead.
- `- files were modified by this hook` is not a bug. A fixer hook (`trailing-whitespace`, `end-of-file-fixer`, `pretty-format-json --autofix`) rewrote a staged file, so the staged snapshot is stale and the commit is blocked on purpose. The fix is to re-stage and re-run the same commit: `git add -u && git commit`. Do NOT reach for `pre-commit install -f` here — that flag overwrites hook files in `.git/hooks/` and has nothing to do with re-staging.
## Gate — `pre-commit clean`
Confirm with the user before running `pre-commit clean`, on every path that reaches it — including when it turns up as the fix for a stale or broken environment. It wipes the whole cache at `~/.cache/pre-commit`, which is machine-wide and shared by every repo on the box, forcing every hook environment to be re-downloaded.
> "This will wipe the entire pre-commit cache. All hook environments will be re-downloaded on next run. Proceed?"
`pre-commit gc` drops only unused environments and needs no confirmation — prefer it when the goal is just to reclaim disk.
## Route ## Route
@@ -36,7 +45,7 @@ Determine intent from the user's request, then execute the matching operation. W
| "autoupdate", "update versions", "bump revs" | `pre-commit autoupdate` — read `references/autoupdate.md` | | "autoupdate", "update versions", "bump revs" | `pre-commit autoupdate` — read `references/autoupdate.md` |
| "gc", "garbage collect" | `pre-commit gc` — drops unused cached environments only, safe at any time | | "gc", "garbage collect" | `pre-commit gc` — drops unused cached environments only, safe at any time |
| "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" | 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`. If the intent is ambiguous, default to `pre-commit run --all-files`.
@@ -47,5 +56,5 @@ Default to `pre-commit run --all-files`; never silently narrow to staged files.
When hooks fail: When hooks fail:
1. Name the hook and the specific cause. Be concrete — "gitleaks blocked `config.json` (high-entropy string on line 12)", not "gitleaks failed". 1. Name the hook and the specific cause. Be concrete — "gitleaks blocked `config.json` (high-entropy string on line 12)", not "gitleaks failed".
2. Suggest one concrete next step. If the cause is not obvious from the output, read `references/failure-patterns.md`. 2. Suggest one concrete next step. Common causes and their concrete fixes are in `references/failure-patterns.md` — read it whenever the output does not already name the fix.
3. Do not auto-fix code files, and do not edit `.pre-commit-config.yaml` — those belong to the user or to `pc-author`. 3. Do not auto-fix code files, and do not edit `.pre-commit-config.yaml` — those belong to the user or to `pc-author`.

View File

@@ -89,13 +89,14 @@ Fix: The user (or `pc-author`) must add `args: [--autofix]` to the hook override
Cause: A hook's cached environment is corrupted or out of date. Cause: A hook's cached environment is corrupted or out of date.
Fix: Fix: `pre-commit clean` is gated. It wipes the machine-wide cache at `~/.cache/pre-commit`, shared by every repo on the box, so get explicit confirmation before running it — "This will wipe the entire pre-commit cache. All hook environments will be re-downloaded on next run. Proceed?"
```bash ```bash
pre-commit clean # wipe all environments pre-commit clean # gated — confirm with the user first
pre-commit install-hooks # rebuild everything pre-commit install-hooks # rebuild everything
``` ```
Or less destructively: Or less destructively, needing no confirmation:
```bash ```bash
pre-commit gc # remove only unused environments pre-commit gc # remove only unused environments
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "git", "name": "git",
"version": "1.3.5", "version": "1.4.0",
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "git", "name": "git",
"version": "1.3.5", "version": "1.4.0",
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,5 +1,5 @@
name: git name: git
version: 1.3.5 version: 1.4.0
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it. description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
author: author:
name: Defame1297 name: Defame1297

View File

@@ -1,6 +1,7 @@
--- ---
source_keys: source_keys:
- context7-git-htmldocs - context7-git-htmldocs
- atlassian-gitflow-tutorial
--- ---
# Merging one branch into another # Merging one branch into another

View File

@@ -24,18 +24,19 @@ metadata:
| Operation | Run | | Operation | Run |
|---|---| |---|---|
| Create on an existing branch | `git worktree add <path> <branch>` | | Create on a branch that already exists locally | `git worktree add <path> <branch>` |
| Create on a new branch | `git worktree add -b <branch> <path>` | | Create on a new branch | `git worktree add -b <branch> <path>` |
| Create on the branch named after the path basename | `git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD | | Create on the branch named after the path basename | `git worktree add <path>` — checks that branch out if it exists, else creates it from HEAD |
| Create and reset an existing branch to HEAD — discards its commits | `git worktree add -B <branch> <path>` | | Create and reset an existing branch to HEAD — discards its commits | `git worktree add -B <branch> <path>` |
| Create tracking a remote branch | `git worktree add <path> <remote>/<branch>` | | Create a local branch tracking a remote one | `git worktree add --track -b <branch> <path> <remote>/<branch>` — always correct. `git worktree add <path> <branch>` expands to exactly this, but **only** when `<branch>` has no local copy (gate below) |
| Throwaway experiment, no branch | `git worktree add -d <path>` | | Throwaway experiment, no branch | `git worktree add -d <path>` — detached HEAD |
| **Never** `git worktree add <path> <remote>/<branch>` | That ref resolves, so the shortcut never fires and you get **a detached HEAD, no branch, no upstream**. Commits there go unreachable once HEAD moves, and `git push` needs an explicit refspec. Use the tracking row above |
| List | `git worktree list -v`, or `--porcelain -z` to parse | | List | `git worktree list -v`, or `--porcelain -z` to parse |
| Lock or unlock | `git worktree lock [--reason <str>] <path>` / `git worktree unlock <path>` | | Lock or unlock | `git worktree lock [--reason <str>] <path>` / `git worktree unlock <path>` |
| Move | `git worktree move <from> <to>` | | Move | `git worktree move <from> <to>` |
| Remove | `git worktree remove <path>` | | Remove | `git worktree remove <path>` |
| Prune stale metadata | `git worktree prune --dry-run`, then without the flag | | Prune stale metadata | `git worktree prune --dry-run`, then without the flag |
| Repair after a manual move | `git worktree repair [<path>]` — from the main worktree to fix all links, or from the moved worktree itself | | Repair after a manual move | `git worktree repair` — in the main worktree if *it* moved, or inside a linked worktree that moved. `git worktree repair <path>...` — from any worktree, naming each moved linked worktree's new path |
If the operation needs anything the table does not carry — the full `add` flag If the operation needs anything the table does not carry — the full `add` flag
table, orphan branches, sparse-checkout, locking for removable media, remote table, orphan branches, sparse-checkout, locking for removable media, remote
@@ -47,6 +48,7 @@ Gates:
- **`move`, `remove` — the main worktree cannot be moved or removed.** Only linked worktrees, the ones `git worktree add` created, are candidates. - **`move`, `remove` — the main worktree cannot be moved or removed.** Only linked worktrees, the ones `git worktree add` created, are candidates.
- **`add`, `move`, `remove` — escalate force flags one step at a time.** `-f` overrides a safeguard such as an unclean tree; `move` and `remove` need `-ff` on top of that when the worktree is locked. Confirm with the user before either — both discard state. - **`add`, `move`, `remove` — escalate force flags one step at a time.** `-f` overrides a safeguard such as an unclean tree; `move` and `remove` need `-ff` on top of that when the worktree is locked. Confirm with the user before either — both discard state.
- **`lock`, `move`, `remove`, `repair` — identify a worktree by full path, unique basename, or unique partial path.** An ambiguous name errors rather than picking; `git worktree list` shows the usable identifiers. - **`lock`, `move`, `remove`, `repair` — identify a worktree by full path, unique basename, or unique partial path.** An ambiguous name errors rather than picking; `git worktree list` shows the usable identifiers.
- **`add` — the bare-name tracking shortcut needs exactly one remote.** `git worktree add <path> <branch>` sets up tracking only when `<branch>` is absent locally, no `-b`/`-B`/`-d` is given, and exactly one remote carries the name. With several, it fires only if `checkout.defaultRemote` names one. When the remote is ambiguous or unknown, use `--track -b`.
- **`add` — lock at creation, not after.** `git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected. - **`add` — lock at creation, not after.** `git worktree add --lock` is atomic, where add-then-`lock` leaves a window in which the worktree is unprotected.
## Step 2 — Report ## Step 2 — Report

View File

@@ -15,16 +15,29 @@ or moved. Every other worktree is a **linked worktree** created by `git worktree
## `add` forms ## `add` forms
```bash ```bash
git worktree add <path> <branch> # check out an existing branch — non-destructive git worktree add <path> <branch> # <branch> exists locally: check it out — non-destructive
git worktree add -b <branch> <path> # create a new branch; fails if it exists git worktree add -b <branch> <path> # create a new branch; fails if it exists
git worktree add <path> # branch named after $(basename <path>): checked out git worktree add <path> # branch named after $(basename <path>): checked out
# if it exists, else created from HEAD # if it exists, else created from HEAD
git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD, git worktree add -B <branch> <path> # create the branch, or reset an existing one to HEAD,
# discarding the commits it carried # discarding the commits it carried
git worktree add <path> <remote>/<branch> # track a remote branch git worktree add --track -b <branch> <path> <remote>/<branch>
# new local branch tracking the remote — always works
git worktree add <path> <branch> # <branch> absent locally and in exactly one remote:
# Git expands this to the --track -b form above
git worktree add -d <path> # detached HEAD, no branch git worktree add -d <path> # detached HEAD, no branch
``` ```
The same `git worktree add <path> <branch>` spelling appears twice above and does two
different things: it checks out a local branch when one exists, and only otherwise falls
through to the remote-tracking shortcut. Read the local branch list before relying on either.
**Do not write `git worktree add <path> <remote>/<branch>`.** A remote-tracking ref resolves as a
commit-ish, so the tracking shortcut never fires and the worktree lands on a **detached HEAD with
no local branch and no upstream** — commits there go unreachable once HEAD moves or the worktree is
removed, and `git push` fails without an explicit refspec. That spelling is correct only as the
final argument of the `--track -b` form.
## Full `add` flag table ## Full `add` flag table
| Flag | Meaning | | Flag | Meaning |
@@ -69,19 +82,40 @@ git worktree unlock <path> # when reconnected
## Remote-branch disambiguation ## Remote-branch disambiguation
```bash ```bash
git worktree add <path> <remote>/<branch> git worktree add --track -b <branch> <path> <remote>/<branch> # explicit: no guessing at all
git worktree add <path> <branch> # shortcut: needs one clear remote
``` ```
For ambiguous names across remotes, `checkout.defaultRemote` config disambiguates explicitly, or `--guess-remote` auto-matches by path basename (default controlled by `worktree.guessRemote` config). If a branch name matches multiple remotes during `worktree add` and neither is set, Git refuses rather than guessing. The shortcut fires only when `<branch>` is not found locally, none of `-b`/`-B`/`--detach` were
given, and a tracking branch of that name exists in exactly one remote. When several remotes carry
the name, `checkout.defaultRemote` picks one for disambiguation purposes; with no such setting the
shortcut has no single remote to resolve against and does not apply.
`--guess-remote` covers the *other* spelling — `git worktree add <path>` with no `<commit-ish>` at
all. It bases the new branch on the remote-tracking branch matching `$(basename <path>)` when
exactly one remote has it, and marks that branch as upstream. Its default comes from the
`worktree.guessRemote` config.
## Repair after a manual move ## Repair after a manual move
```bash ```bash
git worktree repair # run from the main worktree: fixes the links to every linked worktree git worktree repair # the MAIN worktree moved: run it there to reconnect every linked
git worktree repair <path> # run from a moved linked worktree: fixes its own pointer back to main # worktree back to the main worktree
git worktree repair # a LINKED worktree moved: run it inside that recently-moved worktree
git worktree repair <path>... # reconnect a specific linked worktree — runnable from any worktree,
# naming each moved tree's new path
``` ```
`repair` reestablishes the bidirectional pointers a manual move breaks, but only for the side it is Which form applies depends on what moved:
run from. Run it from the wrong directory and it reports nothing and fixes nothing.
| What moved | Remedy |
|---|---|
| The main worktree (or bare repo) | `git worktree repair` in the main worktree |
| One linked worktree | `git worktree repair` inside that worktree |
| Several linked worktrees | `git worktree repair <path>...` from any worktree, listing each new path |
| Both main and linked worktrees | `git worktree repair <path>...` in the main worktree, naming each linked worktree's new path — this restores the connections in both directions |
Only the no-argument form is tied to the current directory. The `<path>...` form is not — it
reestablishes the connection to every path you name, run from any worktree.
## Configuration ## Configuration
@@ -111,6 +145,10 @@ git worktree remove ../temp
context switch: context switch:
```bash ```bash
git worktree add ../review-pr-123 origin/feature-xyz git worktree add ../review-pr-123 origin/feature-xyz # detached HEAD — read-only review
git worktree add --track -b feature-xyz ../review-pr-123 origin/feature-xyz # if you will commit
# open ../review-pr-123 in a second editor window or terminal # open ../review-pr-123 in a second editor window or terminal
``` ```
Pick the second form the moment you intend to push anything back: the first leaves no branch to
push and no upstream to push to.

View File

@@ -20,6 +20,15 @@ allowed-tools: Bash Read
- The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks git commit -m "msg"`. A space after a comma silently skips nothing instead of erroring. - The `SKIP` env var takes exact hook `id` values, comma-separated with no spaces: `SKIP=check-yaml,gitleaks git commit -m "msg"`. A space after a comma silently skips nothing instead of erroring.
- Never bypass a failing hook with `git commit --no-verify` (or `-n`). Hooks are the automated QA gate, so a bypassed commit pushes the failure downstream where it costs more — diagnose it instead. - Never bypass a failing hook with `git commit --no-verify` (or `-n`). Hooks are the automated QA gate, so a bypassed commit pushes the failure downstream where it costs more — diagnose it instead.
- `- files were modified by this hook` is not a bug. A fixer hook (`trailing-whitespace`, `end-of-file-fixer`, `pretty-format-json --autofix`) rewrote a staged file, so the staged snapshot is stale and the commit is blocked on purpose. The fix is to re-stage and re-run the same commit: `git add -u && git commit`. Do NOT reach for `pre-commit install -f` here — that flag overwrites hook files in `.git/hooks/` and has nothing to do with re-staging.
## Gate — `pre-commit clean`
Confirm with the user before running `pre-commit clean`, on every path that reaches it — including when it turns up as the fix for a stale or broken environment. It wipes the whole cache at `~/.cache/pre-commit`, which is machine-wide and shared by every repo on the box, forcing every hook environment to be re-downloaded.
> "This will wipe the entire pre-commit cache. All hook environments will be re-downloaded on next run. Proceed?"
`pre-commit gc` drops only unused environments and needs no confirmation — prefer it when the goal is just to reclaim disk.
## Route ## Route
@@ -36,7 +45,7 @@ Determine intent from the user's request, then execute the matching operation. W
| "autoupdate", "update versions", "bump revs" | `pre-commit autoupdate` — read `references/autoupdate.md` | | "autoupdate", "update versions", "bump revs" | `pre-commit autoupdate` — read `references/autoupdate.md` |
| "gc", "garbage collect" | `pre-commit gc` — drops unused cached environments only, safe at any time | | "gc", "garbage collect" | `pre-commit gc` — drops unused cached environments only, safe at any time |
| "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" | 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`. If the intent is ambiguous, default to `pre-commit run --all-files`.
@@ -47,5 +56,5 @@ Default to `pre-commit run --all-files`; never silently narrow to staged files.
When hooks fail: When hooks fail:
1. Name the hook and the specific cause. Be concrete — "gitleaks blocked `config.json` (high-entropy string on line 12)", not "gitleaks failed". 1. Name the hook and the specific cause. Be concrete — "gitleaks blocked `config.json` (high-entropy string on line 12)", not "gitleaks failed".
2. Suggest one concrete next step. If the cause is not obvious from the output, read `references/failure-patterns.md`. 2. Suggest one concrete next step. Common causes and their concrete fixes are in `references/failure-patterns.md` — read it whenever the output does not already name the fix.
3. Do not auto-fix code files, and do not edit `.pre-commit-config.yaml` — those belong to the user or to `pc-author`. 3. Do not auto-fix code files, and do not edit `.pre-commit-config.yaml` — those belong to the user or to `pc-author`.

View File

@@ -89,13 +89,14 @@ Fix: The user (or `pc-author`) must add `args: [--autofix]` to the hook override
Cause: A hook's cached environment is corrupted or out of date. Cause: A hook's cached environment is corrupted or out of date.
Fix: Fix: `pre-commit clean` is gated. It wipes the machine-wide cache at `~/.cache/pre-commit`, shared by every repo on the box, so get explicit confirmation before running it — "This will wipe the entire pre-commit cache. All hook environments will be re-downloaded on next run. Proceed?"
```bash ```bash
pre-commit clean # wipe all environments pre-commit clean # gated — confirm with the user first
pre-commit install-hooks # rebuild everything pre-commit install-hooks # rebuild everything
``` ```
Or less destructively: Or less destructively, needing no confirmation:
```bash ```bash
pre-commit gc # remove only unused environments pre-commit gc # remove only unused environments
``` ```

View File

@@ -25,7 +25,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
## Gotchas ## Gotchas
- **`list_issues` returns PRs too.** It has no `type` filter and both share one repo number space. `is_pull` appears only on `issue_read method: "get"`, never on a list item — check it there before treating a number as an issue. - **`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.
- **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 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 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.
@@ -46,7 +46,7 @@ One invocation takes one row. Read only the reference(s) that row names — the
| Invocation | Flow | Read | | Invocation | Flow | Read |
|---|---|---| |---|---|---|
| `/gitea-issues` or `/gitea-issues list` | List issues, optionally filtered by state | `references/issues.md` | | `/gitea-issues` or `/gitea-issues list` | List issues with `type: "issues"` so PRs are excluded, optionally filtered by state | `references/issues.md` |
| `/gitea-issues <N>` | Get one issue, routing to `gitea-prs` when the number turns out to be a PR | `references/issues.md` | | `/gitea-issues <N>` | Get one issue, routing to `gitea-prs` when the number turns out to be a PR | `references/issues.md` |
| `/gitea-issues <N> comments` | Get an issue's comments | `references/issues.md` | | `/gitea-issues <N> comments` | Get an issue's comments | `references/issues.md` |
| `/gitea-issues <N> labels` | Get an issue's labels as full objects, IDs included | `references/issues.md` | | `/gitea-issues <N> labels` | Get an issue's labels as full objects, IDs included | `references/issues.md` |

View File

@@ -7,11 +7,13 @@ source_keys:
# Issue operations # Issue operations
Call signatures below were verified live against the deployed `gitea-mcp` server via `ToolSearch` Call signatures below were verified live against the deployed `gitea-mcp` server via `ToolSearch`,
at authoring time, not copied from `api-reference.md` — this is deliberate: research docs are not copied from `api-reference.md` — this is deliberate: research docs are generated from source at
generated from source at a point in time and can drift from the server actually deployed (see the a point in time and can drift from the server actually deployed. Last verified against gitea-mcp
`list_issues` gotcha below, which is the exact drift this policy exists to catch). Re-verify against **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Drift runs in both directions: this file
the live schema if these tools appear to behave differently than documented here. previously recorded `list_issues` as having neither a `type` nor a `milestones` parameter, and
v1.7.0 has both. Re-verify against the live schema if these tools appear to behave differently than
documented here.
## `list_issues` ## `list_issues`
@@ -21,18 +23,22 @@ the live schema if these tools appear to behave differently than documented here
- `state` (string, optional, default `"all"`) — conventional values `"open"`/`"closed"`/`"all"`, not - `state` (string, optional, default `"all"`) — conventional values `"open"`/`"closed"`/`"all"`, not
schema-enforced as an enum schema-enforced as an enum
- `labels` (array of strings, optional) — filter by label *name* (not ID) - `labels` (array of strings, optional) — filter by label *name* (not ID)
- `milestones` (array of strings, optional) — filter by milestone name or numeric ID, both passed as
strings
- `type` (string, enum `"issues"` | `"pulls"`, optional) — omit it and the response mixes both
- `since` (string, optional) — ISO 8601, issues updated after this time - `since` (string, optional) — ISO 8601, issues updated after this time
- `before` (string, optional) — ISO 8601, issues updated before this time - `before` (string, optional) — ISO 8601, issues updated before this time
- `page` (number, optional, default `1`) - `page` (number, optional, default `1`)
- `per_page` (number, optional, default `30`) - `per_page` (number, optional, default `30`)
**There is no `type` parameter and no `milestones` parameter**, despite both appearing in **Pass `type: "issues"` on any listing meant to show issues.** Issues and PRs share one repo number
`api-reference.md`. This tool cannot filter issues-vs-PRs or by milestone — see the Gotchas section space and the unfiltered response interleaves them; the only thing distinguishing them on a list
of SKILL.md for the consequence (PR entries can appear in results with no way to exclude them here). item is the `html_url` path segment (`/issues/` vs `/pulls/`), since `is_pull` is not returned on
list items — see the Gotchas section of SKILL.md.
**Call:** **Call:**
``` ```
list_issues owner: <owner> repo: <repo> state: "open" list_issues owner: <owner> repo: <repo> state: "open" type: "issues"
``` ```
**Response (list item):** `number`, `title`, `state`, `html_url`, `user`, `comments`, `created_at`, **Response (list item):** `number`, `title`, `state`, `html_url`, `user`, `comments`, `created_at`,
@@ -117,6 +123,13 @@ issue_write method: "add_labels" owner: <owner> repo: <repo> issue_number: <N> l
To replace all labels atomically instead of adding: `method: "replace_labels"`. To replace all labels atomically instead of adding: `method: "replace_labels"`.
To remove one: `method: "remove_label" label_id: <single ID>`. To remove one: `method: "remove_label" label_id: <single ID>`.
**Default to `add_labels`.** `replace_labels` clears every label not in the array, so it drops
labels the caller never mentioned. Reach for it only when the caller asked for the issue's label
set to become exactly what they listed. In particular, do not use it to enforce one-label-per-scope:
exclusivity is a per-label property — the server drops the sibling itself for a label whose
`exclusive` field is `true`, and a label whose `exclusive` is `false` (every `Kind/*` on this
instance) is legitimately stackable. See `gitea-labels-milestones` for how to read that field.
## Token scope ## Token scope
All of `list_issues`, `issue_read`, and `issue_write` are verified working under a token holding All of `list_issues`, `issue_read`, and `issue_write` are verified working under a token holding

View File

@@ -13,8 +13,8 @@ time (see `references/sources.md`) — confirmed to match `api-reference.md`.
**Parameters:** **Parameters:**
- `query` (string, required) — the only hard-required parameter - `query` (string, required) — the only hard-required parameter
- `state` (string, enum `"open"` | `"closed"` | `"all"`, optional) - `state` (string, enum `"open"` | `"closed"` | `"all"`, optional)
- `type` (string, enum `"issues"` | `"pulls"`, optional) — **this tool has a working type filter**, - `type` (string, enum `"issues"` | `"pulls"`, optional) — the same filter, with the same values,
unlike `list_issues` (see `references/issues.md`) that `list_issues` takes (see `references/issues.md`)
- `labels` (string, optional) — comma-separated label **names** — a plain string, not the array form - `labels` (string, optional) — comma-separated label **names** — a plain string, not the array form
`list_issues` uses `list_issues` uses
- `owner` (string, optional) — restrict results to one owner - `owner` (string, optional) — restrict results to one owner

View File

@@ -1,12 +1,13 @@
# Sources # Sources
**Note on call signatures:** per `docs/adr/0011-gitea-skill-deep-modules.md`, the tool parameter **Note on call signatures:** per `docs/adr/0011-gitea-skill-deep-modules.md`, the tool parameter
signatures in `references/issues.md` and `references/search.md` were re-verified live via signatures in `references/issues.md` and `references/search.md` are re-verified live via
`ToolSearch` against the deployed `gitea-mcp` server at authoring time — they are not copied `ToolSearch` against the deployed `gitea-mcp` server — they are not copied verbatim from
verbatim from `api-reference.md`. This resolves issue #6 comment #849's root-cause finding that a `api-reference.md`. This resolves issue #6 comment #849's root-cause finding that a prior skill was
prior skill was authored from API docs that had drifted from the actual MCP tool schema; the live authored from API docs that had drifted from the actual MCP tool schema. That re-verification is
check caught exactly this drift on `list_issues` (see `references/issues.md` — the research doc ongoing, not one-off: an earlier live check recorded `list_issues` as lacking the `type` and
documents a `type` and a `milestones` parameter that do not exist on the deployed server). `milestones` parameters `api-reference.md` documents, and both are present on the deployed
gitea-mcp **v1.7.0**, which is the version these signatures are current as of.
## gitea-mcp-repo ## gitea-mcp-repo

View File

@@ -24,7 +24,7 @@ allowed-tools: Bash mcp__gitea__label_read mcp__gitea__label_write mcp__gitea__m
- **Applying a label takes a numeric ID, but issue/PR responses slim labels down to name strings.** An issue's existing labels yield no IDs — resolve name → ID with `label_read`. - **Applying a label takes a numeric ID, but issue/PR responses slim labels down to name strings.** An issue's existing labels yield no IDs — resolve name → ID with `label_read`.
- **`pull_request_read` returns `milestone` as a bare title string** where `issue_read` returns `{id, title}` — recover the milestone's ID by listing milestones and matching the title. - **`pull_request_read` returns `milestone` as a bare title string** where `issue_read` returns `{id, title}` — recover the milestone's ID by listing milestones and matching the title.
- **`Kind/*`/`Priority/*`/`Status/*` exclusivity is a client-side convention.** `exclusive` is an org-labels-only flag, so applying a label in such a scope must replace the one already there, not stack on it. - **Never assume a `Kind/*`/`Priority/*`/`Status/*` scope is exclusive — read each label's own `exclusive` field.** `list_repo_labels` returns it per repo label, it is not org-only, and where it is `true` Gitea enforces one-per-scope itself. Replacing rather than stacking on a label whose `exclusive` is `false` destroys a valid label.
## Step 1 — Resolve owner, repo and org ## Step 1 — Resolve owner, repo and org
@@ -36,7 +36,9 @@ git remote get-url origin
If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL." If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
The `*_org_label*` methods take `org`, not `owner`/`repo`. Pass that same `owner` as `org` — it is the org name whenever the owner is an organisation, and the remote URL does not say whether it is one. So let the call itself decide: a failure means the owner is a user account with no org label pool, which is an answer, not an error to report. The `*_org_label*` methods take `org`, not `owner`/`repo`. Pass that same `owner` as `org` — it is the org name whenever the owner is an organisation, and the remote URL does not say whether it is one.
Read the failure text before interpreting it. `list_org_labels` needs the `read:organization` token scope, which this skill's declared scopes (`write:issue`, `write:repository`) do not carry, so it fails with `token does not have at least one of required scope(s), required=[read:organization]` *before* it ever determines org-vs-user. Report that: the org pool went unchecked, not empty. Only a not-found response is evidence the owner is a user account with no org pool.
## Step 2 — Dispatch ## Step 2 — Dispatch

View File

@@ -12,19 +12,24 @@ description) to this repo's `Kind/*` / `Priority/*` / `Status/*` label taxonomy.
`gitea-issues` and `gitea-prs` before creating or updating an issue/PR, and directly when the user `gitea-issues` and `gitea-prs` before creating or updating an issue/PR, and directly when the user
asks to label something without naming exact labels. asks to label something without naming exact labels.
## Scoped labels are mutually exclusive — replace, don't stack ## Exclusivity is per label — read it, never infer it
Each of `Kind/*`, `Priority/*`, `Status/*` is treated as a scoped-label group by convention (the `/` Gitea's `exclusive` flag is a real per-label boolean returned by `list_repo_labels`, and where it is
delimiter naming pattern). Gitea's `exclusive` flag — the mechanism that would let the server itself `true` the server enforces one-label-per-scope itself. It is not an org-only setting, and the `/`
enforce one-label-per-scope — is documented as an org-labels-only setting, and the repo-level delimiter in a name says nothing about it. Verified on `Defame1297/holocron`: every `Priority/*`,
`label_write` methods used here don't accept it at all. So exclusivity within these scopes is a `Reviewed/*` and `Status/*` label is `exclusive: true`, while every `Kind/*` label — and
convention this skill enforces client-side, not something the server guarantees: applying a new `Compat/Breaking` — is `exclusive: false` and is used stacked.
label within a scope is expected to replace any existing label in that same scope on the target
issue/PR, not add alongside it. When inference So read each candidate label's own `exclusive` value from the resolution call and branch on it:
selects a `Priority/High` label and the issue already carries `Priority/Medium`, the write should
result in only `Priority/High` remaining — use `replace_labels` scoped to that group's labels, or at - **`exclusive: true`** — the server drops the sibling on write. Add the label and let it; do not
minimum remove the superseded label before adding the new one. Never leave two labels from the same pre-remove the label already there, and do not compute a replacement set client-side. Inferring
scope applied at once. `Priority/High` onto an issue carrying `Priority/Medium` needs no special handling.
- **`exclusive: false`** — **add alongside, never replace.** Stripping a co-existing label in the
same scope destroys a valid one: an issue can legitimately carry `Kind/Bug` and `Kind/Security`
at once.
There is no client-side exclusivity convention for this skill to enforce.
## Signal → label mapping ## Signal → label mapping
@@ -57,16 +62,18 @@ scope applied at once.
1. Read the conversation context (issue/PR title, body, or the triggering discussion) for the 1. Read the conversation context (issue/PR title, body, or the triggering discussion) for the
signals above. signals above.
2. Call `label_read method: "list_repo_labels"` (see `references/labels.md`) to get the current 2. Call `label_read method: "list_repo_labels"` (see `references/labels.md`) to get the current
label set with IDs — inference must never guess an ID, only a name, then resolve it. Because label set with IDs and each label's `exclusive` value — inference must never guess an ID, only a
`exclusive` is an org-labels-only flag, this taxonomy plausibly lives at org scope too: for any name, then resolve it. Both pools can apply to one issue, so for any inferred name absent from
inferred name absent from the repo pool, also call `label_read method: "list_org_labels"` with the repo pool, also call `label_read method: "list_org_labels"` with `org` set to the repo's
`org` set to the repo's `owner` before treating it as unresolved. A failure there means the owner `owner` before treating it as unresolved. Read that call's failure text: a
is a user account, not an organisation, so no org pool exists and the name is genuinely absent. `required=[read:organization]` scope error means the org pool was never queried — report the
3. Match inferred label names against the resolved list (case-insensitive). If a scope group missing token scope rather than reporting the label unresolved. Only a not-found response means
already has a different label applied on the target and a new one is inferred for that same there is no org pool and the name is genuinely absent.
scope, plan to replace rather than add (see above). 3. Match inferred label names against the resolved list (case-insensitive) and carry each match's
`exclusive` value forward: `true` means the server replaces the sibling on write, `false` means
the label is added alongside whatever is already applied (see above).
4. **Low-confidence inference omits the label.** If no signal confidently maps to a `Kind/*` value, 4. **Low-confidence inference omits the label.** If no signal confidently maps to a `Kind/*` value,
do not guess — omit `Kind/*` entirely rather than default to one. `Priority/Medium` is the one do not guess — omit `Kind/*` entirely rather than default to one. `Priority/Medium` is the one
exception: it's the explicit default when no urgency signal is present, not a guess. exception: it's the explicit default when no urgency signal is present, not a guess.
5. Hand the resolved IDs (plus which scopes to replace) to the caller's `issue_write`/ 5. Hand the resolved IDs, each with its `exclusive` value, to the caller's `issue_write`/
`pull_request_write` call — this skill does not apply labels to an issue or PR itself. `pull_request_write` call — this skill does not apply labels to an issue or PR itself.

View File

@@ -37,7 +37,7 @@ or **org-scoped** labels — never both in one call. Pick the method family (`*_
| `name` | string | required for create | | `name` | string | required for create |
| `color` | string | hex `#RRGGBB`, required for create | | `color` | string | hex `#RRGGBB`, required for create |
| `description` | string | optional | | `description` | string | optional |
| `exclusive` | boolean | org labels only | | `exclusive` | boolean | accepted as a *write* param on org creates only — repo labels still carry and enforce `exclusive`, set outside this tool surface |
| `is_archived` | boolean | repo labels only | | `is_archived` | boolean | repo labels only |
Note: unlike `milestone_read`/`milestone_write`, `owner`/`repo`/`org` are **not** schema-required on Note: unlike `milestone_read`/`milestone_write`, `owner`/`repo`/`org` are **not** schema-required on
@@ -53,6 +53,12 @@ label_read method: "list_repo_labels" owner: <owner> repo: <repo> per_page:
Paginate (`page: 1, 2, ...`) until the returned count is less than `per_page`. This is the only way Paginate (`page: 1, 2, ...`) until the returned count is less than `per_page`. This is the only way
to build a complete name → ID map — there is no lookup-by-name endpoint. to build a complete name → ID map — there is no lookup-by-name endpoint.
Every returned repo label carries its own `exclusive` boolean; the field is not org-only. Verified on
`Defame1297/holocron`: all `Priority/*`, `Reviewed/*` and `Status/*` labels are `exclusive: true`,
while all `Kind/*` labels and `Compat/Breaking` are `exclusive: false`. Where it is `true` Gitea
enforces one-label-per-scope server-side; where it is `false` labels in that scope stack legitimately.
Read the field — never infer exclusivity from the `/` in a name.
## Get one label ## Get one label
``` ```
@@ -65,8 +71,10 @@ There is no direct name lookup. List all repo labels (paginating if needed), sca
case-insensitive name match, and extract `id`. Both pools can apply to one issue: if the name is case-insensitive name match, and extract `id`. Both pools can apply to one issue: if the name is
not in `list_repo_labels`, also check `list_org_labels` before reporting it unresolved. That method not in `list_repo_labels`, also check `list_org_labels` before reporting it unresolved. That method
takes `org`, not `owner`/`repo` — pass the repo's `owner` as `org`, which is what it means when the takes `org`, not `owner`/`repo` — pass the repo's `owner` as `org`, which is what it means when the
owner is an organisation. If that call fails, the owner is a user account, there is no org pool, and owner is an organisation. Its failure modes are not interchangeable. `token does not have at least
the miss is a real miss. one of required scope(s), required=[read:organization]` means the org pool was never queried — report
that missing scope rather than reporting the label unresolved. Only a not-found response means the
owner is a user account with no org pool, making the miss a real miss.
Resolution is the required first step before any label application on an issue or PR — the actual Resolution is the required first step before any label application on an issue or PR — the actual
`add_labels`/`replace_labels`/`remove_label` call lives in `gitea-issues`/`gitea-prs` via `add_labels`/`replace_labels`/`remove_label` call lives in `gitea-issues`/`gitea-prs` via
@@ -83,7 +91,10 @@ label_write method: "create_repo_label"
``` ```
For an org label, use `method: "create_org_label"` with `org:` instead of `owner`/`repo`, and For an org label, use `method: "create_org_label"` with `org:` instead of `owner`/`repo`, and
`exclusive: true` if the label belongs to a mutually-exclusive scope group. `exclusive: true` if the label belongs to a mutually-exclusive scope group. `label_write` accepts
`exclusive` on org methods only, so a repo label's exclusivity cannot be set or cleared through this
tool — it is set in the Gitea UI or against the REST API directly, and read back via
`list_repo_labels`.
## Edit a label ## Edit a label

View File

@@ -7,7 +7,7 @@ source_keys:
# Pull request read/write execution detail # Pull request read/write execution detail
Parameter signatures below are cross-checked live against the deployed gitea-mcp server tool schemas at authoring time — not copied verbatim from the plugin's research doc for this domain, which has a known history of drifting from the deployed server (e.g. a prior `type` parameter that no longer exists on `list_issues`). These files were last verified against gitea-mcp **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Re-verify via `ToolSearch` before trusting this file if the deployed version differs — drift has bitten this skill in both directions, adding methods it does not list and fixing quirks it still warns about. Parameter signatures below are cross-checked live against the deployed gitea-mcp server tool schemas at authoring time — not copied verbatim from the plugin's research doc for this domain, which has a known history of drifting from the deployed server. These files were last verified against gitea-mcp **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Re-verify via `ToolSearch` before trusting this file if the deployed version differs — drift has bitten this skill in both directions, adding methods it does not list and fixing quirks it still warns about.
## `list_pull_requests` ## `list_pull_requests`

View File

@@ -18,7 +18,7 @@ metadata:
## Gotchas ## Gotchas
- **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives. - **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives.
- **`is_draft`/`is_pre_release` are booleans the caller sets — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected. - **Set `is_draft`/`is_pre_release` explicitly on every `create_release` — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** A tag named `v2.0.0-beta.1` publishes as a full release, and becomes the repo's latest, unless `is_pre_release: true` is passed in the same call. The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected.
- **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs. - **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs.
## Dispatch table ## Dispatch table
@@ -37,11 +37,13 @@ metadata:
`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. Semver with a `v` prefix is a tooling convention, not a Gitea constraint — the API accepts any string — so never validate or rewrite it.
## Workflow ## Workflow
- [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, and `title`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it. - [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. **There is no update or edit tool on this surface**: `create_release`, `get_release`, `get_latest_release`, `list_releases` and `delete_release` are the whole set. A release published with the wrong flag therefore has no non-destructive repair — the only fix is `delete_release` plus a fresh `create_release`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it.
- [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first. - [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first.
- [ ] **Deleting 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. - [ ] **Deleting 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.
- [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. - [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates.
If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`. If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, draft/prerelease semantics, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`.

View File

@@ -32,7 +32,7 @@ for brevity.
**`get_latest_release`** **`get_latest_release`**
- No parameters beyond `owner`/`repo`. - No parameters beyond `owner`/`repo`.
- Returns a single release object for the most recently published release. It is assumed (by analogy with typical "latest release" semantics) that this excludes drafts and prereleases, but that exclusion is not directly confirmed by any of the research docs — verify with `list_releases` if the caller depends on this. - Returns a single release object for the most recently published release. The deployed tool's own description reads "the most recent published (non-draft) release" — it names drafts as excluded and is silent on prereleases, so whether prereleases are also excluded is *unconfirmed*; verify with `list_releases` if the caller depends on it. Either way a release created without `is_pre_release: true` is eligible, which is why that flag has to be set in the create call (see `conventions.md`).
**`create_release`** **`create_release`**
- Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string) - Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string)

View File

@@ -33,6 +33,13 @@ commonly used to signal a prerelease to humans. When a user asks to "cut a beta"
release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on
the tag string to carry that meaning. the tag string to carry that meaning.
Getting this wrong is not cheaply repairable. The gitea-mcp release surface is `create_release`,
`get_release`, `get_latest_release`, `list_releases` and `delete_release` — there is **no update or
edit tool**, so a published release's flags cannot be corrected in place. The only remedy is
`delete_release` plus a fresh `create_release`, a destructive round trip; meanwhile a beta published
without `is_pre_release` is the release `get_latest_release` returns. Set the flags in the create
call.
Note the input/output naming mismatch: the input param is `is_draft`, but the release object Note the input/output naming mismatch: the input param is `is_draft`, but the release object
returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid
input key — passing `draft: true` to `create_release` is silently ignored rather than erroring. input key — passing `draft: true` to `create_release` is silently ignored rather than erroring.

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
## Gotchas ## Gotchas
- **`list_issues` returns PRs too.** It has no `type` filter and both share one repo number space. `is_pull` appears only on `issue_read method: "get"`, never on a list item — check it there before treating a number as an issue. - **`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.
- **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 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 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.
@@ -46,7 +46,7 @@ One invocation takes one row. Read only the reference(s) that row names — the
| Invocation | Flow | Read | | Invocation | Flow | Read |
|---|---|---| |---|---|---|
| `/gitea-issues` or `/gitea-issues list` | List issues, optionally filtered by state | `references/issues.md` | | `/gitea-issues` or `/gitea-issues list` | List issues with `type: "issues"` so PRs are excluded, optionally filtered by state | `references/issues.md` |
| `/gitea-issues <N>` | Get one issue, routing to `gitea-prs` when the number turns out to be a PR | `references/issues.md` | | `/gitea-issues <N>` | Get one issue, routing to `gitea-prs` when the number turns out to be a PR | `references/issues.md` |
| `/gitea-issues <N> comments` | Get an issue's comments | `references/issues.md` | | `/gitea-issues <N> comments` | Get an issue's comments | `references/issues.md` |
| `/gitea-issues <N> labels` | Get an issue's labels as full objects, IDs included | `references/issues.md` | | `/gitea-issues <N> labels` | Get an issue's labels as full objects, IDs included | `references/issues.md` |

View File

@@ -7,11 +7,13 @@ source_keys:
# Issue operations # Issue operations
Call signatures below were verified live against the deployed `gitea-mcp` server via `ToolSearch` Call signatures below were verified live against the deployed `gitea-mcp` server via `ToolSearch`,
at authoring time, not copied from `api-reference.md` — this is deliberate: research docs are not copied from `api-reference.md` — this is deliberate: research docs are generated from source at
generated from source at a point in time and can drift from the server actually deployed (see the a point in time and can drift from the server actually deployed. Last verified against gitea-mcp
`list_issues` gotcha below, which is the exact drift this policy exists to catch). Re-verify against **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Drift runs in both directions: this file
the live schema if these tools appear to behave differently than documented here. previously recorded `list_issues` as having neither a `type` nor a `milestones` parameter, and
v1.7.0 has both. Re-verify against the live schema if these tools appear to behave differently than
documented here.
## `list_issues` ## `list_issues`
@@ -21,18 +23,22 @@ the live schema if these tools appear to behave differently than documented here
- `state` (string, optional, default `"all"`) — conventional values `"open"`/`"closed"`/`"all"`, not - `state` (string, optional, default `"all"`) — conventional values `"open"`/`"closed"`/`"all"`, not
schema-enforced as an enum schema-enforced as an enum
- `labels` (array of strings, optional) — filter by label *name* (not ID) - `labels` (array of strings, optional) — filter by label *name* (not ID)
- `milestones` (array of strings, optional) — filter by milestone name or numeric ID, both passed as
strings
- `type` (string, enum `"issues"` | `"pulls"`, optional) — omit it and the response mixes both
- `since` (string, optional) — ISO 8601, issues updated after this time - `since` (string, optional) — ISO 8601, issues updated after this time
- `before` (string, optional) — ISO 8601, issues updated before this time - `before` (string, optional) — ISO 8601, issues updated before this time
- `page` (number, optional, default `1`) - `page` (number, optional, default `1`)
- `per_page` (number, optional, default `30`) - `per_page` (number, optional, default `30`)
**There is no `type` parameter and no `milestones` parameter**, despite both appearing in **Pass `type: "issues"` on any listing meant to show issues.** Issues and PRs share one repo number
`api-reference.md`. This tool cannot filter issues-vs-PRs or by milestone — see the Gotchas section space and the unfiltered response interleaves them; the only thing distinguishing them on a list
of SKILL.md for the consequence (PR entries can appear in results with no way to exclude them here). item is the `html_url` path segment (`/issues/` vs `/pulls/`), since `is_pull` is not returned on
list items — see the Gotchas section of SKILL.md.
**Call:** **Call:**
``` ```
list_issues owner: <owner> repo: <repo> state: "open" list_issues owner: <owner> repo: <repo> state: "open" type: "issues"
``` ```
**Response (list item):** `number`, `title`, `state`, `html_url`, `user`, `comments`, `created_at`, **Response (list item):** `number`, `title`, `state`, `html_url`, `user`, `comments`, `created_at`,
@@ -117,6 +123,13 @@ issue_write method: "add_labels" owner: <owner> repo: <repo> issue_number: <N> l
To replace all labels atomically instead of adding: `method: "replace_labels"`. To replace all labels atomically instead of adding: `method: "replace_labels"`.
To remove one: `method: "remove_label" label_id: <single ID>`. To remove one: `method: "remove_label" label_id: <single ID>`.
**Default to `add_labels`.** `replace_labels` clears every label not in the array, so it drops
labels the caller never mentioned. Reach for it only when the caller asked for the issue's label
set to become exactly what they listed. In particular, do not use it to enforce one-label-per-scope:
exclusivity is a per-label property — the server drops the sibling itself for a label whose
`exclusive` field is `true`, and a label whose `exclusive` is `false` (every `Kind/*` on this
instance) is legitimately stackable. See `gitea-labels-milestones` for how to read that field.
## Token scope ## Token scope
All of `list_issues`, `issue_read`, and `issue_write` are verified working under a token holding All of `list_issues`, `issue_read`, and `issue_write` are verified working under a token holding

View File

@@ -13,8 +13,8 @@ time (see `references/sources.md`) — confirmed to match `api-reference.md`.
**Parameters:** **Parameters:**
- `query` (string, required) — the only hard-required parameter - `query` (string, required) — the only hard-required parameter
- `state` (string, enum `"open"` | `"closed"` | `"all"`, optional) - `state` (string, enum `"open"` | `"closed"` | `"all"`, optional)
- `type` (string, enum `"issues"` | `"pulls"`, optional) — **this tool has a working type filter**, - `type` (string, enum `"issues"` | `"pulls"`, optional) — the same filter, with the same values,
unlike `list_issues` (see `references/issues.md`) that `list_issues` takes (see `references/issues.md`)
- `labels` (string, optional) — comma-separated label **names** — a plain string, not the array form - `labels` (string, optional) — comma-separated label **names** — a plain string, not the array form
`list_issues` uses `list_issues` uses
- `owner` (string, optional) — restrict results to one owner - `owner` (string, optional) — restrict results to one owner

View File

@@ -1,12 +1,13 @@
# Sources # Sources
**Note on call signatures:** per `docs/adr/0011-gitea-skill-deep-modules.md`, the tool parameter **Note on call signatures:** per `docs/adr/0011-gitea-skill-deep-modules.md`, the tool parameter
signatures in `references/issues.md` and `references/search.md` were re-verified live via signatures in `references/issues.md` and `references/search.md` are re-verified live via
`ToolSearch` against the deployed `gitea-mcp` server at authoring time — they are not copied `ToolSearch` against the deployed `gitea-mcp` server — they are not copied verbatim from
verbatim from `api-reference.md`. This resolves issue #6 comment #849's root-cause finding that a `api-reference.md`. This resolves issue #6 comment #849's root-cause finding that a prior skill was
prior skill was authored from API docs that had drifted from the actual MCP tool schema; the live authored from API docs that had drifted from the actual MCP tool schema. That re-verification is
check caught exactly this drift on `list_issues` (see `references/issues.md` — the research doc ongoing, not one-off: an earlier live check recorded `list_issues` as lacking the `type` and
documents a `type` and a `milestones` parameter that do not exist on the deployed server). `milestones` parameters `api-reference.md` documents, and both are present on the deployed
gitea-mcp **v1.7.0**, which is the version these signatures are current as of.
## gitea-mcp-repo ## gitea-mcp-repo

View File

@@ -24,7 +24,7 @@ allowed-tools: Bash mcp__gitea__label_read mcp__gitea__label_write mcp__gitea__m
- **Applying a label takes a numeric ID, but issue/PR responses slim labels down to name strings.** An issue's existing labels yield no IDs — resolve name → ID with `label_read`. - **Applying a label takes a numeric ID, but issue/PR responses slim labels down to name strings.** An issue's existing labels yield no IDs — resolve name → ID with `label_read`.
- **`pull_request_read` returns `milestone` as a bare title string** where `issue_read` returns `{id, title}` — recover the milestone's ID by listing milestones and matching the title. - **`pull_request_read` returns `milestone` as a bare title string** where `issue_read` returns `{id, title}` — recover the milestone's ID by listing milestones and matching the title.
- **`Kind/*`/`Priority/*`/`Status/*` exclusivity is a client-side convention.** `exclusive` is an org-labels-only flag, so applying a label in such a scope must replace the one already there, not stack on it. - **Never assume a `Kind/*`/`Priority/*`/`Status/*` scope is exclusive — read each label's own `exclusive` field.** `list_repo_labels` returns it per repo label, it is not org-only, and where it is `true` Gitea enforces one-per-scope itself. Replacing rather than stacking on a label whose `exclusive` is `false` destroys a valid label.
## Step 1 — Resolve owner, repo and org ## Step 1 — Resolve owner, repo and org
@@ -36,7 +36,9 @@ git remote get-url origin
If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL." If origin is not set or the URL is not a Gitea URL, stop and report: "No Gitea remote found — set origin to your Gitea instance URL."
The `*_org_label*` methods take `org`, not `owner`/`repo`. Pass that same `owner` as `org` — it is the org name whenever the owner is an organisation, and the remote URL does not say whether it is one. So let the call itself decide: a failure means the owner is a user account with no org label pool, which is an answer, not an error to report. The `*_org_label*` methods take `org`, not `owner`/`repo`. Pass that same `owner` as `org` — it is the org name whenever the owner is an organisation, and the remote URL does not say whether it is one.
Read the failure text before interpreting it. `list_org_labels` needs the `read:organization` token scope, which this skill's declared scopes (`write:issue`, `write:repository`) do not carry, so it fails with `token does not have at least one of required scope(s), required=[read:organization]` *before* it ever determines org-vs-user. Report that: the org pool went unchecked, not empty. Only a not-found response is evidence the owner is a user account with no org pool.
## Step 2 — Dispatch ## Step 2 — Dispatch

View File

@@ -12,19 +12,24 @@ description) to this repo's `Kind/*` / `Priority/*` / `Status/*` label taxonomy.
`gitea-issues` and `gitea-prs` before creating or updating an issue/PR, and directly when the user `gitea-issues` and `gitea-prs` before creating or updating an issue/PR, and directly when the user
asks to label something without naming exact labels. asks to label something without naming exact labels.
## Scoped labels are mutually exclusive — replace, don't stack ## Exclusivity is per label — read it, never infer it
Each of `Kind/*`, `Priority/*`, `Status/*` is treated as a scoped-label group by convention (the `/` Gitea's `exclusive` flag is a real per-label boolean returned by `list_repo_labels`, and where it is
delimiter naming pattern). Gitea's `exclusive` flag — the mechanism that would let the server itself `true` the server enforces one-label-per-scope itself. It is not an org-only setting, and the `/`
enforce one-label-per-scope — is documented as an org-labels-only setting, and the repo-level delimiter in a name says nothing about it. Verified on `Defame1297/holocron`: every `Priority/*`,
`label_write` methods used here don't accept it at all. So exclusivity within these scopes is a `Reviewed/*` and `Status/*` label is `exclusive: true`, while every `Kind/*` label — and
convention this skill enforces client-side, not something the server guarantees: applying a new `Compat/Breaking` — is `exclusive: false` and is used stacked.
label within a scope is expected to replace any existing label in that same scope on the target
issue/PR, not add alongside it. When inference So read each candidate label's own `exclusive` value from the resolution call and branch on it:
selects a `Priority/High` label and the issue already carries `Priority/Medium`, the write should
result in only `Priority/High` remaining — use `replace_labels` scoped to that group's labels, or at - **`exclusive: true`** — the server drops the sibling on write. Add the label and let it; do not
minimum remove the superseded label before adding the new one. Never leave two labels from the same pre-remove the label already there, and do not compute a replacement set client-side. Inferring
scope applied at once. `Priority/High` onto an issue carrying `Priority/Medium` needs no special handling.
- **`exclusive: false`** — **add alongside, never replace.** Stripping a co-existing label in the
same scope destroys a valid one: an issue can legitimately carry `Kind/Bug` and `Kind/Security`
at once.
There is no client-side exclusivity convention for this skill to enforce.
## Signal → label mapping ## Signal → label mapping
@@ -57,16 +62,18 @@ scope applied at once.
1. Read the conversation context (issue/PR title, body, or the triggering discussion) for the 1. Read the conversation context (issue/PR title, body, or the triggering discussion) for the
signals above. signals above.
2. Call `label_read method: "list_repo_labels"` (see `references/labels.md`) to get the current 2. Call `label_read method: "list_repo_labels"` (see `references/labels.md`) to get the current
label set with IDs — inference must never guess an ID, only a name, then resolve it. Because label set with IDs and each label's `exclusive` value — inference must never guess an ID, only a
`exclusive` is an org-labels-only flag, this taxonomy plausibly lives at org scope too: for any name, then resolve it. Both pools can apply to one issue, so for any inferred name absent from
inferred name absent from the repo pool, also call `label_read method: "list_org_labels"` with the repo pool, also call `label_read method: "list_org_labels"` with `org` set to the repo's
`org` set to the repo's `owner` before treating it as unresolved. A failure there means the owner `owner` before treating it as unresolved. Read that call's failure text: a
is a user account, not an organisation, so no org pool exists and the name is genuinely absent. `required=[read:organization]` scope error means the org pool was never queried — report the
3. Match inferred label names against the resolved list (case-insensitive). If a scope group missing token scope rather than reporting the label unresolved. Only a not-found response means
already has a different label applied on the target and a new one is inferred for that same there is no org pool and the name is genuinely absent.
scope, plan to replace rather than add (see above). 3. Match inferred label names against the resolved list (case-insensitive) and carry each match's
`exclusive` value forward: `true` means the server replaces the sibling on write, `false` means
the label is added alongside whatever is already applied (see above).
4. **Low-confidence inference omits the label.** If no signal confidently maps to a `Kind/*` value, 4. **Low-confidence inference omits the label.** If no signal confidently maps to a `Kind/*` value,
do not guess — omit `Kind/*` entirely rather than default to one. `Priority/Medium` is the one do not guess — omit `Kind/*` entirely rather than default to one. `Priority/Medium` is the one
exception: it's the explicit default when no urgency signal is present, not a guess. exception: it's the explicit default when no urgency signal is present, not a guess.
5. Hand the resolved IDs (plus which scopes to replace) to the caller's `issue_write`/ 5. Hand the resolved IDs, each with its `exclusive` value, to the caller's `issue_write`/
`pull_request_write` call — this skill does not apply labels to an issue or PR itself. `pull_request_write` call — this skill does not apply labels to an issue or PR itself.

View File

@@ -37,7 +37,7 @@ or **org-scoped** labels — never both in one call. Pick the method family (`*_
| `name` | string | required for create | | `name` | string | required for create |
| `color` | string | hex `#RRGGBB`, required for create | | `color` | string | hex `#RRGGBB`, required for create |
| `description` | string | optional | | `description` | string | optional |
| `exclusive` | boolean | org labels only | | `exclusive` | boolean | accepted as a *write* param on org creates only — repo labels still carry and enforce `exclusive`, set outside this tool surface |
| `is_archived` | boolean | repo labels only | | `is_archived` | boolean | repo labels only |
Note: unlike `milestone_read`/`milestone_write`, `owner`/`repo`/`org` are **not** schema-required on Note: unlike `milestone_read`/`milestone_write`, `owner`/`repo`/`org` are **not** schema-required on
@@ -53,6 +53,12 @@ label_read method: "list_repo_labels" owner: <owner> repo: <repo> per_page:
Paginate (`page: 1, 2, ...`) until the returned count is less than `per_page`. This is the only way Paginate (`page: 1, 2, ...`) until the returned count is less than `per_page`. This is the only way
to build a complete name → ID map — there is no lookup-by-name endpoint. to build a complete name → ID map — there is no lookup-by-name endpoint.
Every returned repo label carries its own `exclusive` boolean; the field is not org-only. Verified on
`Defame1297/holocron`: all `Priority/*`, `Reviewed/*` and `Status/*` labels are `exclusive: true`,
while all `Kind/*` labels and `Compat/Breaking` are `exclusive: false`. Where it is `true` Gitea
enforces one-label-per-scope server-side; where it is `false` labels in that scope stack legitimately.
Read the field — never infer exclusivity from the `/` in a name.
## Get one label ## Get one label
``` ```
@@ -65,8 +71,10 @@ There is no direct name lookup. List all repo labels (paginating if needed), sca
case-insensitive name match, and extract `id`. Both pools can apply to one issue: if the name is case-insensitive name match, and extract `id`. Both pools can apply to one issue: if the name is
not in `list_repo_labels`, also check `list_org_labels` before reporting it unresolved. That method not in `list_repo_labels`, also check `list_org_labels` before reporting it unresolved. That method
takes `org`, not `owner`/`repo` — pass the repo's `owner` as `org`, which is what it means when the takes `org`, not `owner`/`repo` — pass the repo's `owner` as `org`, which is what it means when the
owner is an organisation. If that call fails, the owner is a user account, there is no org pool, and owner is an organisation. Its failure modes are not interchangeable. `token does not have at least
the miss is a real miss. one of required scope(s), required=[read:organization]` means the org pool was never queried — report
that missing scope rather than reporting the label unresolved. Only a not-found response means the
owner is a user account with no org pool, making the miss a real miss.
Resolution is the required first step before any label application on an issue or PR — the actual Resolution is the required first step before any label application on an issue or PR — the actual
`add_labels`/`replace_labels`/`remove_label` call lives in `gitea-issues`/`gitea-prs` via `add_labels`/`replace_labels`/`remove_label` call lives in `gitea-issues`/`gitea-prs` via
@@ -83,7 +91,10 @@ label_write method: "create_repo_label"
``` ```
For an org label, use `method: "create_org_label"` with `org:` instead of `owner`/`repo`, and For an org label, use `method: "create_org_label"` with `org:` instead of `owner`/`repo`, and
`exclusive: true` if the label belongs to a mutually-exclusive scope group. `exclusive: true` if the label belongs to a mutually-exclusive scope group. `label_write` accepts
`exclusive` on org methods only, so a repo label's exclusivity cannot be set or cleared through this
tool — it is set in the Gitea UI or against the REST API directly, and read back via
`list_repo_labels`.
## Edit a label ## Edit a label

View File

@@ -7,7 +7,7 @@ source_keys:
# Pull request read/write execution detail # Pull request read/write execution detail
Parameter signatures below are cross-checked live against the deployed gitea-mcp server tool schemas at authoring time — not copied verbatim from the plugin's research doc for this domain, which has a known history of drifting from the deployed server (e.g. a prior `type` parameter that no longer exists on `list_issues`). These files were last verified against gitea-mcp **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Re-verify via `ToolSearch` before trusting this file if the deployed version differs — drift has bitten this skill in both directions, adding methods it does not list and fixing quirks it still warns about. Parameter signatures below are cross-checked live against the deployed gitea-mcp server tool schemas at authoring time — not copied verbatim from the plugin's research doc for this domain, which has a known history of drifting from the deployed server. These files were last verified against gitea-mcp **v1.7.0**, as reported by `get_gitea_mcp_server_version`. Re-verify via `ToolSearch` before trusting this file if the deployed version differs — drift has bitten this skill in both directions, adding methods it does not list and fixing quirks it still warns about.
## `list_pull_requests` ## `list_pull_requests`

View File

@@ -18,7 +18,7 @@ metadata:
## Gotchas ## Gotchas
- **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives. - **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives.
- **`is_draft`/`is_pre_release` are booleans the caller sets — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected. - **Set `is_draft`/`is_pre_release` explicitly on every `create_release` — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** A tag named `v2.0.0-beta.1` publishes as a full release, and becomes the repo's latest, unless `is_pre_release: true` is passed in the same call. The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected.
- **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs. - **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs.
## Dispatch table ## Dispatch table
@@ -37,11 +37,13 @@ metadata:
`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. Semver with a `v` prefix is a tooling convention, not a Gitea constraint — the API accepts any string — so never validate or rewrite it.
## Workflow ## Workflow
- [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, and `title`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it. - [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. **There is no update or edit tool on this surface**: `create_release`, `get_release`, `get_latest_release`, `list_releases` and `delete_release` are the whole set. A release published with the wrong flag therefore has no non-destructive repair — the only fix is `delete_release` plus a fresh `create_release`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it.
- [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first. - [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first.
- [ ] **Deleting 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. - [ ] **Deleting 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.
- [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. - [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates.
If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`. If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, draft/prerelease semantics, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`.

View File

@@ -32,7 +32,7 @@ for brevity.
**`get_latest_release`** **`get_latest_release`**
- No parameters beyond `owner`/`repo`. - No parameters beyond `owner`/`repo`.
- Returns a single release object for the most recently published release. It is assumed (by analogy with typical "latest release" semantics) that this excludes drafts and prereleases, but that exclusion is not directly confirmed by any of the research docs — verify with `list_releases` if the caller depends on this. - Returns a single release object for the most recently published release. The deployed tool's own description reads "the most recent published (non-draft) release" — it names drafts as excluded and is silent on prereleases, so whether prereleases are also excluded is *unconfirmed*; verify with `list_releases` if the caller depends on it. Either way a release created without `is_pre_release: true` is eligible, which is why that flag has to be set in the create call (see `conventions.md`).
**`create_release`** **`create_release`**
- Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string) - Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string)

View File

@@ -33,6 +33,13 @@ commonly used to signal a prerelease to humans. When a user asks to "cut a beta"
release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on
the tag string to carry that meaning. the tag string to carry that meaning.
Getting this wrong is not cheaply repairable. The gitea-mcp release surface is `create_release`,
`get_release`, `get_latest_release`, `list_releases` and `delete_release` — there is **no update or
edit tool**, so a published release's flags cannot be corrected in place. The only remedy is
`delete_release` plus a fresh `create_release`, a destructive round trip; meanwhile a beta published
without `is_pre_release` is the release `get_latest_release` returns. Set the flags in the create
call.
Note the input/output naming mismatch: the input param is `is_draft`, but the release object Note the input/output naming mismatch: the input param is `is_draft`, but the release object
returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid
input key — passing `draft: true` to `create_release` is silently ignored rather than erroring. input key — passing `draft: true` to `create_release` is silently ignored rather than erroring.

View File

@@ -20,7 +20,7 @@ You resolve the package root once per dispatched operation (the directory contai
These are non-negotiable regardless of `confirm` or any skill-local override: These are non-negotiable regardless of `confirm` or any skill-local override:
- `apm publish` claims a version on a registry — treat it as irreversible. Refuse without explicit `confirm: true`; always dispatch with `--dry-run -v` first and surface that output to the caller before the real publish, even when `confirm: true` was given. - `apm publish` claims a version on a registry — treat it as irreversible. Refuse without explicit `confirm: true`; always dispatch with `--dry-run -v` first and surface that output to the caller before the real publish, even when `confirm: true` was given.
- Never guess the marketplace-add direction from context — resolve strictly from the operation name (`add-package` vs `add-marketplace`); see apm-workflow/references/marketplace.md Gotchas for why the two are easy to conflate. - Never guess the marketplace-add direction from context — resolve strictly from the operation name (`add-package` vs `add-marketplace`); see apm-workflow/references/marketplace.md Gotchas for why the two are easy to conflate.
- `apm.yml`'s `type:` field constrains what `.apm/` may contain — when scaffolding (`init-package`), set `type:` before any primitive content is added; do not defer it. - `apm.yml`'s `type:` field routes processing (native skill install vs AGENTS.md compilation); it never validates `.apm/` content, and a mismatch is silent rather than an error. When scaffolding (`init-package`), set `type:` to cover every primitive the package will ship, and report the deployed output rather than the exit code — see apm-workflow/references/configure.md Gotchas.
- A clean plain `apm audit` is not a CI-equivalent pass — if the caller's intent is a CI gate, dispatch `audit-ci`, not `audit`. - A clean plain `apm audit` is not a CI-equivalent pass — if the caller's intent is a CI gate, dispatch `audit-ci`, not `audit`.
- Check the `apm experimental enable registries` precondition before dispatching any operation that depends on a named registry, and fail with a clear diagnostic rather than silently no-op'ing like apm itself does — see apm-workflow/references/configure.md Gotchas for the underlying constraint (summarised in its SKILL.md Gotchas). - Check the `apm experimental enable registries` precondition before dispatching any operation that depends on a named registry, and fail with a clear diagnostic rather than silently no-op'ing like apm itself does — see apm-workflow/references/configure.md Gotchas for the underlying constraint (summarised in its SKILL.md Gotchas).
- You are read-only against the working tree. Never create, edit, or delete a file — not an `apm.yml`, not a `.apm/` primitive, not compiled output, not a scratch note. `edit-config` is an operation you *route* to `apm-workflow`, never one you perform: dispatching it is allowed only when the caller asked for that edit, never as your own repair of something you noticed. - You are read-only against the working tree. Never create, edit, or delete a file — not an `apm.yml`, not a `.apm/` primitive, not compiled output, not a scratch note. `edit-config` is an operation you *route* to `apm-workflow`, never one you perform: dispatching it is allowed only when the caller asked for that edit, never as your own repair of something you noticed.

View File

@@ -131,6 +131,15 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE) return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug): def parse_contributing_files(content, slug):
"""Find the Contributing files for a given slug H2 in content.
Accepts the inline form and the bullet form; recognising only the
inline one silently skips the contributing-file checks on every
sources.md written the other way. Returns a list of paths with any
trailing parenthetical note stripped; "(none)" returns an empty list
and a slug with no entry returns None. Kept behaviourally identical to
skill-audit's copy, which is where the bug was found.
"""
pattern = re.compile( pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL re.MULTILINE | re.DOTALL
@@ -139,10 +148,36 @@ def parse_contributing_files(content, slug):
if not m: if not m:
return None return None
block = m.group(1) block = m.group(1)
def strip_note(entry):
return re.sub(r'\s*\(.*$', '', entry).strip()
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if cf_m:
value = cf_m.group(1).strip()
if value.startswith("(none"):
return []
return [p for p in (strip_note(x) for x in value.split(",")) if p]
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
if not cf_m: if not cf_m:
return None return None
return cf_m.group(1).strip() files = []
for line in block[cf_m.end():].splitlines():
line = line.strip()
if not line:
if files:
break
continue
if not line.startswith("- "):
break
entry = line[2:].strip()
if entry.startswith("(none"):
return []
entry = strip_note(entry)
if entry:
files.append(entry)
return files
def parse_research_doc(content, slug): def parse_research_doc(content, slug):
pattern = re.compile( pattern = re.compile(
@@ -242,9 +277,8 @@ 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) # Check 3: Contributing files exist (paths relative to plugin root)
cf_value = parse_contributing_files(sources_content, slug) cf_files = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"): if cf_files:
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
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):

View File

@@ -25,7 +25,7 @@ version: 1.0.0
- `name`, `version` — required (see above) - `name`, `version` — required (see above)
- `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata - `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata
- `type` — `instructions | skill | hybrid | prompts`; constrains what `.apm/` may contain, so set it before scaffolding content (see Gotchas) - `type` — `instructions | skill | hybrid | prompts`; selects how the package is processed at install/compile time. It is a routing selector, not a constraint on what `.apm/` may contain (see Gotchas)
- `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted) - `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted)
- `includes` — `auto` publishes the authoritative local layout as-is; it is not scoped down to what's relevant, so anything narrower needs an explicit repo-path list. Note: `auto` also does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md` - `includes` — `auto` publishes the authoritative local layout as-is; it is not scoped down to what's relevant, so anything narrower needs an explicit repo-path list. Note: `auto` also does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md`
- `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact - `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact
@@ -85,6 +85,6 @@ apm config set registry.corp-main.default true
## Gotchas ## Gotchas
- `apm.yml`'s `type:` field constrains what `.apm/` may contain — set it before scaffolding content, not after. Changing it later does not retroactively validate what is already on disk. - `apm.yml`'s `type:` field validates nothing about `.apm/`. It selects processing: `instructions` compiles to AGENTS.md only, `skill` installs a native skill only, `prompts` emits commands only, `hybrid` does both (see `apm_cli/models/validation.py`, `PackageContentType`). apm checks only that the value parses to one of those four strings; no check anywhere compares it against the primitives actually on disk, and no mismatch diagnostic exists. A package declaring `type: instructions` while shipping `.apm/skills/` therefore raises no error — the mismatch resolves silently, either by omitting that primitive from the install/compile output or, in apm 0.28.0 where `get_effective_type()` routes off the on-disk layout and never reads the declared field, by ignoring the declared value outright. Both directions are silent: `apm install` and `apm compile` can exit 0 having shipped none of the primitives you expected. Set `type:` to cover every primitive the package ships, and confirm the deployed output rather than the exit code.
- `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning. - `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning.
- `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead. - `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead.

View File

@@ -81,7 +81,7 @@ table** plus the gates common to every branch, and each flow lives in its own se
invocation then pays for every branch it did not take. invocation then pays for every branch it did not take.
The reference shape in this repo is `apm-workflow`: a **237-word body** dispatching to roughly The reference shape in this repo is `apm-workflow`: a **237-word body** dispatching to roughly
3,200 words of references across five mutually exclusive invocations. Its whole-file count is 304 3,400 words of references across five mutually exclusive invocations. Its whole-file count is 304
words — cite 237 when calibrating a body, or the conflation this section warns against reappears words — cite 237 when calibrating a body, or the conflation this section warns against reappears
in the finding itself. in the finding itself.

View File

@@ -94,8 +94,24 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE) return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug): def parse_contributing_files(content, slug):
"""Find the Contributing files value for a given slug H2 in content.""" """Find the Contributing files for a given slug H2 in content.
# Find the H2 block for slug, then look for Contributing files line
Both authored forms are accepted, because both are in use across the
corpus and only recognising the first silently skipped checks 4 and 5
on every skill using the second:
- **Contributing files:** SKILL.md, references/a.md
**Contributing files:**
- SKILL.md (what this source contributed)
- references/a.md (what this source contributed)
Returns a list of paths with any trailing parenthetical note stripped.
A "(none)" value returns an empty list; a slug with no Contributing
files entry at all returns None. Note the bullet form's notes may
themselves contain commas, so the list is built per bullet rather than
by splitting the joined value.
"""
pattern = re.compile( pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL re.MULTILINE | re.DOTALL
@@ -104,10 +120,40 @@ def parse_contributing_files(content, slug):
if not m: if not m:
return None return None
block = m.group(1) block = m.group(1)
def strip_note(entry):
# "references/a.md (why)" -> "references/a.md"
return re.sub(r'\s*\(.*$', '', entry).strip()
# Inline form: value on the same line, comma-separated, no notes.
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if cf_m:
value = cf_m.group(1).strip()
if value.startswith("(none"):
return []
return [p for p in (strip_note(x) for x in value.split(",")) if p]
# Bullet form: heading on its own line, one file per following bullet.
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
if not cf_m: if not cf_m:
return None return None
return cf_m.group(1).strip() rest = block[cf_m.end():]
files = []
for line in rest.splitlines():
line = line.strip()
if not line:
if files:
break
continue
if not line.startswith("- "):
break
entry = line[2:].strip()
if entry.startswith("(none"):
return []
entry = strip_note(entry)
if entry:
files.append(entry)
return files
def parse_research_doc(content, slug): def parse_research_doc(content, slug):
"""Find the Research doc value for a given slug H2 in content.""" """Find the Research doc value for a given slug H2 in content."""
@@ -300,10 +346,8 @@ research_docs_seen = {} # abs_path → set of slugs in sources.md that referenc
for slug in parse_h2_slugs(sources_content): for slug in parse_h2_slugs(sources_content):
# Check 4: Contributing files exist # Check 4: Contributing files exist
cf_value = parse_contributing_files(sources_content, slug) cf_files = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"): if cf_files:
# Split by comma
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
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):
@@ -374,8 +418,8 @@ for rd_abs, (rd_rel, known_slugs) in research_docs_seen.items():
# Parse this slug's Contributing files and Status in the research doc # Parse this slug's Contributing files and Status in the research doc
rd_cf = parse_contributing_files(rd_content, rd_slug) rd_cf = parse_contributing_files(rd_content, rd_slug)
rd_status = parse_status(rd_content, rd_slug) rd_status = parse_status(rd_content, rd_slug)
# Skip if contributing files start with (none # Skip if the research doc explicitly records no contributing files
if rd_cf and rd_cf.startswith("(none"): if rd_cf == []:
continue continue
# Skip if status is not `extracted` # Skip if status is not `extracted`
if rd_status != "`extracted`": if rd_status != "`extracted`":

View File

@@ -120,7 +120,7 @@ A generic pointer ("see references/ for details") is a Vale error — the agent
**Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch **Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
table and the gates common to every branch; each flow gets its own self-contained `references/` table and the gates common to every branch; each flow gets its own self-contained `references/`
file. Exemplar: the `apm-workflow` skill — a **237-word body** dispatching to 3,222 words of file. Exemplar: the `apm-workflow` skill — a **237-word body** dispatching to 3,416 words of
references. Calibrate against 237: that file's whole-file count is 304 words, and aiming at that references. Calibrate against 237: that file's whole-file count is 304 words, and aiming at that
number instead overshoots the body budget by ~30%. number instead overshoots the body budget by ~30%.

View File

@@ -1,6 +1,6 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"version": "1.6.0", "version": "1.7.0",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "kyberforge", "name": "kyberforge",
"version": "1.6.0", "version": "1.7.0",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -20,7 +20,7 @@ You resolve the package root once per dispatched operation (the directory contai
These are non-negotiable regardless of `confirm` or any skill-local override: These are non-negotiable regardless of `confirm` or any skill-local override:
- `apm publish` claims a version on a registry — treat it as irreversible. Refuse without explicit `confirm: true`; always dispatch with `--dry-run -v` first and surface that output to the caller before the real publish, even when `confirm: true` was given. - `apm publish` claims a version on a registry — treat it as irreversible. Refuse without explicit `confirm: true`; always dispatch with `--dry-run -v` first and surface that output to the caller before the real publish, even when `confirm: true` was given.
- Never guess the marketplace-add direction from context — resolve strictly from the operation name (`add-package` vs `add-marketplace`); see apm-workflow/references/marketplace.md Gotchas for why the two are easy to conflate. - Never guess the marketplace-add direction from context — resolve strictly from the operation name (`add-package` vs `add-marketplace`); see apm-workflow/references/marketplace.md Gotchas for why the two are easy to conflate.
- `apm.yml`'s `type:` field constrains what `.apm/` may contain — when scaffolding (`init-package`), set `type:` before any primitive content is added; do not defer it. - `apm.yml`'s `type:` field routes processing (native skill install vs AGENTS.md compilation); it never validates `.apm/` content, and a mismatch is silent rather than an error. When scaffolding (`init-package`), set `type:` to cover every primitive the package will ship, and report the deployed output rather than the exit code — see apm-workflow/references/configure.md Gotchas.
- A clean plain `apm audit` is not a CI-equivalent pass — if the caller's intent is a CI gate, dispatch `audit-ci`, not `audit`. - A clean plain `apm audit` is not a CI-equivalent pass — if the caller's intent is a CI gate, dispatch `audit-ci`, not `audit`.
- Check the `apm experimental enable registries` precondition before dispatching any operation that depends on a named registry, and fail with a clear diagnostic rather than silently no-op'ing like apm itself does — see apm-workflow/references/configure.md Gotchas for the underlying constraint (summarised in its SKILL.md Gotchas). - Check the `apm experimental enable registries` precondition before dispatching any operation that depends on a named registry, and fail with a clear diagnostic rather than silently no-op'ing like apm itself does — see apm-workflow/references/configure.md Gotchas for the underlying constraint (summarised in its SKILL.md Gotchas).
- You are read-only against the working tree. Never create, edit, or delete a file — not an `apm.yml`, not a `.apm/` primitive, not compiled output, not a scratch note. `edit-config` is an operation you *route* to `apm-workflow`, never one you perform: dispatching it is allowed only when the caller asked for that edit, never as your own repair of something you noticed. - You are read-only against the working tree. Never create, edit, or delete a file — not an `apm.yml`, not a `.apm/` primitive, not compiled output, not a scratch note. `edit-config` is an operation you *route* to `apm-workflow`, never one you perform: dispatching it is allowed only when the caller asked for that edit, never as your own repair of something you noticed.

View File

@@ -1,5 +1,5 @@
name: kyberforge name: kyberforge
version: 1.6.0 version: 1.7.0
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace. description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
author: author:
name: Defame1297 name: Defame1297

View File

@@ -131,6 +131,15 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE) return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug): def parse_contributing_files(content, slug):
"""Find the Contributing files for a given slug H2 in content.
Accepts the inline form and the bullet form; recognising only the
inline one silently skips the contributing-file checks on every
sources.md written the other way. Returns a list of paths with any
trailing parenthetical note stripped; "(none)" returns an empty list
and a slug with no entry returns None. Kept behaviourally identical to
skill-audit's copy, which is where the bug was found.
"""
pattern = re.compile( pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL re.MULTILINE | re.DOTALL
@@ -139,10 +148,36 @@ def parse_contributing_files(content, slug):
if not m: if not m:
return None return None
block = m.group(1) block = m.group(1)
def strip_note(entry):
return re.sub(r'\s*\(.*$', '', entry).strip()
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if cf_m:
value = cf_m.group(1).strip()
if value.startswith("(none"):
return []
return [p for p in (strip_note(x) for x in value.split(",")) if p]
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
if not cf_m: if not cf_m:
return None return None
return cf_m.group(1).strip() files = []
for line in block[cf_m.end():].splitlines():
line = line.strip()
if not line:
if files:
break
continue
if not line.startswith("- "):
break
entry = line[2:].strip()
if entry.startswith("(none"):
return []
entry = strip_note(entry)
if entry:
files.append(entry)
return files
def parse_research_doc(content, slug): def parse_research_doc(content, slug):
pattern = re.compile( pattern = re.compile(
@@ -242,9 +277,8 @@ 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) # Check 3: Contributing files exist (paths relative to plugin root)
cf_value = parse_contributing_files(sources_content, slug) cf_files = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"): if cf_files:
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
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):

View File

@@ -25,7 +25,7 @@ version: 1.0.0
- `name`, `version` — required (see above) - `name`, `version` — required (see above)
- `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata - `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata
- `type` — `instructions | skill | hybrid | prompts`; constrains what `.apm/` may contain, so set it before scaffolding content (see Gotchas) - `type` — `instructions | skill | hybrid | prompts`; selects how the package is processed at install/compile time. It is a routing selector, not a constraint on what `.apm/` may contain (see Gotchas)
- `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted) - `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted)
- `includes` — `auto` publishes the authoritative local layout as-is; it is not scoped down to what's relevant, so anything narrower needs an explicit repo-path list. Note: `auto` also does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md` - `includes` — `auto` publishes the authoritative local layout as-is; it is not scoped down to what's relevant, so anything narrower needs an explicit repo-path list. Note: `auto` also does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md`
- `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact - `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact
@@ -85,6 +85,6 @@ apm config set registry.corp-main.default true
## Gotchas ## Gotchas
- `apm.yml`'s `type:` field constrains what `.apm/` may contain — set it before scaffolding content, not after. Changing it later does not retroactively validate what is already on disk. - `apm.yml`'s `type:` field validates nothing about `.apm/`. It selects processing: `instructions` compiles to AGENTS.md only, `skill` installs a native skill only, `prompts` emits commands only, `hybrid` does both (see `apm_cli/models/validation.py`, `PackageContentType`). apm checks only that the value parses to one of those four strings; no check anywhere compares it against the primitives actually on disk, and no mismatch diagnostic exists. A package declaring `type: instructions` while shipping `.apm/skills/` therefore raises no error — the mismatch resolves silently, either by omitting that primitive from the install/compile output or, in apm 0.28.0 where `get_effective_type()` routes off the on-disk layout and never reads the declared field, by ignoring the declared value outright. Both directions are silent: `apm install` and `apm compile` can exit 0 having shipped none of the primitives you expected. Set `type:` to cover every primitive the package ships, and confirm the deployed output rather than the exit code.
- `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning. - `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning.
- `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead. - `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead.

View File

@@ -81,7 +81,7 @@ table** plus the gates common to every branch, and each flow lives in its own se
invocation then pays for every branch it did not take. invocation then pays for every branch it did not take.
The reference shape in this repo is `apm-workflow`: a **237-word body** dispatching to roughly The reference shape in this repo is `apm-workflow`: a **237-word body** dispatching to roughly
3,200 words of references across five mutually exclusive invocations. Its whole-file count is 304 3,400 words of references across five mutually exclusive invocations. Its whole-file count is 304
words — cite 237 when calibrating a body, or the conflation this section warns against reappears words — cite 237 when calibrating a body, or the conflation this section warns against reappears
in the finding itself. in the finding itself.

View File

@@ -94,8 +94,24 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE) return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug): def parse_contributing_files(content, slug):
"""Find the Contributing files value for a given slug H2 in content.""" """Find the Contributing files for a given slug H2 in content.
# Find the H2 block for slug, then look for Contributing files line
Both authored forms are accepted, because both are in use across the
corpus and only recognising the first silently skipped checks 4 and 5
on every skill using the second:
- **Contributing files:** SKILL.md, references/a.md
**Contributing files:**
- SKILL.md (what this source contributed)
- references/a.md (what this source contributed)
Returns a list of paths with any trailing parenthetical note stripped.
A "(none)" value returns an empty list; a slug with no Contributing
files entry at all returns None. Note the bullet form's notes may
themselves contain commas, so the list is built per bullet rather than
by splitting the joined value.
"""
pattern = re.compile( pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL re.MULTILINE | re.DOTALL
@@ -104,10 +120,40 @@ def parse_contributing_files(content, slug):
if not m: if not m:
return None return None
block = m.group(1) block = m.group(1)
def strip_note(entry):
# "references/a.md (why)" -> "references/a.md"
return re.sub(r'\s*\(.*$', '', entry).strip()
# Inline form: value on the same line, comma-separated, no notes.
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if cf_m:
value = cf_m.group(1).strip()
if value.startswith("(none"):
return []
return [p for p in (strip_note(x) for x in value.split(",")) if p]
# Bullet form: heading on its own line, one file per following bullet.
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
if not cf_m: if not cf_m:
return None return None
return cf_m.group(1).strip() rest = block[cf_m.end():]
files = []
for line in rest.splitlines():
line = line.strip()
if not line:
if files:
break
continue
if not line.startswith("- "):
break
entry = line[2:].strip()
if entry.startswith("(none"):
return []
entry = strip_note(entry)
if entry:
files.append(entry)
return files
def parse_research_doc(content, slug): def parse_research_doc(content, slug):
"""Find the Research doc value for a given slug H2 in content.""" """Find the Research doc value for a given slug H2 in content."""
@@ -300,10 +346,8 @@ research_docs_seen = {} # abs_path → set of slugs in sources.md that referenc
for slug in parse_h2_slugs(sources_content): for slug in parse_h2_slugs(sources_content):
# Check 4: Contributing files exist # Check 4: Contributing files exist
cf_value = parse_contributing_files(sources_content, slug) cf_files = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"): if cf_files:
# Split by comma
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
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):
@@ -374,8 +418,8 @@ for rd_abs, (rd_rel, known_slugs) in research_docs_seen.items():
# Parse this slug's Contributing files and Status in the research doc # Parse this slug's Contributing files and Status in the research doc
rd_cf = parse_contributing_files(rd_content, rd_slug) rd_cf = parse_contributing_files(rd_content, rd_slug)
rd_status = parse_status(rd_content, rd_slug) rd_status = parse_status(rd_content, rd_slug)
# Skip if contributing files start with (none # Skip if the research doc explicitly records no contributing files
if rd_cf and rd_cf.startswith("(none"): if rd_cf == []:
continue continue
# Skip if status is not `extracted` # Skip if status is not `extracted`
if rd_status != "`extracted`": if rd_status != "`extracted`":

View File

@@ -120,7 +120,7 @@ A generic pointer ("see references/ for details") is a Vale error — the agent
**Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch **Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
table and the gates common to every branch; each flow gets its own self-contained `references/` table and the gates common to every branch; each flow gets its own self-contained `references/`
file. Exemplar: the `apm-workflow` skill — a **237-word body** dispatching to 3,222 words of file. Exemplar: the `apm-workflow` skill — a **237-word body** dispatching to 3,416 words of
references. Calibrate against 237: that file's whole-file count is 304 words, and aiming at that references. Calibrate against 237: that file's whole-file count is 304 words, and aiming at that
number instead overshoots the body budget by ~30%. number instead overshoots the body budget by ~30%.

View File

@@ -11,11 +11,14 @@ metadata:
version: "0.1.1" version: "0.1.1"
source_keys: source_keys:
- context7-websites-vale-sh - context7-websites-vale-sh
- house-vale-3-15-2-repro
--- ---
## Gotchas ## Gotchas
- A style in `BasedOnStyles` that is neither built-in nor already under `StylesPath` finds nothing until `vale sync` fetches it — a clean run is not proof anything linted. - A style in `BasedOnStyles` that is neither built-in nor a directory under `StylesPath` fails hard, not silently: `E100 [loadStyles]`, exit 2, nothing linted.
- `vale sync` alone does not clear that `E100`. Sync fetches only what the top-level `Packages` key declares, so against a `BasedOnStyles`-only name it reports `Synced 0 package(s)` and exits 0, fetching nothing. Add the style to `Packages`, then sync. A style lints only once it is in both keys — and the reverse case is silent, exiting 0.
- Only *package* styles need fetching: built-in `Vale`, and any style whose YAML is already committed under `StylesPath`, lint with no `Packages` entry and no sync.
- `.vale.ini` is order-sensitive: core settings first, then `[formats]`, then glob sections. Anything written below a glob header applies only to files matching that glob. - `.vale.ini` is order-sensitive: core settings first, then `[formats]`, then glob sections. Anything written below a glob header applies only to files matching that glob.
- A rule scoped to `text.frontmatter.<key>` silently matches nothing when the field spans multiple lines in most YAML forms. If you scope a rule to frontmatter, read `references/configuration-reference.md` first. - A rule scoped to `text.frontmatter.<key>` silently matches nothing when the field spans multiple lines in most YAML forms. If you scope a rule to frontmatter, read `references/configuration-reference.md` first.
@@ -42,7 +45,7 @@ metadata:
- [ ] **Sync**: run `vale sync` to download everything listed in `Packages` into `StylesPath`. - [ ] **Sync**: run `vale sync` to download everything listed in `Packages` into `StylesPath`.
- [ ] **Verify activation**: confirm every style named in `Packages` also appears in at least one glob's `BasedOnStyles` — an unreferenced package downloads but never lints anything. - [ ] **Verify activation**: confirm every style named in `Packages` also appears in at least one glob's `BasedOnStyles` — an unreferenced package downloads but never lints anything.
For the full `.vale.ini` field reference (formats mapping, vocab, local overrides, custom rule header fields), read `references/configuration-reference.md`. For the full `.vale.ini` field reference (formats mapping, vocab, local overrides, custom rule header fields) and the verified style-resolution matrix — which error each misconfiguration raises, and the two that exit 0 while linting nothing — read `references/configuration-reference.md`.
## Custom styles ## Custom styles

View File

@@ -2,6 +2,7 @@
topic: configuration-reference topic: configuration-reference
source_keys: source_keys:
- context7-websites-vale-sh - context7-websites-vale-sh
- house-vale-3-15-2-repro
--- ---
## Core Settings ## Core Settings
@@ -68,7 +69,7 @@ Individual rule YAML files (under a style's directory) support these header fiel
The underlying functions a rule's `extends` field can reference: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`. The underlying functions a rule's `extends` field can reference: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`.
## Built-in Style ## Style Resolution
Only *package* styles need fetching. A style whose YAML rule files are already committed under `StylesPath` lints immediately, with no `Packages` entry and no `vale sync`; the same is true of the built-in `Vale` style, which ships with the binary and contains four rules: Only *package* styles need fetching. A style whose YAML rule files are already committed under `StylesPath` lints immediately, with no `Packages` entry and no `vale sync`; the same is true of the built-in `Vale` style, which ships with the binary and contains four rules:
@@ -77,11 +78,23 @@ Only *package* styles need fetching. A style whose YAML rule files are already c
- `Vale.Avoid` — enforces the project's rejected vocabulary terms. - `Vale.Avoid` — enforces the project's rejected vocabulary terms.
- `Vale.Repetition` — flags repeated words (e.g. "the the"). - `Vale.Repetition` — flags repeated words (e.g. "the the").
`Packages` (top-level, what `vale sync` downloads) and `BasedOnStyles` (per-glob, what activates) are separate keys: a style lints a file only once it is in both. Every row below reproduced against Vale 3.15.2 (slug `house-vale-3-15-2-repro`):
| Configuration | Result |
|---|---|
| `BasedOnStyles` names a style with no directory under `StylesPath`, not built-in | `E100 [loadStyles] Runtime error` — `style 'X' does not exist on StylesPath`, exit 2 |
| `StylesPath` directory itself absent, even with only `Vale` active | `E201 Invalid value` — `The path '...' does not exist`, exit 2 |
| `vale sync` with a name in `BasedOnStyles` but not `Packages` | `SUCCESS Synced 0 package(s)`, exit 0, nothing downloaded — the next lint repeats the `E100` |
| `vale sync` with the name added to `Packages` | package lands under `StylesPath`, exit 0; lint then loads it |
| Style in `Packages` and synced, but in no glob's `BasedOnStyles` | 0 findings, exit 0 — downloads, never lints, indistinguishable from a clean run |
| `BasedOnStyles` names an *empty* directory under `StylesPath` | 0 findings, exit 0 — loads and lints nothing; `vale sync` never produces this state |
| Built-in `Vale`, or a style's YAML committed under `StylesPath` | lints immediately, no `Packages` entry, no sync |
## Frontmatter Scopes ## Frontmatter Scopes
A rule scoped to `text.frontmatter.<key>` (e.g. `text.frontmatter.description`) matches reliably when that field's value is a single physical line, and breaks on most — not all — multi-line forms. House-verified behaviour, not documented on vale.sh — reproduced locally against Vale 3.15.2 (slug `house-vale-3-15-2-repro`).
Confirmed against Vale 3.15.2, multi-line forms spanning 2+ lines: A rule scoped to `text.frontmatter.<key>` (e.g. `text.frontmatter.description`) matches reliably when that field's value is a single physical line, and breaks on most — not all — multi-line forms. Multi-line forms spanning 2+ lines:
| Frontmatter value form | Result | | Frontmatter value form | Result |
|---|---| |---|---|

View File

@@ -7,3 +7,11 @@
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md - **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
- **Contributing files:** SKILL.md, references/configuration-reference.md - **Contributing files:** SKILL.md, references/configuration-reference.md
- **Status:** `extracted` - **Status:** `extracted`
## house-vale-3-15-2-repro
- **URL:** (house-verified — reproduced locally against the `vale` binary, not an external source)
- **Description:** Behaviour of Vale 3.15.2 established by running it against purpose-built fixtures in this repo, where vale.sh documents nothing: the `E100 [loadStyles]` / exit-2 failure for a `BasedOnStyles` name absent from `StylesPath`, `vale sync` reporting `Synced 0 package(s)` for a name not declared in `Packages`, the `E201` / exit-2 failure when the `StylesPath` directory does not exist, the exit-0 no-op of an empty style directory, and the `text.frontmatter.<key>` scope matrix across multi-line YAML forms.
- **Research doc:** none — house-verified reproduction, not part of the plugin's research corpus (no `plugins/lint/docs/research/` topic file backs this entry)
- **Contributing files:** SKILL.md, references/configuration-reference.md
- **Status:** `extracted`

View File

@@ -1,6 +1,6 @@
{ {
"name": "lint", "name": "lint",
"version": "1.1.6", "version": "1.2.0",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lint", "name": "lint",
"version": "1.1.6", "version": "1.2.0",
"description": "Skills and agents for configuring and running linters.", "description": "Skills and agents for configuring and running linters.",
"author": { "author": {
"name": "Defame1297", "name": "Defame1297",

View File

@@ -1,5 +1,5 @@
name: lint name: lint
version: 1.1.6 version: 1.2.0
description: Skills and agents for configuring and running linters. description: Skills and agents for configuring and running linters.
author: author:
name: Defame1297 name: Defame1297

View File

@@ -11,11 +11,14 @@ metadata:
version: "0.1.1" version: "0.1.1"
source_keys: source_keys:
- context7-websites-vale-sh - context7-websites-vale-sh
- house-vale-3-15-2-repro
--- ---
## Gotchas ## Gotchas
- A style in `BasedOnStyles` that is neither built-in nor already under `StylesPath` finds nothing until `vale sync` fetches it — a clean run is not proof anything linted. - A style in `BasedOnStyles` that is neither built-in nor a directory under `StylesPath` fails hard, not silently: `E100 [loadStyles]`, exit 2, nothing linted.
- `vale sync` alone does not clear that `E100`. Sync fetches only what the top-level `Packages` key declares, so against a `BasedOnStyles`-only name it reports `Synced 0 package(s)` and exits 0, fetching nothing. Add the style to `Packages`, then sync. A style lints only once it is in both keys — and the reverse case is silent, exiting 0.
- Only *package* styles need fetching: built-in `Vale`, and any style whose YAML is already committed under `StylesPath`, lint with no `Packages` entry and no sync.
- `.vale.ini` is order-sensitive: core settings first, then `[formats]`, then glob sections. Anything written below a glob header applies only to files matching that glob. - `.vale.ini` is order-sensitive: core settings first, then `[formats]`, then glob sections. Anything written below a glob header applies only to files matching that glob.
- A rule scoped to `text.frontmatter.<key>` silently matches nothing when the field spans multiple lines in most YAML forms. If you scope a rule to frontmatter, read `references/configuration-reference.md` first. - A rule scoped to `text.frontmatter.<key>` silently matches nothing when the field spans multiple lines in most YAML forms. If you scope a rule to frontmatter, read `references/configuration-reference.md` first.
@@ -42,7 +45,7 @@ metadata:
- [ ] **Sync**: run `vale sync` to download everything listed in `Packages` into `StylesPath`. - [ ] **Sync**: run `vale sync` to download everything listed in `Packages` into `StylesPath`.
- [ ] **Verify activation**: confirm every style named in `Packages` also appears in at least one glob's `BasedOnStyles` — an unreferenced package downloads but never lints anything. - [ ] **Verify activation**: confirm every style named in `Packages` also appears in at least one glob's `BasedOnStyles` — an unreferenced package downloads but never lints anything.
For the full `.vale.ini` field reference (formats mapping, vocab, local overrides, custom rule header fields), read `references/configuration-reference.md`. For the full `.vale.ini` field reference (formats mapping, vocab, local overrides, custom rule header fields) and the verified style-resolution matrix — which error each misconfiguration raises, and the two that exit 0 while linting nothing — read `references/configuration-reference.md`.
## Custom styles ## Custom styles

View File

@@ -2,6 +2,7 @@
topic: configuration-reference topic: configuration-reference
source_keys: source_keys:
- context7-websites-vale-sh - context7-websites-vale-sh
- house-vale-3-15-2-repro
--- ---
## Core Settings ## Core Settings
@@ -68,7 +69,7 @@ Individual rule YAML files (under a style's directory) support these header fiel
The underlying functions a rule's `extends` field can reference: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`. The underlying functions a rule's `extends` field can reference: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`.
## Built-in Style ## Style Resolution
Only *package* styles need fetching. A style whose YAML rule files are already committed under `StylesPath` lints immediately, with no `Packages` entry and no `vale sync`; the same is true of the built-in `Vale` style, which ships with the binary and contains four rules: Only *package* styles need fetching. A style whose YAML rule files are already committed under `StylesPath` lints immediately, with no `Packages` entry and no `vale sync`; the same is true of the built-in `Vale` style, which ships with the binary and contains four rules:
@@ -77,11 +78,23 @@ Only *package* styles need fetching. A style whose YAML rule files are already c
- `Vale.Avoid` — enforces the project's rejected vocabulary terms. - `Vale.Avoid` — enforces the project's rejected vocabulary terms.
- `Vale.Repetition` — flags repeated words (e.g. "the the"). - `Vale.Repetition` — flags repeated words (e.g. "the the").
`Packages` (top-level, what `vale sync` downloads) and `BasedOnStyles` (per-glob, what activates) are separate keys: a style lints a file only once it is in both. Every row below reproduced against Vale 3.15.2 (slug `house-vale-3-15-2-repro`):
| Configuration | Result |
|---|---|
| `BasedOnStyles` names a style with no directory under `StylesPath`, not built-in | `E100 [loadStyles] Runtime error` — `style 'X' does not exist on StylesPath`, exit 2 |
| `StylesPath` directory itself absent, even with only `Vale` active | `E201 Invalid value` — `The path '...' does not exist`, exit 2 |
| `vale sync` with a name in `BasedOnStyles` but not `Packages` | `SUCCESS Synced 0 package(s)`, exit 0, nothing downloaded — the next lint repeats the `E100` |
| `vale sync` with the name added to `Packages` | package lands under `StylesPath`, exit 0; lint then loads it |
| Style in `Packages` and synced, but in no glob's `BasedOnStyles` | 0 findings, exit 0 — downloads, never lints, indistinguishable from a clean run |
| `BasedOnStyles` names an *empty* directory under `StylesPath` | 0 findings, exit 0 — loads and lints nothing; `vale sync` never produces this state |
| Built-in `Vale`, or a style's YAML committed under `StylesPath` | lints immediately, no `Packages` entry, no sync |
## Frontmatter Scopes ## Frontmatter Scopes
A rule scoped to `text.frontmatter.<key>` (e.g. `text.frontmatter.description`) matches reliably when that field's value is a single physical line, and breaks on most — not all — multi-line forms. House-verified behaviour, not documented on vale.sh — reproduced locally against Vale 3.15.2 (slug `house-vale-3-15-2-repro`).
Confirmed against Vale 3.15.2, multi-line forms spanning 2+ lines: A rule scoped to `text.frontmatter.<key>` (e.g. `text.frontmatter.description`) matches reliably when that field's value is a single physical line, and breaks on most — not all — multi-line forms. Multi-line forms spanning 2+ lines:
| Frontmatter value form | Result | | Frontmatter value form | Result |
|---|---| |---|---|

View File

@@ -7,3 +7,11 @@
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md - **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
- **Contributing files:** SKILL.md, references/configuration-reference.md - **Contributing files:** SKILL.md, references/configuration-reference.md
- **Status:** `extracted` - **Status:** `extracted`
## house-vale-3-15-2-repro
- **URL:** (house-verified — reproduced locally against the `vale` binary, not an external source)
- **Description:** Behaviour of Vale 3.15.2 established by running it against purpose-built fixtures in this repo, where vale.sh documents nothing: the `E100 [loadStyles]` / exit-2 failure for a `BasedOnStyles` name absent from `StylesPath`, `vale sync` reporting `Synced 0 package(s)` for a name not declared in `Packages`, the `E201` / exit-2 failure when the `StylesPath` directory does not exist, the exit-0 no-op of an empty style directory, and the `text.frontmatter.<key>` scope matrix across multi-line YAML forms.
- **Research doc:** none — house-verified reproduction, not part of the plugin's research corpus (no `plugins/lint/docs/research/` topic file backs this entry)
- **Contributing files:** SKILL.md, references/configuration-reference.md
- **Status:** `extracted`