Skip to content

Commit 1dcb01a

Browse files
docs: add AFDocs audit + fix skills for agent-friendly docs monitoring (#25)
* docs: add AFDocs audit skill for agent-friendly docs monitoring Add a reusable skill (.agents/skills/afdocs-audit/) that runs the AFDocs scorecard against docs.warp.dev and produces a structured report. Follows the same pattern as the existing docs-seo-audit skill. Includes: - SKILL.md with instructions for running, reporting, and Slack notifications - afdocs_audit.mjs wrapper script that runs npx afdocs check --format json, parses the output, and produces a structured report with scores, issues, and fix guidance - references/known-exceptions.md documenting expected failures and false positives (content-start-position, markdown-content-parity, page-size) The skill is designed to be run by a scheduled Oz agent (weekly cadence) to monitor the docs site's agent-friendliness score over time. Co-Authored-By: Oz <oz-agent@warp.dev> * docs: add afdocs-fix remediation skill (Part B) Add the companion skill that reads the afdocs-audit output and applies automated fixes for each failing check. Includes: - Fix procedures for each AFDocs check (llms.txt directive, content negotiation middleware, llms.txt coverage, MCP discovery, etc.) - Code snippets and file paths for each fix - Diagnostic steps for coverage mismatches - Clear separation of automatable vs. non-automatable checks - PR conventions (title prefix, labels, co-author) This is the 'fix layer' that a scheduled Oz agent runs after the audit skill to automatically remediate regressions. Co-Authored-By: Oz <oz-agent@warp.dev> * chore: hardcode #growth-docs channel ID (C09BVK0PL3Y) in afdocs-audit skill Co-Authored-By: Oz <oz-agent@warp.dev> * chore: simplify PR conventions — shorten prefix, remove nonexistent label Co-Authored-By: Oz <oz-agent@warp.dev> * chore: review fixes — stale MCP URL, URL validation, soften hardcoded counts - Fix stale /sse path in afdocs-fix MCP example (matches PR #23 fix) - Add URL validation in afdocs_audit.mjs to prevent shell injection - Remove hardcoded baseline score from SKILL.md reporting instructions - Soften hardcoded page counts in known-exceptions.md (varies by run) - Scope page-size exception to /changelog/ only, not all large pages Co-Authored-By: Oz <oz-agent@warp.dev> * fix: deploy middleware as Vercel Edge Function for content negotiation The Astro middleware in src/middleware.ts was not running at request time for pre-rendered (static) pages. The default middlewareMode ('classic') only executes middleware at build time for static pages, so Accept: text/markdown requests were served HTML from Vercel's CDN cache. Setting middlewareMode: 'edge' deploys the middleware as a separate Vercel Edge Function that runs at request time for ALL requests, including static pages. This enables content negotiation so agents sending Accept: text/markdown get clean markdown automatically. Co-Authored-By: Oz <oz-agent@warp.dev> * chore: revert Slack channel to placeholder per review feedback Reverts the hardcoded #growth-docs channel ID (C09BVK0PL3Y) back to a <CHANNEL_ID> placeholder, matching the docs-seo-audit pattern. The channel is specified in the scheduled agent's prompt, not in the skill. Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 70571ec commit 1dcb01a

5 files changed

Lines changed: 545 additions & 1 deletion

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
name: afdocs-audit
3+
description: >-
4+
Audit docs.warp.dev for agent-friendly documentation issues using the AFDocs
5+
scorecard. Checks llms.txt, markdown availability, content negotiation, page
6+
size, URL stability, and content structure. Use when asked to check agent
7+
readiness, run an AFDocs audit, improve the docs score, or verify llms.txt
8+
and markdown support.
9+
---
10+
11+
# AFDocs Audit
12+
13+
Run the [AFDocs scorecard](https://agentdocsspec.com/spec/) against docs.warp.dev and report results.
14+
15+
## Running the audit
16+
17+
From the docs repo root:
18+
19+
```bash
20+
node .agents/skills/afdocs-audit/scripts/afdocs_audit.mjs \
21+
--output /tmp/afdocs-report.json
22+
```
23+
24+
The script runs `npx afdocs check https://docs.warp.dev --format json`, parses the output, and writes a structured report.
25+
26+
### Options
27+
28+
- `--output FILE` — Write the JSON report to a file (otherwise prints to stdout).
29+
- `--url URL` — Override the site URL (default: `https://docs.warp.dev`).
30+
31+
## Reading the report
32+
33+
The JSON report contains:
34+
- `score` — Overall score out of 100
35+
- `grade` — Letter grade (A+ through F)
36+
- `total_checks` — Number of checks run
37+
- `summary` — Counts by status (`pass`, `fail`, `warn`, `skip`)
38+
- `categories` — Per-category scores and grades
39+
- `issues` — Array of failing and warning checks with details and fix guidance
40+
41+
Each issue includes:
42+
- `id` — Check identifier (e.g., `llms-txt-directive-html`)
43+
- `category` — Check category (e.g., `content-discoverability`)
44+
- `status``fail` or `warn`
45+
- `message` — Human-readable description
46+
- `fix` — Suggested fix from the AFDocs spec
47+
48+
### Known exceptions
49+
50+
Before reporting, cross-reference every issue against the known exceptions in `references/known-exceptions.md`. Classify each issue into exactly one bucket:
51+
- **Allowlisted** — known exceptions that are intentional (not problems)
52+
- **Remaining** — genuine issues that need attention
53+
54+
Only include a section if its count is > 0. Never list allowlisted issues under "Remaining."
55+
56+
## Reporting results
57+
58+
After running the audit, ALWAYS report the results to the user before taking any action. Include:
59+
60+
1. **Score**: Overall score and grade
61+
2. **Failures first**: List every fail-severity check with its message and fix guidance. These are the most impactful.
62+
3. **Warnings**: List warning-severity checks with context.
63+
4. **Allowlisted**: Briefly note any known exceptions that were flagged.
64+
5. **If all checks pass**: Explicitly tell the user everything looks clean.
65+
66+
Example report format:
67+
```
68+
AFDocs audit complete: 23 checks run, score 82/100 (B).
69+
70+
**Failures (5):**
71+
- llms-txt-directive-html: No llms.txt directive in HTML pages
72+
Fix: Add a visually-hidden element near the top of each page with a link to /llms.txt
73+
- content-negotiation: Server ignores Accept: text/markdown
74+
Fix: Add middleware to serve .md variants when Accept: text/markdown is requested
75+
76+
**Warnings (1):**
77+
- llms-txt-coverage: 80% of sitemap pages covered (247/308)
78+
79+
**Allowlisted (2):**
80+
- page-size-markdown: 1 page over 50K (changelog — intentionally long)
81+
- markdown-content-parity: 7 pages with minor diffs (Turndown escaping, not real content gaps)
82+
```
83+
84+
After reporting, ask the user which issues they want to address.
85+
86+
## Slack notification (optional)
87+
88+
If instructed to send a report to Slack, post a summary after the audit completes.
89+
90+
1. Check if `BUZZ_SLACK_TOKEN` environment variable exists.
91+
2. If the token exists, send a summary to the channel the user specified (or the channel configured in the agent's instructions).
92+
93+
**Format:**
94+
95+
```
96+
*AFDocs Audit — <date>*
97+
Score: <score>/100 (<grade>) | <total_checks> checks | <pass> pass, <fail> fail, <warn> warn
98+
99+
*Failures (<count>):*
100+
• <check_id>: <message>
101+
102+
*Warnings (<count>):*
103+
• <check_id>: <message>
104+
105+
*Allowlisted (<count>):*
106+
• <check_id>: <reason>
107+
```
108+
109+
Send using:
110+
111+
```bash
112+
curl -X POST https://slack.com/api/chat.postMessage \
113+
-H "Authorization: Bearer $BUZZ_SLACK_TOKEN" \
114+
-H "Content-Type: application/json" \
115+
-d '{
116+
"channel": "<CHANNEL_ID>",
117+
"text": "<formatted_summary>",
118+
"unfurl_links": false,
119+
"unfurl_media": false
120+
}'
121+
```
122+
123+
If `BUZZ_SLACK_TOKEN` is not set, skip the notification and note that the token is required.
124+
125+
## Dependencies
126+
127+
Node.js 18+ with npm (for `npx afdocs`). No additional install required — `afdocs` is fetched on demand by npx.
128+
129+
## Checks performed
130+
131+
The AFDocs scorecard evaluates these categories:
132+
133+
**Content Discoverability** — llms.txt existence, validity, size, link resolution, markdown links, and in-page directives
134+
**Markdown Availability** — .md URL support and Accept: text/markdown content negotiation
135+
**Page Size and Truncation Risk** — rendering strategy, page sizes (markdown and HTML), and content start position
136+
**Content Structure** — tabbed content serialization, section header quality, code fence validity
137+
**URL Stability and Redirects** — HTTP status codes and redirect behavior
138+
**Observability and Content Health** — llms.txt coverage, markdown/HTML parity, cache headers
139+
**Authentication and Access** — auth gate detection and alternative access paths
140+
141+
Full spec: https://agentdocsspec.com/spec/
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# AFDocs Known Exceptions
2+
3+
This file lists checks from the afdocs-audit skill that may flag as warnings or failures but are expected and intentional. When reporting audit results, classify these as "Allowlisted" rather than "Remaining."
4+
5+
## content-start-position
6+
7+
**Expected status**: fail or warn
8+
**Reason**: Sampled pages may have content starting past 50% of the HTML output. This is inherent to Starlight's layout — sidebar navigation, header markup, and JavaScript/CSS precede the `<main>` content area.
9+
**Mitigation**: The llms.txt directive, `<link rel="alternate" type="text/markdown">` in `<head>`, and `Accept: text/markdown` content negotiation middleware all steer agents to the clean markdown version, bypassing the HTML boilerplate entirely.
10+
**Action**: No fix needed. This is a structural property of Starlight sites.
11+
12+
## markdown-content-parity
13+
14+
**Expected status**: warn (several pages, ~2% average difference)
15+
**Reason**: False positive. The "missing" segments are numbered heading text like "2. Tabbed File Viewer" where Turndown correctly escapes the period (`### 2\. Tabbed File Viewer`) to prevent markdown parsers from interpreting it as a list item. The content IS present in the markdown — the AFDocs checker's text comparison doesn't account for markdown escaping.
16+
**Affected pages** (as of 2026-05-05):
17+
- `/agent-platform/cloud-agents/triggers/scheduled-agents-quickstart/` — step headings
18+
- `/agent-platform/cloud-agents/integrations/github-actions/` — numbered use case headings
19+
- `/support-and-community/troubleshooting-and-support/troubleshooting-login-issues/` — URLs with special chars
20+
- `/reference/cli/quickstart/` — optional step headings
21+
- `/guides/getting-started/welcome-to-warp/` — numbered section headings
22+
- `/terminal/editor/vim/` — "See Vim docs:" link text
23+
- `/guides/getting-started/10-coding-features-you-should-know/` — numbered feature headings
24+
**Action**: No fix needed. Content is intact.
25+
26+
## page-size-markdown / page-size-html
27+
28+
**Expected status**: warn — but only allowlist `/changelog/`
29+
**Reason**: The changelog page (`/changelog/`) is intentionally a single long page (~4,000 lines of MDX). It is excluded from `llms-full.txt` generation due to a `hast-util-to-text` stack overflow, but is still accessible at its URL and indexed by the sitemap.
30+
**Action**: If the only flagged page is `/changelog/`, classify as allowlisted. If other pages are flagged, treat those as genuine issues that may need splitting.
31+
32+
## section-header-quality
33+
34+
**Expected status**: skip
35+
**Reason**: Only evaluated when tab panels contain section headers. Most sampled pages with tabs don't have headers inside the tab panels, so the check is skipped.
36+
**Action**: None needed.
37+
38+
## auth-alternative-access
39+
40+
**Expected status**: skip
41+
**Reason**: All docs pages are publicly accessible, so no alternative access path is needed.
42+
**Action**: None needed.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env node
2+
/**
3+
* AFDocs audit wrapper script.
4+
*
5+
* Runs `npx afdocs check` against docs.warp.dev, parses the JSON output,
6+
* and produces a structured report with scores, issues, and fix guidance.
7+
*
8+
* Usage:
9+
* node .agents/skills/afdocs-audit/scripts/afdocs_audit.mjs
10+
* node .agents/skills/afdocs-audit/scripts/afdocs_audit.mjs --output /tmp/report.json
11+
* node .agents/skills/afdocs-audit/scripts/afdocs_audit.mjs --url https://preview.docs.warp.dev
12+
*/
13+
14+
import { execSync } from 'node:child_process';
15+
import { writeFileSync } from 'node:fs';
16+
import { resolve } from 'node:path';
17+
18+
const GRADE_THRESHOLDS = [
19+
[97, 'A+'],
20+
[93, 'A'],
21+
[90, 'A-'],
22+
[87, 'B+'],
23+
[83, 'B'],
24+
[80, 'B-'],
25+
[77, 'C+'],
26+
[73, 'C'],
27+
[70, 'C-'],
28+
[67, 'D+'],
29+
[63, 'D'],
30+
[60, 'D-'],
31+
[0, 'F'],
32+
];
33+
34+
function scoreToGrade(score) {
35+
for (const [threshold, grade] of GRADE_THRESHOLDS) {
36+
if (score >= threshold) return grade;
37+
}
38+
return 'F';
39+
}
40+
41+
function parseArgs(argv) {
42+
const args = { output: null, url: 'https://docs.warp.dev' };
43+
for (let i = 0; i < argv.length; i++) {
44+
if (argv[i] === '--output') args.output = argv[++i];
45+
else if (argv[i] === '--url') args.url = argv[++i];
46+
else if (argv[i] === '--help' || argv[i] === '-h') {
47+
console.log('Usage: node afdocs_audit.mjs [--output FILE] [--url URL]');
48+
process.exit(0);
49+
}
50+
}
51+
return args;
52+
}
53+
54+
function runAfdocsCheck(url) {
55+
// Validate URL to prevent shell injection
56+
try {
57+
const parsed = new URL(url);
58+
if (!['http:', 'https:'].includes(parsed.protocol)) {
59+
throw new Error(`Invalid protocol: ${parsed.protocol}`);
60+
}
61+
} catch (e) {
62+
throw new Error(`Invalid URL "${url}": ${e.message}`);
63+
}
64+
65+
try {
66+
const stdout = execSync(`npx afdocs check ${url} --format json`, {
67+
encoding: 'utf8',
68+
maxBuffer: 10 * 1024 * 1024, // 10 MB — the JSON output can be large
69+
timeout: 300_000, // 5 minutes
70+
stdio: ['pipe', 'pipe', 'pipe'],
71+
});
72+
return JSON.parse(stdout);
73+
} catch (error) {
74+
// npx afdocs exits with code 1 when there are failures, but still
75+
// prints valid JSON to stdout. Try to parse it.
76+
if (error.stdout) {
77+
try {
78+
return JSON.parse(error.stdout);
79+
} catch {
80+
// Fall through to error
81+
}
82+
}
83+
throw new Error(`Failed to run afdocs check: ${error.message}`);
84+
}
85+
}
86+
87+
function buildReport(raw) {
88+
const { summary, results } = raw;
89+
const score = raw.summary?.score ?? estimateScore(results);
90+
const grade = scoreToGrade(score);
91+
92+
// Group results by category
93+
const categories = {};
94+
for (const r of results) {
95+
if (!categories[r.category]) {
96+
categories[r.category] = { checks: [], pass: 0, fail: 0, warn: 0, skip: 0 };
97+
}
98+
categories[r.category].checks.push(r);
99+
categories[r.category][r.status] = (categories[r.category][r.status] || 0) + 1;
100+
}
101+
102+
// Extract issues (fail + warn)
103+
const issues = results
104+
.filter((r) => r.status === 'fail' || r.status === 'warn')
105+
.map((r) => ({
106+
id: r.id,
107+
category: r.category,
108+
status: r.status,
109+
message: r.message,
110+
fix: r.details?.fix || r.fix || null,
111+
}));
112+
113+
return {
114+
url: raw.url,
115+
timestamp: raw.timestamp || new Date().toISOString(),
116+
score,
117+
grade,
118+
total_checks: summary.total,
119+
summary: {
120+
pass: summary.pass,
121+
fail: summary.fail,
122+
warn: summary.warn,
123+
skip: summary.skip,
124+
},
125+
categories: Object.fromEntries(
126+
Object.entries(categories).map(([name, cat]) => [
127+
name,
128+
{ pass: cat.pass, fail: cat.fail, warn: cat.warn, skip: cat.skip },
129+
])
130+
),
131+
issues,
132+
all_results: results.map((r) => ({ id: r.id, category: r.category, status: r.status, message: r.message })),
133+
};
134+
}
135+
136+
/**
137+
* Estimate score from results when the raw JSON doesn't include a score field.
138+
* Uses a simple formula: (pass / (total - skip)) * 100.
139+
*/
140+
function estimateScore(results) {
141+
const scored = results.filter((r) => r.status !== 'skip');
142+
if (scored.length === 0) return 100;
143+
const passing = scored.filter((r) => r.status === 'pass').length;
144+
// Warnings count as half-pass
145+
const warnings = scored.filter((r) => r.status === 'warn').length;
146+
return Math.round(((passing + warnings * 0.5) / scored.length) * 100);
147+
}
148+
149+
function printSummary(report) {
150+
console.log(`\nAFDocs Audit — ${report.url}`);
151+
console.log(`Score: ${report.score}/100 (${report.grade})`);
152+
console.log(
153+
`Checks: ${report.total_checks} total | ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.warn} warn, ${report.summary.skip} skip`
154+
);
155+
156+
if (report.issues.length === 0) {
157+
console.log('\n✅ All checks passed!');
158+
return;
159+
}
160+
161+
const failures = report.issues.filter((i) => i.status === 'fail');
162+
const warnings = report.issues.filter((i) => i.status === 'warn');
163+
164+
if (failures.length > 0) {
165+
console.log(`\nFailures (${failures.length}):`);
166+
for (const f of failures) {
167+
console.log(` ✗ ${f.id}: ${f.message}`);
168+
if (f.fix) console.log(` Fix: ${f.fix}`);
169+
}
170+
}
171+
172+
if (warnings.length > 0) {
173+
console.log(`\nWarnings (${warnings.length}):`);
174+
for (const w of warnings) {
175+
console.log(` ⚠ ${w.id}: ${w.message}`);
176+
}
177+
}
178+
}
179+
180+
// Main
181+
const args = parseArgs(process.argv.slice(2));
182+
console.log(`Running AFDocs check on ${args.url}...`);
183+
184+
const raw = runAfdocsCheck(args.url);
185+
const report = buildReport(raw);
186+
187+
printSummary(report);
188+
189+
if (args.output) {
190+
const outputPath = resolve(args.output);
191+
writeFileSync(outputPath, JSON.stringify(report, null, 2));
192+
console.log(`\nReport written to ${outputPath}`);
193+
}

0 commit comments

Comments
 (0)