Skip to content

Code Quality

The agent keeps slipping print() into production modules instead of using your logger, and bare except: clauses show up under deadline pressure. You want layered feedback: a fast advisory on every edit, a structural lint that finds patterns regex cannot, a behavioral nudge when the agent's narration starts repeating the antipattern, and a final LLM gate for ambiguous cases.

from __future__ import annotations

import ast
import re
from collections.abc import Iterator

from captain_hook import (
    Allow,
    Content,
    Event,
    InlineTests,
    Input,
    Signal,
    Signals,
    SourceEdits,
    TestFile,
    Warn,
    hook,
    lint,
    llm_gate,
    nudge,
)

PRINT_TESTS: InlineTests = {
    Input(tool="Edit", file="src/app.py", content='import sys\nprint("debug")\n'): Warn(pattern="logger"),
    Input(tool="Edit", file="src/app.py", content="logger.info('ok')\n"): Allow(),
}

hook(
    Event.PostToolUse,
    only_if=[SourceEdits(lang="py"), Content(r"^\s*print\(")],
    skip_if=[TestFile()],
    message="Use the project logger instead of print(). See docs/logging.md.",
    tests=PRINT_TESTS,
)


def bare_excepts(node: ast.AST) -> Iterator[str]:
    if isinstance(node, ast.ExceptHandler) and node.type is None:
        yield f"line {node.lineno}: bare except"


lint(
    bare_excepts,
    message="Bare except clauses silently swallow errors: {violations}",
    trigger="except",
)


nudge(
    "You keep adding print()s after edits. Switch to logger.debug() and tail the log.",
    signals=Signals(
        patterns=[
            Signal(pattern=r"print\(", weight=1, flags=re.MULTILINE),
            Signal(pattern=r"debug[\s_-]print", weight=2, flags=re.IGNORECASE),
        ],
        threshold=3,
        window=8,
    ),
)


llm_gate(
    "Does this diff add a print() that should be a logger call, where the surrounding "
    "module already imports a logger? Block only if the prod print is unambiguous.",
    message=lambda r: f"Replace print() with logger: {r.reasoning}",
    only_if=[SourceEdits(lang="py"), Content(r"^\s*print\(")],
    skip_if=[TestFile()],
    max_fires=2,
)

What to learn: The same problem expressed at four escalating tiers. hook(... Content(...)) is a fast regex check on the edit; lint(...) parses the file's AST so you can flag structures (bare except) that don't show up as plain text; nudge(... signals=...) watches transcript narration to catch the agent talking about the antipattern; llm_gate(...) lets a model adjudicate the ambiguous "is this really a prod print?" cases. Combine tiers when speed-vs-precision tradeoffs matter.