Compare commits
17 Commits
6c0afb7c81
...
aff5b6c4c8
| Author | SHA1 | Date | |
|---|---|---|---|
| aff5b6c4c8 | |||
| 149d564f6a | |||
| 210b192613 | |||
| 792d3e1852 | |||
| 3324a73225 | |||
| 544392be98 | |||
| bbb0dcd21a | |||
| 8d56290414 | |||
| cbc33d952e | |||
| f326df4861 | |||
| 57bdfa92e8 | |||
| 59ad2a3cbd | |||
| 8b00728374 | |||
| d1afdbeff7 | |||
| 5e22672189 | |||
| 0ba8a95188 | |||
| 533364029a |
@@ -38,7 +38,12 @@
|
||||
"repo": "mattpocock/skills",
|
||||
"source": "github"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"name": "lint",
|
||||
"source": "./plugins/lint"
|
||||
}
|
||||
],
|
||||
"version": "0.2.0"
|
||||
"version": "0.3.1"
|
||||
}
|
||||
|
||||
7
.github/plugin/marketplace.json
vendored
7
.github/plugin/marketplace.json
vendored
@@ -38,7 +38,12 @@
|
||||
"repo": "mattpocock/skills",
|
||||
"source": "github"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"name": "lint",
|
||||
"source": "./plugins/lint"
|
||||
}
|
||||
],
|
||||
"version": "0.2.0"
|
||||
"version": "0.3.1"
|
||||
}
|
||||
|
||||
@@ -98,6 +98,24 @@ repos:
|
||||
fi
|
||||
done
|
||||
|
||||
- id: skill-size-check
|
||||
stages: ['pre-commit']
|
||||
name: SKILL.md size ceiling
|
||||
description: Enforce agentskills.io's 500-line/5,000-token SKILL.md size ceiling
|
||||
entry: scripts/skill-size-check.sh
|
||||
language: script
|
||||
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
|
||||
pass_filenames: true
|
||||
|
||||
- id: vale-audit-prefilter
|
||||
stages: ['pre-commit']
|
||||
name: Vale audit prefilter
|
||||
description: Run Vale against skill/agent markdown files as a deterministic prefilter for skill-audit/agent-audit
|
||||
entry: scripts/vale-wrap.sh --config .vale.ini
|
||||
language: script
|
||||
files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$'
|
||||
pass_filenames: true
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
|
||||
10
.vale.ini
Normal file
10
.vale.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
StylesPath = styles
|
||||
|
||||
[**/SKILL.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
|
||||
[**/agents/*.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
|
||||
[**/*.agent.md]
|
||||
BasedOnStyles = Kyberforge, KyberforgeCopilot
|
||||
39
AGENTS.md
39
AGENTS.md
@@ -1,26 +1,31 @@
|
||||
# Working in this repo
|
||||
|
||||
This repo is the global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects.
|
||||
This repo is the global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects. Built as a homelab tool intended to scale to professional environments.
|
||||
|
||||
## Structure
|
||||
|
||||
- `plugins/` — installable plugin units; each is self-contained (skills, agents, hooks, MCP servers, bundled assets); install separately via `claude plugin install <name>@holocron`
|
||||
- `providers/claude-code/` — Claude Code adapter (deployed to `~/.claude/` via `install.sh`)
|
||||
|
||||
## Setup
|
||||
## Prefer plugin skills over raw shell
|
||||
|
||||
- Install git hooks: `pre-commit install -t pre-commit -t pre-push -t commit-msg`. `.pre-commit-config.yaml` uses all three stages and has no `default_install_hook_types` set, so a plain `pre-commit install` only wires the `pre-commit` stage and silently skips `commit-msg` (Conventional Commits check) and `pre-push` (test suite, manifest check).
|
||||
This repo dogfoods its own plugins. Before shelling out to git, gitea, or lint tooling directly, check whether an installed skill already owns the operation — it usually does:
|
||||
|
||||
## Testing instructions
|
||||
- Commits, branches, history, worktrees, remotes → `git:git-commits`, `git:git-branches`, `git:git-history`, `git:git-worktrees`, `git:git-remotes`
|
||||
- Pre-commit hook install/config/troubleshooting → `git:pc-run` / `git:pc-author`
|
||||
- Issues, PRs, labels, milestones → `bin:gitea`
|
||||
- Vale prose linting → `lint:vale-config` / `lint:vale-run`
|
||||
- This repo's own AGENTS.md → `core:agentsmd-author` / `core:agentsmd-audit`
|
||||
|
||||
- Run `bash tests/run-tests.sh` before considering any change done, and fix failures — it runs every `test-*.sh` script in the repo plus the bats suite (`tests/run-bats.sh`).
|
||||
- `bash tests/run-tests.sh --bats-only` runs just the bats suite.
|
||||
- The bats suite auto-initializes its submodules (`tests/bats`, `tests/test_helper/bats-support`, `tests/test_helper/bats-assert`) on first run if the `bats` binary is missing — no manual `git submodule update` needed.
|
||||
Fall back to raw shell only when no skill covers it.
|
||||
|
||||
## Commit / PR conventions
|
||||
## Setup and testing
|
||||
|
||||
- Commit messages must follow Conventional Commits — enforced by the `conventional-pre-commit` hook at the `commit-msg` stage.
|
||||
- Pushing runs the full test suite (`tests/run-tests.sh`) and `scripts/check-manifests.sh` (validates `marketplace.json`/`plugin.json` paths resolve) via pre-push hooks — run both locally first so a failing push isn't a surprise.
|
||||
- Install git hooks via `git:pc-run`, wiring all three stages — this repo's `.pre-commit-config.yaml` has no `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (tests, manifest check).
|
||||
- Install the `vale` binary — required by the `vale-audit-prefilter` pre-commit hook, which runs on every commit touching a `SKILL.md` or agent `.md` file. Without it the hook fails with a bare "command not found" and no install pointer. `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or see https://vale.sh/docs/vale-cli/installation/. No `vale sync` needed — the `Kyberforge` styles are committed under `styles/`, not downloaded packages.
|
||||
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
||||
- Pushing re-runs the full suite plus `scripts/check-manifests.sh` via the pre-push hook — same commands, so run them locally first.
|
||||
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
||||
|
||||
## Key documents
|
||||
|
||||
@@ -28,23 +33,9 @@ Read CONTEXT.md at the start of every session in this repo.
|
||||
|
||||
Read these on demand:
|
||||
|
||||
- `docs/VISION.md` — purpose, goals, and long-term Management Application vision
|
||||
- `docs/spec/architecture.md` — current directory structure, install pipeline, provider model
|
||||
- `docs/adr/` — architectural decisions; read before answering design questions or proposing structural changes
|
||||
- `docs/ai-constitution.md` — full governance evidence base; read when a governance decision needs justification
|
||||
- `docs/research/ai-coding-factory/ai-coding-factory-principles.md` — factory design rationale; read when implementing, auditing, or reviewing skills or factory structure
|
||||
- `docs/notes/factory-integration-decisions.md` — decisions from the factory integration grill; read when making skill authoring or factory design decisions
|
||||
- Governance rules are always in effect — `core/instructions/governance.md` (agent rules); `docs/research/governance_principles/CONTROLS.md`
|
||||
|
||||
## Working context
|
||||
|
||||
This repo is built by a junior developer as a homelab tool intended to scale to professional environments. Challenge ideas and reference industry standards rather than validate assumptions. Explain the why behind decisions — assume the user is learning, not just executing. Flag significant actions before taking them.
|
||||
|
||||
## Subagent orchestration
|
||||
|
||||
- **Forks stop when their assigned task is done.** A `fork` inherits the coordinator's full context, including visibility into any shared TaskList. That visibility is not license to keep going: once a fork's assigned task is reported complete, it must stop rather than autonomously picking up further items from the list. Forks that keep pulling work race against the coordinator's own orchestration and can duplicate or conflict with tasks the coordinator has separately delegated.
|
||||
- **Don't hand a fork a TaskList that includes governance-gated actions** (push, publish, merge) unless you are prepared for it to act on those items without a fresh confirmation round. A fork acting on its own initiative is not party to any pending human confirmation the coordinator is mid-flow on, so it can bypass a gate that was meant to hold.
|
||||
- **`TaskGet`/`TaskUpdate`/`TaskList` only work for forks.** Fresh (non-fork) subagents cannot discover or call these tools. When delegating to a fresh subagent, the coordinator owns all task-list bookkeeping itself — claim and complete the entry on the agent's behalf — rather than instructing the fresh agent to self-claim or self-complete.
|
||||
- **Worktree/branch cleanup is part of closing out the PR, not a separate step.** When a coordinator creates a worktree (`Agent(isolation: "worktree")` or `git worktree add` directly) to land a PR, merging that PR is not the end of the task. Immediately after verifying the merge: run `git worktree remove --force --force <path>` (the double `-f` is required whenever the worktree initialized submodules to run tests — assume it did, this repo has several), then `git branch -d` both the feature branch and any `worktree-agent-<id>` isolation branch the `Agent` tool auto-created for that worktree — `git worktree remove` deletes neither branch on its own. Do this without waiting for the user to notice stale branches/worktrees and ask.
|
||||
- **`Agent(isolation: "worktree")` may fork from `main`, not the branch you were on.** Don't assume the isolated worktree is based on your current branch just because that's what you asked for — in practice it has forked from a stale `main` (missing commits the task depended on) even when the coordinator was on a feature branch at call time. Every affected agent has to notice (missing files, unexpected diff base) and self-correct with `git merge --ff-only <target-branch>` or a reset onto `origin/<target-branch>` before it can safely edit. Don't leave this to chance: tell the agent explicitly in the prompt which branch its worktree must be based on and to verify/rebase onto it as a first step before editing anything, and check for this yourself when reviewing a worktree agent's report.
|
||||
- **Don't route already-fully-specified corrective edits through `kyberforge:forge`.** `forge` exists to classify ambiguous "what should I build" intent before routing to an author skill — it isn't needed when the coordinator already knows the exact file, line, and fix. Sending fully-specified fixes through `forge` adds a grill-and-delegate layer that can itself spawn further sub-delegation (forked sub-subagents), which has been observed to lose track of hard constraints handed down the chain (e.g. "don't commit yet," "edit in this worktree") because each hop re-derives instructions from a shorter brief. Call the target author skill (`skill-author`, `plugin-author`, etc.) directly for known fixes; reserve `forge` for genuinely undecided "which artifact type is this" questions.
|
||||
|
||||
14
CONTEXT.md
14
CONTEXT.md
@@ -49,7 +49,7 @@ The provider-agnostic always-on instruction entry point. Two files:
|
||||
Contains always-on rules in plain markdown with no provider-specific syntax (no `@import`). Provider-specific files (`CLAUDE.md`) are thin adapters that import the relevant `AGENTS.md` and add only Claude Code-specific syntax. This pattern means a single source of truth can serve multiple providers without duplication. See ADR-0003.
|
||||
|
||||
### Skill composition
|
||||
A skill calling another skill by name to delegate a sub-task. The calling skill focuses on the orchestration decision ("when to do X"); the called skill owns the mechanics ("how to do X"). Established compositions: `grill-me` calls `write-adr` when a decision crystallises; `implement-feature` calls `tdd` as its implementation methodology; `forge` calls `grill-with-docs` to refine intent, classifies the target artifact type (skill / agent / plugin / marketplace entry), then routes to the matching `*-author` skill — which owns its own create/improve logic and, where applicable, its own inline audit closeout (`skill-author` runs `/skill-audit`, `agent-author` runs `kyberforge:agent-audit`, both in the same context as the authoring work). `forge` additionally runs its own independent recheck after a skill/agent route finishes: a clean-context subagent (not forked, no inherited context) re-runs the same audit skill against the finished artifact, as a distinct verification layer from the author skill's inline audit — the two can share blind spots since the inline audit runs in the same context as the work it checks. If the clean audit surfaces any unresolved finding, `forge` loops — re-invoke the author skill to resolve it, re-run the clean audit — until the clean audit comes back with nothing unresolved; only then is the route done. `plugin-author` and `marketplace-author` have no audit counterpart and get no recheck; their terminal check is `claude plugin validate`.
|
||||
A skill calling another skill by name to delegate a sub-task. The calling skill focuses on the orchestration decision ("when to do X"); the called skill owns the mechanics ("how to do X"). Established compositions: `grill-me` calls `write-adr` when a decision crystallises; `implement-feature` calls `tdd` as its implementation methodology; `forge` calls `grill-with-docs` to refine intent, classifies the target artifact type (skill / agent / plugin / marketplace entry), then routes to the matching `*-author` skill — which owns its own create/improve logic and, where applicable, its own inline audit closeout (`skill-author` runs `/skill-audit`, `agent-author` runs `kyberforge:agent-audit`, both in the same context as the authoring work). Reserve `forge` for genuinely undecided "which artifact type is this" questions — an already-fully-specified corrective edit (exact file, line, and fix already known) should call the target author skill directly instead (`skill-author`, `plugin-author`, `agentsmd-author`, etc.); routing a known fix through `forge`'s grill-and-classify layer adds unnecessary indirection and, in practice, has been observed to lose track of hard constraints handed down the chain (e.g. "don't commit yet," "edit in this worktree") because each hop re-derives instructions from a shorter brief. `forge` additionally runs its own independent recheck after a skill/agent route finishes: a clean-context subagent (not forked, no inherited context) re-runs the same audit skill against the finished artifact, as a distinct verification layer from the author skill's inline audit — the two can share blind spots since the inline audit runs in the same context as the work it checks. If the clean audit surfaces any unresolved finding, `forge` loops — re-invoke the author skill to resolve it, re-run the clean audit — until the clean audit comes back with nothing unresolved; only then is the route done. `plugin-author` and `marketplace-author` have no audit counterpart and get no recheck; their terminal check is `claude plugin validate`.
|
||||
|
||||
### Provider-agnostic issue tracker
|
||||
Skills and workflows reference "linked issue" generically rather than a specific provider. Gitea is the canonical issue tracker for this repo (see ADR-0017). "Issue" is the cross-provider term (GitHub, GitLab, Gitea all use it).
|
||||
@@ -61,10 +61,20 @@ The three-stage traceability record linking a skill back to its research inputs:
|
||||
Files that reference other files should declare those references explicitly. The referencing file carries the forward reference (e.g. content index in `CLAUDE.md`, `references:` in frontmatter). The referenced file carries a `when:` field describing when it is loaded. Both sides should agree — divergence signals staleness. The reverse map ("what files reference this file?") is derived by a reference scanner script, not maintained manually. This principle applies to instruction files, skills, and workflow documents.
|
||||
|
||||
### agentsmd-author / agentsmd-audit
|
||||
A skill pair in the `core` plugin for writing, updating, and reviewing a target repo's `AGENTS.md` file(s) (the generic open-standard file — see the `AGENTS.md` entry above — not this repo's own). `agentsmd-author` creates/updates AGENTS.md content, supports nested monorepo placement (per the standard's nearest-file-wins precedence), and closes out by invoking `agentsmd-audit` inline. `agentsmd-audit` runs a single combined pass checking three mandatory baselines: secrets/credentials (governance.md hard prohibition — AGENTS.md is committed content), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands and paths actually resolve against the repo). `agentsmd-audit` never inspects provider adapter files (see `provider-adapter-author`) — its scope is AGENTS.md content only. Chosen over folding this into `kyberforge` because kyberforge's scope is meta-tooling for the holocron marketplace itself, not generic target-repo documentation; `core` is the intended home for cross-cutting, repo-agnostic utility skills.
|
||||
A skill pair in the `core` plugin for writing, updating, and reviewing a repo's `AGENTS.md` file(s) — the generic open-standard file (see the `AGENTS.md` entry above), including this repo's own. `agentsmd-author` creates/updates AGENTS.md content, supports nested monorepo placement (per the standard's nearest-file-wins precedence), and closes out by invoking `agentsmd-audit` inline. `agentsmd-audit` runs a single combined pass checking three mandatory baselines: secrets/credentials (governance.md hard prohibition — AGENTS.md is committed content), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands and paths actually resolve against the repo). `agentsmd-audit` never inspects provider adapter files (see `provider-adapter-author`) — its scope is AGENTS.md content only. Chosen over folding this into `kyberforge` because kyberforge's scope is meta-tooling for the holocron marketplace itself, not generic target-repo documentation; `core` is the intended home for cross-cutting, repo-agnostic utility skills.
|
||||
|
||||
### provider-adapter-author
|
||||
A companion skill (`core` plugin) that detects a target repo's provider-specific instruction file (`CLAUDE.md`, `.cursor/rules/*.mdc`, `copilot-instructions.md`, etc.) and, where it duplicates content AGENTS.md should own, converts it into a thin adapter that imports AGENTS.md — mirroring this repo's own ADR-0002/ADR-0003 two-tier adapter pattern. Self-validates via its own bundled deterministic script (`scripts/validate-adapter.sh`: checks for an import reference, no duplicated headings, size threshold) rather than a separate paired audit skill — the check is mechanical, so a script suffices per governance.md's "prefer deterministic code for repeatable tasks." `agentsmd-author` calls this skill via skill composition when it detects an existing provider file with overlapping content.
|
||||
|
||||
### lint plugin
|
||||
A standalone, repo-agnostic plugin (`plugins/lint/`) for configuring and running linters — not scoped to kyberforge's own meta-tooling. First linter is Vale (prose style linting), split into two skills per the git/gitea per-concern pattern: `vale-config` (setup — `.vale.ini`, `StylesPath`, styles) and `vale-run` (invoke Vale, interpret/report findings). A `lint-runner` agent composes these for isolated-context lint sweeps; it is report-only (no `Edit` tool) — it flags findings, it does not rewrite prose. Vale's research docs (`docs/research/docs/vale/`) moved from `plugins/kyberforge/` to `plugins/lint/` to keep the provenance chain same-plugin.
|
||||
|
||||
### Vale audit prefilter (skill-audit / agent-audit)
|
||||
Wiring Vale as a deterministic prefilter for `skill-audit`/`agent-audit`'s Description dimension (ADR motivation: issue #84) is repo-specific, not part of the generic `lint` plugin, so its config lives at the repo root rather than inside `plugins/lint/`: `.vale.ini` plus a custom `Kyberforge` style (`styles/Kyberforge/`) covering description-opener banning ("This skill/agent..."), vague-capability wording ("helps with", "utilize", ...), and generic "see references/ for details" padding — and a `KyberforgeCopilot` style (`styles/KyberforgeCopilot/`) scoped only to `.agent.md` files for the Copilot-only "Use proactively has no effect" check. Every rule is `level: error` and every alert is a FAIL — no ignorable tier, same as shellcheck, the test suite, and conventional-pre-commit. Graded severities do not work here: Vale's exit code keys on `error` alerts alone, so `warning`/`suggestion` rules exit 0 and pre-commit swallows the output of a passing hook, leaving them invisible and blocking nothing. `MinAlertLevel` and `--minAlertLevel` are correspondingly absent from `.vale.ini` and the hook, being no-ops under this model. Vale covers the pattern-matchable sub-checks named in issue #84 (imperative opener, vague filler, `Use proactively`, generic reference-pointer padding) plus, per ADR-0013, one body-wide prose-pattern check ("There is/are" sentence openers) — everything else about body discipline (defaults-vs-menus, why-rationale, non-pattern-matchable judgment calls), near-miss exclusion strength, and control calibration stays LLM judgment.
|
||||
|
||||
Both skills' Step 1, and the `vale-audit-prefilter` pre-commit hook, call `scripts/vale-wrap.sh` rather than `vale` directly — a workaround for a confirmed Vale 3.15.2 limitation (see `vale-config`'s Gotchas): `text.frontmatter.description` silently stops matching once the description is a YAML block scalar (`>`/`|`) spanning 2+ physical lines, which is how most skills/agents in this repo write it. The wrapper flattens the description to one physical line in a scratch copy (padding with blank lines so every other line number is unchanged) before handing off to real `vale`; single-line descriptions pass through untouched; a relative `--config` path resolves against the caller's cwd, matching bare `vale`, not against the repo root. `tests/test-vale-wrap.sh` regression-tests this. `.vale.ini`'s section globs are path-agnostic (`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`) and do no scoping: Vale's `*` crosses `/`, so the older `plugins/*/`-prefixed globs already matched `plugins/*/docs/research/examples/**/agents/*.md` and `plugins/*/skills/*/assets/templates/SKILL.md`. Scoping comes from the pre-commit hook's `files:` regex, `^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$` (single-segment, not `.*`, since pre-commit invokes it automatically against whatever staged files match rather than a manually-scoped target), and from the audit skills passing one explicit file per invocation. The path-agnostic globs also close a silent false negative: a skill outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) matched no section, so Vale reported 0 files and exited 0 — which the audits read as clean. Both audits now treat a 0-file Vale run as NOT RUN and fall back to full LLM judgment.
|
||||
|
||||
This scope expands per ADR-0013: one cherry-picked low-noise `write-good`/`alex` rule landed in `styles/Kyberforge`, `Kyberforge.SentenceOpenerThereIs` (22 held-out hits, both in-corpus hits clean rewrites, zero suppressions). A second, `Kyberforge.VagueQualifier`, was cherry-picked and then deleted: 2 hits across the 41 skill/agent files, one marginal and one an unfixable false positive (`caveman/SKILL.md` quotes `of course` as an example of filler — a mention, not a use) that forced the repo's only Vale suppression comments. Also new is a sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling — failing only above 500 lines, matching `skill-audit/scripts/validate.sh`'s `<= 500` pass — scoped to `^plugins/[^/]+/skills/[^/]+/SKILL\.md$` only, same as `vale-audit-prefilter`, so it never lints `docs/research/examples/` reference skills. File scope (`SKILL.md` + agent files) and enforcement model (rules land directly in `styles/Kyberforge`, blocking immediately, no trial tier) stay unchanged; governance.md/CONTROLS.md were evaluated and excluded as rule sources (nothing prose-pattern-matchable to mine). House convention: banned phrasing that must be mentioned rather than used goes in backticks or a fenced code block — Vale skips code spans and fences, so no suppression is needed; inline `<!-- vale Rule = NO -->` (HTML-comment form; the MDX `{/* */}` form does not work in plain Markdown) is the fallback only where backticking is impossible.
|
||||
|
||||
### LESSONS.md
|
||||
Long-loop feedback log for patterns observed across sessions. Three or more entries on the same pattern graduate to the relevant standing file (e.g. a coding convention, a governance rule). Updated by the session-handoff skill or directly by the human. Lives at the repo root.
|
||||
|
||||
@@ -23,3 +23,4 @@ Read these files on demand:
|
||||
|
||||
- **Coding conventions** (`~/.claude/core/instructions/coding.md`) — when writing, editing, or reviewing code
|
||||
- **Testing conventions** (`~/.claude/core/instructions/testing.md`) — when writing or running tests
|
||||
- **Subagent orchestration** (`~/.claude/core/instructions/subagent-orchestration.md`) — when spawning or coordinating subagents/forks
|
||||
|
||||
6
core/instructions/subagent-orchestration.md
Normal file
6
core/instructions/subagent-orchestration.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Subagent orchestration
|
||||
|
||||
- A fork stops when its assigned task is done. It inherits the coordinator's full context, including any shared TaskList — that visibility is not license to keep pulling further items after its assigned task is reported complete; doing so races the coordinator's own orchestration and can duplicate or conflict with separately-delegated work.
|
||||
- Don't hand a fork a TaskList containing governance-gated actions (push, publish, merge) unless prepared for it to act on those without a fresh confirmation round. A fork acting on its own initiative is not party to any pending human confirmation the coordinator is mid-flow on.
|
||||
- `TaskGet`/`TaskUpdate`/`TaskList` only work for forks. Fresh (non-fork) subagents cannot discover or call these tools — when delegating to a fresh subagent, the coordinator owns all task-list bookkeeping itself.
|
||||
- `Agent(isolation: "worktree")` may fork from `main`, not the branch the coordinator was on. Verify and self-correct (`git merge --ff-only <target-branch>` or reset onto `origin/<target-branch>`) before editing. When removing such a worktree afterward, use `git worktree remove --force --force <path>` if the repo has submodules (double `-f` required), then `git branch -d` both the feature branch and the auto-created `worktree-agent-<id>` isolation branch.
|
||||
115
docs/adr/0013-vale-harness-scope-and-rule-sources.md
Normal file
115
docs/adr/0013-vale-harness-scope-and-rule-sources.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Vale audit prefilter expands into a plugin-content harness, scoped to prose-pattern rules only
|
||||
|
||||
Issue #84 wired Vale as a deterministic prefilter for `skill-audit`/`agent-audit`, scoped to
|
||||
exactly four pattern-matchable checks (imperative description opener, vague capability wording,
|
||||
generic reference-pointer padding, Copilot's dead `Use proactively` phrasing), documented only in
|
||||
CONTEXT.md's "Vale audit prefilter" section — never its own ADR — and explicitly excluding body
|
||||
discipline, near-miss exclusion strength, and control calibration as non-goals. This ADR records a
|
||||
deferred PR #85 review item to broaden that coverage, retroactively captures #84's own rationale
|
||||
(since it was never recorded as a decision in its own right), and layers the expansion on top
|
||||
without reversing or weakening the original four rules.
|
||||
|
||||
**File scope stays the same.** `SKILL.md` plus agent files (`**/agents/*.md`,
|
||||
`**/*.agent.md`) only — matching the existing prefilter's globs. Skill-level
|
||||
`README.md` files and `plugin.json` manifests are not added: README.md files are navigational, not
|
||||
spec-governed content, and `plugin.json` is JSON, not prose Vale can meaningfully lint.
|
||||
|
||||
**Rule categories are prose-pattern-matchable only.** Structural, schema, and security concerns
|
||||
stay out of this Vale-based harness because this repo already has dedicated tools for them:
|
||||
`skill-frontmatter` (required frontmatter fields), `validate-plugins`/`validate-marketplace`
|
||||
(`claude plugin validate --strict`, schema), and `gitleaks`/`detect-private-key` (secrets).
|
||||
Duplicating those concerns as Vale rules would fight tools that already own them better.
|
||||
|
||||
**Governance docs are excluded as a rule source.** `docs/research/governance_principles/CONTROLS.md`
|
||||
and `governance.md` were investigated and found to contribute nothing minable: CONTROLS.md is
|
||||
org/CI-infrastructure controls (secret scanning, dependency/license scanning, agent permission
|
||||
scoping, audit logging, human approval gates, periodic reviews) — none of it is a prose pattern
|
||||
expressible as a Vale rule against SKILL.md/agent-file text, and what it does cover is either
|
||||
already handled elsewhere (gitleaks) or genuinely out of scope for a plugin-content prose harness
|
||||
(dependency/license scanning is a code-dependency concern, not skill authoring).
|
||||
|
||||
**Spec-derived custom rules stay mostly as-is.** Re-reading agentskills.io's
|
||||
`optimizing-descriptions.md` and `skill-authoring.md`, plus `claude-code-plugins/agent-definition.md`
|
||||
and `github-copilot-plugins/agent-definition.md`, found that the existing four Kyberforge rules
|
||||
already cover the pattern-matchable surface those specs describe. The remaining spec guidance —
|
||||
calibrating control vs. giving freedom, avoiding menus of options, coherent skill scope, moderate
|
||||
detail level — is semantic judgment, already `skill-audit`'s job via LLM review, not new lintable
|
||||
rules. One confirmation surfaced: Claude Code's `Use proactively` phrasing is meaningful for `.md`
|
||||
agent files (it triggers auto-invocation), unlike Copilot's `.agent.md` files where it's dead
|
||||
phrasing — so `KyberforgeCopilot/ProactivePhrase`'s existing `.agent.md`-only scope is correct and
|
||||
must not be extended to `.md` files.
|
||||
|
||||
**`write-good`/`alex` are trialed, not adopted wholesale.** These built-in/third-party Vale
|
||||
packages are tuned for general blog-style prose (passive voice, weasel words, wordy phrases) and
|
||||
are expected to be noisy against this repo's terse, imperative instruction-file corpus. Only
|
||||
individual rules proven low-noise against the existing corpus get cherry-picked into
|
||||
`styles/Kyberforge`; the packages are never referenced wholesale in `BasedOnStyles`.
|
||||
|
||||
**A new non-Vale check closes a real gap.** `skill-authoring.md` states `SKILL.md` should stay
|
||||
under 500 lines / 5,000 tokens — currently unenforced anywhere in this repo. This is a whole-file
|
||||
length ceiling, not a text pattern, so it isn't a Vale rule — it becomes a new deterministic script
|
||||
and pre-commit hook, sibling to the existing `skill-frontmatter` hook.
|
||||
|
||||
**Rules land directly in `styles/Kyberforge`, enforcing immediately.** No trial/report-only tier
|
||||
is introduced (see Considered Options). "Enforcing immediately" holds only because every rule in
|
||||
both styles is `level: error`: Vale's exit code keys on `error`-level alerts alone, so a
|
||||
`warning`- or `suggestion`-level rule prints an alert and still exits 0, and pre-commit suppresses
|
||||
output from hooks that pass — such a rule is invisible and blocks nothing. Every Vale alert is
|
||||
therefore a FAIL, in the audit skills and in the blocking pre-commit hook alike, with no ignorable
|
||||
tier; that matches every other gate in this repo (shellcheck, the test suite,
|
||||
conventional-pre-commit). The implementation pass finalizes the cherry-picked
|
||||
`write-good`/`alex` rules and any new spec-derived rule wording, runs the full set against the
|
||||
existing SKILL.md/agent-file corpus, fixes any resulting violations across that corpus, and lands
|
||||
the rule changes and the corpus fixes as one atomic commit — the same enforcement model as the
|
||||
original four rules, never a partial or opt-in state.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Phased rollout via a separate trial style + config (rejected).** A `styles/KyberforgeTrial/`
|
||||
directory plus a parallel `.vale.trial.ini` (mirroring the root config's globs but with
|
||||
`BasedOnStyles = Kyberforge, KyberforgeTrial`) would let new rules be swept report-only via
|
||||
`lint-runner`/`vale-run` before promotion into the enforcing `styles/Kyberforge` + root
|
||||
`.vale.ini`. This was considered because `BasedOnStyles = Kyberforge` activates every rule file
|
||||
under that directory automatically — there's no partial/opt-in application within a style, so a
|
||||
rule dropped straight into `styles/Kyberforge` goes live in the blocking pre-commit hook
|
||||
immediately. Rejected in favor of finalizing rules directly and fixing violations via subagent
|
||||
before committing: simpler, no new trial-config machinery to build or maintain — at the cost of no
|
||||
standing report-only tier for future candidate rules. Note that the first implementation shipped
|
||||
graded severities (`error`/`warning`/`suggestion`) and thereby recreated the rejected option by
|
||||
accident: the five non-`error` rules never affected an exit code and never surfaced output through
|
||||
a passing pre-commit hook, so they were a report-only tier that reported to nobody. Flattening
|
||||
every rule to `level: error` is what actually implements this decision.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `styles/Kyberforge/` gained one new rule file, cherry-picked from `write-good`/`alex` as
|
||||
low-noise against this repo's corpus: `SentenceOpenerThereIs.yml` (22 hits across 273 held-out
|
||||
markdown files; both in-corpus hits were clean rewrites, needing no suppression).
|
||||
- A second candidate, `VagueQualifier.yml`, was cherry-picked and then dropped. Against the 41
|
||||
skill/agent files it hit twice: one marginal real finding (`prototype/SKILL.md`, "very different"
|
||||
→ "fundamentally different") and one false positive (`caveman/SKILL.md`, which *quotes* `of
|
||||
course` as an example of filler — a mention, not a use) that no rewrite could clear, forcing the
|
||||
repo's only Vale suppression comments. Of its 15 held-out hits, 9 were in `docs/research/examples/`
|
||||
(out-of-scope upstream material) and the remaining 6 were the word "very" in two idioms in a
|
||||
single research doc, each already adjacent to the hard number carrying the fact. One marginal
|
||||
catch does not pay for a permanent suppression, so the rule is deleted and this ADR's
|
||||
"cherry-picked rules" is one rule, not two.
|
||||
- A new pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforces the
|
||||
500-line/5,000-token `SKILL.md` ceiling, sibling to `skill-frontmatter`. It fails only *above*
|
||||
500 lines, matching `skill-audit/scripts/validate.sh`'s long-standing `<= 500` pass.
|
||||
- `styles/KyberforgeTrial/` and `.vale.trial.ini` were deliberately not created — noted here so a
|
||||
future reader doesn't wonder if a trial tier was forgotten.
|
||||
- The styles-portability question — whether `styles/` and `.vale.ini` should move into
|
||||
`plugins/lint/` so the prefilter also works for repos that install `kyberforge@holocron` as an
|
||||
external plugin, rather than living at this repo's root — was deliberately deferred, not fixed,
|
||||
in this pass. This repo-root placement remains intentional: this ADR's "File scope stays the
|
||||
same" framing is specific to Kyberforge's own authoring conventions in this repo, not a generic
|
||||
`lint`-plugin feature. Portability is a known limitation, tracked for a separate future session,
|
||||
not silently forgotten.
|
||||
|
||||
**What this ADR's implementation pass did:** synced and trialed `write-good`/`alex` against the
|
||||
existing SKILL.md/agent-file corpus, cherry-picked the one low-noise rule above into
|
||||
`styles/Kyberforge`, wrote `scripts/skill-size-check.sh` and its pre-commit hook, fixed the
|
||||
resulting corpus violations, and landed the rule changes and corpus fixes as one atomic commit —
|
||||
matching the enforcement model described above (no partial or opt-in state), with every rule at
|
||||
`level: error` so that model is real rather than nominal.
|
||||
@@ -8,5 +8,5 @@
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"name": "bin",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.1"
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"skills": [
|
||||
"skills/"
|
||||
],
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.1"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Identify which question is being answered — from the user's prompt, the surrou
|
||||
- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Build a tiny interactive terminal app that pushes the state machine through cases that are hard to reason about on paper.
|
||||
- **"What should this look like?"** → [UI.md](UI.md). Generate several radically different UI variations on a single route, switchable via a URL search param and a floating bottom bar.
|
||||
|
||||
The two branches produce very different artifacts — getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype.
|
||||
The two branches produce fundamentally different artifacts — getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype.
|
||||
|
||||
## Rules that apply to both
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ allowed-tools: Bash mcp__gitea__list_issues mcp__gitea__issue_read mcp__gitea__i
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`list_issues` has no `type` or `milestones` parameter — despite `api-reference.md` documenting both.** The live MCP schema (re-verified via `ToolSearch` at authoring time — see `references/sources.md`) only accepts `owner`, `repo` (required), `state` (default `"all"`), `labels` (array of label *names*), `since`, `before` (ISO 8601), `page`, `per_page` (default 30). There is no way to filter issues-vs-PRs or by milestone through this tool. Since issues and PRs share one number space, `list_issues` results can include PR entries with no client-side filter to exclude them. If you need to know whether a specific number is a PR, call `issue_read method: "get"` and check `is_pull` — that field only appears on the single-item response, never in a list item. This exact drift (a prior skill trusted the research doc's `type` param and broke) is why this skill's reference files were re-verified live rather than copied from `api-reference.md`.
|
||||
- **`list_issues` has no `type` or `milestones` parameter — despite `api-reference.md` documenting both.** The live MCP schema (re-verified via `ToolSearch` at authoring time — see `references/sources.md`) only accepts `owner`, `repo` (required), `state` (default `"all"`), `labels` (array of label *names*), `since`, `before` (ISO 8601), `page`, `per_page` (default 30). This tool provides no way to filter issues-vs-PRs or by milestone. Since issues and PRs share one number space, `list_issues` results can include PR entries with no client-side filter to exclude them. If you need to know whether a specific number is a PR, call `issue_read method: "get"` and check `is_pull` — that field only appears on the single-item response, never in a list item. This exact drift (a prior skill trusted the research doc's `type` param and broke) is why this skill's reference files were re-verified live rather than copied from `api-reference.md`.
|
||||
- **`search_issues` does have a working `type` filter** (`"issues"` | `"pulls"`) — unlike `list_issues`. Its `labels` parameter is also shaped differently: a comma-separated string, not an array of names.
|
||||
- **Labels are numeric IDs on write, name strings on read.** `issue_write`'s `labels` parameter (used by `add_labels`/`replace_labels`) takes IDs. `list_issues`/`issue_read` return names. Never resolve this yourself — compose `gitea-labels-milestones` (see `references/enrichments.md`) to get IDs.
|
||||
- **Milestone on `issue_read` is `{id, title}`** — an object, not a bare string. This skill only ever needs the `id`. (The bare-title-string case only happens on the PR side, which is `gitea-prs`' problem, not this skill's.)
|
||||
@@ -86,7 +86,7 @@ Call `issue_read method: "get_comments" owner: <owner> repo: <repo> issue_number
|
||||
|
||||
### close `<N>`
|
||||
|
||||
Call `issue_write method: "update" owner: <owner> repo: <repo> issue_number: <N> state: "closed"`. There is no `method: "close"`.
|
||||
Call `issue_write method: "update" owner: <owner> repo: <repo> issue_number: <N> state: "closed"`. No `method: "close"` exists.
|
||||
|
||||
### comment `<N>`
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ allowed-tools: mcp__gitea__list_pull_requests mcp__gitea__pull_request_read mcp_
|
||||
|
||||
- **Issues and PRs share one number space.** A number the user mentions (`#42`) might be an issue, not a PR — there is only one counter per repo. If you're not certain, call `pull_request_read method: "get"` and treat a 404 as "this number is an issue, not a PR" (or check `is_pull` on an `issue_read` response first if you already have one).
|
||||
- **`pull_request_read method: "get"` returns `review_scomments`, not `review_comments`.** Source-level typo in gitea-mcp v1.3.0. Never reference `review_comments` — it will always be undefined.
|
||||
- **`draft: true` on create prepends `"WIP:"` to the title.** There is no first-class draft field — Gitea implements draft PRs via title prefix. To un-draft, call `update` and pass the title without the `WIP:` prefix.
|
||||
- **`draft: true` on create prepends `"WIP:"` to the title.** Gitea has no first-class draft field — it implements draft PRs via title prefix. To un-draft, call `update` and pass the title without the `WIP:` prefix.
|
||||
- **Cross-repo fork PRs require `head` as `"fork-owner:branch-name"`.** A bare branch name causes Gitea to search the base repo for it and return 422. Same-repo PRs use a bare branch name.
|
||||
- **PR `milestone` is a bare title string, not `{id, title}`.** Unlike issues, you cannot recover a milestone's ID from a PR response. If you need the ID (e.g. to filter or to pass to another write), call into `gitea-labels-milestones` and match by title via `milestone_read method: "list"`.
|
||||
- **CI status and review/approval state are independent merge gates.** `get_status` only reports CI. Branch-protection rules (required approvals, requested-reviewer coverage, stale-approval handling) are enforced server-side by the merge call itself and will error if unmet — passing CI does not mean the merge will succeed.
|
||||
|
||||
@@ -22,7 +22,7 @@ metadata:
|
||||
|
||||
- **`delete_release` takes a numeric `id`, never a tag name.** `delete_tag` is the mirror opposite — it takes the `tag_name` string, never a numeric id. These two tools are asymmetric on purpose; passing a tag name to `delete_release` or a numeric id to `delete_tag` fails. Always resolve the numeric release id via `list_releases` or `get_release` first if you only have a tag name in hand.
|
||||
- **Deleting a release does not delete its tag.** They are separate destructive operations against separate resources — a release is a wrapper (title, notes, draft/prerelease flags, assets) around a tag, not the tag itself. If the intent is to remove both, call `delete_release` and `delete_tag` separately.
|
||||
- **`list_releases`/`list_tags` default to `per_page: 20`**, unlike most other gitea-mcp tools which default to 30. There is no auto-pagination in the MCP layer — to get a complete result set, loop `page` upward until a page returns fewer than `per_page` results.
|
||||
- **`list_releases`/`list_tags` default to `per_page: 20`**, unlike most other gitea-mcp tools which default to 30. The MCP layer does no auto-pagination — to get a complete result set, loop `page` upward until a page returns fewer than `per_page` results.
|
||||
- **`is_draft`/`is_pre_release` are explicit booleans the caller sets on `create_release` — never inferred from `tag_name`.** Note the input param is `is_draft`, which maps to the `draft` field on the *response* object (see Dispatch table below and `references/call-signatures.md`) — `draft` is never a valid input key. Practitioner convention (per the `tea` CLI) uses `-beta`/`-rc` suffixes for prereleases (e.g. `v2.0.0-beta.1`), but Gitea does not enforce or infer this from the tag string. If the user names a tag that looks like a prerelease, set `is_pre_release: true` explicitly rather than assuming the flag is redundant with the name.
|
||||
- **Tag names are conventionally semver, `v`-prefixed** (`v1.2.0`, `v2.0.0-beta.1`), but this is a practitioner convention, not a Gitea constraint — don't reject or rewrite a caller-supplied tag name that doesn't follow it.
|
||||
|
||||
@@ -47,6 +47,6 @@ metadata:
|
||||
- [ ] **Creating a release:** Call `create_release` directly with `tag_name` + `target` + `title` — Gitea is assumed to create the underlying tag automatically if `tag_name` doesn't already exist (this is plausible behavior inferred from the API shape, not directly confirmed in the research docs), so a separate `create_tag` call is only needed when you want to tag a commit without wrapping it in a release yet. Verify the tag exists afterward if this matters to the caller. Set `is_pre_release`/`is_draft` explicitly per the Gotchas above; don't leave them to default inference.
|
||||
- [ ] **Deleting a release safely:** Resolve the numeric id first — call `list_releases` (paginate if needed, see Gotchas) or `get_release` if the id is already known, find the entry matching the target `tag_name`, then call `delete_release` with that `id`. Never pass `tag_name` to `delete_release`.
|
||||
- [ ] **Deleting a tag along with its release:** Delete the release first (frees the id lookup), then call `delete_tag` with the `tag_name` separately — confirm both are intended before proceeding, since each is an independent irreversible operation.
|
||||
- [ ] **Listing completely:** If the caller needs all releases or tags (not just the first page), loop `page: 1, 2, 3...` until a response has fewer than `per_page` entries.
|
||||
- [ ] **Listing every page:** If the caller needs all releases or tags (not just the first page), loop `page: 1, 2, 3...` until a response has fewer than `per_page` entries.
|
||||
|
||||
If exact response field shapes or additional conventions are needed, read `references/call-signatures.md` and `references/conventions.md`.
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"name": "kyberforge",
|
||||
"version": "1.2.3"
|
||||
"version": "1.2.4"
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"skills": [
|
||||
"skills/"
|
||||
],
|
||||
"version": "1.2.3"
|
||||
"version": "1.2.4"
|
||||
}
|
||||
|
||||
@@ -36,10 +36,13 @@ metadata:
|
||||
```bash
|
||||
bash scripts/validate.sh <path-to-agent-file>
|
||||
bash scripts/validate-provenance.sh <path-to-agent-file>
|
||||
"$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini" <path-to-cc-file> <path-to-copilot-file>
|
||||
```
|
||||
|
||||
The script accepts either the CC file or the Copilot file. It detects provider from extension, derives the counterpart, and runs all structural checks. Note FAILs and SUGGESTIONs for the `### Structure` and `### Provider safety` report dimensions. Findings about missing fields, bad name format, empty body, or missing frontmatter → `### Structure`. Findings about CC-only fields in a Copilot file, Copilot-only fields in a CC file, plugin-silently-ignored fields, body length, or subagent-unavailable tools → `### Provider safety`. A missing counterpart file → `### Pair consistency`.
|
||||
|
||||
`vale-wrap.sh` resolves its own path and `.vale.ini` via `git rev-parse --show-toplevel`; pass the absolute paths shown above so the invocation holds from any cwd. Run it against both files of the pair (not just the one passed in). `Kyberforge` applies to both files; `KyberforgeCopilot` applies to the `.agent.md` file only, since its one rule (`Use proactively`) flags CC-specific phrasing that's meaningless in a Copilot description — there's nothing to flag in the CC file, so it isn't scoped there. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one in the `### Description` / `### Body` dimensions citing its rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 2 judgment for the dimensions it would have covered.
|
||||
|
||||
`validate-provenance.sh` validates the provenance chain between the agent pair's `source_keys` and the plugin-scoped `sources.md` (plugin root — see ADR-0010). It exits 0 silently for non-plugin-scope agents and when no provenance data exists. Note FAILs from this script for the `### Provenance` dimension — surface them verbatim with Why and Fix.
|
||||
|
||||
If the scripts cannot run (Bash denied, python3 unavailable), perform checks manually: counterpart file exists, required fields present (`name`, `description`, non-empty body), `name` is kebab-case, Copilot CLI `.agent.md` `name` must match filename stem (CC files are exempt — the CC platform does not require name to match filename), no `FILL IN:` placeholders, no CC-only fields in Copilot file, no Copilot-only fields in CC file (read `references/field-inventory.md` for the authoritative field lists).
|
||||
@@ -49,15 +52,17 @@ If the scripts cannot run (Bash denied, python3 unavailable), perform checks man
|
||||
Read both agent files. Work through each dimension internally. Collect findings only; report in Step 3.
|
||||
|
||||
**Description (both files):**
|
||||
- Action-verb opening: description starts with a verb ("Reviews...", "Analyzes...", "Generates...") — FAIL if absent
|
||||
- Specificity: is the trigger condition stated precisely? — SUGGESTION if vague
|
||||
- `Use proactively` in a Copilot description: CC-specific phrasing, has no effect in Copilot — SUGGESTION to remove
|
||||
- Action-verb opening: description starts with a verb ("Reviews...", "Analyzes...", "Generates...") — FAIL if absent. Vale's `Kyberforge.DescriptionOpener` alert flags the specific known-bad "This agent..." opener directly; verifying an arbitrary opening word is genuinely a strong verb still requires judgment.
|
||||
- Specificity: is the trigger condition stated precisely? — SUGGESTION if vague. Vale's `Kyberforge.VagueWording` alert covers known filler ("helps with", "utilize", ...) directly; report those as FAILs without re-deriving by judgment.
|
||||
- `Use proactively` in a Copilot description: Vale's `KyberforgeCopilot.ProactivePhrase` alert (Copilot file only) flags this directly — report it without re-deriving by judgment.
|
||||
|
||||
If a description finding is borderline, read `references/description-quality.md`.
|
||||
|
||||
**Body:**
|
||||
- Direct role instruction: system prompt opens with `You are a [role]. When invoked, [action].` — SUGGESTION if absent
|
||||
- One job per agent: system prompt describes a single bounded task — SUGGESTION if scope appears unbounded
|
||||
- Generic, non-specific reference pointers to the `references/` directory: Vale's `Kyberforge.PaddingPhrase` alert flags this directly — report it without re-deriving by judgment
|
||||
- Sentences that open with "There is"/"There are": Vale's `Kyberforge.SentenceOpenerThereIs` alert flags this directly — report it without re-deriving by judgment
|
||||
|
||||
**Body/Frontmatter comments:**
|
||||
- Inspect each comment block in the YAML frontmatter. For each comment, apply: *"Would the agent get this wrong without this comment?"* Flag any that answer "no" as padding.
|
||||
|
||||
@@ -123,7 +123,7 @@ source_keys:
|
||||
|
||||
### Step 3 — Fill in the Copilot agent file
|
||||
|
||||
There are **two distinct Copilot agent formats** with different paths and field sets. Choose one based on the deployment target:
|
||||
**Two distinct Copilot agent formats** exist, with different paths and field sets. Choose one based on the deployment target:
|
||||
|
||||
**CLI format** (default — what the scaffold creates):
|
||||
- Path: `.github/agents/<name>.agent.md` (project) or `<plugin>/agents/<name>.agent.md` (plugin)
|
||||
|
||||
@@ -34,12 +34,15 @@ metadata:
|
||||
```bash
|
||||
bash scripts/validate.sh <skill-dir>
|
||||
bash scripts/validate-provenance.sh <skill-dir>
|
||||
"$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini" <skill-dir>/SKILL.md
|
||||
```
|
||||
|
||||
Note any structural FAILs — they will appear in the report as a `### Structure` dimension. If the script cannot execute (python3 unavailable, Bash denied, or permission error), perform structural checks manually: name format, name matches directory, description length ≤1024 chars, SKILL.md ≤500 lines, no unfilled `FILL IN:` placeholders, scripts executable and free of interactive prompts.
|
||||
|
||||
Note any Provenance FAILs and INFO findings from `validate-provenance.sh` — they surface in the report as a `### Provenance` dimension (separate from `### Structure`). The script embeds full FAIL/INFO format with Why and Fix per finding; surface them verbatim.
|
||||
|
||||
`vale-wrap.sh` resolves its own path and `.vale.ini` via `git rev-parse --show-toplevel`; pass the absolute paths shown above so the invocation holds from any cwd. It applies `.vale.ini`'s `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns/Body dimensions below, not a replacement for Step 3. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one citing its rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 3 judgment for the dimensions it would have covered.
|
||||
|
||||
## Step 2 — Read all skill files
|
||||
|
||||
Read every file in the skill directory: `SKILL.md`, `README.md` (if present), all files in `scripts/`, `references/`, `assets/`, and `tests/`. Skip binary files only. Do not skip text files — internal consistency checks require the full picture.
|
||||
@@ -50,8 +53,9 @@ Work through each dimension internally. Collect findings only; report them in St
|
||||
|
||||
### Description
|
||||
|
||||
- **Imperative phrasing**: does it use "Use when..." not "This skill..."?
|
||||
- **Specificity**: are capabilities stated precisely ("parses OpenAPI specs") or vaguely ("helps with APIs")?
|
||||
Vale's `Kyberforge.DescriptionOpener` ("This skill..." openers) and `Kyberforge.VagueWording` (filler like "helps with", "utilize") alerts from Step 1 — both FAILs — cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call:
|
||||
|
||||
- **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")?
|
||||
- **Indirect triggers**: does it cover cases where the user doesn't name the domain directly?
|
||||
- **Near-miss exclusions**: are "Do not use when..." clauses present if a near-miss skill could steal activations?
|
||||
- **Length**: under 1024 characters?
|
||||
@@ -66,6 +70,8 @@ For each sentence in the body, apply: *"Would the agent get this wrong without t
|
||||
- **Why rationale**: include/exclude rules explain why, not just what
|
||||
- **Control calibration**: prescriptive for fragile or critical sequences (e.g. a script invocation where flag order or exact arguments must not change); flexible where multiple approaches are valid
|
||||
|
||||
Vale's `Kyberforge.SentenceOpenerThereIs` alert from Step 1 (FAIL — sentences starting with "There is"/"There are") covers pattern-matchable body-wide filler directly; report it as a finding without re-deriving by judgment.
|
||||
|
||||
If uncertain whether a sentence is padding or whether a control decision is correctly calibrated, read `references/body-discipline.md`.
|
||||
|
||||
### Patterns
|
||||
@@ -75,7 +81,7 @@ Check each pattern is appropriate and correctly formed:
|
||||
- **Gotchas**: placed near the top; each entry is a specific fact that defies a reasonable assumption — not a general tip
|
||||
- **Prescriptive sequence**: inner code fences escaped as `\`\`\`` when nested inside a markdown block
|
||||
- **Checklists**: used for multi-step workflows, not single steps
|
||||
- **Conditional references**: specific trigger stated ("If X, read `references/file.md`") — not a generic "see references/"
|
||||
- **Conditional references**: specific trigger stated ("If X, read `references/file.md`") — not a generic "see references/". Vale's `Kyberforge.PaddingPhrase` alert from Step 1 flags the generic phrasing directly; other malformed conditional-reference forms still require judgment.
|
||||
- **Output templates**: present when the agent must produce a specific format; absent otherwise
|
||||
|
||||
### File structure
|
||||
|
||||
17
plugins/lint/.claude-plugin/plugin.json
Normal file
17
plugins/lint/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"url": "https://git.dev.rkdr.net/Defame1297/"
|
||||
},
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"displayName": "Lint",
|
||||
"keywords": [
|
||||
"lint",
|
||||
"style",
|
||||
"prose",
|
||||
"linter"
|
||||
],
|
||||
"license": "MIT",
|
||||
"name": "lint",
|
||||
"version": "1.1.3"
|
||||
}
|
||||
3
plugins/lint/.mcp.json
Normal file
3
plugins/lint/.mcp.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mcpServers": {}
|
||||
}
|
||||
41
plugins/lint/agents/lint-runner.agent.md
Normal file
41
plugins/lint/agents/lint-runner.agent.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: lint-runner
|
||||
|
||||
description: Runs a linter sweep over a target file or directory scope and reports findings. Currently backs onto Vale (prose/style linting) via the vale-config and vale-run skills; built to add other linters later without changing its own contract. Use when a caller needs a lint pass run in an isolated context and wants findings back, not fixes applied.
|
||||
|
||||
tools: ["execute", "read", "search"]
|
||||
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
|
||||
---
|
||||
|
||||
You are a linter runner. When invoked, you run the appropriate linter(s) over the requested scope, collect their findings, and report them back in a structured, reviewable form. You never edit files.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **scope:** file path, directory path, or glob to lint
|
||||
- **linter:** which linter to run (defaults to `vale` — the only backend currently wired up)
|
||||
- **config context:** any project-specific linter configuration already in place (e.g. an existing `.vale.ini` for Vale, or whatever config format the requested linter expects); if none exists, say so in your report rather than inventing one
|
||||
|
||||
## Process
|
||||
|
||||
1. Determine whether the target scope already has configuration in place for the requested `linter` (whatever config format that linter expects). If not, use the `<linter>-config` skill (e.g. `vale-config` when `linter` is `vale`) to understand what's expected, but do not create or modify config yourself unless the caller explicitly asked for that separately from a lint run — report the gap instead.
|
||||
2. Use the `<linter>-run` skill (e.g. `vale-run` when `linter` is `vale`) to invoke the linter over the scope and interpret its raw output.
|
||||
3. Normalize findings into one shape regardless of backend linter: file, line, rule/check, severity, message.
|
||||
4. Do not edit, fix, or rewrite any flagged content. If a finding looks trivially fixable, note that in the report — do not act on it.
|
||||
5. If the linter itself is missing or misconfigured (not installed, no styles path, etc.), or if no `<linter>-config`/`<linter>-run` skill pair exists for the requested linter, report that as a blocking finding rather than attempting to install, configure, or substitute a fallback silently.
|
||||
|
||||
## Output
|
||||
|
||||
Report findings as a flat list, most-severe first:
|
||||
|
||||
```
|
||||
- file: <path>
|
||||
line: <line number or range>
|
||||
rule: <check/rule name>
|
||||
severity: <error | warning | suggestion>
|
||||
message: <finding text>
|
||||
```
|
||||
|
||||
Follow with a one-line summary: total findings by severity, and whether the run was blocked (e.g. linter not configured). If there are zero findings, say so explicitly — do not omit the report.
|
||||
41
plugins/lint/agents/lint-runner.md
Normal file
41
plugins/lint/agents/lint-runner.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: lint-runner
|
||||
|
||||
description: Runs a linter sweep over a target file or directory scope and reports findings. Currently backs onto Vale (prose/style linting) via the vale-config and vale-run skills; built to add other linters later without changing its own contract. Use when a caller needs a lint pass run in an isolated context and wants findings back, not fixes applied.
|
||||
|
||||
tools: Bash, Read, Grep, Glob
|
||||
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
|
||||
---
|
||||
|
||||
You are a linter runner. When invoked, you run the appropriate linter(s) over the requested scope, collect their findings, and report them back in a structured, reviewable form. You never edit files.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **scope:** file path, directory path, or glob to lint
|
||||
- **linter:** which linter to run (defaults to `vale` — the only backend currently wired up)
|
||||
- **config context:** any project-specific linter configuration already in place (e.g. an existing `.vale.ini` for Vale, or whatever config format the requested linter expects); if none exists, say so in your report rather than inventing one
|
||||
|
||||
## Process
|
||||
|
||||
1. Determine whether the target scope already has configuration in place for the requested `linter` (whatever config format that linter expects). If not, use the `<linter>-config` skill (e.g. `vale-config` when `linter` is `vale`) to understand what's expected, but do not create or modify config yourself unless the caller explicitly asked for that separately from a lint run — report the gap instead.
|
||||
2. Use the `<linter>-run` skill (e.g. `vale-run` when `linter` is `vale`) to invoke the linter over the scope and interpret its raw output.
|
||||
3. Normalize findings into one shape regardless of backend linter: file, line, rule/check, severity, message.
|
||||
4. Do not edit, fix, or rewrite any flagged content. If a finding looks trivially fixable, note that in the report — do not act on it.
|
||||
5. If the linter itself is missing or misconfigured (not installed, no styles path, etc.), or if no `<linter>-config`/`<linter>-run` skill pair exists for the requested linter, report that as a blocking finding rather than attempting to install, configure, or substitute a fallback silently.
|
||||
|
||||
## Output
|
||||
|
||||
Report findings as a flat list, most-severe first:
|
||||
|
||||
```
|
||||
- file: <path>
|
||||
line: <line number or range>
|
||||
rule: <check/rule name>
|
||||
severity: <error | warning | suggestion>
|
||||
message: <finding text>
|
||||
```
|
||||
|
||||
Follow with a one-line summary: total findings by severity, and whether the run was blocked (e.g. linter not configured). If there are zero findings, say so explicitly — do not omit the report.
|
||||
27
plugins/lint/docs/README.md
Normal file
27
plugins/lint/docs/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# docs/
|
||||
|
||||
Plugin documentation. Not read automatically by Claude Code or GitHub Copilot CLI — reference specific files from skill bodies or agent prompts as needed.
|
||||
|
||||
## research/
|
||||
|
||||
Upstream reference material gathered during skill authoring. Not shipped with the plugin — used at development time only.
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `research/docs/vale/` | Vale documentation (vale.sh), gathered while authoring `vale-config`/`vale-run` — see below |
|
||||
|
||||
### research/docs/vale/
|
||||
|
||||
| File | Covers |
|
||||
|------|--------|
|
||||
| `overview.md` | What Vale is; the style/rule/check configuration model; the built-in `Vale` style's four rules; styles directory layout |
|
||||
| `installation.md` | Installing Vale via OS package managers (Homebrew, Snap, Chocolatey) and Docker |
|
||||
| `configuration.md` | `.vale.ini` structure — global, `[formats]`, and per-glob sections |
|
||||
| `cli-reference.md` | Core invocation and key subcommands/flags (`vale sync`, `vale ls-config`, etc.) |
|
||||
| `examples.md` | Walkthroughs — project initialization, typical `.vale.ini` configs |
|
||||
| `troubleshooting.md` | Suppressing false positives via inline markup (format-specific `vale off`/`vale on` syntax, spelling ignore lists) |
|
||||
| `sources.md` | Provenance record for this directory — source URL, description, and which files above were extracted from it |
|
||||
|
||||
Each file's frontmatter carries a `source_keys` entry keyed to the same provenance record. The plugin-level provenance file, `plugins/lint/sources.md`, cross-references this directory from the `context7-websites-vale-sh` entry and lists the agents/skills whose content drew on it.
|
||||
|
||||
`vale-config` and `vale-run` are the skills that consume this research — read the relevant file here before changing either skill's Vale-facing behavior.
|
||||
29
plugins/lint/docs/research/docs/vale/cli-reference.md
Normal file
29
plugins/lint/docs/research/docs/vale/cli-reference.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
topic: cli-reference
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Core Invocation
|
||||
|
||||
```bash
|
||||
$ vale README.md
|
||||
```
|
||||
|
||||
Lints the given file(s)/glob against the styles configured in `.vale.ini`.
|
||||
|
||||
## Key Flags and Subcommands
|
||||
|
||||
| Command/Flag | Purpose |
|
||||
|---|---|
|
||||
| `vale sync` | Downloads and installs packages/styles declared in `.vale.ini`. Run after install and whenever `Packages` changes. |
|
||||
| `vale ls-config` | Prints the currently active, fully-resolved configuration as JSON. Useful for debugging what settings actually apply to a file. |
|
||||
| `--output=<style>` | Sets the output format/template: `line`, `JSON`, `CLI` (default), or a custom template. |
|
||||
| `--no-exit` | Suppresses the non-zero exit code Vale normally returns when alerts are found — useful in CI pipelines that shouldn't hard-fail on lint output. |
|
||||
| `--ignore-syntax` | Treats input as plain, unformatted text, skipping syntax-aware parsing (Markdown/HTML/etc). |
|
||||
| `--minAlertLevel=<level>` | Overrides `MinAlertLevel` from the config for this run (`suggestion`, `warning`, `error`). |
|
||||
| `--version` | Prints the Vale binary version. |
|
||||
|
||||
## Exit Codes
|
||||
|
||||
By default, `vale` exits non-zero when it finds any alert at or above `MinAlertLevel` — this is what makes it usable as a CI gate. Pass `--no-exit` to always exit `0` regardless of findings.
|
||||
106
plugins/lint/docs/research/docs/vale/configuration.md
Normal file
106
plugins/lint/docs/research/docs/vale/configuration.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
topic: configuration
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## `.vale.ini` Structure
|
||||
|
||||
Configuration is INI-formatted with three sections, in order:
|
||||
|
||||
```ini
|
||||
# Core settings appear at the top
|
||||
# (the "global" section).
|
||||
|
||||
[formats]
|
||||
# Format associations appear under
|
||||
# the optional "formats" section.
|
||||
|
||||
[*]
|
||||
# Format-specific settings appear
|
||||
# under a user-provided "glob"
|
||||
# pattern.
|
||||
```
|
||||
|
||||
Core (global) settings apply application-wide; glob sections (`[*]`, `[*.md]`, etc.) scope settings to files matching that pattern.
|
||||
|
||||
## Core Settings
|
||||
|
||||
| Key | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `StylesPath` | string | Path to all Vale-related resources (styles, dictionaries, vocab). |
|
||||
| `Packages` | string[] | Packages to download and install via `vale sync`. |
|
||||
| `Vocab` | string[] | Vocabularies to load. |
|
||||
| `MinAlertLevel` | enum | Minimum severity to report: `suggestion`, `warning`, or `error`. |
|
||||
| `IgnoredScopes` | enum | Inline-level HTML tags to ignore. |
|
||||
| `SkippedScopes` | enum | Block-level HTML tags to ignore entirely. |
|
||||
|
||||
Example:
|
||||
|
||||
```ini
|
||||
StylesPath = styles
|
||||
MinAlertLevel = suggestion
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale
|
||||
```
|
||||
|
||||
## Format Associations
|
||||
|
||||
Map an unrecognized extension onto a supported one so Vale lints it with the right parser. This is an extension-level substitution only — it does not add new file-type support:
|
||||
|
||||
```ini
|
||||
[formats]
|
||||
mdx = md
|
||||
```
|
||||
|
||||
## Vocabularies
|
||||
|
||||
Reference a named vocabulary (a folder of accept/reject word lists under `StylesPath`) via `Vocab`, then apply styles per glob:
|
||||
|
||||
```ini
|
||||
StylesPath = styles
|
||||
|
||||
Vocab = Blog
|
||||
|
||||
[*]
|
||||
BasedOnStyles = Vale, MyStyle
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
Third-party style packages are declared via `Packages` and then activated per glob with `BasedOnStyles`:
|
||||
|
||||
```ini
|
||||
Packages = Google, write-good
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale, Google, write-good
|
||||
```
|
||||
|
||||
## Local Overrides
|
||||
|
||||
A project can layer a local `.vale.ini` that overrides `StylesPath`, adds packages, and changes `BasedOnStyles` for a subset of files — local settings merge with or override the global ones:
|
||||
|
||||
```ini
|
||||
StylesPath = localpath
|
||||
|
||||
Packages = write-good
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = write-good
|
||||
```
|
||||
|
||||
## Rule Header Fields
|
||||
|
||||
Individual rule YAML files (under a style's directory) support these header fields:
|
||||
|
||||
| Field | Required | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `extends` | yes | — | Check this rule extends (e.g. `existence`). |
|
||||
| `message` | yes | — | Message shown when triggered; supports `%s` formatting per check type. |
|
||||
| `level` | no | `suggestion` | Severity: `suggestion`, `warning`, or `error`. |
|
||||
| `scope` | no | `text` | Scope the rule applies to (e.g. `heading`). |
|
||||
| `link` | no | — | URL with more info about the rule. |
|
||||
| `limit` | no | — | Max number of triggers per file. |
|
||||
| `vocab` | no | `true` | Set `false` to disable active vocabularies for this rule. |
|
||||
52
plugins/lint/docs/research/docs/vale/examples.md
Normal file
52
plugins/lint/docs/research/docs/vale/examples.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
topic: examples
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Project Initialization Walkthrough
|
||||
|
||||
```bash
|
||||
$ cd some-project
|
||||
# create .vale.ini with StylesPath + BasedOnStyles
|
||||
$ vale sync # downloads declared packages/styles into StylesPath
|
||||
$ ls styles # confirms styles were installed
|
||||
$ vale README.md # lint a file
|
||||
```
|
||||
|
||||
The `.vale.ini` file must exist before `vale sync` — it declares which packages to fetch.
|
||||
|
||||
## Typical Project Config
|
||||
|
||||
```ini
|
||||
StylesPath = styles
|
||||
MinAlertLevel = error
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = ProjectStyle
|
||||
```
|
||||
|
||||
## pre-commit Integration
|
||||
|
||||
Vale ships a pre-commit hook definition. A typical setup runs `vale sync` once (with `pass_filenames: false`) plus the actual lint pass with CI-appropriate flags:
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/errata-ai/vale
|
||||
rev: 16d3a7f
|
||||
hooks:
|
||||
- id: vale
|
||||
name: vale sync
|
||||
pass_filenames: false
|
||||
args: [sync]
|
||||
- id: vale
|
||||
args: [--output=line, --minAlertLevel=error]
|
||||
```
|
||||
|
||||
## CI Output for Machine Parsing
|
||||
|
||||
```bash
|
||||
$ vale --output=JSON README.md
|
||||
```
|
||||
|
||||
Use `--output=JSON` when a CI step needs to parse results programmatically rather than read the default CLI-formatted output.
|
||||
41
plugins/lint/docs/research/docs/vale/installation.md
Normal file
41
plugins/lint/docs/research/docs/vale/installation.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
topic: installation
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Package Managers
|
||||
|
||||
Vale is distributed via standard OS package managers:
|
||||
|
||||
```bash
|
||||
brew install vale # macOS
|
||||
snap install vale # Linux
|
||||
```
|
||||
|
||||
```powershell
|
||||
choco install vale # Windows
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
An official image is available on Docker Hub:
|
||||
|
||||
```bash
|
||||
docker pull jdkato/vale
|
||||
```
|
||||
|
||||
## Post-Install: Syncing Styles
|
||||
|
||||
Installing the `vale` binary alone does not install any styles. After install, run `vale sync` to download and install the styles/packages declared in `.vale.ini`:
|
||||
|
||||
```bash
|
||||
$ vale sync
|
||||
```
|
||||
|
||||
## Format-Specific Extras
|
||||
|
||||
Some input formats need an external converter installed separately before Vale can process them:
|
||||
|
||||
- reStructuredText: `pip install docutils` (provides `rst2html`)
|
||||
- MDX: `npm install -g mdx2vast`
|
||||
43
plugins/lint/docs/research/docs/vale/overview.md
Normal file
43
plugins/lint/docs/research/docs/vale/overview.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
topic: overview
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## What Vale Is
|
||||
|
||||
Vale is a cross-platform command-line tool that brings code-like linting to prose. Rather than checking general grammar, it enforces project-specific writing style rules — consistency of terminology, phrasing, and formatting — the same way a linter enforces a code style guide.
|
||||
|
||||
## Styles, Rules, and Checks
|
||||
|
||||
Vale's configuration model has three layers:
|
||||
|
||||
- **Styles** — a named collection of rules (e.g. the built-in `Vale` style, or third-party styles like `Google` or `write-good`). A project can apply multiple styles at once via `BasedOnStyles`.
|
||||
- **Rules** — individual YAML files that define one specific check (e.g. flag a term, enforce a heading capitalization pattern). Each rule `extends` a check and sets a `message`, `level`, and other header fields.
|
||||
- **Checks** — the underlying functions a rule extends to perform analysis: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`.
|
||||
|
||||
## Built-in Style
|
||||
|
||||
Vale ships with a default `Vale` style containing four rules:
|
||||
|
||||
- `Vale.Spelling` — spell-checks against Hunspell-compatible dictionaries in `<StylesPath>/config/dictionaries`.
|
||||
- `Vale.Terms` — enforces the project's accepted vocabulary terms.
|
||||
- `Vale.Avoid` — enforces the project's rejected vocabulary terms.
|
||||
- `Vale.Repetition` — flags repeated words (e.g. "the the").
|
||||
|
||||
## Styles Directory Layout
|
||||
|
||||
Styles live under `StylesPath` in a nested folder structure, one subdirectory per style, each holding YAML rule files:
|
||||
|
||||
```
|
||||
styles/
|
||||
├── base/
|
||||
│ ├── ComplexWords.yml
|
||||
│ ├── SentenceLength.yml
|
||||
├── blog/
|
||||
│ ├── TechTerms.yml
|
||||
└── docs/
|
||||
├── Branding.yml
|
||||
```
|
||||
|
||||
This lets a project mix a shared base style with format- or section-specific styles, all activated per-glob in `.vale.ini`.
|
||||
8
plugins/lint/docs/research/docs/vale/sources.md
Normal file
8
plugins/lint/docs/research/docs/vale/sources.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Sources
|
||||
|
||||
## context7-websites-vale-sh
|
||||
|
||||
- **URL:** context7:/websites/vale_sh
|
||||
- **Description:** Official Vale documentation site (vale.sh) indexed by Context7 — `.vale.ini` config reference, style/rule/check model, CLI commands and flags, installation across package managers and Docker, format-specific inline disable syntax, pre-commit integration, spelling ignore lists.
|
||||
- **Contributing files:** overview.md, installation.md, configuration.md, cli-reference.md, examples.md, troubleshooting.md
|
||||
- **Status:** `extracted`
|
||||
56
plugins/lint/docs/research/docs/vale/troubleshooting.md
Normal file
56
plugins/lint/docs/research/docs/vale/troubleshooting.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
topic: troubleshooting
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Suppressing False Positives Inline
|
||||
|
||||
Vale supports inline markup comments to disable checks for a section of content. Syntax varies by format:
|
||||
|
||||
Markdown/MDX:
|
||||
```mdx
|
||||
{/* vale off */}
|
||||
This text will be ignored.
|
||||
{/* vale on */}
|
||||
```
|
||||
|
||||
Org mode:
|
||||
```org
|
||||
# vale off
|
||||
This text will be ignored.
|
||||
# vale on
|
||||
```
|
||||
|
||||
## Disabling a Specific Rule for Specific Matches
|
||||
|
||||
Rather than disabling all checks, target one rule and specific known-exception strings, then re-enable:
|
||||
|
||||
```mdx
|
||||
{/* vale Style.Redundancy["ACT test","OTHER"] = NO */}
|
||||
This is some text ACT test
|
||||
{/* vale Style.Redundancy["ACT test","OTHER"] = YES */}
|
||||
```
|
||||
|
||||
This is the preferred fix for recurring false positives on specific terms — it keeps the rule active everywhere else instead of disabling it project-wide.
|
||||
|
||||
## Ignoring Words in Spell Check
|
||||
|
||||
The `spelling` check accepts an `ignore` list of external plain-text files, so known project-specific terms don't need touching the dictionary:
|
||||
|
||||
```yaml
|
||||
extends: spelling
|
||||
message: "Did you really mean '%s'?"
|
||||
level: error
|
||||
ignore:
|
||||
- ignore1.txt
|
||||
- ignore2.txt
|
||||
```
|
||||
|
||||
## Plain-Text Fallback
|
||||
|
||||
If a file's syntax-aware parsing produces noisy/incorrect results (e.g. an unsupported or malformed format), rerun with `--ignore-syntax` to treat it as plain text instead of relying on the format-specific parser.
|
||||
|
||||
## CI Failing Unexpectedly
|
||||
|
||||
If a CI job fails solely because Vale returns a non-zero exit code on found alerts (not because the content is actually wrong for that pipeline stage), add `--no-exit` rather than suppressing the rule itself — this preserves the lint output while not gating the build on it.
|
||||
3
plugins/lint/hooks.json
Normal file
3
plugins/lint/hooks.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"hooks": {}
|
||||
}
|
||||
22
plugins/lint/plugin.json
Normal file
22
plugins/lint/plugin.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"agents": "agents/",
|
||||
"author": {
|
||||
"email": "defame1297@rkdr.net",
|
||||
"name": "Defame1297"
|
||||
},
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"hooks": "hooks.json",
|
||||
"keywords": [
|
||||
"lint",
|
||||
"style",
|
||||
"prose",
|
||||
"linter"
|
||||
],
|
||||
"license": "MIT",
|
||||
"mcpServers": ".mcp.json",
|
||||
"name": "lint",
|
||||
"skills": [
|
||||
"skills/"
|
||||
],
|
||||
"version": "1.1.3"
|
||||
}
|
||||
23
plugins/lint/skills/vale-config/README.md
Normal file
23
plugins/lint/skills/vale-config/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# vale-config
|
||||
|
||||
Install and configure Vale, the prose/style linter — `.vale.ini`, `StylesPath`, built-in/third-party/custom styles, and activation via `BasedOnStyles`.
|
||||
|
||||
## What it does
|
||||
|
||||
Covers the setup side of Vale: getting a project from no config to a working `.vale.ini` where `vale sync` runs clean and every declared style is actually activated for the right files. Does not run Vale or interpret its output — see `vale-run` for that.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/vale-config
|
||||
```
|
||||
|
||||
Describe what you want configured: initial setup, adding a third-party style package, or a custom rule. The skill covers install, `StylesPath` layout, `.vale.ini` structure, and `BasedOnStyles` activation.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Skill instructions for agents |
|
||||
| `references/configuration-reference.md` | Full `.vale.ini` field and rule-header reference |
|
||||
| `references/sources.md` | Research sources backing the Vale configuration guidance |
|
||||
62
plugins/lint/skills/vale-config/SKILL.md
Normal file
62
plugins/lint/skills/vale-config/SKILL.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: vale-config
|
||||
|
||||
description: >
|
||||
Use when installing or configuring Vale, the cross-platform prose/style linter — setting up
|
||||
.vale.ini, choosing a StylesPath, adding built-in, third-party, or custom styles, and activating
|
||||
them per file glob via BasedOnStyles. Covers the setup side of Vale only: getting a project from
|
||||
"no Vale config" to "vale sync runs clean and BasedOnStyles is wired up correctly". Use even if the
|
||||
user doesn't say "Vale" explicitly — "set up prose linting", "lint our docs for style", "enforce a
|
||||
vocabulary/terminology list in markdown" all apply. Do not use when the user wants to actually run
|
||||
Vale and interpret its output on existing config — use vale-run for that.
|
||||
|
||||
metadata:
|
||||
category: lint
|
||||
version: "0.1.0"
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Installing the `vale` binary installs no styles. A fresh `.vale.ini` with `BasedOnStyles` set will fail or find nothing until `vale sync` runs and downloads the `Packages` it declares.
|
||||
- `.vale.ini` is order-sensitive: global (core) settings first, then the optional `[formats]` section, then glob sections (`[*]`, `[*.md]`, …). Settings in a glob section only apply to files matching that glob.
|
||||
- `Packages` (top-level, fetched by `vale sync`) and `BasedOnStyles` (per-glob, activates) are separate keys — a style only lints files once it's in both. This is the step people forget.
|
||||
- A rule scoped to `text.frontmatter.<key>` (e.g. `text.frontmatter.description`) only reliably matches when that field's value is a single physical line. If it's a YAML block scalar (`>`/`|`) spanning 2+ physical lines, the scope silently stops matching — no error, just 0 findings — confirmed against Vale 3.15.2. Verify with a deliberately-bad multi-line fixture before trusting a frontmatter-scoped rule in production; if the field is commonly authored as a multi-line block scalar, flatten it to one line ahead of the `vale` call rather than relying on the scope alone.
|
||||
|
||||
## Setup workflow
|
||||
|
||||
- [ ] **Install** the `vale` binary: `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or `docker pull jdkato/vale`.
|
||||
- [ ] **Pick a `StylesPath`** (conventionally `styles`) and create it. This is where all styles, dictionaries, and vocab live.
|
||||
- [ ] **Write `.vale.ini`** at the project root with at minimum:
|
||||
```ini
|
||||
StylesPath = styles
|
||||
MinAlertLevel = suggestion
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale
|
||||
```
|
||||
`Vale` here is the built-in style (`Vale.Spelling`, `Vale.Terms`, `Vale.Avoid`, `Vale.Repetition`) — no download needed, it always works.
|
||||
- [ ] **Add third-party styles** (optional) by declaring them in `Packages`, then activating them in the same or another glob's `BasedOnStyles`:
|
||||
```ini
|
||||
Packages = Google, write-good
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale, Google, write-good
|
||||
```
|
||||
- [ ] **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.
|
||||
|
||||
For the full `.vale.ini` field reference (formats mapping, vocab, local overrides, custom rule header fields), read `references/configuration-reference.md`.
|
||||
|
||||
## Custom styles
|
||||
|
||||
A custom style is just a new subdirectory under `StylesPath`, holding one YAML file per rule:
|
||||
|
||||
```
|
||||
styles/
|
||||
└── MyStyle/
|
||||
└── NoJargon.yml
|
||||
```
|
||||
|
||||
Each rule file needs `extends` (the check it implements, e.g. `existence`) and `message` at minimum. Activate the style the same way as any other: add `MyStyle` to `BasedOnStyles` for the relevant glob. See `references/configuration-reference.md` for the full rule header field table.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
topic: configuration-reference
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Core Settings
|
||||
|
||||
| Key | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `StylesPath` | string | Path to all Vale-related resources (styles, dictionaries, vocab). |
|
||||
| `Packages` | string[] | Packages to download and install via `vale sync`. |
|
||||
| `Vocab` | string[] | Vocabularies to load. |
|
||||
| `MinAlertLevel` | enum | Minimum severity to report: `suggestion`, `warning`, or `error`. |
|
||||
| `IgnoredScopes` | enum | Inline-level HTML tags to ignore. |
|
||||
| `SkippedScopes` | enum | Block-level HTML tags to ignore entirely. |
|
||||
|
||||
## Format Associations
|
||||
|
||||
Map an unrecognized extension onto a supported one so Vale lints it with the right parser — an extension-level substitution only, it does not add new file-type support:
|
||||
|
||||
```ini
|
||||
[formats]
|
||||
mdx = md
|
||||
```
|
||||
|
||||
## Vocabularies
|
||||
|
||||
Reference a named vocabulary (a folder of accept/reject word lists under `StylesPath`) via `Vocab`, then apply styles per glob:
|
||||
|
||||
```ini
|
||||
StylesPath = styles
|
||||
|
||||
Vocab = Blog
|
||||
|
||||
[*]
|
||||
BasedOnStyles = Vale, MyStyle
|
||||
```
|
||||
|
||||
## Local Overrides
|
||||
|
||||
A project can layer a local `.vale.ini` that overrides `StylesPath`, adds packages, and changes `BasedOnStyles` for a subset of files — local settings merge with or override the global ones:
|
||||
|
||||
```ini
|
||||
StylesPath = localpath
|
||||
|
||||
Packages = write-good
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = write-good
|
||||
```
|
||||
|
||||
## Rule Header Fields
|
||||
|
||||
Individual rule YAML files (under a style's directory) support these header fields:
|
||||
|
||||
| Field | Required | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `extends` | yes | — | Check this rule extends (e.g. `existence`). |
|
||||
| `message` | yes | — | Message shown when triggered; supports `%s` formatting per check type. |
|
||||
| `level` | no | `suggestion` | Severity: `suggestion`, `warning`, or `error`. |
|
||||
| `scope` | no | `text` | Scope the rule applies to (e.g. `heading`). |
|
||||
| `link` | no | — | URL with more info about the rule. |
|
||||
| `limit` | no | — | Max number of triggers per file. |
|
||||
| `vocab` | no | `true` | Set `false` to disable active vocabularies for this rule. |
|
||||
|
||||
## Checks
|
||||
|
||||
The underlying functions a rule's `extends` field can reference: `existence`, `substitution`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`.
|
||||
|
||||
## Built-in Style
|
||||
|
||||
Vale ships with a default `Vale` style containing four rules, usable without `vale sync`:
|
||||
|
||||
- `Vale.Spelling` — spell-checks against Hunspell-compatible dictionaries in `<StylesPath>/config/dictionaries`.
|
||||
- `Vale.Terms` — enforces the project's accepted vocabulary terms.
|
||||
- `Vale.Avoid` — enforces the project's rejected vocabulary terms.
|
||||
- `Vale.Repetition` — flags repeated words (e.g. "the the").
|
||||
9
plugins/lint/skills/vale-config/references/sources.md
Normal file
9
plugins/lint/skills/vale-config/references/sources.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Sources
|
||||
|
||||
## context7-websites-vale-sh
|
||||
|
||||
- **URL:** context7:/websites/vale_sh
|
||||
- **Description:** Official Vale documentation site (vale.sh) indexed by Context7 — `.vale.ini` config reference, style/rule/check model, installation across package managers and Docker.
|
||||
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
|
||||
- **Contributing files:** SKILL.md, references/configuration-reference.md
|
||||
- **Status:** `extracted`
|
||||
23
plugins/lint/skills/vale-run/README.md
Normal file
23
plugins/lint/skills/vale-run/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# vale-run
|
||||
|
||||
Run Vale (a prose/style linter) against an already-configured project and interpret its results.
|
||||
|
||||
## What it does
|
||||
|
||||
This skill covers invoking the `vale` CLI against files or directories, choosing an output format (human-readable CLI, `line`, or machine-parseable `JSON`), filtering by severity via `--minAlertLevel`, and handling exit codes in scripts and CI. It also covers resolving common runtime issues: false positives, format-specific inline suppression, and CI failures caused solely by Vale's non-zero exit code. It assumes the project already has a working `.vale.ini` and installed styles — setting those up is the sibling `vale-config` skill's job.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/vale-run
|
||||
```
|
||||
|
||||
Describe what you want to lint and how (human-readable output, CI/JSON output, filtered by severity). The skill will pick the right flags and, if results include false positives, walk through the narrowest applicable fix.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Core invocation, key flags, output format guidance, false-positive triage order |
|
||||
| `references/troubleshooting.md` | Inline suppression syntax, rule-specific disabling, spelling ignore lists, pre-commit integration, CI edge cases |
|
||||
| `references/sources.md` | Research provenance |
|
||||
63
plugins/lint/skills/vale-run/SKILL.md
Normal file
63
plugins/lint/skills/vale-run/SKILL.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: vale-run
|
||||
description: >
|
||||
Use when running Vale (a prose/style linter) against files or directories in an
|
||||
already-configured project — one that already has a .vale.ini — and interpreting
|
||||
or reporting its results: choosing an output format for humans vs. CI, filtering
|
||||
by severity, handling Vale's exit codes in scripts, or resolving common runtime
|
||||
issues like false positives and unexpected CI failures. Use even if the user
|
||||
doesn't say "vale" explicitly, e.g. "lint the docs", "check prose style", "run
|
||||
the style linter", "why is CI failing on the docs check". Do not use when the
|
||||
project has no .vale.ini yet, or needs styles installed/configured — that's the
|
||||
vale-config skill.
|
||||
metadata:
|
||||
version: "0.1.1"
|
||||
category: lint
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Vale's exit code is driven by `error`-level alerts only. `warning` and `suggestion` alerts are reported but still exit `0`. `MinAlertLevel` and `--minAlertLevel` control display, never the exit code — no flag makes warnings fail. A rule that must gate CI or a commit hook has to be `level: error`. This is the single most common way a Vale gate silently passes everything.
|
||||
- `vale ls-config` prints the fully-resolved, currently active configuration as JSON — the fastest way to check why a rule "isn't applying" is what's actually active, not what's written in `.vale.ini`.
|
||||
- Inline suppression syntax is format-specific: Markdown uses HTML comments `<!-- vale off -->` / `<!-- vale on -->`, MDX uses `{/* vale off */}` / `{/* vale on */}`, Org mode uses `# vale off` / `# vale on`. The MDX form does nothing in a plain `.md` file — the alert still fires. Don't assume one syntax works across formats.
|
||||
- Before calling the `vale` binary directly, check whether the target repo documents its own wrapper script for Vale (look in its README, CONTRIBUTING docs, pre-commit config, or a `scripts/` directory). Some projects wrap `vale` to work around real bugs — e.g. a scope that silently stops matching multi-line YAML block-scalar frontmatter fields — and calling bare `vale` in a repo that has such a wrapper silently skips whatever the wrapper works around. If a wrapper is documented, invoke it with the same arguments instead of calling `vale` directly; otherwise fall back to the default below.
|
||||
|
||||
## Running vale
|
||||
|
||||
Default invocation (when the target repo has no documented Vale wrapper — see Gotchas):
|
||||
|
||||
```bash
|
||||
vale <path-or-glob>
|
||||
```
|
||||
|
||||
Key flags:
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--output=<style>` | Output format/template: `CLI` (default, human-readable), `line` (compact, one alert per line, good for grep/piping), `JSON` (for programmatic parsing), or a custom template. |
|
||||
| `--minAlertLevel=<suggestion\|warning\|error>` | Overrides `MinAlertLevel` from `.vale.ini` for this run only, without editing config. Filters what is displayed; does not affect the exit code. |
|
||||
| `--no-exit` | Suppresses the nonzero exit that `error`-level alerts would otherwise cause; a no-op when no rule is `error`-level. Use in CI stages that should surface lint output without hard-failing the build. |
|
||||
| `--ignore-syntax` | Treats input as plain text, skipping format-aware parsing — use when a file's syntax-aware parser produces noisy or wrong results. |
|
||||
|
||||
`vale sync` downloads the packages/styles declared in `.vale.ini` — that's a one-time-per-change setup step (vale-config's territory), not part of a normal lint run. If a run behaves as though no styles are active, that's a sign `vale sync` hasn't been run yet, not a `vale-run` problem.
|
||||
|
||||
Prefer `--output=JSON` whenever the caller (a script, a CI step, another agent) needs to act on individual alerts rather than just get a pass/fail signal — `CLI` and `line` are for humans reading the terminal.
|
||||
|
||||
## Fixing false positives
|
||||
|
||||
Scope the fix as narrowly as possible, in this order:
|
||||
|
||||
1. **Mentioning banned phrasing rather than using it**: wrap it in backticks or a fenced code block. Vale skips code spans and fences, so no suppression is needed at all. Try this before any suppression markup.
|
||||
2. **One-off**: inline-suppress the specific text run with the format's `vale off`/`vale on` markup.
|
||||
3. **Recurring known-exception string, one rule**: disable that specific rule for that specific match inline (e.g. `<!-- vale Style.Redundancy["ACT test","OTHER"] = NO -->` ... `= YES`), rather than the whole rule.
|
||||
4. **Known project term failing spell check**: add it to the style's `ignore` list, not an inline suppression.
|
||||
|
||||
Never disable a rule project-wide to fix one false positive — editing `.vale.ini`/`BasedOnStyles` is vale-config's job, and it silences the rule everywhere, not just the false-positive case.
|
||||
|
||||
If output looks wrong because Vale mis-parsed a file's format, rerun with `--ignore-syntax` before assuming the rule itself is broken.
|
||||
|
||||
For CI that fails solely because Vale returned non-zero on `error`-level alerts — not because the content is wrong for that pipeline stage — add `--no-exit` rather than disabling the rule. If the failing alerts are warnings or suggestions, Vale is not what failed the build; look elsewhere.
|
||||
|
||||
If setting up Vale as a pre-commit hook or need the full inline-suppression/spelling-ignore syntax reference, read `references/troubleshooting.md`.
|
||||
9
plugins/lint/skills/vale-run/references/sources.md
Normal file
9
plugins/lint/skills/vale-run/references/sources.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Sources
|
||||
|
||||
## context7-websites-vale-sh
|
||||
|
||||
- **URL:** context7:/websites/vale_sh
|
||||
- **Description:** Official Vale documentation site (vale.sh) indexed by Context7 — `.vale.ini` config reference, style/rule/check model, CLI commands and flags, installation across package managers and Docker, format-specific inline disable syntax, pre-commit integration, spelling ignore lists.
|
||||
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
|
||||
- **Contributing files:** SKILL.md, references/troubleshooting.md
|
||||
- **Status:** `extracted`
|
||||
103
plugins/lint/skills/vale-run/references/troubleshooting.md
Normal file
103
plugins/lint/skills/vale-run/references/troubleshooting.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-vale-sh
|
||||
---
|
||||
|
||||
# Vale troubleshooting reference
|
||||
|
||||
## Inline suppression syntax by format
|
||||
|
||||
Markdown uses HTML comments — the MDX `{/* */}` form does not suppress anything in a plain `.md` file:
|
||||
```markdown
|
||||
<!-- vale off -->
|
||||
This text will be ignored.
|
||||
<!-- vale on -->
|
||||
```
|
||||
|
||||
MDX:
|
||||
```mdx
|
||||
{/* vale off */}
|
||||
This text will be ignored.
|
||||
{/* vale on */}
|
||||
```
|
||||
|
||||
Org mode:
|
||||
```org
|
||||
# vale off
|
||||
This text will be ignored.
|
||||
# vale on
|
||||
```
|
||||
|
||||
## Disabling a specific rule for specific matches
|
||||
|
||||
Targets one rule and specific known-exception strings, then re-enables — the preferred fix for a recurring false positive on a specific term, since it keeps the rule active everywhere else:
|
||||
|
||||
Markdown:
|
||||
```markdown
|
||||
<!-- vale Style.Redundancy["ACT test","OTHER"] = NO -->
|
||||
This is some text ACT test
|
||||
<!-- vale Style.Redundancy["ACT test","OTHER"] = YES -->
|
||||
```
|
||||
|
||||
MDX:
|
||||
```mdx
|
||||
{/* vale Style.Redundancy["ACT test","OTHER"] = NO */}
|
||||
This is some text ACT test
|
||||
{/* vale Style.Redundancy["ACT test","OTHER"] = YES */}
|
||||
```
|
||||
|
||||
## Ignoring words in spell check
|
||||
|
||||
The `spelling` check accepts an `ignore` list of external plain-text files, so project-specific terms don't need touching the dictionary:
|
||||
|
||||
```yaml
|
||||
extends: spelling
|
||||
message: "Did you really mean '%s'?"
|
||||
level: error
|
||||
ignore:
|
||||
- ignore1.txt
|
||||
- ignore2.txt
|
||||
```
|
||||
|
||||
## Plain-text fallback
|
||||
|
||||
If a file's syntax-aware parsing produces noisy or incorrect results (an unsupported or malformed format), rerun with `--ignore-syntax` to treat it as plain text instead of relying on the format-specific parser.
|
||||
|
||||
## CI failing unexpectedly
|
||||
|
||||
Only `error`-level alerts make Vale exit non-zero; `warning` and `suggestion` alerts are printed but exit `0`. If a CI job fails solely because of `error`-level alerts — not because the content is actually wrong for that pipeline stage — add `--no-exit` rather than suppressing the rule itself. This preserves the lint output while not gating the build on it. If the alerts are warnings or suggestions, Vale did not fail the job — look elsewhere.
|
||||
|
||||
## pre-commit integration
|
||||
|
||||
Vale ships a pre-commit hook definition. A typical setup runs `vale sync` once (with `pass_filenames: false`) plus the actual lint pass with CI-appropriate flags:
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/errata-ai/vale
|
||||
rev: 16d3a7f
|
||||
hooks:
|
||||
- id: vale
|
||||
name: vale sync
|
||||
pass_filenames: false
|
||||
args: [sync]
|
||||
- id: vale
|
||||
args: [--output=line, --minAlertLevel=error]
|
||||
```
|
||||
|
||||
If the project has documented a wrapper script for a known Vale scope/escaping limitation (see the Gotchas section of `SKILL.md`), point the second hook's `entry:` at that wrapper instead of at bare `vale`, even though the hook's `repo`/`rev`/`id` still come from the upstream definition above — only the invocation target changes:
|
||||
|
||||
```yaml
|
||||
- id: vale
|
||||
entry: <path-to-project-wrapper>
|
||||
args: [--output=line, --minAlertLevel=error]
|
||||
```
|
||||
|
||||
Using the upstream hook's bare `vale` entry in a project that has such a wrapper reintroduces exactly the bug the wrapper exists to fix.
|
||||
|
||||
## CI output for machine parsing
|
||||
|
||||
```bash
|
||||
$ vale --output=JSON README.md
|
||||
```
|
||||
|
||||
Use `--output=JSON` when a CI step needs to parse results programmatically rather than read the default CLI-formatted output.
|
||||
9
plugins/lint/sources.md
Normal file
9
plugins/lint/sources.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Sources
|
||||
|
||||
## context7-websites-vale-sh
|
||||
|
||||
- **URL:** context7:/websites/vale_sh
|
||||
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
|
||||
- **Description:** Official Vale documentation site (vale.sh) indexed by Context7 — informed `lint-runner`'s report-only design: normalizing findings to a `file, line, rule/check, severity, message` shape and deferring linter-specific invocation to `<linter>-config`/`<linter>-run` skills, drawing on Vale's CLI output format and exit-code conventions.
|
||||
- **Contributing files:** agents/lint-runner.md, agents/lint-runner.agent.md
|
||||
- **Status:** `extracted`
|
||||
48
scripts/skill-size-check.sh
Executable file
48
scripts/skill-size-check.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Enforces agentskills.io's skill-authoring.md guidance: keep SKILL.md within
|
||||
# 500 lines and roughly 5,000 tokens, so the full body doesn't crowd out
|
||||
# conversation history and other active skills once loaded into context. Vale
|
||||
# can't express a whole-file length ceiling (its checks operate on text
|
||||
# patterns, not raw file size), so this is a plain script instead of a Vale
|
||||
# rule.
|
||||
#
|
||||
# Both ceilings are inclusive: a file at exactly MAX_LINES or MAX_WORDS passes,
|
||||
# and only one past it fails. That matches skill-audit/scripts/validate.sh,
|
||||
# which has always used `line_count <= 500` as its pass condition — the two
|
||||
# previously disagreed at exactly 500 lines, so a SKILL.md could pass its own
|
||||
# audit and still be blocked by the commit hook.
|
||||
#
|
||||
# Token counts aren't computed exactly here — word count (`wc -w`) is used as
|
||||
# a proxy. This repo's own SKILL.md corpus measures ~5.7-6.5 characters per
|
||||
# word, which at the standard ~4-characters-per-token English approximation
|
||||
# works out to roughly 1.6-1.7 tokens per word. MAX_WORDS below is calibrated
|
||||
# from that measured ratio against the 5,000-token ceiling, with margin — it's
|
||||
# still a proxy, not exact BPE tokenization, but now grounded in actual repo
|
||||
# content rather than an unverified "conservative" assumption.
|
||||
|
||||
MAX_LINES=500
|
||||
MAX_WORDS=2900
|
||||
FAIL=0
|
||||
|
||||
for f in "$@"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
|
||||
# awk's NR counts the final line even without a trailing newline, matching
|
||||
# Python's splitlines() semantics (used by skill-audit/scripts/validate.sh
|
||||
# for its own line count) — `wc -l` undercounts by 1 in that case.
|
||||
lines=$(awk 'END{print NR}' "$f")
|
||||
if (( lines > MAX_LINES )); then
|
||||
echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
words=$(wc -w < "$f")
|
||||
if (( words > MAX_WORDS )); then
|
||||
echo "ERROR: $f has $words words (proxy for tokens), exceeding the $MAX_WORDS-word ceiling (~5,000 tokens, agentskills.io skill-authoring.md)" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit $FAIL
|
||||
177
scripts/vale-wrap.sh
Executable file
177
scripts/vale-wrap.sh
Executable file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
|
||||
# silently stops matching once the `description:` value is a YAML block scalar
|
||||
# (`>`/`|`) spanning 2+ physical lines — the style used by most skills/agents in
|
||||
# this repo. Flattens the description to one physical line in a scratch copy
|
||||
# (padding with blank lines so every other line number is unchanged), then runs
|
||||
# the real `vale` binary against the copies. Drop-in replacement for calling
|
||||
# `vale` directly: same args, same exit code.
|
||||
#
|
||||
# "Same args" means relative paths — `--config` values and file arguments alike
|
||||
# — resolve against the caller's current directory, exactly as bare `vale`
|
||||
# resolves them. (An earlier version resolved them against the repo root, an
|
||||
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
||||
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
||||
# resolve from the repo root — skipping the flattening this script exists for.)
|
||||
#
|
||||
# Vale prints each file path exactly as it was handed to it, so the scratch tree
|
||||
# mirrors the caller's absolute cwd: a relative file argument is passed through
|
||||
# verbatim and resolves to its flattened copy, keeping the report byte-identical
|
||||
# to bare `vale`'s. An absolute file argument inside the cwd is relativized to
|
||||
# keep that property. Only an absolute path outside the cwd is rewritten to its
|
||||
# scratch copy and so reports a scratch path — unavoidable, since a file can
|
||||
# only be read from where it actually is.
|
||||
|
||||
cwd="$(pwd -P)"
|
||||
|
||||
vale_args=()
|
||||
file_args=()
|
||||
config_next=false
|
||||
for arg in "$@"; do
|
||||
if [[ "$config_next" == true ]]; then
|
||||
config_next=false
|
||||
if [[ "$arg" == /* ]]; then
|
||||
vale_args+=("$arg")
|
||||
else
|
||||
vale_args+=("$cwd/$arg")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
case "$arg" in
|
||||
--config)
|
||||
vale_args+=("$arg")
|
||||
config_next=true
|
||||
continue
|
||||
;;
|
||||
--config=/*)
|
||||
vale_args+=("$arg")
|
||||
continue
|
||||
;;
|
||||
--config=*)
|
||||
vale_args+=("--config=$cwd/${arg#--config=}")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
# `-f` resolves relative paths against the caller's cwd, same as vale does.
|
||||
if [[ "$arg" != -* && -f "$arg" ]]; then
|
||||
# An absolute path inside the caller's cwd is relativized so the report cites
|
||||
# a path that resolves against the real tree. Left absolute, it would be
|
||||
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
|
||||
# path to a file that is deleted on exit, which reads as a bug in any report
|
||||
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
||||
# the scratch-path behaviour documented above.
|
||||
if [[ "$arg" == "$cwd"/* ]]; then
|
||||
file_args+=("${arg#"$cwd"/}")
|
||||
else
|
||||
file_args+=("$arg")
|
||||
fi
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#file_args[@]} -eq 0 ]]; then
|
||||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||||
# block waiting on a pipe that will never carry content.
|
||||
exec vale "${vale_args[@]}" < /dev/null
|
||||
fi
|
||||
|
||||
tmpdir="$(realpath -m "$(mktemp -d)")"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# Mirror of the caller's cwd inside the scratch tree; relative file arguments
|
||||
# are resolved from here.
|
||||
mirror="$tmpdir$cwd"
|
||||
mkdir -p "$mirror"
|
||||
|
||||
argv_files=()
|
||||
for arg in "${file_args[@]}"; do
|
||||
if [[ "$arg" == /* ]]; then
|
||||
dest="$tmpdir$arg"
|
||||
else
|
||||
dest="$mirror/$arg"
|
||||
fi
|
||||
dest="$(realpath -m "$dest")"
|
||||
# A file argument with enough leading `..` to climb past the mirror root would
|
||||
# write outside the scratch dir. The real filesystem clamps such a path at
|
||||
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
||||
case "$dest" in
|
||||
"$tmpdir"/*) ;;
|
||||
*)
|
||||
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
python3 - "$arg" "$dest" <<'PYTHON'
|
||||
import re
|
||||
import sys
|
||||
|
||||
src, dest = sys.argv[1], sys.argv[2]
|
||||
with open(src) as fh:
|
||||
content = fh.read()
|
||||
|
||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||
if fm_match:
|
||||
fm = fm_match.group(2)
|
||||
# Only `>`/`>-`/`>+` (folded) scalars break Vale's frontmatter-description
|
||||
# scope. `|`/`|-`/`|+` (literal) scalars already work fine with bare vale,
|
||||
# so they're deliberately left unmatched here.
|
||||
header_m = re.search(r'^description:[ \t]*(>[+-]?)[ \t]*\n', fm, re.MULTILINE)
|
||||
if header_m:
|
||||
# Body capture is indentation-based and blank-line-tolerant, per YAML
|
||||
# block-scalar rules: a blank line (any amount of whitespace) always
|
||||
# stays inside the block; the indent is set by the first content line;
|
||||
# the block ends at the first line indented less than that, or EOF.
|
||||
rest = fm[header_m.end():]
|
||||
indent = None
|
||||
body_lines = []
|
||||
for line in rest.splitlines(keepends=True):
|
||||
text = line.rstrip('\n')
|
||||
if text.strip() == '':
|
||||
body_lines.append(line)
|
||||
continue
|
||||
line_indent = len(text) - len(text.lstrip(' \t'))
|
||||
if indent is None:
|
||||
indent = line_indent
|
||||
elif line_indent < indent:
|
||||
break
|
||||
body_lines.append(line)
|
||||
raw = ''.join(body_lines)
|
||||
if raw.count('\n') >= 2:
|
||||
flat = re.sub(r'\s+', ' ', raw).strip()
|
||||
# YAML single-quoted scalars have no backslash-escape mechanism at
|
||||
# all, so wrapping in single quotes sidesteps the backslash-escape
|
||||
# bug entirely for embedded double quotes, backslashes, and
|
||||
# non-ASCII text. The one YAML-spec-correct way to embed a literal
|
||||
# apostrophe is to double it ('') — but Vale's own frontmatter
|
||||
# scanner isn't a full YAML parser and doesn't understand that
|
||||
# doubling: empirically, it silently truncates the value at the
|
||||
# first ' it sees, hiding everything after it from the NLP scope
|
||||
# (a different flavor of the same bug this whole script exists to
|
||||
# work around). Since this copy is scratch-only and never written
|
||||
# back, sidestep it by substituting a Unicode right single
|
||||
# quotation mark (U+2019) for any literal apostrophe instead of
|
||||
# doubling it — visually a smart quote, but never triggers a YAML
|
||||
# escape sequence at all.
|
||||
flat_q = "'" + flat.replace("'", "’") + "'"
|
||||
pad = '\n' * raw.count('\n')
|
||||
start = header_m.start()
|
||||
end = header_m.end() + len(raw)
|
||||
new_fm = fm[:start] + f'description: {flat_q}\n{pad}' + fm[end:]
|
||||
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
|
||||
|
||||
with open(dest, 'w') as fh:
|
||||
fh.write(content)
|
||||
PYTHON
|
||||
if [[ "$arg" == /* ]]; then
|
||||
argv_files+=("$dest")
|
||||
else
|
||||
argv_files+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
cd "$mirror"
|
||||
vale "${vale_args[@]}" "${argv_files[@]}"
|
||||
7
styles/Kyberforge/DescriptionOpener.yml
Normal file
7
styles/Kyberforge/DescriptionOpener.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
extends: existence
|
||||
message: "Description opens with '%s' — use an imperative 'Use when...' opener instead"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
raw:
|
||||
- '^This (skill|agent)\b'
|
||||
7
styles/Kyberforge/PaddingPhrase.yml
Normal file
7
styles/Kyberforge/PaddingPhrase.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
extends: existence
|
||||
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
|
||||
level: error
|
||||
scope: text
|
||||
ignorecase: true
|
||||
raw:
|
||||
- 'see references?/? for (more )?(info|information|details)\b'
|
||||
7
styles/Kyberforge/SentenceOpenerThereIs.yml
Normal file
7
styles/Kyberforge/SentenceOpenerThereIs.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
extends: existence
|
||||
message: "Don't start a sentence with '%s' — name the subject directly"
|
||||
level: error
|
||||
scope: sentence
|
||||
ignorecase: false
|
||||
raw:
|
||||
- '^There\s(is|are)\b'
|
||||
10
styles/Kyberforge/VagueWording.yml
Normal file
10
styles/Kyberforge/VagueWording.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
extends: existence
|
||||
message: "Vague capability wording: '%s' — state the capability precisely instead"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
- helps with
|
||||
- utilize
|
||||
- assists with
|
||||
- used for
|
||||
7
styles/KyberforgeCopilot/ProactivePhrase.yml
Normal file
7
styles/KyberforgeCopilot/ProactivePhrase.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
extends: existence
|
||||
message: "'%s' is CC-specific phrasing with no effect in Copilot descriptions — remove it"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
- Use proactively
|
||||
163
tests/test-skill-size-check.sh
Executable file
163
tests/test-skill-size-check.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for scripts/skill-size-check.sh: enforces agentskills.io's
|
||||
# 500-line/5,000-word(proxy-for-token) SKILL.md size ceiling.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
make_fixture() {
|
||||
local name="$1" lines="$2" words_per_line="$3" file
|
||||
file="$TMPDIR/$name.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: Test fixture."
|
||||
echo "---"
|
||||
for ((i = 1; i <= lines; i++)); do
|
||||
w=""
|
||||
for ((j = 1; j <= words_per_line; j++)); do
|
||||
w="$w word"
|
||||
done
|
||||
echo "$w"
|
||||
done
|
||||
} > "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- passes a file under both limits ---"
|
||||
SMALL="$(make_fixture small 10 5)"
|
||||
if "$SCRIPT" "$SMALL"; then
|
||||
pass "file under both limits exits 0"
|
||||
else
|
||||
fail "file under both limits should have exited 0"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- fails a file over the line limit ---"
|
||||
MANY_LINES="$(make_fixture many-lines 600 1)"
|
||||
if "$SCRIPT" "$MANY_LINES" 2>/dev/null; then
|
||||
fail "file over the 500-line ceiling should have exited non-zero"
|
||||
else
|
||||
pass "file over the 500-line ceiling exits non-zero"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- fails a file over the word-count limit ---"
|
||||
MANY_WORDS="$(make_fixture many-words 10 600)"
|
||||
if "$SCRIPT" "$MANY_WORDS" 2>/dev/null; then
|
||||
fail "file over the 5,000-word ceiling should have exited non-zero"
|
||||
else
|
||||
pass "file over the 5,000-word ceiling exits non-zero"
|
||||
fi
|
||||
|
||||
# Boundary-pair tests below read the script's current MAX_WORDS rather than
|
||||
# hardcoding it, so they don't silently drift if the threshold changes again.
|
||||
MAX_WORDS="$(grep -oE '^MAX_WORDS=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
MAX_LINES="$(grep -oE '^MAX_LINES=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
|
||||
# make_line_fixture builds a file with an exact total line count (frontmatter
|
||||
# included), independent of word count, for the line-boundary tests.
|
||||
make_line_fixture() {
|
||||
local name="$1" total_lines="$2" file body_lines
|
||||
file="$TMPDIR/$name.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: Test fixture."
|
||||
echo "---"
|
||||
} > "$file"
|
||||
body_lines=$((total_lines - 4))
|
||||
for ((i = 1; i <= body_lines; i++)); do
|
||||
echo "word"
|
||||
done >> "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
# The line ceiling is inclusive of the limit itself, enforced via `>` — so
|
||||
# exactly $MAX_LINES must pass and $((MAX_LINES + 1)) must fail. This matches
|
||||
# skill-audit/scripts/validate.sh's `line_count <= 500` pass condition; the two
|
||||
# previously disagreed at exactly $MAX_LINES lines, so a SKILL.md could pass its
|
||||
# own audit and still be blocked by the commit hook.
|
||||
echo ""
|
||||
echo "--- passes a file at exactly the $MAX_LINES-line boundary ---"
|
||||
AT_LINES="$(make_line_fixture at-line-limit "$MAX_LINES")"
|
||||
ACTUAL_LINES=$(awk 'END{print NR}' "$AT_LINES")
|
||||
if [[ "$ACTUAL_LINES" -ne "$MAX_LINES" ]]; then
|
||||
fail "fixture has $ACTUAL_LINES lines, expected exactly $MAX_LINES"
|
||||
elif "$SCRIPT" "$AT_LINES"; then
|
||||
pass "file at exactly $MAX_LINES lines exits 0"
|
||||
else
|
||||
fail "file at exactly $MAX_LINES lines should have exited 0 (the off-by-one this test guards against)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- fails a file one line over the $MAX_LINES-line boundary ---"
|
||||
OVER_LINES="$(make_line_fixture over-line-limit "$((MAX_LINES + 1))")"
|
||||
ACTUAL_OVER_LINES=$(awk 'END{print NR}' "$OVER_LINES")
|
||||
if [[ "$ACTUAL_OVER_LINES" -ne "$((MAX_LINES + 1))" ]]; then
|
||||
fail "fixture has $ACTUAL_OVER_LINES lines, expected exactly $((MAX_LINES + 1))"
|
||||
elif "$SCRIPT" "$OVER_LINES" 2>/dev/null; then
|
||||
fail "file at $((MAX_LINES + 1)) lines should have exited non-zero"
|
||||
else
|
||||
pass "file at $((MAX_LINES + 1)) lines exits non-zero"
|
||||
fi
|
||||
|
||||
# make_word_fixture builds a file with an exact total word count (frontmatter
|
||||
# words included, since the script's `wc -w` counts the whole file) by padding
|
||||
# a body line with just enough "word" tokens to close the gap to the target.
|
||||
make_word_fixture() {
|
||||
local name="$1" target="$2" file cur remaining body
|
||||
file="$TMPDIR/$name.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: Test fixture."
|
||||
echo "---"
|
||||
} > "$file"
|
||||
cur=$(wc -w < "$file")
|
||||
remaining=$((target - cur))
|
||||
body=""
|
||||
for ((i = 1; i <= remaining; i++)); do
|
||||
body="$body word"
|
||||
done
|
||||
echo "$body" >> "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- passes a file at exactly the $MAX_WORDS-word boundary ---"
|
||||
AT_WORDS="$(make_word_fixture at-word-limit "$MAX_WORDS")"
|
||||
ACTUAL_WORDS=$(wc -w < "$AT_WORDS")
|
||||
if [[ "$ACTUAL_WORDS" -ne "$MAX_WORDS" ]]; then
|
||||
fail "fixture has $ACTUAL_WORDS words, expected exactly $MAX_WORDS"
|
||||
elif "$SCRIPT" "$AT_WORDS"; then
|
||||
pass "file at exactly $MAX_WORDS words exits 0"
|
||||
else
|
||||
fail "file at exactly $MAX_WORDS words should have exited 0"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- fails a file one word over the $MAX_WORDS-word boundary ---"
|
||||
OVER_WORDS="$(make_word_fixture over-word-limit "$((MAX_WORDS + 1))")"
|
||||
ACTUAL_OVER_WORDS=$(wc -w < "$OVER_WORDS")
|
||||
if [[ "$ACTUAL_OVER_WORDS" -ne "$((MAX_WORDS + 1))" ]]; then
|
||||
fail "fixture has $ACTUAL_OVER_WORDS words, expected exactly $((MAX_WORDS + 1))"
|
||||
elif "$SCRIPT" "$OVER_WORDS" 2>/dev/null; then
|
||||
fail "file at $((MAX_WORDS + 1)) words should have exited non-zero"
|
||||
else
|
||||
pass "file at $((MAX_WORDS + 1)) words exits non-zero"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
345
tests/test-vale-wrap.sh
Executable file
345
tests/test-vale-wrap.sh
Executable file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for scripts/vale-wrap.sh: Vale's `text.frontmatter.description`
|
||||
# NLP scope silently stops matching when the description value is a YAML block
|
||||
# scalar spanning 2+ physical lines. vale-wrap.sh flattens it to one line before
|
||||
# handing off to the real vale binary — this asserts that actually happens.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/vale-wrap.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
if ! command -v vale &>/dev/null; then
|
||||
echo "vale is not installed — skipping (matches skill-audit/agent-audit's own fallback behavior)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
make_fixture() {
|
||||
local dir desc_lines file
|
||||
dir="$(mktemp -d)"
|
||||
(cd "$dir" && git init -q)
|
||||
mkdir -p "$dir/plugins/testplugin/skills/zzzskill"
|
||||
desc_lines="$1"
|
||||
file="$dir/plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: zzzskill"
|
||||
echo "description: >"
|
||||
for ((i = 1; i <= desc_lines; i++)); do
|
||||
echo " Line $i mentions helps with and utilize, plus a colon: like this."
|
||||
done
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Body."
|
||||
} > "$file"
|
||||
echo "$dir"
|
||||
}
|
||||
|
||||
# Every Kyberforge rule is `level: error`, so vale exits non-zero whenever a
|
||||
# fixture trips one — which is the expected outcome for nearly every case here.
|
||||
# run_wrap therefore captures output and swallows the exit status; assertions
|
||||
# are made on the report text. Cases that genuinely care about the exit code
|
||||
# capture it explicitly instead.
|
||||
run_wrap() {
|
||||
local dir="$1"
|
||||
shift
|
||||
(cd "$dir" && bash "$SCRIPT" "$@" 2>&1) || true
|
||||
}
|
||||
|
||||
# --- 1. A known-bad single-line description is caught (sanity check on Vale itself) ---
|
||||
echo ""
|
||||
echo "--- catches vague wording in a single-line description ---"
|
||||
FIXTURE1="$(make_fixture 1)"
|
||||
trap 'rm -rf "$FIXTURE1"' EXIT
|
||||
if run_wrap "$FIXTURE1" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
|
||||
pass "flags vague wording when description is a single physical line"
|
||||
else
|
||||
fail "did not flag known-bad single-line description"
|
||||
fi
|
||||
|
||||
# --- 2. The same known-bad wording across 2+ physical lines is still caught ---
|
||||
echo ""
|
||||
echo "--- catches vague wording in a multi-line folded description ---"
|
||||
FIXTURE2="$(make_fixture 2)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT
|
||||
if run_wrap "$FIXTURE2" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
|
||||
pass "flags vague wording when description spans 2+ physical lines"
|
||||
else
|
||||
fail "silently missed known-bad wording in a multi-line description — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 3. Line count is preserved so unrelated report line numbers don't shift ---
|
||||
echo ""
|
||||
echo "--- preserves total line count when flattening ---"
|
||||
FIXTURE3="$(make_fixture 3)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3"' EXIT
|
||||
ORIG_LINES=$(wc -l < "$FIXTURE3/plugins/testplugin/skills/zzzskill/SKILL.md")
|
||||
OUT=$(run_wrap "$FIXTURE3" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md)
|
||||
MAX_LINE=$(echo "$OUT" | grep -oE '^[[:space:]]*[0-9]+:[0-9]+' | tr -d '[:space:]' | cut -d: -f1 | sort -n | tail -1)
|
||||
if [[ -n "$MAX_LINE" ]] && (( MAX_LINE <= ORIG_LINES )); then
|
||||
pass "reported line numbers stay within the original file's line count"
|
||||
else
|
||||
fail "reported line number ($MAX_LINE) exceeds original file line count ($ORIG_LINES)"
|
||||
fi
|
||||
|
||||
# make_raw_fixture writes stdin verbatim to a fresh fixture's SKILL.md, for
|
||||
# cases where the exact description body needs to be hand-crafted rather than
|
||||
# generated from the desc_lines loop above.
|
||||
make_raw_fixture() {
|
||||
local dir
|
||||
dir="$(mktemp -d)"
|
||||
(cd "$dir" && git init -q)
|
||||
mkdir -p "$dir/plugins/testplugin/skills/zzzskill"
|
||||
cat > "$dir/plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
echo "$dir"
|
||||
}
|
||||
|
||||
# --- 4. A folded description containing a double quote is still caught ---
|
||||
# This is the exact case that silently passed (zero alerts) before switching
|
||||
# from json.dumps (double-quoted, backslash-escaped) to a single-quoted scalar.
|
||||
echo ""
|
||||
echo "--- catches vague wording when the folded description contains a double quote ---"
|
||||
FIXTURE4="$(make_raw_fixture <<'EOF'
|
||||
---
|
||||
name: zzzskill
|
||||
description: >
|
||||
Use when the user says "audit this skill" and helps with and utilize things.
|
||||
Second line continues the same folded scalar for flattening.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
|
||||
if run_wrap "$FIXTURE4" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
|
||||
pass "flags vague wording when the description contains a double quote"
|
||||
else
|
||||
fail "silently missed vague wording in a description containing a double quote — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 5. A folded description containing an apostrophe fires and stays valid YAML ---
|
||||
echo ""
|
||||
echo "--- catches vague wording when the folded description contains an apostrophe ---"
|
||||
FIXTURE5="$(make_raw_fixture <<'EOF'
|
||||
---
|
||||
name: zzzskill
|
||||
description: >
|
||||
Use when the user's task helps with and utilize things across two lines.
|
||||
Second continuation line for the fold.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
|
||||
OUT5=$(run_wrap "$FIXTURE5" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md)
|
||||
if echo "$OUT5" | grep -q "VagueWording"; then
|
||||
pass "flags vague wording when the description contains an apostrophe"
|
||||
else
|
||||
fail "silently missed vague wording in a description containing an apostrophe"
|
||||
fi
|
||||
if echo "$OUT5" | grep -qi "yaml:"; then
|
||||
fail "flattened copy with an apostrophe produced a YAML parse error"
|
||||
else
|
||||
pass "flattened copy with an apostrophe is valid YAML (no parse error)"
|
||||
fi
|
||||
|
||||
# --- 6. A folded description with a backslash and a non-ASCII character ---
|
||||
echo ""
|
||||
echo "--- catches vague wording when the folded description has a backslash and non-ASCII text ---"
|
||||
FIXTURE6="$(make_raw_fixture <<'EOF'
|
||||
---
|
||||
name: zzzskill
|
||||
description: >
|
||||
Use when the café résumé naïve thing helps with and utilize things here.
|
||||
Path is C:\Users\test and this is the second continuation line.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT
|
||||
if run_wrap "$FIXTURE6" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
|
||||
pass "flags vague wording when the description has a backslash and non-ASCII text"
|
||||
else
|
||||
fail "silently missed vague wording in a description with a backslash and non-ASCII text"
|
||||
fi
|
||||
|
||||
# --- 7. A folded description with a blank line between two paragraphs ---
|
||||
echo ""
|
||||
echo "--- handles a blank line inside a folded description without crashing ---"
|
||||
FIXTURE7="$(make_raw_fixture <<'EOF'
|
||||
---
|
||||
name: zzzskill
|
||||
description: >
|
||||
Use when the user needs a general helper.
|
||||
|
||||
Do not use when this helps with and utilize things instead.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT
|
||||
OUT7=$(run_wrap "$FIXTURE7" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md)
|
||||
if echo "$OUT7" | grep -q "Traceback"; then
|
||||
fail "crashed while flattening a description with a blank line between paragraphs"
|
||||
elif echo "$OUT7" | grep -q "VagueWording"; then
|
||||
pass "still flags vague wording in the second paragraph after a blank line"
|
||||
else
|
||||
fail "silently missed vague wording in the second paragraph after a blank line — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 8. Relative paths resolve against the caller's cwd, exactly as bare vale
|
||||
# resolves them. Every path below is deliberately relative to $SUBDIR8, not to
|
||||
# the fixture's repo root: an earlier version of the wrapper resolved relative
|
||||
# paths against the git toplevel instead, which (a) hard-errored on a
|
||||
# `--config ../../..` that bare vale accepts and (b) silently dropped file
|
||||
# arguments that didn't resolve from the repo root, skipping the flattening the
|
||||
# wrapper exists to perform. The old tests only ever passed repo-root-relative
|
||||
# paths from a subdirectory, so neither failure mode was caught.
|
||||
echo ""
|
||||
echo "--- resolves a cwd-relative --config from a subdirectory (equals and two-argv forms) ---"
|
||||
FIXTURE8="$(mktemp -d)"
|
||||
(cd "$FIXTURE8" && git init -q)
|
||||
cp "$REPO_ROOT/.vale.ini" "$FIXTURE8/.vale.ini"
|
||||
cp -r "$REPO_ROOT/styles" "$FIXTURE8/styles"
|
||||
mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: zzzskill"
|
||||
echo "description: >"
|
||||
echo " Line one mentions helps with and utilize, plus a colon: like this."
|
||||
echo " Line two continues the same folded scalar for flattening."
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Body."
|
||||
} > "$FIXTURE8/plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8"' EXIT
|
||||
SUBDIR8="$FIXTURE8/plugins/testplugin/skills/zzzskill"
|
||||
# Both paths are relative to $SUBDIR8 (four levels below the fixture root).
|
||||
REL_CFG8="../../../../.vale.ini"
|
||||
REL_FILE8="SKILL.md"
|
||||
OUT_EQ=$(run_wrap "$SUBDIR8" "--config=$REL_CFG8" "$REL_FILE8")
|
||||
OUT_TWO=$(run_wrap "$SUBDIR8" --config "$REL_CFG8" "$REL_FILE8")
|
||||
if echo "$OUT_EQ" | grep -q "VagueWording" && [[ "$OUT_EQ" == "$OUT_TWO" ]]; then
|
||||
pass "cwd-relative --config resolves from a subdirectory in both argv forms"
|
||||
else
|
||||
fail "cwd-relative --config did not resolve from a subdirectory (equals form vs two-argv form)"
|
||||
fi
|
||||
|
||||
# --- 8b. A cwd-relative --config matches the equivalent absolute invocation ---
|
||||
# Regression for failure mode (a): resolving --config against the repo root made
|
||||
# `--config ../../../../.vale.ini` expand to a path above the toplevel, and vale
|
||||
# hard-errored with "does not exist" (exit 2) on args bare vale handles fine.
|
||||
echo ""
|
||||
echo "--- a cwd-relative --config produces the same result as the absolute-path form ---"
|
||||
set +e
|
||||
OUT_REL_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$REL_CFG8" "$REL_FILE8" 2>&1)
|
||||
RC_REL_CFG=$?
|
||||
OUT_ABS_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1)
|
||||
RC_ABS_CFG=$?
|
||||
set -e
|
||||
if echo "$OUT_REL_CFG" | grep -qi "does not exist"; then
|
||||
fail "cwd-relative --config hard-errored ('does not exist') — the bug this test guards against"
|
||||
elif [[ "$OUT_REL_CFG" == "$OUT_ABS_CFG" && "$RC_REL_CFG" -eq "$RC_ABS_CFG" ]]; then
|
||||
pass "cwd-relative --config matches the absolute-path invocation (output and exit code)"
|
||||
else
|
||||
fail "cwd-relative --config (rc=$RC_REL_CFG) diverged from the absolute-path form (rc=$RC_ABS_CFG)"
|
||||
fi
|
||||
|
||||
# --- 8c. A cwd-relative FILE argument is still flattened, not silently skipped ---
|
||||
# Regression for failure mode (b): a relative file path that didn't resolve from
|
||||
# the repo root failed the wrapper's file test, fell through to the vale flag
|
||||
# list, and left the file list empty — so the wrapper exec'd bare vale and
|
||||
# silently skipped the flattening. Bare vale reports nothing here, so asserting
|
||||
# on the alert (not just the exit code) is what makes the silence detectable.
|
||||
echo ""
|
||||
echo "--- flattens a cwd-relative file argument passed from a subdirectory ---"
|
||||
WRAPPED_REL=$(run_wrap "$SUBDIR8" --config "$FIXTURE8/.vale.ini" "$REL_FILE8")
|
||||
BARE_REL=$(cd "$SUBDIR8" && vale --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1 || true)
|
||||
if ! echo "$WRAPPED_REL" | grep -q "VagueWording"; then
|
||||
fail "cwd-relative file argument produced no alert — flattening was silently skipped, the bug this test guards against"
|
||||
elif echo "$BARE_REL" | grep -q "VagueWording"; then
|
||||
fail "bare vale already flags this fixture, so the test can't detect a silently-skipped flattening"
|
||||
else
|
||||
pass "cwd-relative file argument is flattened and flagged where bare vale reports nothing"
|
||||
fi
|
||||
|
||||
# --- 9. Zero file args (or a file list that filters to nothing) exits promptly ---
|
||||
echo ""
|
||||
echo "--- exits promptly instead of hanging on stdin when no files are passed ---"
|
||||
if timeout 5 bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" < <(sleep 100) >/dev/null 2>&1; then
|
||||
pass "exits promptly with zero file args"
|
||||
else
|
||||
RC=$?
|
||||
if [[ $RC -eq 124 ]]; then
|
||||
fail "hung waiting on stdin with zero file args — the bug this test guards against"
|
||||
else
|
||||
pass "exits promptly (nonzero exit) with zero file args"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- exits promptly when a file list filters down to nothing ---"
|
||||
if timeout 5 bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" --no-such-flag < <(sleep 100) >/dev/null 2>&1; then
|
||||
pass "exits promptly when no file-shaped args remain"
|
||||
else
|
||||
RC=$?
|
||||
if [[ $RC -eq 124 ]]; then
|
||||
fail "hung waiting on stdin when the file list filtered to nothing — the bug this test guards against"
|
||||
else
|
||||
pass "exits promptly (nonzero exit) when the file list filters to nothing"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 10. An absolute path to the fixture SKILL.md is still linted, not skipped ---
|
||||
echo ""
|
||||
echo "--- lints an absolute path to a skill file instead of silently skipping it ---"
|
||||
FIXTURE10="$(make_fixture 2)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10"' EXIT
|
||||
ABS_FILE10="$FIXTURE10/plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
if run_wrap "$FIXTURE10" --config "$REPO_ROOT/.vale.ini" "$ABS_FILE10" | grep -q "VagueWording"; then
|
||||
pass "an absolute path is linted, not silently skipped"
|
||||
else
|
||||
fail "an absolute path was silently skipped — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 11. A literal (|) block scalar passes through unflattened (no regression) ---
|
||||
echo ""
|
||||
echo "--- leaves a literal (|) block scalar untouched (narrowed >-only scope) ---"
|
||||
FIXTURE11="$(make_raw_fixture <<'EOF'
|
||||
---
|
||||
name: zzzskill
|
||||
description: |
|
||||
Line one mentions helps with and utilize things here.
|
||||
Line two continues the literal block scalar for this test.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT
|
||||
REL11="plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
WRAPPED_OUT=$(run_wrap "$FIXTURE11" --config "$REPO_ROOT/.vale.ini" "$REL11")
|
||||
BARE_OUT=$(cd "$FIXTURE11" && vale --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
|
||||
if [[ "$WRAPPED_OUT" == "$BARE_OUT" ]]; then
|
||||
pass "literal (|) block scalar output matches bare vale exactly — untouched by flattening"
|
||||
else
|
||||
fail "wrapper altered output for a literal (|) block scalar description — should be left untouched"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Reference in New Issue
Block a user