forked from retoor/devplacepy
chore: replace python -m agents.validator with hawk in all agent mode instructions
This commit is contained in:
+17
-3
@@ -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."),
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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>
|
||||
@@ -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 →</a>
|
||||
<a href="/docs/claude.html" class="landing-section-link">How it works →</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">🎼</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">🛡</div>
|
||||
<h3>Security</h3>
|
||||
|
||||
Reference in New Issue
Block a user