# Automating `validate-repo`

The `validate-repo` skill is **interactive by design**: it prints a report to
the conversation and, in Phase C5, asks whether to save it. It is also
**read-only** — it never edits the audited repo. This guide shows how to run it
**unattended** (on a schedule, in CI, or from an orchestrator) and, optionally,
how to turn its findings into pull requests.

Nothing here is tied to a specific scheduler. The reusable unit is a container
that runs one script; cron, GitHub Actions, GitLab CI, or an Airflow operator
all invoke it the same way.

## Files in this folder

| File | Purpose |
|---|---|
| `audit-prompt.md` | Wrapper prompt that runs the skill headless, pins the report path, and suppresses the Phase C5 "save?" question. |
| `fixes-prompt.md` | Wrapper prompt for the **optional** conservative auto-fixes pass (see below). |
| `run-audit.sh` | Orchestrator-agnostic entrypoint: report → optional report PR → optional fixes PR. Configured entirely by env vars. |
| `Dockerfile.example` | Example container baking the CLI, dbt + packages, the plugin, and these scripts. |

---

## 1. Manual run (no automation)

The quickest path — no container, no script:

```bash
# In an interactive Claude Code session with the gemma-dbt plugin installed
# (or started with --plugin-dir pointing at a checkout):
/validate-repo /path/to/your/dbt
```

Answer the Phase C5 prompt to save the report where you want it. That's it.

---

## 2. Headless run (the core of all automation)

To run without a human answering prompts, drive the CLI in `--print` mode and
hand it the **wrapper prompt** (`audit-prompt.md`) instead of calling the skill
directly. The wrapper does two things the bare skill can't do unattended: it
**pins the output path** and **tells the orchestrator not to ask** whether to
save (Phase C5).

```bash
export ANTHROPIC_API_KEY=...          # the CLI reads this directly under --bare
export DBT_PROJECT_DIR=/path/to/dbt
export REPORT_PATH="$DBT_PROJECT_DIR/audit/repo-validation-$(date -u +%F).md"

PROMPT="$(DBT_PROJECT_DIR="$DBT_PROJECT_DIR" REPORT_PATH="$REPORT_PATH" \
  envsubst '$DBT_PROJECT_DIR $REPORT_PATH' < audit-prompt.md)"

claude \
  --bare \
  --print \
  --dangerously-skip-permissions \
  --disallowed-tools AskUserQuestion \
  --plugin-dir /path/to/gemma-dbt \
  "$PROMPT"
```

Flag notes:
- **`--bare`** — reproducible minimal mode: no hooks, no auto-memory, no
  `CLAUDE.md` auto-discovery; auth is strictly `ANTHROPIC_API_KEY`. The skill
  still loads via `--plugin-dir` and still reads the project's `CLAUDE.md`
  itself in Phase A. Good for clean, deterministic runs.
- **`--dangerously-skip-permissions`** — required for unattended use (no human
  to approve tool calls). The CLI **refuses to run this as root**, so your
  container must use a non-root user.
- **`--disallowed-tools AskUserQuestion`** — belt-and-suspenders: blocks any
  interactive question even if the wrapper prompt is ignored.
- **`--plugin-dir`** — load the `gemma-dbt` plugin from a checkout/.zip. Omit if
  the plugin is already installed for this CLI. When loaded this way the skill's
  `${CLAUDE_SKILL_DIR}` placeholders resolve automatically.
- Add `--output-format stream-json --verbose` if you want a machine-readable
  event stream to `tee` into your logs.

> **Gotcha — non-blocking stdio after headless `claude`.** `claude` runs on
> Node, and libuv sets `O_NONBLOCK` on the stdio pipes it inherits. That flag
> lives on the shared pipe description and **outlives the process**, so if your
> orchestrator reads this command's stdout through a pipe (Prefect, Airflow, a CI
> log collector, `| tee`), the next large writer — e.g. `git commit`'s file
> summary — can die with `EAGAIN` / `Resource temporarily unavailable` (exit
> 128). `run-audit.sh` clears `O_NONBLOCK` after each `claude` run; if you
> hand-roll the command and pipe its output, reset it yourself before the next
> big write.

`run-audit.sh` wraps exactly this, plus report verification and the optional PR
steps. Prefer it over hand-rolling the command.

### Prerequisites for headless runs

- `ANTHROPIC_API_KEY`.
- The `gemma-dbt` plugin available (installed, or a checkout for `--plugin-dir`).
- **dbt installed, `dbt deps` run, and a resolvable profile** — the skill's
  Phase A runs `dbt parse` for richer dependency analysis. `dbt parse` does
  **not** query the warehouse and returns no rows, so it is safe to run anywhere
  (including on the Anthropic-API connection). If `dbt parse` can't run, the
  skill degrades gracefully to filename-only analysis.
- A **non-root** user.

---

## 3. Containerize it

`Dockerfile.example` bakes the CLI, dbt + the project's packages, the plugin,
and these scripts into one image — the portable unit every scheduler can run.
Build it, then smoke-test report-only (no git, no PR):

