Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/becwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dogfood the official becwright action on our own pull requests: it installs the
# checked-out becwright (version: .) and checks only the files this PR changes
# against the repo's own .bec/rules.yaml. This both verifies the action works and
# guards this repo the same way we ask others to guard theirs.
name: becwright

on:
pull_request:

jobs:
becwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so the merge-base with the PR base exists
- uses: ./
with:
version: .
26 changes: 22 additions & 4 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
[![npm](https://img.shields.io/npm/v/becwright?logo=npm)](https://www.npmjs.com/package/becwright)
[![PyPI](https://img.shields.io/pypi/v/becwright?logo=pypi&logoColor=white)](https://pypi.org/project/becwright/)

**Reglas que se ejecutan, no notas que se ignoran.**
Tu `CLAUDE.md` es un *cartel*. becwright es el *guardia*.
**La capa de enforcement para agentes de IA.**

Reglas que se ejecutan, no notas que se ignoran. Tu `CLAUDE.md` es un *cartel*;
becwright es el *guardia* — corre tus reglas sobre el código y frena el commit
cuando una se rompe, sin importar qué modelo (o persona) lo escribió.

<sub>Determinista, no probabilístico · cualquier lenguaje · sin Python · frena el commit **y** lleva el *por qué*.</sub>

Expand Down Expand Up @@ -202,6 +205,7 @@ Comandos disponibles:
| `becwright list` | Lista los checks incluidos |
| `becwright check` | Corre las reglas sobre los archivos en staging |
| `becwright check --diff <base>` | Corre las reglas solo sobre los archivos cambiados vs `<base>` (para CI/PR) |
| `becwright why [id]` | Muestra la intención + el por qué de las reglas — la memoria de decisiones del repo (`--json` para agentes) |
| `becwright search [texto]` | Lista BECs listas del catálogo incluido |
| `becwright add <nombre>` | Instala una BEC del catálogo en `.bec/rules.yaml` (sin conexión) |
| `becwright install` | Instala el hook `pre-commit` nativo |
Expand Down Expand Up @@ -295,8 +299,22 @@ Agrega un skill `becwright` y un comando `/becwright`. Ver
Para resultados estructurados, `becwright check --json` imprime un resumen
legible por máquina, y `becwright mcp` (instalá el extra `mcp`: `pipx install
"becwright[mcp]"`) levanta un servidor MCP — MCP es una forma estándar de que
las herramientas de IA se conecten a habilidades extra — que expone `check` y
`list_checks` a cualquier agente. Ver [`documentation/mcp.md`](documentation/mcp.md).
las herramientas de IA se conecten a habilidades extra — que expone `check`,
`list_checks` y `list_rules` a cualquier agente. Ver [`documentation/mcp.md`](documentation/mcp.md).

Mejor aún, un agente puede leer las reglas *antes* de escribir código: `becwright
why --json` le entrega las decisiones que no puede violar (la intención de cada
regla y su razón), así las esquiva en vez de descubrir la regla recién cuando el
commit se bloquea. El catálogo `.bec/rules.yaml` se vuelve la memoria de
decisiones consultable del repo.

En ambos casos la señal se mantiene magra. Un commit bloqueado devuelve la única
regla que se rompió, su *por qué* y las líneas exactas — el agente arregla justo
eso en vez de releer la guía de estilo entera en el contexto. El consejo de
siempre es "dale más contexto al modelo"; becwright lo da vuelta — le pasás la
constraint puntual que rompió, verificada de forma determinista, no el reglamento
completo. Menos tokens, loop más ajustado, y la garantía no depende de que el
modelo haya leído nada.

Una regla en `.bec/rules.yaml`:

Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
[![npm](https://img.shields.io/npm/v/becwright?logo=npm)](https://www.npmjs.com/package/becwright)
[![PyPI](https://img.shields.io/pypi/v/becwright?logo=pypi&logoColor=white)](https://pypi.org/project/becwright/)

**Rules that run, not notes that get ignored.**
Your `CLAUDE.md` is a *sign*. becwright is the *guard*.
**The enforcement layer for AI coding agents.**

Rules that run, not notes that get ignored. Your `CLAUDE.md` is a *sign*;
becwright is the *guard* — it runs your rules against the code and blocks the
commit when one breaks, no matter which model (or person) wrote it.

<sub>Deterministic, not probabilistic · any language · no Python required · blocks the commit **and** carries the *why*.</sub>

Expand Down Expand Up @@ -194,6 +197,7 @@ Available commands:
| `becwright list` | List the built-in checks |
| `becwright check` | Runs the rules over the staged files |
| `becwright check --diff <base>` | Runs the rules over only the files changed vs `<base>` (for CI/PR) |
| `becwright why [id]` | Shows the intent + why behind the rules — the repo's decision memory (`--json` for agents) |
| `becwright search [query]` | Lists ready-made BECs from the built-in catalog |
| `becwright add <name>` | Installs a catalog BEC into `.bec/rules.yaml` (offline) |
| `becwright install` | Installs the native `pre-commit` hook |
Expand Down Expand Up @@ -287,8 +291,21 @@ It adds a `becwright` skill and a `/becwright` command. See
For structured results, `becwright check --json` prints a machine-readable
summary, and `becwright mcp` (install the `mcp` extra: `pipx install
"becwright[mcp]"`) runs an MCP server — MCP is a standard way for AI tools to
plug in extra abilities — exposing `check` and `list_checks` to any agent. See
[`documentation/mcp.md`](documentation/mcp.md).
plug in extra abilities — exposing `check`, `list_checks` and `list_rules` to any
agent. See [`documentation/mcp.md`](documentation/mcp.md).

Better yet, an agent can read the rules *before* it writes code: `becwright why
--json` hands it the decisions it must not violate (each rule's intent and the
reason behind it), so it steers clear of a broken commit instead of discovering
the rule only when the commit is blocked. The `.bec/rules.yaml` catalog becomes
the repo's queryable decision memory.

Either way the signal stays lean. A blocked commit returns the one rule that
broke, its *why*, and the exact lines — the agent fixes precisely that instead of
re-reading the whole style guide into context. The usual advice is "give the
model more context"; becwright inverts it — you hand it the specific constraint it
broke, checked deterministically, not the entire rulebook. Fewer tokens, tighter
loop, and the guarantee doesn't depend on the model having read anything at all.

A rule in `.bec/rules.yaml`:

Expand Down
70 changes: 70 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: becwright
description: >-
Enforce BECs (Bound Executable Constraints) on the files a pull request changes.
A commit hook can be skipped with `git commit --no-verify`; a required CI check
cannot — so this closes that gap and makes the rules infrastructure, not a suggestion.
author: DataDave-Dev
branding:
icon: shield
color: red

inputs:
base:
description: >-
Git ref to diff against. Defaults to the PR base branch
(origin/$GITHUB_BASE_REF) on pull_request events, or the repository default
branch otherwise. Only files changed vs this ref are checked.
required: false
default: ''
version:
description: >-
pip requirement specifier used to install becwright — e.g. "becwright",
"becwright==0.4.0", or "." to install the checked-out repo (for dogfooding).
required: false
default: becwright
python-version:
description: Python version used to run becwright.
required: false
default: '3.x'
args:
description: Extra arguments appended to `becwright check` (e.g. "--json").
required: false
default: ''

runs:
using: composite
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}

- name: Install becwright
shell: bash
run: pip install "${{ inputs.version }}"

- name: Check changed files
shell: bash
env:
BEC_BASE: ${{ inputs.base }}
BEC_ARGS: ${{ inputs.args }}
GH_BASE_REF: ${{ github.base_ref }}
GH_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
base="$BEC_BASE"
if [ -z "$base" ]; then
if [ -n "${GH_BASE_REF:-}" ]; then
base="origin/${GH_BASE_REF}"
else
base="origin/${GH_DEFAULT_BRANCH}"
fi
fi
# Make the base ref available even if the checkout was shallow. Harmless if
# it is already present; requires fetch-depth: 0 for the merge-base to exist.
branch="${GH_BASE_REF:-$GH_DEFAULT_BRANCH}"
if [ -n "$branch" ]; then
git fetch --no-tags --quiet origin "$branch" || true
fi
echo "becwright: checking files changed vs ${base}"
becwright check --diff "$base" $BEC_ARGS
7 changes: 7 additions & 0 deletions documentation/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,17 @@ pipx install "becwright[mcp]" # or: pip install "becwright[mcp]"
|---|---|---|
| `check` | `all_files` (bool), `path` (optional repo dir) | the same summary as `check --json` |
| `list_checks` | — | the built-in checks as `{name, description}` |
| `list_rules` | `path` (optional repo dir) | the repo's rules as decision records (`id`, `severity`, `intent`, `why_it_matters`, `rejected_alternatives`, `paths`, `check`) |
| `preview_rule` | `check`, `paths`, `exclude` (optional), `all_files`, `path` | `{matched_files, passed, output, note}` — a dry-run without writing the rule |
| `propose_rules_from_claude_md` | `path` (optional repo dir) | `{rules, unmapped_hint}` — the rules becwright can derive from the repo's CLAUDE.md |
| `add_rule` | `id`, `check`, `paths`, `intent`, `why_it_matters`, `severity`, `exclude`, `confirm`, `path` | writes a rule to `.bec/rules.yaml` — preview unless `confirm=true` |

**`list_rules`** is the decision memory: it returns every rule with its intent,
the reason behind it and the check that enforces it, so an agent can read the
decisions it must not violate *before* writing code — the same data `check`
surfaces on failure, but available up front. It mirrors the CLI `becwright why
--json`.

**`propose_rules_from_claude_md`** returns the rules becwright can derive
deterministically from the prose (each with the phrase that triggered it) — the
agent's *starting point*. **`preview_rule`** lets the agent *validate* a rule
Expand Down
2 changes: 1 addition & 1 deletion integrations/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "becwright",
"description": "Deterministic, commit-blocking constraints (BECs) on your code. Install and use becwright from any session; the guaranteed safety net that complements CLAUDE.md. No Python required.",
"description": "The enforcement layer for AI coding agents — deterministic, commit-blocking constraints (BECs). Install and use becwright from any session; the guaranteed safety net that complements CLAUDE.md. No Python required.",
"version": "0.2.0",
"author": {
"name": "Alonso David De Leon Rodarte"
Expand Down
5 changes: 3 additions & 2 deletions integrations/claude-code/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# becwright — Claude Code plugin

A Claude Code plugin so any agent session can install and drive becwright: the
deterministic, commit-blocking safety net that complements `CLAUDE.md`.
A Claude Code plugin so any agent session can install and drive becwright — the
enforcement layer for AI coding agents: the deterministic, commit-blocking safety
net that complements `CLAUDE.md`.

## Install

Expand Down
2 changes: 1 addition & 1 deletion npm/becwright/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "becwright",
"version": "0.4.0",
"description": "Deterministic, portable constraints (BECs) that block commits violating your rules — a safety net for AI-written and human-written code. No Python required.",
"description": "The enforcement layer for AI coding agents — deterministic, portable constraints (BECs) that block a commit when a rule breaks, for AI-written and human-written code. No Python required.",
"bin": {
"becwright": "bin/becwright.js"
},
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "becwright"
version = "0.4.0"
description = "Deterministic, portable constraints (BECs) that block commits violating your rules — a safety net for AI-written and human-written code."
description = "The enforcement layer for AI coding agents — deterministic, portable constraints (BECs) that block a commit when a rule breaks, for AI-written and human-written code."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "Alonso David De Leon Rodarte" }]
Expand Down
2 changes: 1 addition & 1 deletion src/becwright/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.0"
__version__ = "0.4.0"
84 changes: 81 additions & 3 deletions src/becwright/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,76 @@ def _print_result(result: Result) -> None:
print()


def _severity_label(rule, width: int = 0) -> str:
codes = {"blocking": (RED, BOLD), "advisory": (CYAN,)}.get(rule.severity, (YELLOW,))
return _style(rule.severity.ljust(width), *codes)


def _one_line(text: str, limit: int = 72) -> str:
collapsed = " ".join(text.split())
return collapsed if len(collapsed) <= limit else collapsed[: limit - 1].rstrip() + "…"


def _print_rules_overview(rules) -> None:
print(f"{_style('becwright why', BOLD)} "
f"{_style(f'— the decisions this repo enforces ({len(rules)} rule(s))', DIM)}\n")
width = max(len(r.id) for r in rules)
for r in rules:
intent = _one_line(r.intent) if r.intent else _style("(no intent recorded)", DIM)
print(f" {_style(r.id.ljust(width), GREEN)} {_severity_label(r, 8)} {intent}")
print(_style("\n Run `becwright why <id>` for the full record of one rule, "
"or add --json for agents.", DIM))


def _print_rule_detail(rule) -> None:
print(f"{_style('becwright why', BOLD)} {_style(rule.id, GREEN)} ({_severity_label(rule)})\n")
if rule.intent:
print(f" {_style('Intent:', DIM)}")
print(f" {rule.intent}")
if rule.why_it_matters:
print(f" {_style('Why it matters:', DIM)}")
print(f" {rule.why_it_matters}")
if rule.rejected_alternatives:
print(f" {_style('Rejected alternatives:', DIM)}")
for alt in rule.rejected_alternatives:
print(f" - {alt}")
applies = "the commit message" if rule.target == "commit-msg" else (
", ".join(rule.paths) or "(no paths)")
print(f" {_style('Applies to:', DIM)} {applies}")
if rule.exclude:
print(f" {_style('Excluding:', DIM)} {', '.join(rule.exclude)}")
print(f" {_style('Check:', DIM)} {rule.check}")


def _cmd_why(args: argparse.Namespace) -> int:
root = git.repo_root()
rules = load_rules(root / ".bec" / "rules.yaml")
if args.rule_id:
rule = next((r for r in rules if r.id == args.rule_id), None)
if rule is None:
print(_style(f"No rule with id '{args.rule_id}' in .bec/rules.yaml.", RED),
file=sys.stderr)
if rules:
print(_style(f" Known ids: {', '.join(r.id for r in rules)}", DIM),
file=sys.stderr)
return 1
if args.json:
import json
print(json.dumps(report.rule_record(rule), indent=2))
return 0
_print_rule_detail(rule)
return 0
if args.json:
import json
print(json.dumps({"rules": [report.rule_record(r) for r in rules]}, indent=2))
return 0
if not rules:
print(_style("No .bec/rules.yaml with rules. Run `becwright init` to create some.", YELLOW))
return 0
_print_rules_overview(rules)
return 0


def _unknown_builtin_checks(rules, root: Path) -> list[tuple[str, str]]:
"""Rules whose `check` uses the `becwright run <name>` form with a <name> that
is not a built-in check. Such a rule can never pass — the check exits with an
Expand All @@ -76,7 +146,7 @@ def _print_unknown_checks(unknown: list[tuple[str, str]]) -> None:

def _cmd_check(args: argparse.Namespace) -> int:
root = git.repo_root()
rules, files, result = report.gather(root, all_files=args.all)
rules, files, result = report.gather(root, all_files=args.all, diff_base=args.diff)

unknown = _unknown_builtin_checks(rules, root)
if unknown:
Expand Down Expand Up @@ -784,7 +854,10 @@ def _build_parser() -> argparse.ArgumentParser:
sub = parser.add_subparsers(dest="command", required=True)

p_check = sub.add_parser("check", help="check the code against the rules")
p_check.add_argument("--all", action="store_true", help="check the whole repo, not just staging")
scope = p_check.add_mutually_exclusive_group()
scope.add_argument("--all", action="store_true", help="check the whole repo, not just staging")
scope.add_argument("--diff", metavar="BASE",
help="check only files changed vs BASE ref (e.g. origin/main) — for CI/PR")
p_check.add_argument("--json", action="store_true", help="output results as JSON")
p_check.set_defaults(func=_cmd_check)

Expand All @@ -796,6 +869,11 @@ def _build_parser() -> argparse.ArgumentParser:
help="derive rules from the repo's CLAUDE.md (best-effort: maps prohibitions to enforceable checks)")
p_init.set_defaults(func=_cmd_init)

p_why = sub.add_parser("why", help="show the intent + why behind the rules (the repo's decision memory)")
p_why.add_argument("rule_id", nargs="?", help="rule id to explain (default: list every rule)")
p_why.add_argument("--json", action="store_true", help="output as JSON (for AI agents to consult before writing code)")
p_why.set_defaults(func=_cmd_why)

p_run = sub.add_parser("run", help="run a built-in check against files on stdin (used inside rules)")
p_run.add_argument("module", help="built-in check name (see `becwright list`)")
p_run.add_argument("args", nargs=argparse.REMAINDER, help="arguments forwarded to the check")
Expand Down Expand Up @@ -836,7 +914,7 @@ def main(argv: list[str] | None = None) -> int:
args = _build_parser().parse_args(argv)
try:
return args.func(args)
except (git.NotAGitRepo, RulesError) as e:
except (git.NotAGitRepo, git.GitError, RulesError) as e:
print(_style(str(e), RED), file=sys.stderr)
return 2

Expand Down
Loading
Loading