Add CI (Forgejo Actions) + green quality baseline: run the declared dev tools (black/ruff/mypy) and gate on them #31

Closed
opened 2026-07-02 17:04:57 +00:00 by coilysiren · 2 comments
Owner

Problem

agent-proxy has no CI and its declared dev tooling is never actually run, so every landing is ungated and formatting/typing drift accumulates silently. This is pure tower-independent cleanup - none of it needs the tower reachable, and all of it is locally verifiable with ward test and the dev tools already in pyproject.toml.

Scoped from the read-only director surface. Baseline observed on main at filing time:

  • Tests: 58 passed, ward test green.
  • ruff: uv run ruff check . - all checks pass.
  • black: uv run black --check . wants to reformat 17 of 23 files. There is no [tool.black], [tool.ruff], or [tool.mypy] config in pyproject.toml, so the declared dev tools have never been run against the tree.
  • mypy: uv run mypy app reports 17 errors in 5 files - a mix of missing third-party stubs (boto3 in config.py, sentry_sdk in obs.py) and real type gaps (queue.py None-handling on the optional Queue, resilience.py passing RequestTraceContext | None where non-optional is expected in 7 spots, obs.py dict variance, main.py:410 hypercorn serve arg type).
  • No .forgejo/workflows/ directory exists.

Why these couple

CI is only worth adding if the tree is green under the gates it runs. If a workflow lands while black and mypy are red, CI is red on arrival. So the format-normalization and the mypy cleanup are prerequisites for the CI gate, and all three belong in one coherent landing rather than parallel runs.

Scope (do all of it in one run, commit in clean stages)

  1. Format normalization (black). Add [tool.black] with line-length = 100 (or match whatever the codebase already trends to - pick one and set it), add a matching [tool.ruff] line-length, then run uv run black . and commit the reformat as its own commit so the CI commit stays reviewable. Keep black as the formatter - it is already a declared dev dependency, no new deps.
  2. mypy config + fixes. Add [tool.mypy] with ignore_missing_imports overrides (or per-module [[tool.mypy.overrides]]) for boto3 and sentry_sdk, then fix the real type errors in queue.py, resilience.py, obs.py, and main.py so uv run mypy app is clean. If any single fix looks behavior-risky or uncertain, do NOT force it - make mypy advisory (non-blocking / continue-on-error in CI) for that case and file a follow-up issue naming the residual, rather than changing runtime behavior to satisfy the checker.
  3. CI workflow (.forgejo/workflows/ci.yml). On push and PR, run the full local gate off pyproject.toml: uv sync --extra dev, then ward test (pytest), uv run ruff check ., uv run black --check ., and uv run mypy app. Also run the daemonless smoke paths that need no Docker daemon: ./boot_probe.sh and ./test-fixes.sh. Do NOT put the Docker-daemon acceptance test (test-container.sh) or any tower-dependent script (scripts/truncation_proof.py, scripts/reliability_loop.py) in CI - those need a daemon or the tower and would break a daemonless runner.
  4. Wire test-fixes.sh into ward.yaml as a verb (e.g. smoke: ./test-fixes.sh) so the orphan daemonless smoke test is discoverable and CI calls it through ward, consistent with the other verbs.
  5. Docs. If CI is a user-facing capability, add it to docs/FEATURES.md per the trifecta rule, and mention the CI gate in README.md's developer-workflow section.

Constraints

  • Everything here must pass on a daemonless runner with no tower. Verify locally before landing: ward test, uv run ruff check ., uv run black --check ., uv run mypy app, ./boot_probe.sh, and ./test-fixes.sh all green.
  • Assumes the Forgejo instance has an Actions runner available for this repo. If it does not, the workflow is still the correct artifact - note the assumption in the PR/commit and land it anyway.
  • No behavior changes to the proxy runtime beyond what the mypy real-error fixes strictly require. This is a hygiene pass, not a refactor.

Done condition

.forgejo/workflows/ci.yml exists and encodes the gate above, the tree is green under black/ruff/mypy/pytest/boot_probe/test-fixes locally, ward.yaml exposes the smoke verb, docs updated, landed on main.

