Files
charleswiltgen__axiom/scripts/audit-parity.ts
T
Charles Wiltgen 888fd0a566 perf(context): ship agent descriptions as their trigger sentence
Claude Code's always-on cost was 54,966 chars, 39,359 of it agent descriptions —
the same 42 agents Cursor and Codex carry for ~6,100, because both emitters
already truncate each description to its first sentence. The <example> dialogues
exist to teach triggering; the first sentence already carries the trigger, and
the dialogues now live in the body, which loads only when the agent runs.

Measured before landing — 169 prompts (each agent's own <example> user lines,
labelled with the agent that example names) classified against all 42
descriptions, two runs per condition:

  full text       169/169, 169/169  (the examples contain their own answer, so
                                     this level is an artifact, not a baseline)
  first sentence  168/169, 166/169

The three deviations are two-way calls between plausible agents —
spm-conflict-resolver over build-fixer for "No such module after I updated
packages", test-debugger over build-fixer for "tests passed yesterday but now
fail", security-privacy-scanner over grdb-performance-auditor for "scan for SQL
injection in GRDB code". No cross-domain misroutes. The corpus cannot rule out a
larger effect on other model classes, and it is built from the removed examples,
so it is the hardest available test for the truncated form, not a field result.

Why the risk is bounded: the surface that changes is discretionary selection.
The skills carry 102 "Launch `<agent>` agent" directives covering all 42 agents,
and commands another 33, so designed flows choose their agent from skill text
either way — untouched here.

Always-on: claude-code 54,966 -> 22,059 chars (~18,322 -> ~7,353 tokens, -60%).
Ceiling ratcheted 56,000 -> 24,000.

Gates that had to move, with reasons:
- always-on-footprint.test.ts asserted the 6.4x Cursor/Claude Code gap. The gap
  was the defect, so it now asserts near-parity; re-opening it fails the test.
- audit-parity's advertised-area and advertised-command parsers read the
  frontmatter description only. The `Explicit command:` hints now live in the
  body, so both scan the whole file — a ghost command promised in the body is
  exactly as broken as one promised in the frontmatter, which is the check's
  purpose. Two tests encoded the old frontmatter-only contract; they now assert
  the new one, plus a new case proving a body-promised ghost is caught.
- Four agents (energy, memory, swift-performance, swiftui-performance) had lost
  the vocabulary their audit area checks for; it is back in the trigger
  sentence, which also routes on those words now. foundation-models and
  spritekit gained theirs as well.
- `/axiom:audit all` carries "(Claude Code only)": Codex has no commands and the
  emitter cannot map `all` to an area.

Note: 250 chars is Axiom's own emitter budget, not a platform cap. The Agent
Skills spec allows 1024; Claude Code documents 1536 for description +
when_to_use; Codex enforces 1024 and shortens long descriptions for its listing.
2026-09-15 13:34:40 -07:00

