feat: add --changed fast mode to maintenance agents restricting scope to git-modified files under devplacepy/ and tests/

Implement a new `--changed` flag for the maintenance agent fleet that limits checking and fixing to only files git reports as modified or new (untracked) under `devplacepy/` and `tests/`. The change introduces `agents/changed.py` with `changed_paths()` parsing `git status --porcelain`, adds `_WRITE_ALLOWLIST` guard logic in `agents/agent.py` (`set_write_allowlist`, `clear_write_allowlist`, `_allowlist_guard`) wired into `_mutation_guard` and `create_file`, threads the file list through `orchestrator.run_fleet` -> `agent.run` -> `_execute` -> `task_prompt` with a dedicated "CHANGED-FILES RUN" prompt branch, and exposes `make maintenance` (read-only) and `make maintenance-fix` (fix mode) targets in the Makefile. Documentation is updated in `AGENTS.md`, `CLAUDE.md`, and `README.md`.
This commit is contained in:
2026-06-12 06:15:19 +00:00
parent 425e9bdca1
commit 3ef1d02265
12 changed files with 220 additions and 23 deletions
@@ -75,6 +75,25 @@ time in dependency order, so file changes never collide.
This is the command to run before a release, or in a continuous-integration job
(in `CHECK=1` mode it returns a non-zero exit code if anything is wrong).
## Checking only what you changed
While you work, you usually only want the fleet to look at the files you just
touched, not the whole project. These two targets run the whole fleet but limit it
to the files git reports as modified or new (untracked) under `devplacepy/` and
`tests/`, so a sweep takes seconds instead of minutes:
```bash
make maintenance # report only, all agents at once, just your changed files
make maintenance-fix # fix, just your changed files
```
`make maintenance` is read-only and concurrent, exactly like `make agents-all
CHECK=1` but narrowed to your work in progress. `make maintenance-fix` fixes, and a
built-in safety rule guarantees it can only edit the files in that changed set,
never anything else. If nothing under `devplacepy/` or `tests/` has changed, the run
prints "nothing to do" and exits cleanly. The same scope is available on a single
agent with the `--changed` flag, for example `python -m agents.security --changed`.
## What you see while it runs
The output is meant to be read live, so you always know what is happening:
+3 -3
View File
@@ -18,11 +18,11 @@ The `make` targets that produce coverage:
| Target | What it does |
|--------|--------------|
| `make coverage` | Runs the full suite single-process (`-p no:xdist`) under coverage, then `coverage combine` and `coverage report`. |
| `make coverage` | Runs the full serial suite under coverage, then `coverage combine` and `coverage report`. |
| `make coverage-headed` | The same, with a visible browser. |
| `make coverage-html` | Runs `make coverage`, then `coverage html` and writes the browsable report to `htmlcov/index.html`. |
Coverage runs single-process on purpose: xdist is disabled so the combined profile is stable and the subprocess data merges cleanly.
The whole suite already runs in a single process (serial execution is enforced globally in `pyproject.toml`), so `coverage combine` only has to merge two profiles: the test process and the application's Uvicorn subprocess. There are no per-worker profiles to reconcile, which keeps the combined report stable.
## Continuous integration: Gitea Actions
@@ -31,7 +31,7 @@ The pipeline is defined in `.gitea/workflows/test.yaml` and runs on every **push
1. Check out the repository.
2. Set up Python 3.13.
3. Install dependencies: `pip install -e ".[dev]"` and `python -m playwright install chromium --with-deps`.
4. Run the full suite under coverage: `python -m coverage run -m pytest tests/ -p no:xdist --tb=line`, with `PLAYWRIGHT_HEADLESS=1` and `COVERAGE_PROCESS_START` pointing at `.coveragerc`.
4. Run the full serial suite under coverage: `python -m coverage run -m pytest tests/`, with `PLAYWRIGHT_HEADLESS=1` and `COVERAGE_PROCESS_START` pointing at `.coveragerc`. The serial flags (`--tb=line -p no:xdist`) come from `addopts` in `pyproject.toml`, so the command line stays minimal.
5. Build the coverage report (`coverage combine`, `coverage report`, `coverage html`). This step runs with `if: always()` so a report is produced even when tests fail.
6. Publish the `coverage-html` artifact (the `htmlcov/` directory), always.
7. On failure only, upload the `failure-screenshots` artifact from `/tmp/devplace_test_screenshots/` (saved by the `pytest_runtest_makereport` hook in `conftest.py`).
@@ -13,7 +13,7 @@ All fixtures live in `tests/conftest.py`. They layer from a shared server up to
| Fixture | Scope | Purpose |
|---------|-------|---------|
| `app_server` | session | Spawns Uvicorn as a subprocess on a per-worker port with a tempfile SQLite database, then waits for it to answer. |
| `app_server` | session | Spawns Uvicorn as a subprocess on port 10501 with a tempfile SQLite database, then waits for it to answer. |
| `playwright_instance` / `browser` | session | One Playwright runtime and one Chromium browser for the whole run. |
| `browser_context` | module | One shared browser context. |
| `page` | function | Clears cookies on the shared context, then yields a fresh page. |
@@ -22,9 +22,11 @@ All fixtures live in `tests/conftest.py`. They layer from a shared server up to
In-process tests skip the browser entirely and call a FastAPI `TestClient` or run coroutines through the `run_async` helper, which executes them on a worker thread and refreshes the database snapshot afterward.
## Isolation and ports
## Serial execution, isolation, and ports
Each xdist worker runs its own server on its own port, derived from the worker index: master uses `10501`, `gw0` uses `10501`, `gw1` uses `10502`, and so on. Every worker gets a separate tempfile database. Background services are disabled in tests via `DEVPLACE_DISABLE_SERVICES=1`, and the rate limit is seeded to `1000000` so the suite is never throttled.
The suite runs **serially, one test at a time, in a single process**. There is one server on port `10501`, backed by one tempfile database and one `DEVPLACE_DATA_DIR`, shared for the whole session. Serial execution is enforced centrally in `pyproject.toml` (`[tool.pytest.ini_options]` sets `addopts = "--tb=line -p no:xdist"`), so passing `-n` is rejected and the suite can never run concurrently. Background services are disabled in tests via `DEVPLACE_DISABLE_SERVICES=1`, and the rate limit is seeded to `1000000` so the suite is never throttled.
Because every test shares one server and one database, tests that mutate global or seeded state MUST clean up after themselves (see the `try/finally` rule below), and a test that changes a cached user's row through a direct database write should invalidate the auth cache (`clear_user_cache(uid)`) so the running server observes the change.
## Mandatory patterns
+5 -5
View File
@@ -5,7 +5,7 @@ Every test workflow has a `make` target so the commands are short and identical
## Setup
Install the package with its development extras (`pytest`, Playwright, xdist, coverage) and the browser binary once:
Install the package with its development extras (`pytest`, Playwright, coverage) and the browser binary once:
```bash
make install # pip install -e .
@@ -17,13 +17,13 @@ python -m playwright install chromium # browser used by the E2E suite
| Target | What it does |
|--------|--------------|
| `make test` | Full suite headless, fail-fast (`-x`), `--tb=line`. The everyday command. |
| `make test` | Full suite headless, serial (one test at a time), fail-fast (`-x`). The everyday command. |
| `make test-headed` | The same suite in a visible browser, for watching or debugging an E2E test. |
| `make coverage` | Full suite under coverage, single process (`-p no:xdist`), then prints a coverage report. |
| `make coverage` | Full suite under coverage, then prints a coverage report. |
| `make coverage-headed` | Coverage run with a visible browser. |
| `make coverage-html` | Runs `coverage`, then writes the browsable HTML report to `htmlcov/index.html`. |
`make test` sets `PLAYWRIGHT_HEADLESS=1`; `make test-headed` sets it to `0`. The coverage targets disable xdist so coverage is collected in one process, which is also exactly how CI runs.
`make test` sets `PLAYWRIGHT_HEADLESS=1`; `make test-headed` sets it to `0`. The whole suite runs in a single process: serial execution is enforced in `pyproject.toml` (`addopts = "--tb=line -p no:xdist"`), so there are no parallel workers and the coverage targets collect everything in one process, exactly how CI runs.
To run one test instead of the suite, call pytest directly:
@@ -48,5 +48,5 @@ make clean # remove __pycache__, *.pyc, the egg-info, and .venv
## What CI runs
`.gitea/workflows/test.yaml` mirrors these targets: it installs `".[dev]"` and Chromium, then runs the suite under coverage with `-p no:xdist`, publishes the coverage HTML, and on failure uploads the screenshots from `/tmp/devplace_test_screenshots/`. Running `make coverage` locally reproduces the CI result before you push.
`.gitea/workflows/test.yaml` mirrors these targets: it installs `".[dev]"` and Chromium, then runs the serial suite under coverage (`coverage run -m pytest tests/`), publishes the coverage HTML, and on failure uploads the screenshots from `/tmp/devplace_test_screenshots/`. Running `make coverage` locally reproduces the CI result before you push.
</div>