## Problem `agent-proxy` has no CI and its declared dev tooling is never actually run, so every landing is ungated and formatting/typing drift accumulates silently. This is pure tower-independent cleanup - none of it needs the tower reachable, and all of it is locally verifiable with `ward test` and the dev tools already in `pyproject.toml`. Scoped from the read-only director surface. Baseline observed on `main` at filing time: - **Tests**: 58 passed, `ward test` green. - **ruff**: `uv run ruff check .` - all checks pass. - **black**: `uv run black --check .` wants to reformat **17 of 23 files**. There is no `[tool.black]`, `[tool.ruff]`, or `[tool.mypy]` config in `pyproject.toml`, so the declared dev tools have never been run against the tree. - **mypy**: `uv run mypy app` reports **17 errors in 5 files** - a mix of missing third-party stubs (`boto3` in `config.py`, `sentry_sdk` in `obs.py`) and real type gaps (`queue.py` `None`-handling on the optional `Queue`, `resilience.py` passing `RequestTraceContext | None` where non-optional is expected in 7 spots, `obs.py` dict variance, `main.py:410` hypercorn `serve` arg type). - **No `.forgejo/workflows/`** directory exists. ## Why these couple CI is only worth adding if the tree is green under the gates it runs. If a workflow lands while black and mypy are red, CI is red on arrival. So the format-normalization and the mypy cleanup are prerequisites for the CI gate, and all three belong in one coherent landing rather than parallel runs. ## Scope (do all of it in one run, commit in clean stages) 1. **Format normalization (black).** Add `[tool.black]` with `line-length = 100` (or match whatever the codebase already trends to - pick one and set it), add a matching `[tool.ruff]` `line-length`, then run `uv run black .` and commit the reformat as its own commit so the CI commit stays reviewable. Keep black as the formatter - it is already a declared dev dependency, no new deps. 2. **mypy config + fixes.** Add `[tool.mypy]` with `ignore_missing_imports` overrides (or per-module `[[tool.mypy.overrides]]`) for `boto3` and `sentry_sdk`, then fix the real type errors in `queue.py`, `resilience.py`, `obs.py`, and `main.py` so `uv run mypy app` is clean. If any single fix looks behavior-risky or uncertain, do NOT force it - make mypy advisory (non-blocking / `continue-on-error` in CI) for that case and file a follow-up issue naming the residual, rather than changing runtime behavior to satisfy the checker. 3. **CI workflow (`.forgejo/workflows/ci.yml`).** On push and PR, run the full local gate off `pyproject.toml`: `uv sync --extra dev`, then `ward test` (pytest), `uv run ruff check .`, `uv run black --check .`, and `uv run mypy app`. Also run the daemonless smoke paths that need no Docker daemon: `./boot_probe.sh` and `./test-fixes.sh`. Do NOT put the Docker-daemon acceptance test (`test-container.sh`) or any tower-dependent script (`scripts/truncation_proof.py`, `scripts/reliability_loop.py`) in CI - those need a daemon or the tower and would break a daemonless runner. 4. **Wire `test-fixes.sh` into `ward.yaml`** as a verb (e.g. `smoke: ./test-fixes.sh`) so the orphan daemonless smoke test is discoverable and CI calls it through ward, consistent with the other verbs. 5. **Docs.** If CI is a user-facing capability, add it to `docs/FEATURES.md` per the trifecta rule, and mention the CI gate in `README.md`'s developer-workflow section. ## Constraints - Everything here must pass on a **daemonless** runner with **no tower**. Verify locally before landing: `ward test`, `uv run ruff check .`, `uv run black --check .`, `uv run mypy app`, `./boot_probe.sh`, and `./test-fixes.sh` all green. - Assumes the Forgejo instance has an Actions runner available for this repo. If it does not, the workflow is still the correct artifact - note the assumption in the PR/commit and land it anyway. - No behavior changes to the proxy runtime beyond what the mypy real-error fixes strictly require. This is a hygiene pass, not a refactor. ## Done condition `.forgejo/workflows/ci.yml` exists and encodes the gate above, the tree is green under black/ruff/mypy/pytest/boot_probe/test-fixes locally, `ward.yaml` exposes the smoke verb, docs updated, landed on `main`.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-agent-proxy-31 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-02T17:05:06Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-agent-proxy-31` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-02T17:05:06Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - .forgejo/workflows/ci.yml gates pytest/ruff/black/mypy + daemonless smoke; tree normalized and green under all four, landed on main.

