fix(lint): resolve round-1 and round-2 review findings on the Vale prefilter

Addresses PR #85's outstanding review items after grilling the open
questions against ADR-0013/CONTEXT.md/ADR-0010:

Blocking fixes:
- vale-wrap.sh: replace json.dumps() escaping (which silently defeated
  Vale's frontmatter scope on any description containing a quote,
  backslash, or non-ASCII char — ~58% of the corpus) with a single-quoted
  YAML scalar, substituting a Unicode right single quote for embedded
  apostrophes rather than '' doubling (Vale's frontmatter scanner isn't a
  full YAML parser and silently truncates on '' too).
- vale-wrap.sh: fix a blank-line-inside-a-folded-description truncation
  bug via indentation-based, blank-line-tolerant body capture; narrow
  flattening to `>`-style scalars only (`|` already works unflattened).
- skill-audit/agent-audit Step 1: make the vale-wrap.sh invocation
  cwd-independent via git rev-parse --show-toplevel, fixing a bug where
  no single cwd satisfied all three Step 1 commands.
- styles/Kyberforge/VagueQualifier.yml: prune 17 tokens verified
  false-positive-dominated on this repo's own voice via a real corpus
  sweep (obvious, clearly, usually, several, simple, easy, completely,
  simply, tiny, etc.), keep 13 with real or unattested noise. Revert the
  28 prose "fixes" those tokens drove across 14 skill files back to their
  original, correct wording, including a functional regression to
  caveman/SKILL.md's own filler-word list (a mention, not a use) — now
  guarded with vale-off comments against recurrence.

Gaps:
- --minAlertLevel=warning on the pre-commit hook and Step 1 invocation
  so warning-level rules actually surface, without collapsing the
  FAIL/SUGGESTION severity mapping skill-audit/agent-audit rely on.
- vale-wrap.sh: fix --config=<path> equals-form, absolute-path silent
  no-op, and a zero-file-argument stdin hang.
- Route vale-run and lint-runner through a documented wrapper script
  when a target repo has one, instead of unconditionally recommending
  bare `vale`.
- Wire Kyberforge.VagueQualifier/SentenceOpenerThereIs into skill-audit/
  agent-audit's dimension-mapping prose (Body discipline).
- Add plugins/lint/sources.md provenance for lint-runner (ADR-0010).
- Sync both marketplace.json lint-entry descriptions with plugin.json.
- Retune skill-size-check.sh's MAX_WORDS 5000->2900 (measured ~1.6-1.7
  tokens/word on this repo's corpus, the old value gated at ~8,500
  tokens against a stated 5,000 ceiling); fix the >/>= line-count
  boundary and wc -l undercount on files with no trailing newline.
- Document the vale binary as a Setup prerequisite in AGENTS.md.
- Fix SentenceOpenerThereIs's dead regex alternative and add a real
  sentence-start anchor/scope.
- Fix a stale docs/research/docs/vale/ index pointer in kyberforge's
  docs README (moved to plugins/lint/ in e1a5403).
- Rewrite ADR-0013's Consequences section past-tense to describe what
  actually landed, and record the styles-portability limitation
  (repo-root placement stays intentional; deferred to a separate
  session per this PR's review).

Test coverage: 9 new vale-wrap.sh fixtures (quotes, backslash/unicode,
blank-line paragraphs, --config= form, zero-arg/absolute-path handling,
literal-block no-regression) and boundary-pair tests for
skill-size-check.sh's line/word ceilings.

bash tests/run-tests.sh: 9 scripts + 125 bats assertions, all passing.
scripts/check-manifests.sh and claude plugin validate --strict: clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
This commit is contained in:
2026-08-08 19:24:40 +00:00
parent 2eb13f702e
commit 55dc065644
35 changed files with 462 additions and 85 deletions

View File

@@ -40,10 +40,10 @@
}
},
{
"description": "Skills and agents for configuring and running linters, starting with Vale.",
"description": "Skills and agents for configuring and running linters.",
"name": "lint",
"source": "./plugins/lint"
}
],
"version": "0.3.0"
"version": "0.3.1"
}

View File

@@ -40,10 +40,10 @@
}
},
{
"description": "Skills and agents for configuring and running linters, starting with Vale.",
"description": "Skills and agents for configuring and running linters.",
"name": "lint",
"source": "./plugins/lint"
}
],
"version": "0.3.0"
"version": "0.3.1"
}

View File

@@ -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
entry: scripts/vale-wrap.sh --config .vale.ini --minAlertLevel=warning
language: script
files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$'
pass_filenames: true