```bash
docker build -f Dockerfile.example -t dbt-audit:dev .
docker run --rm \
  -e ANTHROPIC_API_KEY \
  -v "$PWD/out:/work/dbt/audit" \
  dbt-audit:dev
```

The image bakes the dbt tree at build time, so an audit reflects the code that
was present when the image was built. Rebuild to pick up new dbt changes.

---

## 4. Opening PRs automatically (optional)

Set `OPEN_PR=true` and give `run-audit.sh` a token plus a commit identity. It
creates a date-stamped branch, commits the report, pushes, and opens a PR with
`gh`.

```bash
docker run --rm \
  -e ANTHROPIC_API_KEY \
  -e OPEN_PR=true \
  -e GITHUB_TOKEN \
  -e GIT_AUTHOR_NAME="data-audit-bot" \
  -e GIT_AUTHOR_EMAIL="data-audit-bot@example.com" \
  dbt-audit:dev
```

`GITHUB_TOKEN` can be:
- a **personal access token** (simplest — `repo` scope), or
- a **GitHub App installation token** minted at runtime for a clean bot
  identity (recommended for org repos; the App's private key lives in your
  secret store and you exchange it for a short-lived token before the run).

---

## 5. Optional second pass — conservative auto-fixes

The skill never edits the repo. If you also want a **fixes PR**, set
`RUN_FIXES=true` (requires `OPEN_PR=true`). After the report PR, `run-audit.sh`
runs a **separate, deliberately narrow** headless pass (`fixes-prompt.md`) that:

- applies **only** mechanical fixes — missing `unique`/`not_null` PK tests and
  Gemma SQL-style reformatting — and nothing that changes SQL logic;
- runs on a **separate branch off the original base commit**, so the fixes PR
  contains only code, never the report;
- **verifies `dbt parse` still succeeds**, discarding the edits if not;
- **refuses to open a destructive PR** — before committing it rejects the staged
  diff (and skips the fixes PR) if it contains any deletion or more than
  `FIXES_MAX_FILES` files (default 50). A drifted container tree — a
  `.dockerignore`/`COPY` that omits tracked files, or generated
  `dbt_packages/`/`target/`/`logs/` not covered by `.gitignore` — otherwise turns
  `git add -A` into phantom deletions/additions. "Best-effort, exit 0" protects
  the *job*; this guard is what protects the *repo*;
- is **best-effort** — every failure path exits 0, so a problem here never
  affects the report PR.

This keeps the safe, high-value fixes flowing automatically while leaving
logic, renames, and structural changes for human review.

---

## 6. Scheduling — pick your orchestrator

The container is orchestrator-agnostic. A few common ways to run it on a
schedule:

### cron

```cron
# 02:00 UTC on the 1st of each month
0 2 1 * * docker run --rm -e ANTHROPIC_API_KEY -e OPEN_PR=true \
  -e GITHUB_TOKEN -e GIT_AUTHOR_NAME=... -e GIT_AUTHOR_EMAIL=... dbt-audit:latest
```

### Airflow

Run the image as a task. For Kubernetes-executor setups use
`KubernetesPodOperator`; for a Docker host use `DockerOperator`. Pass secrets
as env (sourced from an Airflow Connection/Variable or a k8s Secret). The
`gemma-airflow` plugin in this toolkit can scaffold the surrounding DAG.

```python
from airflow import DAG
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
import pendulum

with DAG(
    dag_id="dbt_repo_audit",
    schedule="0 2 1 * *",            # monthly, 1st @ 02:00 UTC
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
) as dag:
    KubernetesPodOperator(
        task_id="validate_repo",
        name="dbt-repo-audit",
        image="<registry>/dbt-audit:latest",
        env_vars={
            "OPEN_PR": "true",
            "RUN_FIXES": "true",
            "GIT_AUTHOR_NAME": "data-audit-bot",
            "GIT_AUTHOR_EMAIL": "data-audit-bot@example.com",
        },
        # ANTHROPIC_API_KEY + GITHUB_TOKEN from a mounted k8s Secret:
        secrets=[...],
        get_logs=True,
    )
```

A `BashOperator` / `DockerOperator` that runs the same image with the same env
works equally well — the contract is just "run the container with these env
vars."

### CI on a schedule (GitHub Actions example)

No container registry needed — build (or pull) and run inline:

```yaml
on:
  schedule:
    - cron: "0 2 1 * *"   # monthly
  workflow_dispatch:
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -f Dockerfile.example -t dbt-audit:ci .
      - run: |
          docker run --rm \
            -e ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} \
            -e OPEN_PR=true -e RUN_FIXES=true \
            -e GITHUB_TOKEN=${{ secrets.AUDIT_BOT_TOKEN }} \
            -e GIT_AUTHOR_NAME="data-audit-bot" \
            -e GIT_AUTHOR_EMAIL="data-audit-bot@example.com" \
            dbt-audit:ci
```

---

## 7. Cost & runtime

The skill fans out six sub-agents; cost scales with repo size (see the table in
`../SKILL.md`). As a rule of thumb a medium repo is ~5–10 min and a few hundred
thousand tokens for the report. Enabling `RUN_FIXES` roughly doubles both,
since it is a second full model pass.

Schedule accordingly — monthly is a sensible default for an ongoing project.
