BlockWatch

SkillDev tools

Use when writing or modifying code in a project that uses BlockWatch — proactively link co-dependent code with `<block affects=...>`/`<block name=...>` so it catches drift when one side changes without the other (an enum and its docs, a constant and its config), or assert two places hold the same value with `<block same-as=...>`. Also for lists that must stay sorted/unique or values with a strict format/size, and when editing files that contain `<block ...>` tags (affects, same-as, keep-sorted, keep-unique, line-pattern, line-count, check-ai, check-lua).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the BlockWatch skill

What this skill tells your AI

The instructions your AI receives, as published by mennanov/blockwatch in .agents/skills/blockwatch/SKILL.md and read by ahel’s review.

BlockWatch is a language-agnostic linter that enforces rules declared inside HTML-like <block ...> tags placed in source-file comments. It works across Rust, Python, JS/TS, Go, Java, Markdown, YAML, TOML, HTML, and more. By default it checks the whole tree; given a git diff on stdin plus --diff --only-changed it checks only the blocks that diff changed.

Use this skill in three situations:

  • As you write code (the default): the moment you write something a block would guard, add the block in the same change — don't wait for a separate pass.
  • First-time / bulk pass: annotating an existing project that has no blocks yet.
  • Maintaining blocks: keeping existing blocks valid when you edit files that already contain them.

Two rules apply to all three:

  • High-value only. A block must catch a real mistake someone could plausibly make, not decorate. Too many blocks create noise and get ignored. When in doubt, leave it out.
  • Only in projects that use BlockWatch. This skill being installed, or existing <block> tags in the tree, means the project opted in. Don't add blocks to a project that doesn't use the tool.

Annotate as you write code

This is the primary way blocks should get added: incrementally, as part of normal coding. Whenever you write or change code matching a row in Where blocks add value (below), add the tag right then, using the Validator reference for the syntax.

Introduce a list that should stay ordered → wrap it in keep-sorted in the same edit. Add a fact that also lives in the docs or config → add affects in the same edit. Retrofitting later is exactly the cost this avoids.

Then run git diff --patch | blockwatch --diff --only-changed to confirm the new tags pass (see Running and verifying).

Annotating a new project

  1. Survey the repo for the patterns in the catalog below. Read the code and the docs/config; use rg/grep to find lists, enums, match arms, tables, and constants.
  2. For each candidate, add the minimal block tag using the comment syntax of that file's language.
  3. Run blockwatch list to confirm every new tag parses and is recognized, then run blockwatch to confirm all blocks pass on the current (clean) tree. Fix any tag you placed on already-inconsistent content.
  4. Commit, then wire BlockWatch into hooks/CI (see below) so the rules are enforced from now on.

Where blocks add value (catalog)

You see...AddWhy
A hand-maintained list/enum/match that should stay ordered (dependencies, CLI flags, feature lists, route tables)keep-sortedEliminates "please sort this" review nits
A list that must not repeat (allowlists, IDs, registered names)keep-uniquePrevents accidental duplicates
The same fact in two places — an enum and its docs, a version constant and a changelog row, a config key and its README tableaffects + nameForces docs/config to be updated alongside code
The same value duplicated across places — a constant and its docs, a port in code and in a manifest, an env-var set and its README tablesame-as + nameFails when the copies actually disagree, not just when one side is touched
A list whose items have a strict format (slugs, semver, env-var names)line-pattern="<regex>"Catches typos at the source
A block that must not grow past N lines (public API surface, a switch mapped to a fixed enum)line-count="<=N"Flags unbounded growth
Prose or config with a natural-language rule ("must mention X", "no TODOs left")check-ai="..."Rules regex can't express
Domain logic too complex for regexcheck-lua="script.lua"Custom programmable checks

Prefer the deterministic validators (keep-sorted, keep-unique, affects, same-as, line-pattern, line-count) first — they are free, fast, and need no API keys. Reserve check-ai for rules the cheaper validators genuinely can't express.

When two blocks should hold the same value, prefer same-as over a bare affects: affects only notices that one side was edited, while same-as fails when the copies actually disagree. Put reciprocal blocks on both sides (each named), and — because same-as also fires without a diff — a periodic bare blockwatch run over the whole tree (see CI below) catches drift that a changed-blocks-only check would miss.

