chore: replace python -m agents.validator with hawk in all agent mode instructions

This commit is contained in:
2026-06-14 00:36:10 +00:00
parent 64c8c967e5
commit 1076696dec
85 changed files with 682 additions and 5442 deletions
+17 -3
View File
@@ -3255,10 +3255,11 @@ status and report.
title="Queue an SEO audit",
summary="Start a background SEO audit of a URL or sitemap. Returns the job uid plus status and websocket URLs.",
auth="public",
encoding="form",
params=[
field("url", "body", "string", True, "https://example.com", "Page URL or sitemap.xml URL to audit."),
field("mode", "body", "string", False, "url", "'url' (single page) or 'sitemap' (crawl)."),
field("max_pages", "body", "integer", False, "10", "Max pages to crawl in sitemap mode (1-50)."),
field("url", "form", "string", True, "https://example.com", "Page URL or sitemap.xml URL to audit."),
field("mode", "form", "enum", False, "url", "'url' (single page) or 'sitemap' (crawl).", ["url", "sitemap"]),
field("max_pages", "form", "integer", False, "10", "Max pages to crawl in sitemap mode (1-50)."),
],
sample_response={
"uid": "SEO_JOB_UID",
@@ -3328,6 +3329,19 @@ status and report.
"generated_at": "2026-06-14T10:00:18+00:00",
},
),
endpoint(
id="tools-seo-screenshot",
method="GET",
path="/tools/seo/{uid}/screenshot/{index}",
title="SEO audit page screenshot",
summary="Stream the rendered screenshot (image/png) captured for the audited page at the given zero-based index.",
auth="public",
interactive=True,
params=[
field("uid", "path", "string", True, "SEO_JOB_UID", "SEO job uid of a finished audit."),
field("index", "path", "integer", True, "0", "Zero-based index of the audited page."),
],
),
],
},
{
+13
View File
@@ -8,6 +8,8 @@ import socket
from typing import Any
from urllib.parse import urlparse
import httpx
NAT64_PREFIXES = (
ipaddress.ip_network("64:ff9b::/96"),
ipaddress.ip_network("64:ff9b:1::/48"),
@@ -60,3 +62,14 @@ async def guard_public_url(url: str, *, allow_private: bool = False) -> str:
f"Refusing to reach a private or local address ({effective_address(address)})."
)
return host
class _GuardedTransport(httpx.AsyncHTTPTransport):
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
await guard_public_url(str(request.url))
return await super().handle_async_request(request)
def guarded_async_client(**kwargs: Any) -> httpx.AsyncClient:
transport = _GuardedTransport()
return httpx.AsyncClient(transport=transport, **kwargs)
-15
View File
@@ -14,7 +14,6 @@ SECTION_SERVICES = "Services"
SECTION_ARCH = "Architecture"
SECTION_TESTING = "Testing"
SECTION_PROD = "Production"
SECTION_MAINTENANCE = "Maintenance agents"
SECTION_CLAUDE = "Claude Code"
AUDIENCE_START = "Start here"
@@ -33,7 +32,6 @@ AUDIENCES = [
SECTION_DEVII,
SECTION_BOTS,
SECTION_TESTING,
SECTION_MAINTENANCE,
SECTION_CLAUDE,
],
),
@@ -80,19 +78,6 @@ DOCS_PAGES = [
"kind": "prose",
"section": SECTION_TOOLS,
},
# Maintenance agents - the self-maintaining quality fleet (public)
{
"slug": "maintenance-agents",
"title": "Maintenance agents",
"kind": "prose",
"section": SECTION_MAINTENANCE,
},
{
"slug": "maintenance-usage",
"title": "Running the agents",
"kind": "prose",
"section": SECTION_MAINTENANCE,
},
# Claude Code - the native subagent, command, and workflow setup under .claude/
{
"slug": "claude",
+25 -8
View File
@@ -11,7 +11,7 @@ from devplacepy.config import SEO_REPORTS_DIR
from devplacepy.models import SeoRunForm
from devplacepy.responses import respond
from devplacepy.schemas import SeoJobOut, SeoReportOut
from devplacepy.seo import list_page_seo
from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.seo.progress import hub
from devplacepy.services.manager import service_manager
@@ -60,18 +60,26 @@ def _status_payload(job: dict) -> dict:
@router.get("", response_class=HTMLResponse)
async def seo_page(request: Request):
user = get_current_user(request)
seo_ctx = list_page_seo(
base = site_url(request)
description = (
"Run a deep SEO audit on any URL or sitemap: technical, on-page, structured "
"data, Core Web Vitals, accessibility and AI-readiness checks with live progress."
)
seo_ctx = base_seo_context(
request,
title="SEO Diagnostics",
description=(
"Run a deep SEO audit on any URL or sitemap: technical, on-page, structured "
"data, Core Web Vitals, accessibility and AI-readiness checks with live progress."
),
description=description,
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Tools", "url": "/tools"},
{"name": "SEO Diagnostics", "url": "/tools/seo"},
],
schemas=[
website_schema(base),
web_application_schema(
"SEO Diagnostics", description, "/tools/seo", base
),
],
)
return templates.TemplateResponse(
request,
@@ -88,7 +96,17 @@ async def seo_run(request: Request, data: Annotated[SeoRunForm, Form()]):
for job in queue.list_jobs(kind="seo", owner=(owner_kind, owner_id))
if job.get("status") in ACTIVE_STATES
]
from devplacepy.services.audit import record as audit
if active:
audit.record(
request,
"seo.run.request",
result="denied",
summary=f"SEO diagnostics denied for {data.url}: audit already running",
metadata={"target": data.url, "reason": "active_job", "uid": active[0]["uid"]},
links=[audit.job(active[0]["uid"])],
)
return JSONResponse(
{
"error": {
@@ -106,8 +124,6 @@ async def seo_run(request: Request, data: Annotated[SeoRunForm, Form()]):
owner_id,
f"SEO: {data.url}"[:64],
)
from devplacepy.services.audit import record as audit
audit.record(
request,
"seo.run.request",
@@ -157,6 +173,7 @@ async def seo_report(request: Request, uid: str):
"generated_at": report.get("generated_at"),
"request": request,
"user": get_current_user(request),
"meta_robots": "noindex,nofollow",
}
return respond(request, "tools/seo_report.html", context, model=SeoReportOut)
+14 -2
View File
@@ -137,6 +137,20 @@ def software_application_schema(project, base_url):
}
def web_application_schema(name, description, path, base_url, category="DeveloperApplication"):
return {
"@type": "WebApplication",
"name": name,
"description": truncate(description, 300),
"url": f"{base_url}{path}",
"applicationCategory": category,
"operatingSystem": "All",
"browserRequirements": "Requires JavaScript",
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
"provider": {"@type": "Organization", "name": SITE_NAME, "url": base_url},
}
def organization_schema(base_url):
return {
"@type": "Organization",
@@ -444,8 +458,6 @@ def _build_sitemap(base_url):
"devii",
"tools-seo",
"media-gallery",
"maintenance-agents",
"maintenance-usage",
"components",
"component-dp-avatar",
"component-dp-code",
@@ -6,6 +6,7 @@ from .base import (
HIGH,
MEDIUM,
LOW,
Check,
PageContext,
SiteContext,
ok_check,
@@ -16,7 +17,7 @@ CATEGORY = "mobile_accessibility"
@page_check
def responsive(page: PageContext, site: SiteContext) -> PageContext:
def responsive(page: PageContext, site: SiteContext) -> Check:
mobile = page.mobile or {}
overflow = bool(mobile.get("hasHorizontalOverflow"))
return ok_check(
@@ -33,7 +34,7 @@ def responsive(page: PageContext, site: SiteContext) -> PageContext:
@page_check
def tap_targets(page: PageContext, site: SiteContext):
def tap_targets(page: PageContext, site: SiteContext) -> Check | None:
mobile = page.mobile or {}
small = int(mobile.get("smallTapTargets", 0) or 0)
if "smallTapTargets" not in mobile:
@@ -52,7 +53,7 @@ def tap_targets(page: PageContext, site: SiteContext):
@page_check
def image_alt(page: PageContext, site: SiteContext):
def image_alt(page: PageContext, site: SiteContext) -> Check | None:
images = page.dom.get("images", []) or []
if not images:
return None
@@ -71,7 +72,7 @@ def image_alt(page: PageContext, site: SiteContext):
@page_check
def form_labels(page: PageContext, site: SiteContext):
def form_labels(page: PageContext, site: SiteContext) -> Check | None:
missing = int(page.dom.get("formsMissingLabels", 0) or 0)
if not page.dom.get("formFieldCount"):
return None
@@ -89,7 +90,7 @@ def form_labels(page: PageContext, site: SiteContext):
@page_check
def document_title(page: PageContext, site: SiteContext):
def document_title(page: PageContext, site: SiteContext) -> Check:
return ok_check(
"a11y.document_title",
CATEGORY,
@@ -26,7 +26,7 @@ from . import ai_readiness # noqa: F401
from . import crosspage # noqa: F401
def _collect(result) -> list[Check]:
def _collect(result: object) -> list[Check]:
if result is None:
return []
if isinstance(result, Check):
@@ -5,6 +5,7 @@ from __future__ import annotations
from .base import (
MEDIUM,
LOW,
Check,
PageContext,
SiteContext,
ok_check,
@@ -43,7 +44,7 @@ def security_headers(page: PageContext, site: SiteContext) -> list:
@page_check
def tls_valid(page: PageContext, site: SiteContext):
def tls_valid(page: PageContext, site: SiteContext) -> Check | None:
if "tlsValid" not in page.metrics:
return None
valid = bool(page.metrics.get("tlsValid"))
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
from typing import Iterator
from .base import (
MEDIUM,
@@ -39,7 +40,7 @@ REQUIRED_PROPS = {
}
def _iter_objects(parsed):
def _iter_objects(parsed: object) -> Iterator[dict]:
if isinstance(parsed, list):
for item in parsed:
yield from _iter_objects(item)
@@ -54,7 +55,7 @@ def _iter_objects(parsed):
yield parsed
def _types(node) -> list:
def _types(node: dict) -> list:
raw = node.get("@type")
if isinstance(raw, list):
return [str(item) for item in raw]
+23 -9
View File
@@ -3,12 +3,18 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Any, Callable
from urllib.parse import urljoin, urlparse
from xml.etree import ElementTree
import httpx
from devplacepy.net_guard import BlockedAddressError, guard_public_url
from devplacepy.net_guard import (
BlockedAddressError,
guard_public_url,
guarded_async_client,
)
from .checks.base import PageContext, SiteContext
USER_AGENT = (
@@ -185,7 +191,7 @@ async def _fetch_text(client: httpx.AsyncClient, url: str) -> dict:
response = await client.get(url)
body = response.text[:MAX_RAW_BYTES]
return {"status": response.status_code, "text": body, "url": str(response.url)}
except httpx.HTTPError as exc:
except (httpx.HTTPError, BlockedAddressError) as exc:
return {"status": 0, "text": "", "url": url, "error": str(exc)[:200]}
@@ -256,12 +262,18 @@ async def _site_resources(
return robots, sitemap, llms
async def _build_page(context, client, url: str, output_dir, index: int) -> PageContext:
async def _build_page(
context: Any,
client: httpx.AsyncClient,
url: str,
output_dir: Path | None,
index: int,
) -> PageContext:
page_ctx = PageContext(requested_url=url, url=url)
page = await context.new_page()
console_errors = []
console_errors: list[str] = []
def _on_console(message):
def _on_console(message: Any) -> None:
if message.type == "error":
console_errors.append(message.text[:200])
@@ -286,6 +298,8 @@ async def _build_page(context, client, url: str, output_dir, index: int) -> Page
chain.append({"url": req.url, "status": resp.status if resp else 0})
req = req.redirected_from
page_ctx.redirect_chain = list(reversed(chain))
for hop in [*(h["url"] for h in page_ctx.redirect_chain), page_ctx.url]:
await guard_public_url(hop)
dom = await page.evaluate(EXTRACT_SCRIPT)
page_ctx.metrics = dom.pop("metrics", {})
page_ctx.metrics["consoleErrors"] = len(console_errors)
@@ -315,7 +329,9 @@ async def _build_page(context, client, url: str, output_dir, index: int) -> Page
return page_ctx
async def crawl_target(payload: dict, emit, output_dir) -> SiteContext:
async def crawl_target(
payload: dict, emit: Callable[[dict], None], output_dir: Path | None
) -> SiteContext:
target = _normalize_url(payload.get("url", ""))
mode = payload.get("mode", "url")
allow_private = bool(payload.get("allow_private"))
@@ -332,7 +348,7 @@ async def crawl_target(payload: dict, emit, output_dir) -> SiteContext:
from playwright.async_api import async_playwright
async with httpx.AsyncClient(
async with guarded_async_client(
follow_redirects=True,
timeout=RAW_FETCH_TIMEOUT,
headers={"User-Agent": USER_AGENT},
@@ -355,8 +371,6 @@ async def crawl_target(payload: dict, emit, output_dir) -> SiteContext:
for url in candidates:
host = urlparse(url).netloc
if host and host == site.base_host:
# Same host as the audited target, already validated by the
# top-level guard above; skip the redundant per-URL DNS lookup.
safe_urls.append(url)
continue
try:
+4 -3
View File
@@ -8,6 +8,7 @@ import sys
from datetime import datetime, timezone
from pathlib import Path
from .checks.base import SiteContext
from .checks.registry import compute_score, run_page_checks, run_site_checks
from .crawler import crawl_target
@@ -17,7 +18,7 @@ def _emit(frame: dict) -> None:
sys.stdout.flush()
def _trim_site(site) -> dict:
def _trim_site(site: SiteContext) -> dict:
robots = site.robots or {}
sitemap = site.sitemap or {}
return {
@@ -40,8 +41,8 @@ def _trim_site(site) -> dict:
async def _run(payload: dict, output_dir: Path) -> dict:
site = await crawl_target(payload, _emit, output_dir)
all_checks = []
pages_summary = []
all_checks: list = []
pages_summary: list[dict] = []
_emit({"type": "stage", "stage": "checks", "message": "Running SEO checks"})
for page in site.pages:
page_checks = run_page_checks(page, site)
+19 -15
View File
@@ -77,11 +77,15 @@
color: var(--text-secondary);
}
.seo-pages[hidden] {
display: none;
}
.seo-run-btn { flex-shrink: 0; }
.seo-form-error {
margin: 0.625rem 0 0;
color: var(--danger, #e5484d);
color: var(--danger);
font-size: 0.875rem;
}
@@ -124,7 +128,7 @@
padding: 0;
max-height: 200px;
overflow-y: auto;
font-family: var(--font-mono, monospace);
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text-secondary);
}
@@ -161,11 +165,11 @@
.seo-gauge-score { font-size: 1.75rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
.seo-gauge-grade { font-size: 0.875rem; font-weight: 600; color: var(--text-muted); }
.seo-grade-a { border-color: #30a46c; }
.seo-grade-b { border-color: #5dbb7a; }
.seo-grade-c { border-color: #e2a336; }
.seo-grade-d { border-color: #e08c3b; }
.seo-grade-f { border-color: #e5484d; }
.seo-grade-a { border-color: var(--success); }
.seo-grade-b { border-color: var(--success); }
.seo-grade-c { border-color: var(--warning); }
.seo-grade-d { border-color: var(--warning); }
.seo-grade-f { border-color: var(--danger); }
.seo-score-meta h2 { margin: 0 0 0.5rem; font-size: 1.0625rem; word-break: break-all; }
.seo-report-sub { color: var(--text-muted); font-size: 0.8125rem; margin: 0.375rem 0 0; }
@@ -180,9 +184,9 @@
border: 1px solid var(--border);
}
.seo-chip-pass { color: #30a46c; }
.seo-chip-warn { color: #e2a336; }
.seo-chip-fail { color: #e5484d; }
.seo-chip-pass { color: var(--success); }
.seo-chip-warn { color: var(--warning); }
.seo-chip-fail { color: var(--danger); }
.seo-chip-info { color: var(--text-muted); }
.seo-report-link {
@@ -228,9 +232,9 @@
background: var(--bg-card);
}
.seo-check-pass { border-left-color: #30a46c; }
.seo-check-warn { border-left-color: #e2a336; }
.seo-check-fail { border-left-color: #e5484d; }
.seo-check-pass { border-left-color: var(--success); }
.seo-check-warn { border-left-color: var(--warning); }
.seo-check-fail { border-left-color: var(--danger); }
.seo-check-info { border-left-color: var(--text-muted); }
.seo-check-skip { border-left-color: var(--border); }
@@ -259,8 +263,8 @@
text-transform: uppercase;
}
.seo-sev-critical { color: #e5484d; }
.seo-sev-high { color: #e08c3b; }
.seo-sev-critical { color: var(--danger); }
.seo-sev-high { color: var(--warning); }
.seo-empty {
padding: 1.5rem;
+2 -15
View File
@@ -55,24 +55,11 @@ export class SeoDiagnostics {
};
this._resetLive();
try {
const response = await fetch("/tools/seo/run", {
method: "POST",
headers: {
"X-Requested-With": "fetch",
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(params),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
this._setError((data.error && data.error.message) || "Could not start the audit.");
this.live.hidden = true;
return;
}
const data = await Http.send("/tools/seo/run", params);
this.uid = data.uid;
this._watch(data.uid);
} catch (error) {
this._setError("Could not start the audit.");
this._setError(error.message || "Could not start the audit.");
this.live.hidden = true;
}
}
+1 -8
View File
@@ -30,7 +30,7 @@ Each subagent operates in one of two modes, chosen by how it is invoked.
- **Report** (default): record findings only, change nothing.
- **Fix**: apply a minimal root-cause fix per the doctrine, then run the project
validator (`python -m agents.validator .`) and confirm the build still imports.
validator (`hawk .`) and confirm the build still imports.
A subagent never runs the test suite and never performs a git write.
@@ -47,11 +47,4 @@ on its `description`:
Each subagent's `model` is set to `inherit`, so it runs on the model the session is
using. To run the whole fleet at once, use the
[`/maintenance` command or the `/fleet` workflow](/docs/claude-workflows.html).
## The off-limits directory
Every subagent is instructed never to read, scan, or modify the `agents/` directory.
That directory holds the Python fleet's own source, which intentionally contains the
patterns the reviewers hunt for as detection data. A finding inside `agents/` is
never real, so it is excluded from every search.
</div>
+2 -2
View File
@@ -197,8 +197,8 @@ output already conforms:
authorization; DD/MM/YYYY dates.
- The validator and the app import must pass before a build step finishes.
It will not commit, will not run tests outside `/test`, will not weaken a guard or a
test to make a finding disappear, and will not touch the `agents/` directory.
It will not commit, will not run tests outside `/test`, and will not weaken a guard or a
test to make a finding disappear.
## Troubleshooting
@@ -34,7 +34,7 @@ when done, and never run the test suite or commit.
### Adversarial verification
The read-only workflows do something the simple command and the Python fleet do not:
The read-only workflows do something the simple `/maintenance` command does not:
each candidate finding is handed to a second, independent subagent instance whose
only task is to **refute** it against the source. A finding is reported only if it
survives that refutation. This directly enforces the project rule that a wrong
+1 -16
View File
@@ -6,8 +6,7 @@ Anthropic's command-line coding agent. Everything lives under the `.claude/`
directory in the repository root, so it is shared with every contributor through
git and applies the moment the repository is opened in Claude Code.
This setup mirrors the autonomous Python
[maintenance agents](/docs/maintenance-agents.html) and extends them into
This setup enforces the platform's ten quality dimensions and extends them into
feature work, all expressed in Claude Code's own primitives.
## What is in `.claude/`
@@ -36,20 +35,6 @@ interactive to most deterministic.
structured output and adversarial verification of every finding. Best for a
repeatable audit or a feature build.
## Relationship to the Python fleet
The repository also contains the standalone Python agent fleet in `agents/`, run
through the `make` targets (`make agents-all`, `make maintenance`). Both the
Python fleet and the Claude Code subagents enforce the **same ten quality
dimensions**; they differ only in the engine that runs them. The Python fleet uses
its own model engine and is suited to headless and continuous-integration runs;
the Claude Code setup runs inside an interactive Claude Code session and reuses its
tools and models.
The `agents/` directory is deliberately excluded from every reviewer in both
systems: it contains the very patterns the reviewers search for as detection data,
not as mistakes, so scanning it would produce false findings.
## Quick reference
- Run one dimension: mention the subagent, for example
@@ -124,6 +124,13 @@ through them. The platform API enforces the rest: an **administrator** operating
| `fork_project` | auth | Fork a project into a new project owned by the current user. |
| `fork_status` | public | Check a fork job and obtain the new project once finished. |
### Tools (`http`)
| Tool | Scope | What it does |
|---|---|---|
| `seo_diagnostics` | public | Queue an SEO audit of a URL or sitemap and obtain its job uid and status URL. |
| `seo_status` | public | Check an SEO audit and obtain its score, grade, and report link once finished. |
### Messages and notifications (`http`)
| Tool | Scope | What it does |
+2 -13
View File
@@ -29,7 +29,6 @@ devplacepy/ the application package
models.py Pydantic Form models schemas.py *Out JSON models
services/ background + async-job services, Devii, containers
tests/ unit / api / e2e, mirroring the route or source path
agents/ autonomous maintenance fleet (developer tooling, off-limits)
```
## How a feature is shaped
@@ -54,18 +53,10 @@ Never declare work done with a broken import or a validation error:
```bash
python -c "from devplacepy.main import app" # must import clean
make validate # Python, JS, CSS, templates
hawk . # Python, JS, CSS, templates
```
Run the maintenance fleet over your changed files to catch style, duplication, and
documentation drift before review:
```bash
make maintenance # read-only report on changed files
make maintenance-fix # apply fixes to changed files
```
See [Running the agents](/docs/maintenance-usage.html) for the full fleet. Tests live in
Tests live in
`tests/` and run with `make test-unit`, `make test-api`, and `make test-e2e`.
## Read next
@@ -73,8 +64,6 @@ See [Running the agents](/docs/maintenance-usage.html) for the full fleet. Tests
- [Conventions and Errors](/docs/conventions.html) - the rules every endpoint shares.
- [Components overview](/docs/components.html) and the [design system](/docs/styles.html) -
the frontend building blocks and styling rules.
- [Maintenance agents](/docs/maintenance-agents.html) - how the quality fleet keeps the
codebase consistent.
{% if is_admin(user) %}
- [Architecture overview](/docs/architecture.html) - the request pipeline, backend, and
frontend in depth.
@@ -1,69 +0,0 @@
<div class="docs-content" data-render>
# Maintenance agents
DevPlace is **self-maintaining**. A fleet of autonomous AI agents continuously
reviews the codebase for security, audit coverage, documentation accuracy, and
code quality, fixes what it finds, and proves the build still works.
This page covers what the fleet is and what each agent does. The companion page,
[Running the agents](/docs/maintenance-usage.html), shows how to run them with
copy-paste commands.
> New here? You do not need to know the codebase. Ask **Maestro**, the conductor,
> in plain language ("is my audit coverage complete?"); it runs the right agent and
> explains the result.
## How it works in one minute
- Each agent owns **one quality dimension** and sweeps the whole repository for
problems in that dimension only.
- Every agent has two modes: **check** (report only) and **fix** (correct the
problem, then verify the build).
- After any change, an agent runs a built-in **validator** (Python, JavaScript,
CSS, and HTML/templates) plus an application import, and refuses to finish if the
build is broken.
- Every run writes a small **report** (a JSON file and a readable summary) showing
what was found and fixed.
- Each agent runs **in isolation**, using only its own reviewing tools, so one agent
can never trigger another or kick off the whole fleet by accident.
- When you ask Maestro to review **everything**, it runs each agent **once** and
remembers the results for your follow-up questions instead of repeating the work.
## Meet the fleet
| Agent | What it watches |
|-------|-----------------|
| **Maestro** | The conductor you talk to. It picks the right agent (or the whole fleet) and explains the result. |
| **Security** | Every route is correctly authorized; private data and admin actions are protected; user input is validated. |
| **Audit** | Every action that changes data leaves an audit-log entry, including denials and failures. |
| **Devii** | The Devii assistant can do everything the site offers for your role, and only shows tools your role is allowed to use. |
| **Docs** | CLAUDE.md, AGENTS.md, README, and these docs pages stay in step with the code, shown to the right audience. |
| **Feature completeness** | A new feature is wired all the way through: form, JSON schema, Devii tool, API docs, SEO, and README. |
| **Duplication** | Shared helpers are reused instead of copy-pasted logic. |
| **Style** | Naming, headers, typing, and formatting follow the project rules, applied with context so code that intentionally uses a pattern or character is left alone. |
| **Frontend** | JavaScript modules, web components, and CSS follow the project's strict structure. |
| **SEO** | Public pages carry the right search metadata and appear in the sitemap. |
| **Tests** | Routes without an integration test get one written (the agent never runs the suite itself). |
## One directory they never touch
The `agents/` directory holds the agents' source, which deliberately contains the
very patterns they search for (special characters, example bad names, dangerous
command text) as **detection data**, not mistakes. Scanning it would flag false
problems, and fixing those would break the agents. So every agent run hard-blocks
any change under `agents/`, and the agents are told never to read or scan it. The
directory is off-limits to every checker and cleanup, by design.
## Why this exists
A single feature in DevPlace touches many layers at once: a route is rendered as a
page, served as JSON, exposed to the Devii assistant, written into the API docs,
audited, and indexed for search. It is easy to change one layer and forget a
connected one. The fleet catches exactly that, on every dimension, across the whole
project, without anyone having to remember the full map.
## Where to go next
- [Running the agents](/docs/maintenance-usage.html) - the commands, with examples.
- [Devii Assistant](/docs/devii.html) - the in-app assistant the Devii agent keeps honest.
</div>
@@ -1,130 +0,0 @@
<div class="docs-content" data-render>
# Running the agents
How to run the [maintenance agents](/docs/maintenance-agents.html). You do not need
to know the codebase to use them. Every command below runs from the project root.
> The golden rule: **check** never changes anything, **fix** does. When in doubt,
> run check first and read the report.
## The easiest way: talk to Maestro
Maestro is the conductor. Ask in plain language and it runs the right agent, then
explains what it found.
```bash
make maestro
```
That opens a prompt. Try:
- `is my audit coverage complete?`
- `check the security of the projects router`
- `get the whole project in shape`
You can also ask a single question without the prompt:
```bash
python -m agents.maestro "is my documentation up to date?"
```
Maestro defaults to **check** (read-only) for questions and confirms before any
change. When you ask it to review the whole project, it runs each agent **once** and
reuses those results to answer follow-up questions, never repeating a sweep.
## Running one agent yourself
Every agent has its own command. By default an agent **fixes** what it finds; add
`CHECK=1` to only report.
```bash
make audit-agent # find and fix audit-log gaps
make audit-agent CHECK=1 # only report them, change nothing
```
The full set of agent commands:
```bash
make security-agent # data and role security
make audit-agent # audit-log coverage
make devii-agent # Devii capability and tool visibility
make docs-agent # documentation accuracy
make fanout-agent # feature wired across every layer
make dry-agent # duplication and reuse
make style-agent # naming, headers, typing, formatting
make frontend-agent # JavaScript, components, CSS
make seo-agent # search metadata and sitemap
make test-agent # integration-test coverage
```
Add `CHECK=1` to any of them to report without changing files.
## Running the whole fleet
```bash
make agents-all # fix: run every agent in dependency order
make agents-all CHECK=1 # report only: run the whole fleet at once
```
Because reporting changes nothing, **`CHECK=1` runs every agent concurrently** for a
fast, read-only health check. A **fix** run goes one agent at a time in dependency
order, so file changes never collide.
Run this 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 want the fleet to look only at the files you just
touched. 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, 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 edit only the files in that changed set. 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:
- **A start banner** for every agent: its name, a memorable codename (like
`brave-otter`), what it is about to do, and where its report will be written.
- **A timestamp and elapsed time** on every line, so you can see how long things take.
- **A running cost** after each AI step (per call and total), with money icons.
- **A diff** of every file change as it happens, so nothing is edited silently.
- **Live command output** streamed line by line while a command runs.
## Reading the reports
Every run writes its findings to `agents/reports/`, named
`<agent>-<codename>-<date>`:
- `<agent>-<codename>-<date>.json` - the machine-readable findings.
- `<agent>-<codename>-<date>.md` - a readable summary grouped by file.
- `fleet-<codename>-<date>.json` - the combined report when you run the whole fleet.
A report marked `incomplete` means the run ran out of its step budget before
finishing; its findings are partial, so run it again.
## Validating the code yourself
The agents verify their own changes with a built-in validator that needs no extra
tools. You can run it directly:
```bash
make validate # check Python, JavaScript, CSS, and templates
```
## Where to go next
- [Maintenance agents](/docs/maintenance-agents.html) - what each agent does and why.
</div>
+2 -7
View File
@@ -152,15 +152,10 @@
<section class="landing-section landing-bots">
<div class="landing-section-header">
<h2>Self-Maintaining by Design</h2>
<a href="/docs/maintenance-agents.html" class="landing-section-link">How it works &rarr;</a>
<a href="/docs/claude.html" class="landing-section-link">How it works &rarr;</a>
</div>
<p class="landing-bots-intro">DevPlace keeps its own house in order. A fleet of autonomous AI agents continuously reviews the codebase for security, audit coverage, documentation accuracy, and code quality, then fixes what it finds and proves the build still works. You talk to one conductor, Maestro, and it runs the rest.</p>
<p class="landing-bots-intro">DevPlace keeps its own house in order. A set of single-purpose Claude Code subagents reviews the codebase for security, audit coverage, documentation accuracy, and code quality, then fixes what it finds and proves the build still works.</p>
<div class="landing-bots-grid">
<div class="landing-bot-card landing-bot-card-lead">
<div class="landing-bot-icon">&#x1F3BC;</div>
<h3>Maestro</h3>
<p>The conductor you talk to in plain language. It decides which agent to run and explains the result.</p>
</div>
<div class="landing-bot-card">
<div class="landing-bot-icon">&#x1F6E1;</div>
<h3>Security</h3>