View File

@@ -10,6 +10,7 @@ This repo is the global AI development configuration repository — the authorit
## Setup
- 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`.
## Testing instructions

View File

@@ -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 only replaces the specific pattern-matchable sub-checks named in issue #84 (imperative opener, vague filler, `Use proactively`, generic reference-pointer padding) — body discipline, near-miss exclusion strength, and control calibration stay LLM judgment per the issue's explicit non-goals.
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.
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.
This scope expands per ADR-0013: cherry-picked low-noise `write-good`/`alex` rules into `styles/Kyberforge` (still pending implementation) plus a new sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing `skill-authoring.md`'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: 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).
### 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.

View File

@@ -72,13 +72,22 @@ standing report-only tier for future candidate rules.
## Consequences
- `styles/Kyberforge/` will gain new rule files once the (separate, later) implementation pass
finalizes the exact cherry-picked `write-good`/`alex` rules and any new wording — none are named
by this ADR, since none have been chosen yet.
- A new pre-commit hook (name TBD by the implementer) enforces the 500-line/5,000-token `SKILL.md`
ceiling, sibling to `skill-frontmatter`.
- `styles/KyberforgeTrial/` and `.vale.trial.ini` are deliberately not created — noted here so a
- `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`.
- 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`.
- `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.
- Follow-up work — not part of this ADR — is: syncing and trialing `write-good`/`alex`, cherry-picking
low-noise rules, writing the size-ceiling script and hook, fixing existing corpus violations, and
the atomic commit landing all of it.
- 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 two low-noise rules 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).

View File

@@ -15,7 +15,12 @@ ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drif
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/literally), pleasantries (sure/certainly/no worries/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.
<!-- 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.
Technical terms stay exact. Code blocks unchanged. Errors quoted exact.

View File

@@ -86,7 +86,7 @@ Tool preference:
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
**Perf branch.** For performance regressions, logs rarely reveal the cause. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
## Phase 5 — Fix + regression test
@@ -111,7 +111,7 @@ Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
- [ ] Throwaway prototypes deleted (or moved to an explicitly-marked debug location)
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns
**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.

View File

@@ -55,7 +55,7 @@ Present a numbered list of deepening opportunities. For each candidate:
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Flag it explicitly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"

View File

@@ -1,6 +1,6 @@
---
name: prototype
description: Build a throwaway prototype to flush out a design before committing to it. Routes between two branches — a runnable terminal app for state/business-logic questions, or multiple radically different UI variations toggleable from one route. Use when the user wants to prototype, sanity-check a data model or state machine, mock up a UI, explore design options, or says "prototype this", "let me play with it", "try a few designs".
description: Build a throwaway prototype to flush out a design before committing to it. Routes between two branches — a runnable terminal app for state/business-logic questions, or several radically different UI variations toggleable from one route. Use when the user wants to prototype, sanity-check a data model or state machine, mock up a UI, explore design options, or says "prototype this", "let me play with it", "try a few designs".
---
# Prototype
@@ -11,14 +11,14 @@ A prototype is **throwaway code that answers a question**. The question decides
Identify which question is being answered — from the user's prompt, the surrounding code, or by asking if the user is around:
- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Build a minimal 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 multiple radically different UI variations on a single route, switchable via a URL search param and a floating bottom bar.
- **"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 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
1. **Throwaway from day one, and labeled as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so the context is unambiguous — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure.
1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure.
2. **One command to run.** Whatever the project's existing task runner supports — `pnpm <name>`, `python <path>`, `bun <path>`, etc. The user must be able to start it without thinking.
3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is *checking*, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name.
4. **Skip the polish.** No tests, no error handling beyond what makes the prototype *runnable*, no abstractions. The point is to learn something fast and then delete it.

View File

@@ -91,7 +91,7 @@ Rules:
After all tests pass, look for [refactor candidates](refactoring.md):
- [ ] Extract duplication
- [ ] Deepen modules (move complexity behind narrow interfaces)
- [ ] Deepen modules (move complexity behind simple interfaces)
- [ ] Apply SOLID principles where natural
- [ ] Consider what new code reveals about existing code
- [ ] Run tests after each refactor step

View File

@@ -61,7 +61,7 @@ You are a technical writer that produces documentation by reading code and spec
1. **Identify scope.** User names specific files or sections. If not provided, propose candidates based on the description — wait for explicit approval before reading.
2. **Read and extract.** Read approved files. Extract: public API surface, described behaviour, visible constraints, hidden invariants. Note what the code does NOT explain (caller intent, error handling rationale, hidden side effects).
2. **Read and extract.** Read approved files. Extract: public API surface, described behaviour, visible constraints, non-obvious invariants. Note what the code does NOT explain (caller intent, error handling rationale, non-obvious side effects).
3. **Gap check.** Present extracted behaviour to the user. Ask them to fill only the gaps — what the code does not explain. Log any explicitly deferred gaps. If the user requests to skip this step, log the reason and proceed.

View File

@@ -48,7 +48,7 @@ Default to **GitHub Flow** (simpler, modern, CI/CD-friendly). Fall back to **Git
## Workflow
- [ ] **Determine pattern:** Check git plugin config (`.claude/plugins/git/config.json`, if present — see `config.example.json` in the plugin root for the expected shape) for `branching_pattern` (default: `github-flow`). If not set, inspect repo for `develop` branch or `release/*` branches; if present, assume Gitflow.
- [ ] **Create branch:** Use `git switch -c <branch> <base>`. Base defaults to config's `base_branch` (typically `main` or `develop`). Include intent metadata in branch name or return as structured result (e.g., `{ "branch": "feature/x", "intent": "implement feature X" }`).
- [ ] **Create branch:** Use `git switch -c <branch> <base>`. Base defaults to config's `base_branch` (usually `main` or `develop`). Include intent metadata in branch name or return as structured result (e.g., `{ "branch": "feature/x", "intent": "implement feature X" }`).
- [ ] **Track remote:** If pushing, always use `git push -u origin <branch>` to establish tracking.
- [ ] **Safety checks before destructive ops:** Before delete/force-push/rebase with history loss, check: (1) Is this branch tracking a remote? Warn if yes. (2) Are there unpushed commits? Warn if yes. (3) Does the orchestrator call include `confirm: true`? Fail if not. For humans, prompt interactively.
- [ ] **Return structured results:** Always return branch operations as JSON or structured text: `{ "action": "create", "branch": "feature/x", "base": "main", "tracking": "origin/feature/x", "intent": "implement feature X" }`. Agents need to parse this for subsequent operations.

View File

@@ -66,7 +66,7 @@ Read `.pre-commit-config.yaml` first. Note any stale `rev` values (see **Rev sta
1. Identify the hook entry and its repo block.
2. State what will be removed: hook ID, and whether the parent repo block will also be deleted (if it would have zero hooks remaining). Wait for confirmation.
3. Remove the hook entry. If the repo block now has zero hooks remaining, remove the entire repo block.
4. Write. Run `pre-commit validate-config`. If non-zero: revert the edit, show the error, and stop — do not leave a broken config (removal edits are not safely auto-fixable, unlike a bad new hook block, which can often be corrected in place).
4. Write. Run `pre-commit validate-config`. If non-zero: revert the edit, show the error, and stop — do not leave a broken config (removal edits are not safely auto-fixable, unlike a bad new hook block, which can usually be corrected in place).
### Configuring top-level keys

View File

@@ -72,7 +72,7 @@ Call `list_issues owner: <owner> repo: <repo> state: <"open"|"closed"|"all", def
### create
1. Extract `title` and `body` from conversation context (the most recent task, bug description, or explicit statement). Fall back to an empty body if nothing is available.
2. Run the enrichment sequence in `references/enrichments.md`: infer labels (composing `gitea-labels-milestones`), check for a well-matched open milestone (composing the same skill), and check for a configured default assignee.
2. Run the enrichment sequence in `references/enrichments.md`: infer labels (composing `gitea-labels-milestones`), check for a clearly-fitting open milestone (composing the same skill), and check for a configured default assignee.
3. Call `issue_write method: "create" owner: <owner> repo: <repo> title: <title> body: <body> labels: [<resolved IDs, or omit>] milestone: <resolved ID, or omit> assignees: [<default login, or omit>]`.
4. Fire immediately — no confirmation step for the create itself.

View File

@@ -56,7 +56,7 @@ Never dispatch to `gitea-issues` or `gitea-prs` based on guessing from phrasing
## Step 3 — Route explicit but domain-unclear requests
For requests that name a capability without a clear owning skill, use this index:
For requests that name a capability but not obviously which skill owns it, use this index:
| Skill | Covers |
|---|---|
@@ -67,7 +67,7 @@ For requests that name a capability without a clear owning skill, use this index
| `gitea-files` | Read/write/delete individual files, list a directory, walk the full repo tree. |
| `gitea-releases` | Release and tag CRUD — draft/prerelease flags, release notes, semver tags. |
If a request unambiguously names one of these (e.g. "create a milestone" → `gitea-labels-milestones`, "read this file from the repo" → `gitea-files`), invoke that skill directly rather than routing through here. Use this table only when the user or an upstream agent is unsure which skill applies.
If a request clearly names one of these (e.g. "create a milestone" → `gitea-labels-milestones`, "read this file from the repo" → `gitea-files`), invoke that skill directly rather than routing through here. Use this table only when the user or an upstream agent is unsure which skill applies.
## Step 4 — Report

View File

@@ -17,5 +17,4 @@ Upstream reference material gathered during skill authoring. Not shipped with th
|------|---------|
| `research/docs/agentskillsio/` | agentskills.io spec, skill authoring, description optimization, eval design, scripts |
| `research/docs/agentsmd/` | agents.md format spec and cross-tool configuration reference |
| `research/docs/vale/` | Vale (vale.sh) prose linter — config, styles/rules/checks model, CLI reference, installation |
| `research/examples/skill-write/` | Upstream skill examples reviewed when authoring skill-write and skill-audit |

View File

@@ -36,12 +36,12 @@ metadata:
```bash
bash scripts/validate.sh <path-to-agent-file>
bash scripts/validate-provenance.sh <path-to-agent-file>
scripts/vale-wrap.sh --config .vale.ini <path-to-cc-file> <path-to-copilot-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`.
Run `vale-wrap.sh` from the repo root against both files of the pair (not just the one passed in), using `.vale.ini`. `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`, 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.
`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.
@@ -62,6 +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
**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.

View File

@@ -32,7 +32,7 @@ This step always runs inline, in the current conversation — grilling is intera
## Step 2 — Classify the artifact type
Match the grilled intent against exactly one row (or more than one, if the intent genuinely spans multiple):
Match the grilled intent against exactly one row (or more than one, if the intent genuinely spans several):
| Intent | Artifact type | Route to |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------| ---------------------------------|

View File

@@ -182,7 +182,7 @@ Read `.claude-plugin/marketplace.json`. Identify the entry to remove. State the
### Step 2 — HITL gate
State the following before proceeding:
State clearly before proceeding:
> "I will remove the `<name>` entry from both `.claude-plugin/marketplace.json` and `.github/plugin/marketplace.json`. This does not delete the plugin files. Confirm?"

View File

@@ -34,14 +34,14 @@ metadata:
```bash
bash scripts/validate.sh <skill-dir>
bash scripts/validate-provenance.sh <skill-dir>
scripts/vale-wrap.sh --config .vale.ini <skill-dir>/SKILL.md
"$(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` runs from the repo root 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`, 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.
## Step 2 — Read all skill files
@@ -70,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.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.
If uncertain whether a sentence is padding or whether a control decision is correctly calibrated, read `references/body-discipline.md`.
### Patterns

View File

@@ -114,7 +114,7 @@ Rename the placeholder section heading to one that fits the skill's structure
Ask of every sentence: "Would the agent get this wrong without it?" Cut anything that answers "no."
**Include:**
- Sequences or ordering constraints that aren't self-evident — the agent may skip or reorder steps without this
- Non-obvious sequences or ordering constraints — the agent may skip or reorder steps without this
- Domain conventions the agent cannot infer from general knowledge — this is the core value a skill adds
- One default per decision point, plus one escape hatch — never a menu; menus cause the agent to pause or pick arbitrarily
- Gotchas — facts that defy reasonable assumptions; the agent will get these wrong every time without them

View File

@@ -13,5 +13,5 @@
],
"license": "MIT",
"name": "lint",
"version": "1.1.1"
"version": "1.1.2"
}

View File

@@ -5,6 +5,9 @@ description: Runs a linter sweep over a target file or directory scope and repor
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.

View File

@@ -5,6 +5,9 @@ description: Runs a linter sweep over a target file or directory scope and repor
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.

View File

@@ -18,5 +18,5 @@
"skills": [
"skills/"
],
"version": "1.1.1"
"version": "1.1.2"
}

View File

@@ -11,7 +11,7 @@ description: >
project has no .vale.ini yet, or needs styles installed/configured — that's the
vale-config skill.
metadata:
version: "0.1.0"
version: "0.1.1"
category: lint
source_keys:
- context7-websites-vale-sh
@@ -22,10 +22,11 @@ metadata:
- 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 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.
- 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:
Default invocation (when the target repo has no documented Vale wrapper — see Gotchas):
```bash
vale <path-or-glob>

View File

@@ -69,6 +69,16 @@ repos:
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

9
plugins/lint/sources.md Normal file
View 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`

View File

@@ -2,32 +2,38 @@
set -euo pipefail
# Enforces agentskills.io's skill-authoring.md guidance: keep SKILL.md under 500
# lines and 5,000 tokens, so the full body doesn't crowd out conversation
# 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.
#
# Token counts aren't computed exactly here — word count (`wc -w`) is used as
# a proxy. For English prose this typically runs somewhat below true BPE token
# counts, so a 5,000-word file is already at or past 5,000 tokens in practice;
# treat this as a conservative, cheap approximation, not an exact measure.
# 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=5000
MAX_WORDS=2900
FAIL=0
for f in "$@"; do
[[ -f "$f" ]] || continue
lines=$(wc -l < "$f")
if (( lines > MAX_LINES )); then
echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
# 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, at or over 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-token ceiling (agentskills.io skill-authoring.md)" >&2
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

View File

@@ -29,15 +29,26 @@ for arg in "$@"; do
config_next=true
continue
fi
if [[ "$arg" == --config=* ]]; then
cfg="${arg#--config=}"
if [[ "$cfg" == /* ]]; then
vale_args+=("--config=$cfg")
else
vale_args+=("--config=$repo_root/$cfg")
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
exec vale "${vale_args[@]}"
exec vale "${vale_args[@]}" < /dev/null
fi
tmpdir="$(mktemp -d)"
@@ -47,7 +58,6 @@ for rel in "${files[@]}"; do
dest="$tmpdir/$rel"
mkdir -p "$(dirname "$dest")"
python3 - "$repo_root/$rel" "$dest" <<'PYTHON'
import json
import re
import sys
@@ -58,17 +68,52 @@ with open(src) as fh:
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
if fm_match:
fm = fm_match.group(2)
desc_m = re.search(r'^description:\s*([>|][+-]?)\n((?:[ \t]+.+\n?)+)', fm, re.MULTILINE)
if desc_m and desc_m.group(2).count('\n') >= 2:
raw = desc_m.group(2)
flat = re.sub(r'\s+', ' ', raw).strip()
# JSON string escaping is a valid subset of YAML double-quoted scalar
# escaping, so this is always a well-formed YAML value regardless of
# colons, quotes, or backslashes in the description text.
flat_q = json.dumps(flat)
pad = '\n' * raw.count('\n')
new_fm = fm[:desc_m.start()] + f'description: {flat_q}\n{pad}' + fm[desc_m.end():]
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
# 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)

View File

@@ -1,7 +1,7 @@
extends: existence
message: "Don't start a sentence with '%s' — name the subject directly"
level: warning
scope: text
scope: sentence
ignorecase: false
raw:
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'
- '^There\s(is|are)\b'

View File

@@ -4,33 +4,16 @@ level: warning
scope: text
ignorecase: true
tokens:
- clearly
- obviously
- obvious
- simply
- simple
- easily
- easy
- of course
- everyone knows
- completely
- exceedingly
- excellent
- extremely
- fairly
- huge
- interestingly
- largely
- mostly
- of course
- quite
- relatively
- remarkably
- several
- significantly
- substantially
- surprisingly
- tiny
- usually
- various
- vast
- very

View File

@@ -60,6 +60,102 @@ 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 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.
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))")"
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))"
elif "$SCRIPT" "$AT_LINES"; then
pass "file at $((MAX_LINES - 1)) lines exits 0"
else
fail "file at $((MAX_LINES - 1)) lines should have exited 0"
fi
echo ""
echo "--- fails a file at exactly the $MAX_LINES-line boundary ---"
OVER_LINES="$(make_line_fixture over-line-limit "$MAX_LINES")"
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"
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)"
else
pass "file at exactly $MAX_LINES 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 ]]

View File

@@ -78,6 +78,210 @@ 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 (cd "$FIXTURE4" && bash "$SCRIPT" --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=$(cd "$FIXTURE5" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1)
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 (cd "$FIXTURE6" && bash "$SCRIPT" --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=$(cd "$FIXTURE7" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1)
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. --config=<path> (equals form) resolves the same as the two-argv form ---
echo ""
echo "--- --config=<path> equals form resolves from a subdirectory like the two-argv form ---"
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"
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)
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"
else
fail "--config=<path> equals form did not resolve the same as the two-argv form"
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 (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"
else
fail "an absolute path under repo_root 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=$(cd "$FIXTURE11" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
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 ]]