Placing tags

  • Tags live inside comments, using the host language's comment syntax. Open with <block ...>, close with </block>.
  • The block's content is the lines between the two tags.
  • Under --diff --only-changed a block is only validated when its content (or its start tag) is touched by the diff, so annotating is safe to do incrementally — adding a tag never retroactively fails unrelated code. A bare blockwatch run checks every block in the tree, so use it to find the tags you placed on already-inconsistent content.
DEPENDENCIES = [
    # <block keep-sorted keep-unique>
    "anyhow",
    "clap",
    "serde",
    # </block>
]
// <block affects="README.md:supported-langs">
pub enum Language { Rust, Python }
// </block>
<!-- <block name="supported-langs"> -->

- Rust
- Python

<!-- </block> -->

(Editing the enum now forces you to touch the supported-langs block in README.md.)

Validator reference

AttributeSyntaxNotes
namename="foo"Names a block; the target of affects; shown by blockwatch list.
affectsaffects="file:foo", affects=":foo" (same file) or affects="file" (whole file); comma-separate multipleIf this block's content changes in a diff, the referenced name="foo" block's content must change too, else a violation. One-way by default; put affects on both blocks (each named) for two-way drift detection. Only fires under --diff. A target with no : names a whole file, satisfied by any change to it — use it for formats that cannot hold a comment (JSON, .env, lockfiles); a missing target file fails the run.
same-assame-as="file:foo", same-as=":foo" (same file) or same-as="file" (whole file); comma-separate multipleThis block and each referenced name="foo" block must hold the same value. The equality comparison is symmetric once it runs, and it also runs on a full-tree scan (unlike affects) — but under --only-changed only a changed block containing same-as starts the comparison, so put reciprocal same-as rules on both blocks if changes to either side must be caught. Whole trimmed content by default. A target with no : compares against the whole file's content, read under this block's same-as-pattern.
same-as-patternsame-as-pattern="id: (?P<value>\d+)"Per line, compare the value capture group (or the whole match); every match on a line counts, and all lines flatten into one list — values are compared, not their layout. Each side reads itself, so put a pattern on both blocks when the two are in different formats.
same-as-modesame-as-mode="set" (default) / sequence / single / subsetset order/duplicate-insensitive; sequence ordered; single exactly one token per side; subset this block's tokens must all appear in the target (directional). Governed by the source block.
same-as-formatsame-as-format="numeric"Parse tokens as numbers before comparing, so 8080 == 8080.0. Governed by the source block.
keep-sortedkeep-sorted / keep-sorted="asc" / keep-sorted="desc"Default asc, compared lexicographically.
keep-sorted-patternkeep-sorted-pattern="id: (?P<value>\d+)"Sort by the regex capture group named value instead of the whole line.
keep-sorted-formatkeep-sorted-format="numeric"Compare the value numerically rather than as text ("10" after "2"). Every line must then be a number, and a trailing comma is part of the trimmed line — on a real list literal pair this with keep-sorted-pattern to lift the number out, or the run is a hard error.
keep-uniquekeep-unique / keep-unique="^ID:(?P<value>\d+)"Uniqueness on the whole line, or on the value capture group.
line-patternline-pattern='^"[a-z0-9-]+",?$'Every line in the block must match, against the trimmed line as the file spells it — quotes, trailing commas and all. A pattern written for the bare value (^[a-z0-9-]+$) rejects every line of a quoted list, valid entries included.
line-countline-count="<=5"Operators: <, >, <=, >=, ==.
check-aicheck-ai="Must mention 'Acme'"LLM validation. Requires BLOCKWATCH_AI_API_KEY (plus optional BLOCKWATCH_AI_MODEL, BLOCKWATCH_AI_API_URL). One network call per block, and the answer is a guess: the same block can pass one run and fail the next.
check-ai-patterncheck-ai-pattern="\$(?P<value>\d+)"Send only the matching parts of the block to the model instead of the whole block, which keeps the prompt focused and the token cost down. Every match is sent, joined by newlines; a pattern matching nothing is a violation, not a silent pass.
check-luacheck-lua="scripts/x.lua"Script defines validate(ctx, content) returning nil (pass) or an error string. ctx has file (repository-relative, always /-separated, in every run mode), line, attrs; if the block also has affects, ctx.affects is a list of the affected targets ({ file, name, content }, same path format) for IO-free cross-block checks — a whole-file target carries the file's text with name == nil.
check-lua-patterncheck-lua-pattern='str = "(?P<value>[^"]+)"'Pass only the extracted values to the script instead of the whole block. Every match contributes its value, and content becomes a 1-based array (read content[1] for a single value); the regex runs against the entire block (not per line), so it may span several lines. Empty array when nothing matches.
check-lua-timeoutcheck-lua-timeout="60"Wall-clock budget for the script, in whole seconds; default 30. A script that runs past it fails the run with an error, not a violation. A value below 1 is a hard error.
severityseverity="error" (default) / warning / info / hintOnly error fails the run (exit 1); the others are reported but exit 0.

