diff --git a/README.md b/README.md index d5baa2d4d..c4ae90865 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ user opts in with `/plugin enable`; an existing install is never flipped by cata - [`bash-format`](plugins/bash-format) — Auto-format and lint shell scripts on edit via shfmt + ShellCheck, using the consuming repo's own .editorconfig and .shellcheckrc. - [`biome-format`](plugins/biome-format) — Auto-format and lint JS/TS/JSX/JSON on edit via Biome, only when a biome.json governs the repo — using the consuming repo's own Biome config. - [`ruff-format`](plugins/ruff-format) — Auto-format and lint Python on edit via Ruff, only when a Ruff config governs the repo — using the consuming repo's own Ruff config. -- [`typos-format`](plugins/typos-format) — Auto-fix spelling typos on edit via typos-cli, only when a typos config governs the repo — using the consuming repo's own typos configuration. +- [`typos-format`](plugins/typos-format) — Auto-fix spelling typos on edit via typos-cli, unconditionally — honoring the consuming repo's own typos configuration when one is present. - [`eol-normalizer`](plugins/eol-normalizer) — Normalize a written file's working-tree line endings to its .gitattributes eol value on edit — symmetric CRLF/LF driven by git check-attr, advisory and never blocking. - [`powershell-format`](plugins/powershell-format) — Auto-format and lint PowerShell on edit via PSScriptAnalyzer, only when a PSScriptAnalyzerSettings.psd1 governs the repo — using the consuming repo's own analyzer settings. - [`actionlint`](plugins/actionlint) — Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context. diff --git a/plugins/typos-format/.claude-plugin/plugin.json b/plugins/typos-format/.claude-plugin/plugin.json index 91c08b5f0..d8e0ef0ad 100644 --- a/plugins/typos-format/.claude-plugin/plugin.json +++ b/plugins/typos-format/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "typos-format", - "version": "0.1.0", - "description": "Auto-fix spelling typos on edit via typos-cli, only when a typos config governs the repo — using the consuming repo's own typos configuration.", + "version": "0.2.0", + "description": "Auto-fix spelling typos on edit via typos-cli, unconditionally — honoring the consuming repo's own typos configuration when one is present.", "author": { "name": "Melodic Software", "email": "info@melodicsoftware.com" @@ -19,7 +19,7 @@ "typos_format_enabled": { "type": "boolean", "title": "typos-format hook", - "description": "Run typos --write-changes on edit of a file, when a typos config governs the repo", + "description": "Run typos --write-changes on edit of any file, unconditionally", "default": true } } diff --git a/plugins/typos-format/CHANGELOG.md b/plugins/typos-format/CHANGELOG.md index b509715e6..f5047fb8a 100644 --- a/plugins/typos-format/CHANGELOG.md +++ b/plugins/typos-format/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the `typos-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.2.0] + +### Changed + +- **Removed the opt-in config-gate.** The hook now runs `typos --write-changes` + unconditionally on every `Write`/`Edit`, matching `markdown-format`'s existing + unconditional pattern — typos ships a built-in spelling dictionary and needs + no configuration to be useful. Previously the hook silently no-op'd on any + repo without a hand-authored `typos.toml`/`_typos.toml`/`.typos.toml`/ + `Cargo.toml`/`pyproject.toml`, defeating the plugin's zero-config auto-fix + purpose on exactly the repos it was meant to help. A consumer typos config, + when present, is still discovered and honored automatically by typos itself + (allowlist/exclude) — this hook never re-implemented that discovery and + still doesn't; only the activation gate is removed. + ## [0.1.0] ### Added diff --git a/plugins/typos-format/README.md b/plugins/typos-format/README.md index ca467c6d7..ec873bf60 100644 --- a/plugins/typos-format/README.md +++ b/plugins/typos-format/README.md @@ -6,22 +6,22 @@ On every `Write` or `Edit` it runs [typos](https://github.com/crate-ci/typos)'s Claude as advisory context — including remediation guidance for allowlisting a false positive. -It uses **your repository's own typos configuration**. It ships no rules of -its own and runs only when your repo has opted into typos. +It ships no rules of its own and runs unconditionally, using typos' built-in +spelling dictionary. If your repository has its own typos configuration +(`typos.toml`, `_typos.toml`, `.typos.toml`, `Cargo.toml` with +`[workspace.metadata.typos]`/`[package.metadata.typos]`, or `pyproject.toml` +with `[tool.typos]`), typos discovers and honors it automatically — no +opt-in required. ## Behavior -- **Opt-in on a typos config.** typos runs **only when a `typos.toml`, - `_typos.toml`, `.typos.toml`, `Cargo.toml` (with - `[workspace.metadata.typos]`/`[package.metadata.typos]`), or `pyproject.toml` - with a `[tool.typos]` section governs the edited file**, found by walking up - from the file to the repository root, in that precedence order — the same - discovery typos itself uses. A repo without a typos config is left untouched - rather than checked against typos' built-in dictionary, so the plugin never - imposes a check you did not choose. +- **Runs on every edit, zero-config or not.** typos ships a built-in spelling + dictionary and needs no configuration to be useful, so this hook never gates + on a consumer typos config existing. When a config IS present, typos' own + file-anchored discovery still finds and applies it (allowlist/exclude), in + its documented precedence order — this plugin never re-implements that walk. - **No extension filter.** Unlike sibling formatter plugins (Ruff, Markdown), - typos is language-agnostic — it runs on any edited file, gated only by the - config opt-in above. + typos is language-agnostic — it runs on any edited file. - **Fix in place.** `typos --write-changes` applies every correction it has confidence in. Residual findings — an entry with no known correction (e.g. a blank-correction `extend-words` entry marking a term "disallowed") — @@ -59,10 +59,8 @@ locking/ordering primitive exists in Claude Code today. per-repo dependency-manager convention — it is a standalone Rust binary, installed at the machine level (cargo, Homebrew, Conda, pacman, or a pre-built binary). typos is never downloaded on the fly; if it is not - present while a typos config governs the repo, the hook skips with a - visible once-per-session notice. [Install typos](https://github.com/crate-ci/typos#install). -- A **typos config** (`typos.toml`, `_typos.toml`, `.typos.toml`, or an - equivalent `Cargo.toml`/`pyproject.toml` section) in the repo — the opt-in. + present, the hook skips with a visible once-per-session notice. + [Install typos](https://github.com/crate-ci/typos#install). The hook itself runs on Bash 3.2+. Telemetry timing uses `EPOCHREALTIME` (Bash 5.0+); on older bash the telemetry envelope is skipped while typo diff --git a/plugins/typos-format/hooks/typos-format.sh b/plugins/typos-format/hooks/typos-format.sh index c994c5f96..17ca48429 100755 --- a/plugins/typos-format/hooks/typos-format.sh +++ b/plugins/typos-format/hooks/typos-format.sh @@ -8,15 +8,16 @@ # entry) findings surface via additionalContext but never block the edit. A # commit hook or CI is the hard gate. # -# Opt-in: typos runs ONLY when a typos configuration governs the edited file — -# found by walking up from the file to the repo root, checking (in precedence -# order, per crate-ci/typos' own docs at -# https://github.com/crate-ci/typos/blob/master/docs/reference.md) -# typos.toml, _typos.toml, .typos.toml, Cargo.toml (with -# [workspace.metadata.typos] or [package.metadata.typos]), or pyproject.toml -# (with [tool.typos]). A repo that has not adopted a typos config is left -# untouched rather than checked against typos' built-in defaults, so the -# plugin never imposes a style it did not choose. The typos binary is resolved +# Unconditional: typos ships a built-in spelling dictionary and runs with zero +# configuration, so this hook runs on every edit regardless of whether the +# repo has adopted a typos config — matching the sibling markdown-format +# hook's unconditional pattern. When a config IS present (typos.toml, +# _typos.toml, .typos.toml, Cargo.toml with [workspace.metadata.typos] or +# [package.metadata.typos], or pyproject.toml with [tool.typos], per +# crate-ci/typos' own docs at +# https://github.com/crate-ci/typos/blob/master/docs/reference.md), typos' +# own file-anchored discovery finds and honors it (allowlist/exclude) — this +# hook never re-implements that walk itself. The typos binary is resolved # from PATH only — never downloaded (typos is a standalone Rust binary with no # per-repo dependency-manager convention, unlike ruff's .venv). # @@ -82,7 +83,7 @@ FILE=$(printf '%s' "$INPUT" | hook::read_file_path) || exit 0 TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) -# Resolve repo root early — used to bound the config opt-in walk and to compute +# Resolve repo root early — used as the CWD typos runs in and to compute # the schema-required repo-relative path in data.file. REPO_ROOT="$(hook::repo_root "$(dirname "$FILE")")" # Repo-relative path: schema requires "relative to the consuming repo root". @@ -122,52 +123,18 @@ emit_skipped() { exit 0 } -# Resolve the file's directory in `pwd` form once. Both walks below start here. -FILE_DIR_POSIX="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || FILE_DIR_POSIX="" root="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || root="" -# Consumer opt-in: a typos configuration that governs the edited file. Walk up -# from the file's directory to the repo root, stopping at the FIRST config -# found — typos itself resolves the closest config per-directory (checking all -# recognized names together before ascending, per crate-ci/typos' -# Config::from_dir), so the closest hit is exactly the config that will govern -# the run. Same-directory precedence mirrors typos' own documented order: -# typos.toml > _typos.toml > .typos.toml > Cargo.toml (workspace/package -# metadata.typos) > pyproject.toml ([tool.typos]). Absence of any config is the -# opt-out: the file is left untouched. -CONFIG_FOUND="" -dir="$FILE_DIR_POSIX" -while [[ -n "$dir" ]]; do - for name in typos.toml _typos.toml .typos.toml; do - [[ -f "$dir/$name" ]] && CONFIG_FOUND="$dir/$name" && break - done - if [[ -z "$CONFIG_FOUND" && -f "$dir/Cargo.toml" ]] && - grep -qE '^[[:space:]]*\[(workspace|package)\.metadata\.typos(\]|[.])' "$dir/Cargo.toml" 2>/dev/null; then - CONFIG_FOUND="$dir/Cargo.toml" - fi - if [[ -z "$CONFIG_FOUND" && -f "$dir/pyproject.toml" ]] && - grep -qE '^[[:space:]]*\[tool\.typos(\]|[.])' "$dir/pyproject.toml" 2>/dev/null; then - CONFIG_FOUND="$dir/pyproject.toml" - fi - [[ -n "$CONFIG_FOUND" ]] && break - [[ -n "$root" && "$dir" == "$root" ]] && break - parent="$(dirname "$dir")" - [[ "$parent" == "$dir" ]] && break # reached filesystem root - dir="$parent" -done - -[[ -n "$CONFIG_FOUND" ]] || emit_skipped - # Resolve the typos binary from PATH — never downloaded (typos is a standalone # Rust binary; no per-repo dependency-manager convention exists for it, unlike # ruff's .venv or markdownlint's node_modules). TYPOS_BIN="$(command -v typos 2>/dev/null)" || TYPOS_BIN="" -# The repo opted in via a typos config but no binary is available → visible -# once-per-session skip notice, not a silent gap (dim-9 doctrine). +# No binary available → visible once-per-session skip notice, not a silent gap +# (dim-9 doctrine). if [[ -z "$TYPOS_BIN" ]]; then if hook::notice_once "typos-format-typos" "$INPUT"; then - hook::emit_skip_notice PostToolUse "typos-format: a typos config governs this repo but no 'typos' binary was found on PATH — spell-check skipped for this session. Install: https://github.com/crate-ci/typos#install" + hook::emit_skip_notice PostToolUse "typos-format: no 'typos' binary was found on PATH — spell-check skipped for this session. Install: https://github.com/crate-ci/typos#install" fi emit_skipped fi @@ -178,11 +145,11 @@ fi # line, not the process CWD (verified empirically: running from the repo root # with a relative subdirectory path still discovers and honors that # subdirectory's own config) — so running from repo root here does not change -# which config governs; it matches the walk above regardless of nesting depth. -# Falls back to the absolute path when the repo root did not resolve or the -# file is outside it. +# which config governs, regardless of nesting depth. Falls back to the +# absolute path and the file's own directory when the repo root did not +# resolve or the file is outside it. TYPOS_ARG="$FILE" -RUN_DIR="${root:-$FILE_DIR_POSIX}" +RUN_DIR="${root:-$(dirname "$FILE")}" if [[ -n "$root" && -n "$FILE_REL" && "$FILE_REL" != "$FILE" ]]; then TYPOS_ARG="$FILE_REL" fi @@ -240,7 +207,7 @@ fi # judgment was made. Surface the diagnostic via additionalContext (NOT stderr — # an advisory hook's exit-0 stderr can trip a false "Hook Error" label). Record # as "skipped" (typos never ran to judgment), the same status as the -# no-config / no-binary paths. +# no-binary path. hook::ctx_reset hook::ctx_append "typos-format: typos failed for $(basename "$FILE") (no diagnostics; tool break, not a finding):" while IFS= read -r line; do diff --git a/plugins/typos-format/hooks/typos-format.test.sh b/plugins/typos-format/hooks/typos-format.test.sh index a868cd8a2..43aafd218 100755 --- a/plugins/typos-format/hooks/typos-format.test.sh +++ b/plugins/typos-format/hooks/typos-format.test.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash # Black-box contract test for typos-format.sh (the typos-format plugin hook). # -# Proves WIRING: the hook fires on any file (no extension filter), gates on a -# consumer typos config (present -> run, absent -> leave bytes untouched), -# honors typos.toml > _typos.toml > .typos.toml > Cargo.toml > pyproject.toml -# precedence, applies typos' safe corrections in place, surfaces residual -# (unfixable) findings via additionalContext with remediation guidance, -# honors the kill switch, and emits a schema-valid telemetry envelope. +# Proves WIRING: the hook fires on any file (no extension filter) UNCONDITIONALLY +# — with or without a consumer typos config — applies typos' safe corrections in +# place, honors typos' own typos.toml > _typos.toml > .typos.toml > Cargo.toml > +# pyproject.toml precedence when a config IS present, surfaces residual +# (unfixable) findings via additionalContext with remediation guidance, honors +# the kill switch, and emits a schema-valid telemetry envelope. # # Self-contained: builds throwaway git repos with runtime-generated fixtures. # The hook is invoked from an UNRELATED cwd so any reliance on the caller's # own working directory would surface (typos resolves config relative to the -# target path passed on the command line, not the process CWD — Case 3b below +# target path passed on the command line, not the process CWD — Case 3 below # locks this in — so the hook's own cd to the repo root does not change which # config governs). # @@ -113,40 +113,26 @@ run_hook_env() { ) } -# --- Case 1: opt-in gate OFF (no typos config) -> file left untouched ------- -# A repo with typos installed but NO config must not have its files checked -# against typos' built-in dictionary. +# --- Case 1: no typos config anywhere -> hook still fixes unconditionally --- +# typos ships a built-in spelling dictionary and runs with zero configuration. +# A repo that has never adopted a typos config must still get its typos fixed +# — the hook must not gate on a consumer config existing. REPO_NO="$WORK/no-config" new_typos_repo "$REPO_NO" NO_CONFIG printf 'this has teh typo\n' >"$REPO_NO/doc.txt" # spellchecker:disable-line -BEFORE_NO="$(cat "$REPO_NO/doc.txt")" OUT=$(run_hook "$REPO_NO/doc.txt") RC=$? -if [[ $RC -eq 0 && -z "$OUT" ]]; then ok "gate OFF (no config) -> exit 0, silent"; else fail "gate OFF not silent (rc=$RC out=$OUT)"; fi -if [[ "$(cat "$REPO_NO/doc.txt")" == "$BEFORE_NO" ]]; then ok "gate OFF -> file left untouched"; else fail "gate OFF -> file was rewritten"; fi - -# --- Case 1b: pyproject.toml WITHOUT [tool.typos] does NOT opt in ----------- -REPO_PP="$WORK/pyproject-plain" -new_typos_repo "$REPO_PP" NO_CONFIG -printf '[project]\nname = "t"\n' >"$REPO_PP/pyproject.toml" -printf 'this has teh typo\n' >"$REPO_PP/plain.txt" # spellchecker:disable-line -BEFORE_PP="$(cat "$REPO_PP/plain.txt")" -OUT=$(run_hook "$REPO_PP/plain.txt") -RC=$? -if [[ $RC -eq 0 && -z "$OUT" ]]; then ok "pyproject without [tool.typos] -> exit 0, silent (no opt-in)"; else fail "plain pyproject opted in (rc=$RC out=$OUT)"; fi -if [[ "$(cat "$REPO_PP/plain.txt")" == "$BEFORE_PP" ]]; then ok "pyproject without [tool.typos] -> file left untouched"; else fail "plain pyproject -> file was rewritten"; fi - -# --- Case 1c: pyproject.toml WITH [tool.typos] opts in ---------------------- -REPO_PT="$WORK/pyproject-typos" -new_typos_repo "$REPO_PT" NO_CONFIG -printf '[project]\nname = "t"\n\n[tool.typos]\n' >"$REPO_PT/pyproject.toml" -printf 'this has teh typo\n' >"$REPO_PT/opt.txt" # spellchecker:disable-line -OUT=$(run_hook "$REPO_PT/opt.txt") -RC=$? -if [[ $RC -eq 0 ]]; then ok "pyproject with [tool.typos] -> exit 0"; else fail "pyproject opt-in exit $RC"; fi -if grep -q ' the ' "$REPO_PT/opt.txt"; then ok "pyproject with [tool.typos] -> file fixed"; else fail "pyproject opt-in -> not fixed: $(cat "$REPO_PT/opt.txt")"; fi +if [[ $RC -eq 0 ]]; then ok "no config anywhere -> exit 0"; else fail "no config anywhere exit $RC"; fi +if grep -q ' the ' "$REPO_NO/doc.txt"; then + ok "no config anywhere -> hook still fixes using typos' built-in dictionary" +else + fail "no config anywhere -> file not fixed (hook incorrectly gated): $(cat "$REPO_NO/doc.txt")" +fi -# --- Case 1d: typos.toml takes precedence over _typos.toml in the same dir -- +# --- Case 2: typos.toml takes precedence over _typos.toml in the same dir --- +# The hook does not walk for a config itself; it passes the target path and +# lets typos resolve config on its own. This proves that resolution — and its +# documented same-directory precedence — still applies automatically. REPO_PREC="$WORK/precedence" new_typos_repo "$REPO_PREC" NO_CONFIG printf '[default.extend-words]\nfooone = "correctone"\n' >"$REPO_PREC/_typos.toml" @@ -159,7 +145,7 @@ else fail "config precedence wrong: $(cat "$REPO_PREC/p.txt")" fi -# --- Case 2: gate ON + clean file -> exit 0, empty stdout ------------------- +# --- Case 3: config present + clean file -> exit 0, empty stdout ------------ REPO="$WORK/consumer" new_typos_repo "$REPO" printf 'this is a clean document\n' >"$REPO/clean.txt" @@ -168,19 +154,19 @@ RC=$? if [[ $RC -eq 0 ]]; then ok "clean file -> exit 0"; else fail "clean file exit $RC"; fi if [[ -z "$OUT" ]]; then ok "clean file -> empty stdout"; else fail "clean file stdout not empty: $OUT"; fi -# --- Case 3: gate ON + fixable typo -> autofixed in place ------------------- +# --- Case 4: config present + fixable typo -> autofixed in place ------------ mkdir -p "$REPO/src" printf 'this has teh typo\n' >"$REPO/src/fix.txt" # spellchecker:disable-line OUT=$(run_hook "$REPO/src/fix.txt") RC=$? if [[ $RC -eq 0 ]]; then ok "fixable typo -> exit 0 (advisory)"; else fail "fixable typo exit $RC"; fi if grep -q ' the ' "$REPO/src/fix.txt"; then - ok "gate ON (subdir file) -> typos autofixed" + ok "config present (subdir file) -> typos autofixed" else - fail "gate ON -> file not fixed: $(cat "$REPO/src/fix.txt")" + fail "config present -> file not fixed: $(cat "$REPO/src/fix.txt")" fi -# --- Case 3b: config nested BELOW repo root (root itself has no config) ----- +# --- Case 5: config nested BELOW repo root (root itself has no config) ------ # The hook runs typos from the repo root (RUN_DIR), never from the config's # own directory. This proves that does not matter: typos resolves config # relative to the target path passed on the command line, not the process @@ -197,7 +183,7 @@ else fail "nested config not applied: $(cat "$REPO_NEST/packages/pkg/file.txt")" fi -# --- Case 4: gate ON + unfixable finding -> advisory context with remediation +# --- Case 6: unfixable finding -> advisory context with remediation --------- mkdir -p "$REPO/lib" printf 'this has a disallowme term\n' >"$REPO/lib/lint.txt" OUT=$(run_hook "$REPO/lib/lint.txt") @@ -219,7 +205,7 @@ else fail "unfixable finding -> no additionalContext JSON: $OUT" fi -# --- Case 4b: fixable + unfixable together -> fixable applied, unfixable reported +# --- Case 6b: fixable + unfixable together -> fixable applied, unfixable reported printf 'this has teh typo and a disallowme term\n' >"$REPO/mixed.txt" # spellchecker:disable-line OUT=$(run_hook "$REPO/mixed.txt") if grep -q ' the ' "$REPO/mixed.txt" && grep -q 'disallowme' "$REPO/mixed.txt"; then @@ -235,7 +221,7 @@ else fail "mixed case: reporting wrong: $CTX_MIXED" fi -# --- Case 5: config excludes the edited file -> untouched, no nag ------------ +# --- Case 7: config excludes the edited file -> untouched, no nag ------------ # --force-exclude honors the config's own exclude/extend-exclude even for an # explicitly-passed path. An excluded file must be left untouched with no # advisory noise, even though it contains a fixable typo. @@ -250,7 +236,7 @@ RC=$? if [[ $RC -eq 0 && -z "$OUT" ]]; then ok "excluded file -> exit 0, silent (no nag)"; else fail "excluded file not silent (rc=$RC out=$OUT)"; fi if [[ "$(cat "$REPO_EX/gen/g.txt")" == "$BEFORE_EX" ]]; then ok "excluded file -> left untouched (respects config exclude)"; else fail "excluded file -> was rewritten"; fi -# --- Case 6: kill switch bypasses hook --------------------------------------- +# --- Case 8: kill switch bypasses hook --------------------------------------- printf 'this has teh typo\n' >"$REPO/kill.txt" # spellchecker:disable-line BEFORE_K="$(cat "$REPO/kill.txt")" OUT=$(run_hook_env "$REPO/kill.txt" PATH="$(dirname "$REAL_TYPOS"):$PATH" CLAUDE_PLUGIN_OPTION_TYPOS_FORMAT_ENABLED=false) @@ -302,25 +288,24 @@ else fi rm -f "$TEL" -# --- Stub sink + gate OFF -> status skipped ----------------------------------- +# --- Stub sink + no config, fully-fixed file -> status ok, no residual findings printf 'this has teh typo\n' >"$REPO_NO/tel2.txt" # spellchecker:disable-line TELS="$(mktemp)" SINKS="$(make_sink "cat >\"$TELS\"")" run_hook_env "$REPO_NO/tel2.txt" PATH="$(dirname "$REAL_TYPOS"):$PATH" CLAUDE_PLUGIN_OPTION_TYPOS_FORMAT_ENABLED=true HOOK_TELEMETRY_SINK="$SINKS" >/dev/null wait_for_sink "$TELS" if [[ -s "$TELS" ]]; then - if [[ "$(jq -r '.status' "$TELS")" == "skipped" ]]; then ok "telemetry/gate-off: status skipped"; else fail "telemetry/gate-off: status=$(jq -r '.status' "$TELS")"; fi - if [[ "$(jq '.data.findings | length' "$TELS")" -eq 0 ]]; then ok "telemetry/gate-off: findings empty array"; else fail "telemetry/gate-off: findings not empty"; fi + if [[ "$(jq -r '.status' "$TELS")" == "ok" ]]; then ok "telemetry/no-config: status ok (unconditional run)"; else fail "telemetry/no-config: status=$(jq -r '.status' "$TELS")"; fi + if [[ "$(jq '.data.findings | length' "$TELS")" -eq 0 ]]; then ok "telemetry/no-config: findings empty array (fully fixed)"; else fail "telemetry/no-config: findings not empty"; fi else - fail "telemetry/gate-off: no envelope written" + fail "telemetry/no-config: no envelope written" fi rm -f "$TELS" # --- Missing-tool visibility (dim-9 doctrine) -------------------------------- -# Fake-bin dir of exec wrappers (no typos): a repo with a governing typos -# config but no binary must produce a visible once-per-session skip notice on -# both channels, silent on the second run. jq removal then exercises the -# input-parsing gate. +# Fake-bin dir of exec wrappers (no typos): with no 'typos' binary on PATH the +# hook must produce a visible once-per-session skip notice on both channels, +# silent on the second run. jq removal then exercises the input-parsing gate. FAKEBIN="$(mktemp -d -p "$WORK" fakebin.XXXXXX)" for t in bash jq git dirname basename cat env printf mktemp mkdir find tr awk grep sed uname sleep cygpath realpath readlink; do real_t="$(command -v "$t" 2>/dev/null)" || continue @@ -345,8 +330,8 @@ run_nt() { OUT_NT=$(run_nt) RC_NT=$? if [[ $RC_NT -eq 0 ]]; then ok "typos-absent -> exit 0"; else fail "typos-absent exit $RC_NT"; fi -if jq -e '(.systemMessage | contains("typos")) and (.hookSpecificOutput.additionalContext | contains("typos config"))' <<<"$OUT_NT" >/dev/null 2>&1; then - ok "typos-absent with governing config -> visible notice on both channels" +if jq -e '(.systemMessage | contains("typos")) and (.hookSpecificOutput.additionalContext | contains("PATH"))' <<<"$OUT_NT" >/dev/null 2>&1; then + ok "typos-absent -> visible notice on both channels" else fail "typos-absent: notice missing or malformed: $OUT_NT" fi diff --git a/plugins/typos-format/skills/setup/SKILL.md b/plugins/typos-format/skills/setup/SKILL.md index 7bdea248c..048ae35f0 100644 --- a/plugins/typos-format/skills/setup/SKILL.md +++ b/plugins/typos-format/skills/setup/SKILL.md @@ -39,16 +39,14 @@ restores the FAIL semantics. once-per-session notice instead of fixing typos. 3. **typos binary** — `command -v typos` (the hook resolves PATH only — no `.venv`-style per-repo convention). Report the resolved path and `typos --version` output when found. - FAIL when absent while a typos config governs the repo; the hook then emits a visible - once-per-session skip notice instead of running. -4. **Consumer typos config** — mirror the hook's opt-in walk: it stops at the FIRST - (closest) governing config found walking from the edited file's directory up to the repo - root, checking (in precedence order) `typos.toml`, `_typos.toml`, `.typos.toml`, a + FAIL when absent; the hook then emits a visible once-per-session skip notice instead of + running. +4. **Consumer typos config (informational only)** — the hook runs unconditionally and never + gates on a config existing; typos resolves its own governing config (if any) directly from + the file path it is given. Report whether a `typos.toml`, `_typos.toml`, `.typos.toml`, a `Cargo.toml` with `[workspace.metadata.typos]`/`[package.metadata.typos]`, or a - `pyproject.toml` with `[tool.typos]` (read the hook for the exact names and the section - test — its test is the authority). Report the governing config the walk discovers, or - INFO that none exists — absence is the opt-out by design, so the plugin is inert (INFO, - not FAIL), matching the README's "ships no rules of its own" stance. + `pyproject.toml` with `[tool.typos]` governs the repo, purely as INFO — its presence or + absence never changes whether the hook runs. 5. **Hook toggle** — report the effective `typos_format_enabled` value: `${user_config.typos_format_enabled}` (unexpanded or empty means default `true`). 6. **Hook registration** — INFO: confirm the plugin is enabled for this project