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:
@@ -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"
|
||||
}
|
||||
|
||||
4
.github/plugin/marketplace.json
vendored
4
.github/plugin/marketplace.json
vendored
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,7 @@ Fall back to raw shell only when no skill covers it.
|
||||
## Setup and testing
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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?"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------| ---------------------------------|
|
||||
|
||||
@@ -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?"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"name": "lint",
|
||||
"version": "1.1.1"
|
||||
"version": "1.1.2"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"skills": [
|
||||
"skills/"
|
||||
],
|
||||
"version": "1.1.1"
|
||||
"version": "1.1.2"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
9
plugins/lint/sources.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Sources
|
||||
|
||||
## context7-websites-vale-sh
|
||||
|
||||
- **URL:** context7:/websites/vale_sh
|
||||
- **Research doc:** plugins/lint/docs/research/docs/vale/sources.md
|
||||
- **Description:** Official Vale documentation site (vale.sh) indexed by Context7 — informed `lint-runner`'s report-only design: normalizing findings to a `file, line, rule/check, severity, message` shape and deferring linter-specific invocation to `<linter>-config`/`<linter>-run` skills, drawing on Vale's CLI output format and exit-code conventions.
|
||||
- **Contributing files:** agents/lint-runner.md, agents/lint-runner.agent.md
|
||||
- **Status:** `extracted`
|
||||
@@ -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
|
||||
|
||||
@@ -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,16 +68,51 @@ 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)
|
||||
# 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()
|
||||
# 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)
|
||||
# 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')
|
||||
new_fm = fm[:desc_m.start()] + f'description: {flat_q}\n{pad}' + fm[desc_m.end():]
|
||||
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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ]]
|
||||
|
||||
@@ -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 ]]
|
||||
|
||||
Reference in New Issue
Block a user