Maintaining blocks (editing annotated files)

When you change code in a file that contains blocks, you MUST:

  1. Never delete <block> / </block> tags unless explicitly told to. Place new content inside the appropriate block boundaries.
  2. Respect each block's directives as you edit: keep keep-sorted lists ordered, never introduce a keep-unique duplicate, make every new line match line-pattern, stay within line-count, and satisfy check-ai / check-lua rules.
  3. Honor affects: if you change a block carrying affects="file:name", you must also update the referenced <block name="name"> in file — they are meant to move together. A target written without a : names a whole file, so that file has to change too.
  4. Verify before claiming the change is done (see below).

Running and verifying

You can run the blockwatch command directly in the shell:

blockwatch                                                   # validate every block in the tree
git diff --patch | blockwatch --diff --only-changed          # only blocks your changes touched (fast)
git diff --cached --patch | blockwatch --diff --only-changed # staged changes only
git diff --patch | blockwatch --diff                         # whole tree, with `affects` enforced
blockwatch list                                              # JSON dump of every block found (audit / debug)
blockwatch "src/**/*.rs" "**/*.md"                           # restrict to globs (quote them)
blockwatch --ignore "**/generated/**"                        # exclude paths

Stdin is read only with --diff; piping a diff without it is silently ignored and the whole tree is scanned instead. --only-changed narrows the run to the blocks the diff touched and requires --diff.

After editing annotated files, run git diff --patch | blockwatch --diff --only-changed. If it fails, read the message, fix the sorting/duplication/pattern/sync issue, and re-run until it passes. Use blockwatch list to confirm a tag you just added is parsed and seen.

The piped diff must carry Git's standard path prefixes, which a plain git diff produces. If BlockWatch reports that a diff target has no recognized prefix or does not exist, the repository sets diff.noprefix, a custom diff.srcPrefix, or diff.relative; re-run as git diff --patch --default-prefix --no-relative | blockwatch --diff --only-changed. Under --diff, stdin that is empty, ANSI-colorized, or not a diff is an error rather than "nothing changed".

If blockwatch is not on PATH, install it with cargo install blockwatch or brew install mennanov/blockwatch/blockwatch.

Wiring into hooks and CI (do this once, after annotating)

Validating only the changed blocks keeps these near-instant.

pre-commit (.pre-commit-config.yaml):

- repo: local
  hooks:
    - id: blockwatch
      name: blockwatch
      entry: bash -c 'set -o pipefail; git diff --patch --cached --unified=0 | blockwatch --diff --only-changed'
      language: system
      stages: [ pre-commit ]
      pass_filenames: false

Without the pre-commit framework, put the same entry command in .git/hooks/pre-commit and chmod +x it.

GitHub Actions (.github/workflows/blockwatch.yml):

name: blockwatch
on:
  pull_request: { branches: [ main ] }
  push: { branches: [ main ] }
permissions: { contents: read }
jobs:
  blockwatch:
    runs-on: ubuntu-latest
    steps:
      - uses: mennanov/blockwatch-action@v1
        # Only needed if you use check-ai:
        # env: { BLOCKWATCH_AI_API_KEY: ${{ secrets.BLOCKWATCH_AI_API_KEY }} }

Validating the PR diff is enough for affects/drift checks. A periodic bare blockwatch run over the whole tree on main is a good extra safety net for the deterministic validators — but note it cannot check affects, which needs a diff to compare against.

Signals

GitHub stars
29
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
blockwatch
Source
github.com/mennanov/blockwatch