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 |
@@ -111,7 +111,7 @@ repos:
|
||||
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 --minAlertLevel=warning
|
||||
entry: scripts/vale-wrap.sh --config .vale.ini
|
||||
language: script
|
||||
files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$'
|
||||
pass_filenames: true
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
StylesPath = styles
|
||||
MinAlertLevel = suggestion
|
||||
|
||||
[plugins/*/skills/*/SKILL.md]
|
||||
[**/SKILL.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
|
||||
[plugins/*/agents/*.md]
|
||||
[**/agents/*.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
|
||||
[plugins/*/agents/*.agent.md]
|
||||
[**/*.agent.md]
|
||||
BasedOnStyles = Kyberforge, KyberforgeCopilot
|
||||
|
||||
40
AGENTS.md
40
AGENTS.md
@@ -1,27 +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).
|
||||
- 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. Install via a package manager (`brew install vale` on macOS, `snap install vale` on Linux, `choco install vale` on Windows) or see https://vale.sh/docs/vale-cli/installation/; then run `vale sync` to pull the styles declared in `.vale.ini`.
|
||||
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
|
||||
|
||||
@@ -29,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.
|
||||
|
||||
10
CONTEXT.md
10
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,7 +61,7 @@ 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.
|
||||
@@ -70,11 +70,11 @@ A companion skill (`core` plugin) that detects a target repo's provider-specific
|
||||
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. `error` alerts map to FAIL, `warning`/`suggestion` map to SUGGESTION. 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, two body-wide prose-pattern checks (vague-qualifier filler, "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.
|
||||
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. `tests/test-vale-wrap.sh` regression-tests this. Both call sites still scope every invocation to the specific file(s) being audited, never a repo-wide sweep — Vale's glob matching crosses directory boundaries (`plugins/*/agents/*.md` matches nested `docs/research/examples/**/agents/*.md` too), so scoping is what keeps research-example files out of the audit's lint pass. The pre-commit hook's own glob is tightened to `^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$` (single-segment, not `.*`) for the same reason, since pre-commit invokes it automatically against whatever staged files match rather than a manually-scoped target.
|
||||
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: cherry-picked low-noise `write-good`/`alex` rules landed in `styles/Kyberforge` as two new rule files, `Kyberforge.VagueQualifier` and `Kyberforge.SentenceOpenerThereIs`, plus a new sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling — 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).
|
||||
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.
|
||||
@@ -9,8 +9,8 @@ deferred PR #85 review item to broaden that coverage, retroactively captures #84
|
||||
(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 (`plugins/*/agents/*.md`,
|
||||
`plugins/*/agents/*.agent.md`) only — matching the existing prefilter's globs. Skill-level
|
||||
**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.
|
||||
|
||||
@@ -51,7 +51,13 @@ length ceiling, not a text pattern, so it isn't a Vale rule — it becomes a new
|
||||
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). The implementation pass finalizes the cherry-picked
|
||||
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
|
||||
@@ -68,14 +74,29 @@ under that directory automatically — there's no partial/opt-in application wit
|
||||
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.
|
||||
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 two new rule files, cherry-picked from `write-good`/`alex` as
|
||||
low-noise against this repo's corpus: `VagueQualifier.yml` and `SentenceOpenerThereIs.yml`.
|
||||
- `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`.
|
||||
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
|
||||
@@ -87,7 +108,8 @@ standing report-only tier for future candidate rules.
|
||||
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 two low-noise rules above into
|
||||
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).
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -15,12 +15,7 @@ ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drif
|
||||
|
||||
## Rules
|
||||
|
||||
<!-- vale Kyberforge.VagueQualifier = NO -->
|
||||
<!-- vale Kyberforge.VagueWording = NO -->
|
||||
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging.
|
||||
<!-- vale Kyberforge.VagueQualifier = YES -->
|
||||
<!-- vale Kyberforge.VagueWording = YES -->
|
||||
Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
|
||||
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
|
||||
|
||||
Technical terms stay exact. Code blocks unchanged. Errors quoted exact.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ bash scripts/validate-provenance.sh <path-to-agent-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`, so it runs correctly regardless of the caller's 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. Map `error` → `FAIL` and `warning`/`suggestion` → `SUGGESTION` in the `### Description` / `### Body` dimensions, citing the rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if vale or `.vale.ini` is unavailable.
|
||||
`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.
|
||||
|
||||
@@ -53,7 +53,7 @@ Read both agent files. Work through each dimension internally. Collect findings
|
||||
|
||||
**Description (both files):**
|
||||
- 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 without re-deriving by 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`.
|
||||
@@ -62,7 +62,7 @@ If a description finding is borderline, read `references/description-quality.md`
|
||||
- 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
|
||||
- Vague filler wording and sentences that open with "There is"/"There are": Vale's `Kyberforge.VagueQualifier` and `Kyberforge.SentenceOpenerThereIs` alerts flag this directly — report them 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.
|
||||
|
||||
@@ -41,7 +41,7 @@ Note any structural FAILs — they will appear in the report as a `### Structure
|
||||
|
||||
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`, so it runs correctly regardless of the caller's cwd, using `.vale.ini`'s `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns dimensions below, not a replacement for Step 3. Map `error` → `FAIL` and `warning`/`suggestion` → `SUGGESTION` in those dimensions, citing the rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if vale or `.vale.ini` is unavailable.
|
||||
`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
|
||||
|
||||
@@ -53,7 +53,7 @@ Work through each dimension internally. Collect findings only; report them in St
|
||||
|
||||
### Description
|
||||
|
||||
Vale's `Kyberforge.DescriptionOpener` (FAIL — "This skill..." openers) and `Kyberforge.VagueWording` (SUGGESTION — filler like "helps with", "utilize") alerts from Step 1 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:
|
||||
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?
|
||||
@@ -70,7 +70,7 @@ 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.VagueQualifier` (SUGGESTION — vague filler like "clearly", "obviously") and `Kyberforge.SentenceOpenerThereIs` (SUGGESTION — sentences starting with "There is"/"There are") alerts from Step 1 cover pattern-matchable body-wide filler directly; report them as findings without re-deriving by judgment.
|
||||
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`.
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"name": "lint",
|
||||
"version": "1.1.2"
|
||||
"version": "1.1.3"
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"skills": [
|
||||
"skills/"
|
||||
],
|
||||
"version": "1.1.2"
|
||||
"version": "1.1.3"
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ metadata:
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Vale exits non-zero whenever it finds an alert at or above `MinAlertLevel` — that's what makes it usable as a CI gate, not a sign the invocation failed. Read the output before concluding the command errored.
|
||||
- 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/MDX uses `{/* vale off */}` / `{/* vale on */}`, Org mode uses `# vale off` / `# vale on`. Don't assume one syntax works across formats.
|
||||
- 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
|
||||
@@ -37,8 +37,8 @@ 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. |
|
||||
| `--no-exit` | Forces exit code `0` regardless of findings. Use in CI stages that should surface lint output without hard-failing the build. |
|
||||
| `--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.
|
||||
@@ -49,14 +49,15 @@ Prefer `--output=JSON` whenever the caller (a script, a CI step, another agent)
|
||||
|
||||
Scope the fix as narrowly as possible, in this order:
|
||||
|
||||
1. **One-off**: inline-suppress the specific text run with the format's `vale off`/`vale on` markup.
|
||||
2. **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.
|
||||
3. **Known project term failing spell check**: add it to the style's `ignore` list, not an inline suppression.
|
||||
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 found alerts — not because the content is wrong for that pipeline stage — add `--no-exit` rather than disabling the rule.
|
||||
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`.
|
||||
|
||||
@@ -7,7 +7,14 @@ source_keys:
|
||||
|
||||
## Inline suppression syntax by format
|
||||
|
||||
Markdown/MDX:
|
||||
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.
|
||||
@@ -25,6 +32,14 @@ This text will be ignored.
|
||||
|
||||
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
|
||||
@@ -50,7 +65,7 @@ If a file's syntax-aware parsing produces noisy or incorrect results (an unsuppo
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Enforces agentskills.io's skill-authoring.md guidance: keep SKILL.md under 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.
|
||||
# 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
|
||||
@@ -26,8 +33,8 @@ for f in "$@"; do
|
||||
# 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, at or over the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
|
||||
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
|
||||
|
||||
|
||||
@@ -8,56 +8,104 @@ set -euo pipefail
|
||||
# (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.
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cwd="$(pwd -P)"
|
||||
|
||||
vale_args=()
|
||||
files=()
|
||||
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+=("$repo_root/$arg")
|
||||
vale_args+=("$cwd/$arg")
|
||||
fi
|
||||
config_next=false
|
||||
continue
|
||||
fi
|
||||
if [[ "$arg" == "--config" ]]; then
|
||||
vale_args+=("$arg")
|
||||
config_next=true
|
||||
continue
|
||||
fi
|
||||
if [[ "$arg" == --config=* ]]; then
|
||||
cfg="${arg#--config=}"
|
||||
if [[ "$cfg" == /* ]]; then
|
||||
vale_args+=("--config=$cfg")
|
||||
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
|
||||
vale_args+=("--config=$repo_root/$cfg")
|
||||
file_args+=("$arg")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [[ "$arg" != -* && -f "$repo_root/$arg" ]]; then
|
||||
files+=("$arg")
|
||||
elif [[ "$arg" == /* && -f "$arg" && "$arg" == "$repo_root"/* ]]; then
|
||||
files+=("${arg#"$repo_root"/}")
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
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="$(mktemp -d)"
|
||||
tmpdir="$(realpath -m "$(mktemp -d)")"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
for rel in "${files[@]}"; do
|
||||
dest="$tmpdir/$rel"
|
||||
# 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 - "$repo_root/$rel" "$dest" <<'PYTHON'
|
||||
python3 - "$arg" "$dest" <<'PYTHON'
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -118,7 +166,12 @@ if fm_match:
|
||||
with open(dest, 'w') as fh:
|
||||
fh.write(content)
|
||||
PYTHON
|
||||
if [[ "$arg" == /* ]]; then
|
||||
argv_files+=("$dest")
|
||||
else
|
||||
argv_files+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
cd "$tmpdir"
|
||||
vale "${vale_args[@]}" "${files[@]}"
|
||||
cd "$mirror"
|
||||
vale "${vale_args[@]}" "${argv_files[@]}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends: existence
|
||||
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
|
||||
level: warning
|
||||
level: error
|
||||
scope: text
|
||||
ignorecase: true
|
||||
raw:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends: existence
|
||||
message: "Don't start a sentence with '%s' — name the subject directly"
|
||||
level: warning
|
||||
level: error
|
||||
scope: sentence
|
||||
ignorecase: false
|
||||
raw:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
extends: existence
|
||||
message: "'%s' is vague filler wording — state the point precisely instead"
|
||||
level: warning
|
||||
scope: text
|
||||
ignorecase: true
|
||||
tokens:
|
||||
- easily
|
||||
- everyone knows
|
||||
- exceedingly
|
||||
- excellent
|
||||
- extremely
|
||||
- huge
|
||||
- interestingly
|
||||
- of course
|
||||
- quite
|
||||
- remarkably
|
||||
- surprisingly
|
||||
- vast
|
||||
- very
|
||||
@@ -1,6 +1,6 @@
|
||||
extends: existence
|
||||
message: "Vague capability wording: '%s' — state the capability precisely instead"
|
||||
level: warning
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends: existence
|
||||
message: "'%s' is CC-specific phrasing with no effect in Copilot descriptions — remove it"
|
||||
level: warning
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
|
||||
@@ -83,31 +83,33 @@ make_line_fixture() {
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
# The line ceiling is exclusive of the limit itself ("stay under $MAX_LINES
|
||||
# lines", per skill-authoring.md), enforced via `>=` — so $((MAX_LINES - 1))
|
||||
# must pass and $MAX_LINES itself must already fail.
|
||||
# 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 $((MAX_LINES - 1)) lines, just under the $MAX_LINES-line boundary ---"
|
||||
AT_LINES="$(make_line_fixture at-line-limit "$((MAX_LINES - 1))")"
|
||||
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 - 1))" ]]; then
|
||||
fail "fixture has $ACTUAL_LINES lines, expected exactly $((MAX_LINES - 1))"
|
||||
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 $((MAX_LINES - 1)) lines exits 0"
|
||||
pass "file at exactly $MAX_LINES lines exits 0"
|
||||
else
|
||||
fail "file at $((MAX_LINES - 1)) lines should have exited 0"
|
||||
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 at exactly the $MAX_LINES-line boundary ---"
|
||||
OVER_LINES="$(make_line_fixture over-line-limit "$MAX_LINES")"
|
||||
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" ]]; then
|
||||
fail "fixture has $ACTUAL_OVER_LINES lines, expected exactly $MAX_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 exactly $MAX_LINES lines should have exited non-zero (>= ceiling, the boundary bug this test guards against)"
|
||||
fail "file at $((MAX_LINES + 1)) lines should have exited non-zero"
|
||||
else
|
||||
pass "file at exactly $MAX_LINES lines exits non-zero"
|
||||
pass "file at $((MAX_LINES + 1)) lines exits non-zero"
|
||||
fi
|
||||
|
||||
# make_word_fixture builds a file with an exact total word count (frontmatter
|
||||
|
||||
@@ -39,13 +39,24 @@ make_fixture() {
|
||||
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 (cd "$FIXTURE1" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then
|
||||
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"
|
||||
@@ -56,8 +67,8 @@ echo ""
|
||||
echo "--- catches vague wording in a multi-line folded description ---"
|
||||
FIXTURE2="$(make_fixture 2)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT
|
||||
if (cd "$FIXTURE2" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then
|
||||
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"
|
||||
@@ -69,8 +80,8 @@ 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=$(cd "$FIXTURE3" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1 || true)
|
||||
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"
|
||||
@@ -107,8 +118,8 @@ Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
|
||||
if (cd "$FIXTURE4" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then
|
||||
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"
|
||||
@@ -129,8 +140,8 @@ Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
|
||||
OUT5=$(cd "$FIXTURE5" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1)
|
||||
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
|
||||
@@ -157,8 +168,8 @@ Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT
|
||||
if (cd "$FIXTURE6" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then
|
||||
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"
|
||||
@@ -180,8 +191,8 @@ Body.
|
||||
EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT
|
||||
OUT7=$(cd "$FIXTURE7" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
|
||||
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1)
|
||||
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
|
||||
@@ -190,9 +201,16 @@ else
|
||||
fail "silently missed vague wording in the second paragraph after a blank line — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 8. --config=<path> (equals form) resolves the same as the two-argv form ---
|
||||
# --- 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 "--- --config=<path> equals form resolves from a subdirectory like the two-argv form ---"
|
||||
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"
|
||||
@@ -210,13 +228,53 @@ mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
|
||||
} > "$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"
|
||||
REL8="plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
OUT_EQ=$(cd "$SUBDIR8" && bash "$SCRIPT" --config=.vale.ini "$REL8" 2>&1)
|
||||
OUT_TWO=$(cd "$SUBDIR8" && bash "$SCRIPT" --config .vale.ini "$REL8" 2>&1)
|
||||
# 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 "--config=<path> from a subdirectory resolves and matches the two-argv form"
|
||||
pass "cwd-relative --config resolves from a subdirectory in both argv forms"
|
||||
else
|
||||
fail "--config=<path> equals form did not resolve the same as the two-argv form"
|
||||
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 ---
|
||||
@@ -252,10 +310,10 @@ 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 (cd "$FIXTURE10" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" "$ABS_FILE10") | grep -q "VagueWording"; then
|
||||
pass "an absolute path under repo_root is linted, not silently skipped"
|
||||
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 under repo_root was silently skipped — the bug this test guards against"
|
||||
fail "an absolute path was silently skipped — the bug this test guards against"
|
||||
fi
|
||||
|
||||
# --- 11. A literal (|) block scalar passes through unflattened (no regression) ---
|
||||
@@ -274,7 +332,7 @@ EOF
|
||||
)"
|
||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT
|
||||
REL11="plugins/testplugin/skills/zzzskill/SKILL.md"
|
||||
WRAPPED_OUT=$(cd "$FIXTURE11" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user