utils/repo_contract/install_web.py
Krzysztof kuhy Rudnicki a0fb279b1b agent_ready: AgentReady v2.0 checker + repo_contract web-site gate
New agent_ready/ package (dist and live-url modes, 100% branch coverage)
checks robots, sitemap, llms.txt, ai-catalog, 404, no-JS text, markdown
mirrors and JSON-LD. repo_contract gains web-repo detection and installs the
agent-ready pre-commit hook, CI workflow and check_agent_ready.sh shim;
scripts/check_agent_ready.sh is the shared entry point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
2026-10-22 21:18:49 +02:00

128 lines
4.1 KiB
Python

"""The AgentReady half of the installer: a website gets the agent-ready gate.
Why it rides on install_repo_contract_gate.sh rather than a separate script:
that installer is already the one step every new repo gets after `git init`,
so a new Vite site cannot come out of scaffolding without the gate. Pieces,
mirroring the repo-contract and dependency-freshness gates: the
`[agent_ready]` table in repo-contract.toml (proposed from detection, never
overwritten), the scripts/check_agent_ready.sh delegate, the pre-commit hook
and the agent-ready CI workflow (skipped when the repo has no Actions).
"""
from __future__ import annotations
from pathlib import Path
from dep_freshness.install import NO_WORKFLOW_MARKER
from repo_contract.precommit import (
PRECOMMIT,
hook_line,
needs_hook,
set_hook_line,
write_hook,
)
from repo_contract.web import (
CONFIG,
HOOK_ID,
SHIM,
TABLE,
WORKFLOW,
AgentReady,
ConfigError,
detect,
hook_files,
load,
render,
)
TEMPLATES = Path(__file__).resolve().parent / "templates"
HEADER = (
"# Repo-contract settings, read by ~/src/utils/repo_contract.\n"
"# [agent_ready]: this repo is a website and must pass the AgentReady\n"
"# checker (~/src/utils/agent_ready). See repo_contract/web.py for keys.\n\n"
)
CONFIG_PIECE = f"{CONFIG} ([{TABLE}])"
HOOK_PIECE = f"{PRECOMMIT} ({HOOK_ID} hook)"
def _template(name: str) -> str:
return (TEMPLATES / name).read_text(encoding="utf-8")
def _drifted(repo: Path, rel: Path, template: str) -> bool:
target = repo / rel
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
template
)
def _files_line(cfg: AgentReady) -> str:
"""The hook's `files:` line: the regex as a single-quoted YAML scalar."""
return " files: '" + hook_files(cfg).replace("'", "''") + "'"
def _settings(repo: Path) -> tuple[AgentReady | None, bool]:
"""(settings to gate with, whether the config must be written)."""
try:
declared = load(repo)
except ConfigError:
return None, False # check.py reports it; never guess over a typo
if declared is not None:
return (None if declared.mode == "skip" else declared), False
proposed = detect(repo)
return proposed, proposed is not None
def _pieces(repo: Path) -> tuple[AgentReady | None, list[str]]:
cfg, write_config = _settings(repo)
if cfg is None:
return None, []
todo = [CONFIG_PIECE] if write_config else []
if _drifted(repo, SHIM, "check_agent_ready.shim.sh"):
todo.append(str(SHIM))
if not (repo / NO_WORKFLOW_MARKER).exists() and _drifted(
repo, WORKFLOW, "agent-ready.yml"
):
todo.append(str(WORKFLOW))
if needs_hook(repo, HOOK_ID) or hook_line(repo, HOOK_ID, "files") != _files_line(
cfg
):
todo.append(HOOK_PIECE)
return cfg, todo
def plan_web(repo: Path) -> list[str]:
"""Which AgentReady gate pieces are missing or drifted."""
return _pieces(repo)[1]
def _write_config(repo: Path, cfg: AgentReady) -> None:
target = repo / CONFIG
if target.is_file():
body = target.read_text(encoding="utf-8")
body = (body if body.endswith("\n") else body + "\n") + "\n"
else:
body = HEADER
target.write_text(body + render(cfg), encoding="utf-8")
def install_web(repo: Path) -> list[str]:
"""Write every missing or drifted AgentReady gate piece. Returns them."""
cfg, todo = _pieces(repo)
if cfg is not None and CONFIG_PIECE in todo:
_write_config(repo, cfg)
if str(SHIM) in todo:
target = repo / SHIM
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(_template("check_agent_ready.shim.sh"), encoding="utf-8")
target.chmod(0o755)
if str(WORKFLOW) in todo:
target = repo / WORKFLOW
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(_template("agent-ready.yml"), encoding="utf-8")
if cfg is not None and HOOK_PIECE in todo:
if needs_hook(repo, HOOK_ID):
write_hook(repo, _template("agent-ready-hook.yaml"))
set_hook_line(repo, HOOK_ID, "files", _files_line(cfg))
return todo