Honestly, this one behaved. The baseline in the issue was exact - 17 unformatted files, 17 mypy errors, same files - so there were no surprises hiding in the diff. The reformat is pure mechanical churn, staged on its own commit so the CI landing stays readable.

The mypy pass was more satisfying than expected: the 7 resilience.py errors and the 3 obs.py variance errors all collapsed into one real fix - request_log_fields genuinely could not take the None its callers were feeding it, so teaching it to accept RequestTraceContext | None was a correctness improvement the checker surfaced, not a checker-appeasement hack. The queue.py future-guarding and the single scoped type: ignore on hypercorn's over-narrow serve stub were the only truly defensive touches. No behavior changed beyond that None-safety hardening.

The one judgement call: the issue asked CI to invoke ward test / ward smoke, but ward is a Go binary from a private brew tap and the stock docker runner has none of the flight-deck tooling on PATH (cli-guard and infrastructure/lint.yml both install their own tools and call uv run directly). Requiring ward would make CI red on arrival, which the issue explicitly warns against. So CI calls the raw uv run / script commands, and ward.yaml keeps the smoke verb as the local source of truth. Worth noting: ward <verb> doesn't resolve config for any verb in this container (test/boot-probe included), so that quirk is orthogonal to my change.

Confidence is high - every gate is verified locally green (pytest 58, ruff, black --check, mypy clean, boot_probe, test-fixes). The only unverifiable piece is whether an Actions runner labelled docker is actually attached to this repo; if not, the workflow is still the correct artifact and starts gating the moment one is. No follow-ups worth filing.

— Claude (she/her), via ward agent

WARD-OUTCOME: done - `.forgejo/workflows/ci.yml` gates pytest/ruff/black/mypy + daemonless smoke; tree normalized and green under all four, landed on `main`. Honestly, this one behaved. The baseline in the issue was exact - 17 unformatted files, 17 mypy errors, same files - so there were no surprises hiding in the diff. The reformat is pure mechanical churn, staged on its own commit so the CI landing stays readable. The mypy pass was more satisfying than expected: the 7 `resilience.py` errors and the 3 `obs.py` variance errors all collapsed into one real fix - `request_log_fields` genuinely could not take the `None` its callers were feeding it, so teaching it to accept `RequestTraceContext | None` was a correctness improvement the checker surfaced, not a checker-appeasement hack. The `queue.py` future-guarding and the single scoped `type: ignore` on hypercorn's over-narrow `serve` stub were the only truly defensive touches. No behavior changed beyond that None-safety hardening. The one judgement call: the issue asked CI to invoke `ward test` / `ward smoke`, but `ward` is a Go binary from a private brew tap and the stock `docker` runner has none of the flight-deck tooling on PATH (cli-guard and infrastructure/lint.yml both install their own tools and call `uv run` directly). Requiring `ward` would make CI red on arrival, which the issue explicitly warns against. So CI calls the raw `uv run` / script commands, and `ward.yaml` keeps the `smoke` verb as the local source of truth. Worth noting: `ward <verb>` doesn't resolve config for *any* verb in this container (test/boot-probe included), so that quirk is orthogonal to my change. Confidence is high - every gate is verified locally green (pytest 58, ruff, black --check, mypy clean, boot_probe, test-fixes). The only unverifiable piece is whether an Actions runner labelled `docker` is actually attached to this repo; if not, the workflow is still the correct artifact and starts gating the moment one is. No follow-ups worth filing. — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No description provided.