822 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* /axiom:audit Source-of-Truth Parity — pure parsing & validation.
*
* The list of audit areas exists in four places that must agree:
*
* A — frontmatter `argument:` line in commands/audit.md. NOT a Claude
* Code field (the recognized ones are description, allowed-tools,
* model, argument-hint, disable-model-invocation) — it is Axiom's
* own canonical registry, read only by this module. The real
* user-facing hint is the sibling `argument-hint: "[area]"`.
* B — body `## Available Audits` table column 1. This is what actually
* drives dispatch: the command prompt tells the model to look the
* agent up in this table.
* C — docs/commands/utility/audit.md "Available Audit Areas" code spans
* D — docs/.vitepress/config.ts commands sidebar `link` paths shaped
* like `/commands/<group>/audit-<area>` — derives an area name from
* each link.
*
* Plus E — every agent name in B's column 2 must resolve to a real file
* under .claude-plugin/plugins/axiom/agents/<agent>.md.
*
* This module is I/O free. Callers read files and pass strings in; the
* caller (pre-deploy.ts) handles agent-file-existence checks and error
* reporting. Tests in audit-parity.test.ts exercise these functions.
*/
export interface BodyRow {
area: string;
agent: string;
/** Column 3 of the body table — short dispatch description shown to the
* model. Compared against the agent file's frontmatter description by
* `validateAgentDescriptionParity` to catch rename drift. */
detects: string;
}
/** A group of audit areas — preserves insertion order from the source. */
export interface AreaGroup {
group: string;
areas: string[];
}
export interface ParityError {
/** Short check identifier — passed to pre-deploy.ts's error() helper. */
check: "audit-parity";
message: string;
}
/**
* Parse the comma-separated audit-area list from the `argument:` frontmatter
* line. The "all" meta-target is excluded — it dispatches to health-check,
* not a regular audit.
*/
export function parseFrontmatterAreas(content: string): string[] {
const m = content.match(/^argument:\s*"[^"]*Which audit to run:\s*([^"]+)"/m);
if (!m) return [];
return m[1]
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0 && s !== "all");
}
/**
* Parse the body table under `## Available Audits` — returns area + agent
* name for each row. Used to derive both the source-side area set and
* the agent-existence check.
*/
export function parseBodyTable(content: string): BodyRow[] {
const section = content.match(/## Available Audits\s*\n([\s\S]*?)\n## /);
if (!section) return [];
const rows: BodyRow[] = [];
for (const line of section[1].split("\n")) {
const cells = line.split("|").map((c) => c.trim());
if (cells.length < 4) continue;
const area = cells[1];
const agent = cells[2];
const detects = cells[3] ?? "";
if (!area || area === "Area" || area.startsWith("---")) continue;
rows.push({ area, agent, detects });
}
return rows;
}
/**
* Parse the docs page Available Audit Areas section — returns every
* `code-span` token, preserving multiplicity so duplicates can be detected.
*/
export function parseDocAreas(content: string): string[] {
const section = content.match(/## Available Audit Areas\s*\n([\s\S]*?)\n## /);
if (!section) return [];
const out: string[] = [];
for (const m of section[1].matchAll(/`([a-z][a-z0-9-]+)`/g)) out.push(m[1]);
return out;
}
/**
* Parse the commands sidebar in config.ts. Returns area names derived
* from `/commands/<group>/audit-<area>` links across every `text: '...'`
* group within the `'/commands/':` block. Captures duplicates.
*/
export function parseSidebarAreas(configTs: string): string[] {
// Isolate the '/commands/': [ ... ] block — single quotes are typical.
const m = configTs.match(/'\/commands\/'\s*:\s*\[([\s\S]*?)\n\s*\],/);
if (!m) return [];
const block = m[1];
const out: string[] = [];
for (const link of block.matchAll(/link:\s*'\/commands\/[^/']+\/audit-([a-z0-9-]+)'/g)) {
out.push(link[1]);
}
return out;
}
/**
* Parse the commands sidebar in config.ts grouped — returns one entry
* per group that contains at least one audit link. Group order and
* within-group item order are preserved. Used to enforce per-group
* count + order parity with the docs page (axiom-imz finding: set
* parity passes while groupings can still drift independently).
*/
export function parseSidebarGroups(configTs: string): AreaGroup[] {
const m = configTs.match(/'\/commands\/'\s*:\s*\[([\s\S]*?)\n\s*\],/);
if (!m) return [];
const block = m[1];
const groups: AreaGroup[] = [];
// Each group: { text: 'Name', items: [ ... ] }
for (const gm of block.matchAll(
/text:\s*'([^']+)',\s*items:\s*\[([\s\S]*?)\]\s*\}/g,
)) {
const name = gm[1];
const items = gm[2];
const areas: string[] = [];
for (const link of items.matchAll(
/link:\s*'\/commands\/[^/']+\/audit-([a-z0-9-]+)'/g,
)) {
areas.push(link[1]);
}
if (areas.length > 0) groups.push({ group: name, areas });
}
return groups;
}
/**
* Parse the docs page Available Audit Areas section grouped — one
* entry per `### GroupName` heading + immediately-following table.
* Group order and within-group item order are preserved.
*/
export function parseDocGroups(content: string): AreaGroup[] {
const section = content.match(/## Available Audit Areas\s*\n([\s\S]*?)\n## /);
if (!section) return [];
const groups: AreaGroup[] = [];
// Split on ### headings, anchoring at start-of-string OR after \n so a
// section that starts directly with `### Foo` (no preamble) splits the
// same way as one with preamble.
const chunks = section[1].split(/(?:^|\n)###\s+/);
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i];
// First line is the group name (until the newline).
const nameEnd = chunk.indexOf("\n");
if (nameEnd === -1) continue;
const name = chunk.slice(0, nameEnd).trim();
const body = chunk.slice(nameEnd);
const areas: string[] = [];
for (const m of body.matchAll(/`([a-z][a-z0-9-]+)`/g)) areas.push(m[1]);
if (areas.length > 0) groups.push({ group: name, areas });
}
return groups;
}
/**
* Validate that two grouped views agree on group names, group order,
* within-group items, and within-group order. Returns error messages
* naming the first divergence in each category — concise reports beat
* walls of diffs.
*/
export function validateGroupedParity(
sidebar: AreaGroup[],
docs: AreaGroup[],
): string[] {
const errors: string[] = [];
// Group-name + group-order check.
const sNames = sidebar.map((g) => g.group);
const dNames = docs.map((g) => g.group);
if (sNames.length !== dNames.length) {
errors.push(
`sidebar has ${sNames.length} groups, docs has ${dNames.length}: ` +
`sidebar=[${sNames.join(", ")}] docs=[${dNames.join(", ")}]`,
);
} else {
for (let i = 0; i < sNames.length; i++) {
if (sNames[i] !== dNames[i]) {
errors.push(
`group order/name mismatch at position ${i}: sidebar='${sNames[i]}' docs='${dNames[i]}'`,
);
break; // Subsequent positions are noise once we're misaligned.
}
}
}
// For matching group-name pairs, check items + order.
const dByName = new Map(docs.map((g) => [g.group, g.areas]));
for (const sg of sidebar) {
const da = dByName.get(sg.group);
if (!da) continue; // Already reported by name check above.
if (sg.areas.length !== da.length) {
errors.push(
`group '${sg.group}' count mismatch: sidebar=${sg.areas.length} docs=${da.length} ` +
`(sidebar=[${sg.areas.join(", ")}] docs=[${da.join(", ")}])`,
);
continue;
}
for (let i = 0; i < sg.areas.length; i++) {
if (sg.areas[i] !== da[i]) {
errors.push(
`group '${sg.group}' item order/name mismatch at position ${i}: ` +
`sidebar='${sg.areas[i]}' docs='${da[i]}'`,
);
break;
}
}
}
return errors;
}
/**
* Find duplicates in a list. Returns map of {area: count} for any area
* appearing more than once.
*/
export function findDuplicates(items: string[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const x of items) counts[x] = (counts[x] || 0) + 1;
const dupes: Record<string, number> = {};
for (const [k, n] of Object.entries(counts)) if (n > 1) dupes[k] = n;
return dupes;
}
/**
* Compare two area lists. Returns the symmetric difference broken out by
* direction (missing vs extra) so the caller can phrase the error.
*/
export function diffAreas(
a: string[],
b: string[],
): { missing: string[]; extra: string[] } {
const aSet = new Set(a);
const bSet = new Set(b);
return {
missing: [...aSet].filter((x) => !bSet.has(x)).sort(),
extra: [...bSet].filter((x) => !aSet.has(x)).sort(),
};
}
/**
* Extract the body of a `## Heading` section — content between the heading
* line and the next `## ` heading (or end of file). Returns null if the
* heading isn't found. Headings with regex meta-chars (parens, etc.) are
* escaped, so callers can pass `Project Analysis (No Area Specified)`.
*/
export function extractSection(content: string, heading: string): string | null {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`## ${escaped}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
const m = content.match(re);
return m ? m[1] : null;
}
/**
* Extract audit-area tokens referenced inline within a `## Heading` section
* of commands/audit.md. Handles four context-anchored patterns that today's
* audit.md uses to mention areas — anchors prevent false positives like
* matching "memory-auditor" (an agent name) when the section just discusses
* area dispatch. Returns tokens in document order, preserving multiplicity
* so duplicates can be reported.
*
* Patterns (any combination of these may appear in a section):
* 1. `\`/axiom:audit AREA\`` — Direct Dispatch examples
* 2. `- AREA → ...` — Priority Order bullets
* 3. `Run AREA + AREA + AREA` — Batch Recommendations
* 4. `→ suggest AREA, AREA` — Project Analysis triggers
*
* If a section uses none of these patterns, returns []. Caller validates
* each token is in the canonical area set.
*/
export function parseInlineAuditReferences(
content: string,
sectionHeading: string,
): string[] {
const section = extractSection(content, sectionHeading);
if (!section) return [];
const out: string[] = [];
// Pattern 1: backtick code spans `/axiom:audit AREA`
for (const m of section.matchAll(/`\/axiom:audit\s+([a-z][a-z0-9-]*)`/g)) {
out.push(m[1]);
}
// Pattern 2: bullet `- AREA →` (only the first token before the arrow).
// Anchored to start-of-line + bullet to avoid matching `agent-name → ...`
// mid-sentence.
for (const m of section.matchAll(/^\s*-\s+([a-z][a-z0-9-]*)\s*→/gm)) {
out.push(m[1]);
}
// Pattern 3: `Run X + Y + Z` — captures every lowercase token in the
// chain. Uppercase placeholders like `CRITICAL + HIGH` are excluded
// because the regex requires `[a-z]` start.
for (const m of section.matchAll(
/\bRun\s+([a-z][a-z0-9-]*(?:\s*\+\s*[a-z][a-z0-9-]*)+)/g,
)) {
for (const tok of m[1].split(/\s*\+\s*/)) out.push(tok);
}
// Pattern 4: `→ suggest X, Y` — captures every lowercase token in the
// comma-separated list. Trailing words like ` audit` after the last
// token are excluded because the regex stops at non-token chars.
for (const m of section.matchAll(
/→\s+suggest\s+([a-z][a-z0-9-]*(?:\s*,\s*[a-z][a-z0-9-]*)*)/g,
)) {
for (const tok of m[1].split(/\s*,\s*/)) out.push(tok);
}
return out;
}
/**
* Validate that every inline reference is in the canonical area set.
* Returns one error message per unknown reference (deduped). Caller
* supplies the section name so messages stay specific:
*
* "section 'Priority Order' references unknown area 'core-data-v2'"
*/
export function validateInlineReferences(
canonical: string[],
references: string[],
sectionLabel: string,
): string[] {
const set = new Set(canonical);
const seen = new Set<string>();
const errors: string[] = [];
for (const ref of references) {
if (set.has(ref) || seen.has(ref)) continue;
seen.add(ref);
errors.push(
`section '${sectionLabel}' references unknown area '${ref}' — not in canonical frontmatter list`,
);
}
return errors;
}
/**
* Extract the `description:` field from agent file frontmatter. Handles
* both single-line scalars and YAML block scalars (`description: |`),
* which is the format every Axiom agent uses today. The line-based
* frontmatter parser in pre-deploy.ts can't distinguish `description: |`
* (block-scalar marker) from a real value, so this function is required
* for the agent-description parity check.
*
* Returns the trimmed description text, or null if no frontmatter or no
* description field is present.
*
* The `description: |` block scalar terminates only at a KNOWN top-level agent
* key (AGENT_FRONTMATTER_KEYS), not at any `key:`-shaped line. So flush-left
* `<example>` content (`user:` / `assistant:`) inside a description never
* silently truncates it (axiom-2jf — previously the terminator matched any
* `/^[a-zA-Z][\w-]*:/`, so a column-0 example line cut the description short and
* `hasSubstantiveOverlap` could still pass on the prefix, hiding rename drift).
*/
export const AGENT_FRONTMATTER_KEYS = new Set([
"name",
"description",
"model",
"color",
"tools",
"skills",
"background",
"mcp",
"hooks",
"exempt-from-routing",
]);
export function parseAgentDescription(content: string): string | null {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) return null;
const fmLines = fmMatch[1].split("\n");
// Block scalar: `description: |` followed by indented lines until the
// next top-level YAML key (line starting with `key:`, no leading
// whitespace) or end of frontmatter. Line-based scan rather than
// a single regex — easier to reason about and avoids `m`-flag traps
// where `$` matches end of every line.
for (let i = 0; i < fmLines.length; i++) {
// Accept every YAML block-scalar header: `|`, `|-`, `|+`, `>`, `>-`,
// `>+`. A `|`-only test sends `|-` down the single-line branch, which
// returns the literal "|-" as the description — silently dropping the
// real text and everything derived from it.
if (/^description:\s*[|>][-+]?\s*$/.test(fmLines[i])) {
const body: string[] = [];
for (let j = i + 1; j < fmLines.length; j++) {
// Stop at the next KNOWN top-level agent key. A column-0 `user:` /
// `assistant:` from a flush-left <example> isn't a frontmatter key, so
// it stays in the description instead of truncating it (axiom-2jf).
const key = fmLines[j].match(/^([a-zA-Z][\w-]*):/);
if (key && AGENT_FRONTMATTER_KEYS.has(key[1])) break;
body.push(fmLines[j]);
}
// Strip up to 2-space leading indent from each line — YAML
// block-scalar convention. Don't trim arbitrary indentation;
// preserve structure for examples/snippets in the description.
const stripped = body.map((l) => l.replace(/^ {2}/, "")).join("\n").trim();
return stripped.length > 0 ? stripped : null;
}
}
// Single-line scalar: `description: ...` or `description: "..."`.
const inline = fmMatch[1].match(/^description:\s*(.+)$/m);
if (inline) {
let val = inline[1].trim();
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
}
return val.length > 0 ? val : null;
}
return null;
}
/**
* Common English + Axiom-template stop words excluded from substantive-
* word overlap. The agent-description boilerplate ("Use this agent when
* the user mentions...") would create false-positive overlap with any
* body description; filtering these forces overlap on actual domain
* vocabulary.
*/
const SUBSTANTIVE_STOP_WORDS = new Set([
// Common English ≥4 chars
"this", "that", "with", "from", "have", "been", "were", "what",
"which", "where", "when", "they", "them", "their", "there", "your",
"would", "could", "should", "will", "must", "than", "then", "such",
"into", "onto", "about", "after", "before", "during", "without",
"within", "between", "while", "also", "some", "many", "most",
"more", "less", "each", "other", "same", "only", "very", "just",
// Axiom agent-template boilerplate
"agent", "user", "users", "scans", "uses", "uses", "code", "review",
"audit", "audits", "auditor", "automatically", "mentions", "wants",
"check", "checks", "checking", "checked", "check",
"scan", "scanning", "scanned", "report", "reports",
"common", "issues", "issue", "patterns", "pattern",
// Verbs/nouns that appear in nearly every description
"find", "finds", "finding", "found", "detect", "detects", "detecting",
"covers", "including", "include", "includes",
]);
/**
* Tokenize a description into substantive lowercase words ≥ 4 chars,
* excluding stop words. Used by hasSubstantiveOverlap.
*/
function tokenizeSubstantive(s: string): Set<string> {
return new Set(
s
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((w) => w.length >= 4 && !SUBSTANTIVE_STOP_WORDS.has(w)),
);
}
/**
* Check whether two descriptions share at least one substantive word.
* Used to detect agent-description drift: if an agent gets renamed or
* repurposed but the body-table dispatch description doesn't get updated,
* the two will share no domain vocabulary.
*
* Match modes:
* 1. Exact word match (e.g., "leaks" ∩ "leaks")
* 2. Shared 5-char prefix (e.g., "allocation" ∩ "allocations" share
* "alloc"; "generic" ∩ "generics" share "gener") — handles plural
* and inflected forms without a real stemmer.
*
* Example: body "Retain cycles, leaks, Timer/observer patterns" vs
* agent "audit SwiftData migrations" — zero domain overlap, drift.
*
* Returns true if they share ≥ 1 substantive word OR 5-char prefix,
* false otherwise.
*/
export function hasSubstantiveOverlap(a: string, b: string): boolean {
const aw = tokenizeSubstantive(a);
const bw = tokenizeSubstantive(b);
if (aw.size === 0 || bw.size === 0) return false;
// Fast path: exact word match.
for (const w of aw) if (bw.has(w)) return true;
// Fallback: shared 5-char prefix handles plural/inflected forms.
const prefixesOf = (set: Set<string>): Set<string> =>
new Set([...set].filter((w) => w.length >= 5).map((w) => w.slice(0, 5)));
const ap = prefixesOf(aw);
const bp = prefixesOf(bw);
for (const p of ap) if (bp.has(p)) return true;
return false;
}
export interface AgentParityArgs {
rows: BodyRow[];
/** Map of agent name → file content. Caller reads files; this module
* stays I/O-free. */
agentFiles: Record<string, string>;
}
/**
* Validate that every body-table row has a corresponding agent file with
* a non-empty frontmatter description, and that the body description
* shares at least one substantive word with the agent description.
* Returns one error message per drift case.
*
* Three failure modes:
* 1. agent file content not in map (caller didn't include it — likely
* missing file, but agent existence is checked elsewhere)
* 2. agent description missing or empty
* 3. body and agent descriptions share zero substantive words —
* strong signal that one was renamed/repurposed without the other
*/
export function validateAgentDescriptionParity(
args: AgentParityArgs,
): string[] {
const errors: string[] = [];
for (const row of args.rows) {
if (!row.agent) continue;
const content = args.agentFiles[row.agent];
if (!content) continue; // Existence check is the caller's job.
const agentDesc = parseAgentDescription(content);
if (!agentDesc || agentDesc.length === 0) {
errors.push(
`agent '${row.agent}' (area '${row.area}') has missing or empty frontmatter description`,
);
continue;
}
if (!row.detects || row.detects.length === 0) {
errors.push(
`area '${row.area}' has empty body-table 'Detects' column — cannot cross-check with agent description`,
);
continue;
}
if (!hasSubstantiveOverlap(row.detects, agentDesc)) {
errors.push(
`area '${row.area}' / agent '${row.agent}': body-table description and agent frontmatter share no substantive vocabulary — likely rename drift. ` +
`body='${row.detects}' agent='${agentDesc.slice(0, 80)}${agentDesc.length > 80 ? "..." : ""}'`,
);
}
}
return errors;
}
/**
* Validate parity across all four sources. Returns a list of human-
* readable error messages. Agent-file existence (E) is left to the
* caller because it requires filesystem access.
*/
export function validateParity(args: {
frontmatter: string[];
body: string[];
docs: string[];
sidebar: string[];
}): string[] {
const errors: string[] = [];
const { frontmatter, body, docs, sidebar } = args;
// Parse failures.
if (frontmatter.length === 0)
errors.push("Could not parse audit areas from commands/audit.md frontmatter `argument:` line");
if (body.length === 0)
errors.push("Could not parse audit areas from commands/audit.md `## Available Audits` body table");
if (docs.length === 0)
errors.push("Could not parse audit areas from docs/commands/utility/audit.md `## Available Audit Areas` section");
if (sidebar.length === 0)
errors.push("Could not parse audit areas from docs/.vitepress/config.ts commands sidebar");
// Pairwise drift. Anchor on frontmatter (the canonical CLI dispatch list).
const pairs: Array<[string, string[], string]> = [
["body table", body, "frontmatter argument:"],
["docs page", docs, "frontmatter argument:"],
["sidebar config", sidebar, "frontmatter argument:"],
];
for (const [bName, b, aName] of pairs) {
if (frontmatter.length === 0 || b.length === 0) continue;
const { missing, extra } = diffAreas(frontmatter, b);
if (missing.length > 0)
errors.push(`${aName}${bName}: missing in ${bName}: ${missing.join(", ")}`);
if (extra.length > 0)
errors.push(`${aName}${bName}: extra in ${bName} (not in ${aName}): ${extra.join(", ")}`);
}
// Duplicate detection within each source.
const sources: Array<[string, string[]]> = [
["frontmatter argument:", frontmatter],
["body table", body],
["docs page", docs],
["sidebar config", sidebar],
];
for (const [name, items] of sources) {
const dupes = findDuplicates(items);
for (const [area, count] of Object.entries(dupes)) {
errors.push(`Duplicate audit area '${area}' appears ${count}× in ${name}`);
}
}
return errors;
}
/**
* The `all` meta-target dispatches to health-check rather than a regular
* audit area, so it is deliberately absent from the area list. Agents may
* still advertise it.
*/
const META_AUDIT_AREAS = new Set(["all"]);
/**
* Extract every audit area an agent advertises in its frontmatter
* description, e.g. "invoke this agent directly with `/axiom:audit
* security` or `/axiom:audit privacy`" → ["security", "privacy"].
*
* Frontmatter only. Agent bodies routinely name other auditors in prose
* ("compound with `/axiom:audit concurrency`"); only the frontmatter
* description is the agent's own dispatch contract.
*
* Order-preserving and deduplicated.
*/
export function parseAdvertisedAuditAreas(content: string): string[] {
// Whole file, not just the frontmatter description — same move as
// parseAdvertisedCommands (Axiom-2fa): the `/axiom:audit <area>` hints now
// live in the agent body, and an area promised there must still exist.
if (!content) return [];
const seen = new Set<string>();
const areas: string[] = [];
// `[^\S\r\n]+` — horizontal whitespace only. A bare `\s+` crosses line
// breaks, so prose ending a line on "/axiom:audit" captures the first
// word of the next line and reports it as an unregistered area.
for (const m of content.matchAll(/\/axiom:audit[^\S\r\n]+([a-z][a-z0-9-]*)/g)) {
const area = m[1];
if (seen.has(area)) continue;
seen.add(area);
areas.push(area);
}
return areas;
}
export interface AdvertisedAreaArgs {
/** Canonical registered areas — the frontmatter `argument:` list. */
registered: string[];
/** Map of agent name → file content. Caller reads files. */
agentFiles: Record<string, string>;
/** Allowed violations, each as `"<agent>:<area>"`. Keyed by the PAIR,
* not the agent — an agent-scoped exemption silently blesses every
* future unregistered area that agent ever advertises, including
* typos unrelated to the reason it was granted. Each entry must
* correspond to a real violation, or it is reported as stale. */
exempt?: string[];
}
/**
* Validate the INVERSE of `validateParity`: every `/axiom:audit <area>`
* an agent advertises must resolve to a registered area.
*
* `validateParity` anchors on the frontmatter list and diffs the other
* three sources against it, so it only detects drift BETWEEN sources. An
* area absent from all four at once presents as a perfectly consistent
* world. `grdb-performance` and `test-failures` both shipped that way:
* the agent promised a command that dispatched nowhere, and
* build-inlined-auditors.ts silently dropped the command line from the
* generated sub-skill because it derives that line from the same table.
*
* Exemptions are checked for staleness so a carve-out can't quietly
* outlive the violation it was granted for.
*/
export function validateAdvertisedAreas(args: AdvertisedAreaArgs): string[] {
// An unparseable registry is one error, reported by validateParity.
// Proceeding would turn it into one bogus error per advertising agent
// and bury the real one. Mirrors validateParity's own empty-list skip.
if (args.registered.length === 0) return [];
const errors: string[] = [];
const registered = new Set(args.registered);
const exempt = new Set(args.exempt ?? []);
const exemptUsed = new Set<string>();
for (const [agent, content] of Object.entries(args.agentFiles)) {
for (const area of parseAdvertisedAuditAreas(content)) {
if (registered.has(area) || META_AUDIT_AREAS.has(area)) continue;
const key = `${agent}:${area}`;
if (exempt.has(key)) {
exemptUsed.add(key);
continue;
}
errors.push(
`agent '${agent}' advertises \`/axiom:audit ${area}\` but '${area}' is not a registered audit area — ` +
`the command will not dispatch, and build-inlined-auditors.ts will omit it from the generated sub-skill. ` +
`Register it in commands/audit.md (frontmatter argument: + body table), docs/commands/utility/audit.md, ` +
`and the docs sidebar; or add '${key}' to the exempt list with a reason.`,
);
}
}
for (const key of exempt) {
if (exemptUsed.has(key)) continue;
const agent = key.slice(0, key.indexOf(":"));
if (!Object.hasOwn(args.agentFiles, agent)) {
errors.push(
`stale exemption: '${key}' is in the advertised-area exempt list but there is no such agent file — remove it`,
);
continue;
}
errors.push(
`stale exemption: '${key}' is in the advertised-area exempt list but that agent no longer advertises that unregistered area — remove it`,
);
}
return errors;
}
/**
* Extract every `/axiom:<command>` an agent advertises in its frontmatter
* description — the COMMAND name only, not its argument. So
* "`/axiom:audit memory`" yields `["audit"]`, and an agent advertising
* two arguments of the same command yields it once.
*
* Complements `parseAdvertisedAuditAreas`, which validates the argument
* of the one command that takes an area. Together they cover both halves
* of `/axiom:audit <area>`; this one alone covers the rest of the family
* (`/axiom:fix-build`, `/axiom:profile`, …).
*
* Order-preserving and deduplicated. Frontmatter only, for the same
* reason as `parseAdvertisedAuditAreas` — bodies cross-reference other
* commands in prose.
*/
export function parseAdvertisedCommands(content: string): string[] {
// Whole file, not just the frontmatter description: the `Explicit command:`
// hints that advertise these commands moved into the agent body (Axiom-2fa —
// the frontmatter listing is always-on in every session, the body is not).
// A ghost command promised in the body is exactly as broken as one promised
// in the frontmatter, which is what this check exists to catch.
if (!content) return [];
const seen = new Set<string>();
const commands: string[] = [];
for (const m of content.matchAll(/\/axiom:([a-z][a-z0-9-]*)/g)) {
const command = m[1];
if (seen.has(command)) continue;
seen.add(command);
commands.push(command);
}
return commands;
}
export interface AdvertisedCommandArgs {
/** Registered command names, e.g. derived from the plugin manifest's
* `commands` array. */
registered: string[];
/** Map of agent name → file content. Caller reads files. */
agentFiles: Record<string, string>;
/** Allowed violations as `"<agent>:<command>"` pairs. Keyed by the
* pair for the same reason as `AdvertisedAreaArgs.exempt`. */
exempt?: string[];
}
/**
* Validate that every `/axiom:<command>` an agent advertises resolves to
* a registered command.
*
* Same blind-spot class as `validateAdvertisedAreas`, one namespace over:
* an agent's "Explicit command:" line is the only place some commands are
* named, so a command that was renamed, never created, or dropped from
* the manifest leaves the agent promising something that cannot run —
* and nothing else in the build notices, because no other source claims
* that command exists either.
*/
export function validateAdvertisedCommands(
args: AdvertisedCommandArgs,
): string[] {
if (args.registered.length === 0) return [];
const errors: string[] = [];
const registered = new Set(args.registered);
const exempt = new Set(args.exempt ?? []);
const exemptUsed = new Set<string>();
for (const [agent, content] of Object.entries(args.agentFiles)) {
for (const command of parseAdvertisedCommands(content)) {
if (registered.has(command)) continue;
const key = `${agent}:${command}`;
if (exempt.has(key)) {
exemptUsed.add(key);
continue;
}
errors.push(
`agent '${agent}' advertises \`/axiom:${command}\` but no such command is registered — ` +
`there is no commands/${command}.md in the plugin manifest, so the command cannot run. ` +
`Create it and add it to claude-code.json's commands array; or drop the claim from the agent; ` +
`or add '${key}' to the exempt list with a reason.`,
);
}
}
for (const key of exempt) {
if (exemptUsed.has(key)) continue;
const agent = key.slice(0, key.indexOf(":"));
if (!Object.hasOwn(args.agentFiles, agent)) {
errors.push(
`stale exemption: '${key}' is in the advertised-command exempt list but there is no such agent file — remove it`,
);
continue;
}
errors.push(
`stale exemption: '${key}' is in the advertised-command exempt list but that agent no longer advertises that unregistered command — remove it`,
);
}
return errors;
}