AXIS (Agent Experience Index Score) is a synthetic testing framework for AI agents. This skill teaches you to author the two files an AXIS user maintains:
- Scenarios under
scenarios/(or any path the config points at): one JSON file per task the agent will be asked to perform. axis.config.jsonat the project root: which agents to run, where scenarios live, and what is shared across them.
When to use this skill
Trigger phrases include "set up AXIS", "add an AXIS scenario", "write an axis.config.json", "evaluate my agent with AXIS", "score my agent on X".
Before authoring, do this:
- Look for an existing
axis.config.json(oraxis.config.{js,ts}) at the project root. If one exists, read it; do not overwrite without confirmation. - List the existing scenarios directory if present. Match its naming and style.
- If no config exists, suggest running
npx @netlify/axis initfirst, or scaffold one yourself using the patterns below.
Conceptual model
For each scenario, AXIS runs every configured agent against the same prompt in an isolated workspace, then scores each run on four dimensions and produces an HTML + JSON report.
- Goal achievement (default weight 0.4): did the agent satisfy the judge's checks?
- Environment (0.2): did filesystem / shell / network operations succeed reliably?
- Service (0.2): did external services (APIs, MCP servers) respond reliably?
- Agent (0.2): were the agent's decisions sound across every tool call?
Refer to the framework's output as the AXIS Result. The acronym expands to Agent Experience Index Score.
Authoring a scenario
A scenario is a JSON file under the scenarios directory. The file path (without extension, relative to that directory) becomes the scenario's key. Only name, prompt, and judge are required.
The annotated examples below are labeled
jsoncfor documentation only. They contain// commentsand trailing commas. Real.jsonfiles do NOT support either. When copying these examples into output, strip every//comment line and every trailing comma. Plain JSON examples (in the Recipes section) are safe to copy verbatim.
Full annotated shape:
{
// Display name shown in reports.
"name": "Refactor utility module",
// Set true to exclude this scenario from runs without deleting it.
"skip": false,
// Run before the agent starts. Two action types: run_script and copy.
"setup": [
{ "action": "run_script", "command": "git init -q && git add -A && git commit -q -m init" },
{ "action": "copy", "match": "fixtures/sample-repo/**", "destination": "." },
// …or stage a real codebase: { "action": "copy", "match": "https://github.com/org/project#v1.2.3", "destination": "." }
],
// The task. Be specific and verifiable.
"prompt": "Refactor src/utils.js to split it into two files: src/strings.js and src/numbers.js. Update all import sites and ensure `npm test` still passes.",
// Judge: either a single string OR an array of weighted checks.
// Use the array form for multi-criterion scoring. Weight is optional;
// remaining weight is distributed equally across unweighted entries.
"judge": [
{ "check": "src/strings.js exists and exports the string utilities", "weight": 0.3 },
{ "check": "src/numbers.js exists and exports the numeric utilities", "weight": 0.3 },
{ "check": "All import sites are updated and reference the new files", "weight": 0.2 },
{ "check": "`npm test` passes after the change", "weight": 0.2 },
],
// Run after the agent finishes, before scoring.
"teardown": [{ "action": "run_script", "command": "rm -rf node_modules" }],
// Only these agents run this scenario. Overrides the top-level agents list.
"agents": ["claude-code"],
// Skills passed to the agent under test. Each entry is a local path,
// GitHub shorthand (owner/repo), or a full GitHub URL.
"skills": ["./skills/repo-conventions", "anthropics/skills"],
// MCP servers available to the agent for this scenario only. Merged
// with the top-level mcp_servers from axis.config.json.
"mcp_servers": {
"filesystem": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] },
},
// Per-scenario time/token limits. Defaults: 15 minutes, no token cap.
"limits": { "time_minutes": 10, "tokens": 200000 },
// How many times to run this scenario against each agent. Overrides
// settings.runs. Defaults to 1. Must be ODD (1, 3, 5, …), max 19. Raise it
// for a scenario whose score swings between runs; each extra run costs a
// full agent execution plus its judge calls. See "Repeated runs" below.
"runs": 3,
// Glob patterns (relative to the workspace) of files to capture into the
// report after teardown. Merged with the top-level artifacts list.
"artifacts": ["src/**/*.js", "test-output.log"],
}
Judge: string vs weighted array
- String when one statement captures the whole pass/fail bar:
"The agent should have written summary.md with at least three sentences." - Weighted array when there are multiple checks the agent could partially satisfy. Each entry has a
check(the statement) and an optionalweight(sum your weights to 1.0; unweighted entries split the remainder evenly).
Write checks as observable facts a third party could verify, not vibes. "Agent wrote a file named summary.md with at least three sentences" beats "Agent did a good job summarizing".
Lifecycle actions
Two action types are allowed in setup and teardown:
{ "action": "run_script", "command": "<shell command>" }: runs with the agent's workspace as cwd. Available env vars includeAXIS_PHASE(setup/teardown),AXIS_WORKSPACE, andAXIS_OUTPUT(a file path where the script can append markdown that will surface in the report).{ "action": "copy", "match": "<glob>", "destination": "<workspace-relative path>" }: copies files matchingmatch(resolved relative to the config file) intodestination(relative to the workspace). The path of each matched file relative to the longest non-glob prefix ofmatchis preserved underdestination.
match also accepts a git URL, which stages a real codebase instead of a local fixture:
{ "action": "copy", "match": "https://github.com/org/project", "destination": "." }clones the repo and copies its working tree (including.git) into the workspace.- Pin a ref with a fragment (
"https://github.com/org/project#v1.2.3") or a"ref"field. Pin whenever you care about comparing results over time; an unpinned repo is fetched once and then reused as-is. - Paste a directory URL (
"https://github.com/org/project/tree/main/examples/blog") to copy just that subtree. The subpath may itself be a glob. "include_git": falsedrops the.gitdirectory, leaving a plain file tree.git@github.com:org/project.git,git://, andssh://URLs work too.
Each repo + ref is cloned once into .axis/repos/ and reused by every scenario, variant, and agent in the run (and by later runs). axis run --refresh-repos re-clones. Dependencies are not installed for you: follow the copy with { "action": "run_script", "command": "npm install" } when the scenario needs them.
Variants
When a scenario should run multiple times with small differences, define variants. The parent does not run by itself; each variant inherits all parent fields and may override prompt, judge, setup, teardown, agents, skills, mcp_servers, limits, runs, artifacts, or skip.
Variants and runs are different axes and should not be confused. A variant is a deliberately different configuration that gets its own key and its own score. runs repeats the same configuration to measure how much its score moves. A scenario with two variants and runs: 3 executes six times and reports two results.
{
"name": "Summarize the docs",
"prompt": "Summarize the contents of docs/ into summary.md.",
"judge": "summary.md exists and is at least 200 words.",
"variants": [
{ "name": "baseline" },
{ "name": "concise", "prompt": "Summarize the contents of docs/ into summary.md in fewer than 100 words." },
],
}
Each variant's key is {scenarioKey}@{variantName}. Variant names must match /^[a-zA-Z0-9_-]+$/.
Authoring axis.config.json
Sits at the project root. Minimum viable file:
{
"scenarios": "./scenarios",
"agents": ["claude-code"]
}
Full annotated shape:
{
// Shown in report headers.
"name": "My Project",
// Where scenarios come from. Three forms:
// - "./path" (a directory, walked recursively for *.json scenarios)
// - ["./path1", "./scenarios/special.json"] (mix of dirs and single files)
// - Mixed with inline scenarios (objects with a required "key" field) when
// authoring axis.config.{js,ts} programmatically.
// - Git URLs ("https://github.com/owner/repo") cloned into .axis/remotes/
// and merged from their own axis.config.
// Defaults to "./scenarios" when omitted.
"scenarios": ["./scenarios", "https://github.com/netlify/agent-runner-orchestrator"],
// Suite selection. Scenario key globs, applied to whatever "scenarios"
// discovered. "include" keeps only matches, then "exclude" drops matches.
// Omitting "include" keeps everything; "include": [] selects nothing.
// A bare "*" in either list means every scenario (plain glob "*" does not
// cross "/", so it would otherwise skip keys like "cms/create-post").
// Use "exclude" to hold a group of scenarios out of the default suite so a
// profile can claim them; to disable a scenario outright, set "skip": true
// on the scenario itself.
"exclude": ["ask-*"],
// Named config overlays, selected with `axis run --profile ask`.
// See "Profiles" below.
"profiles": {
"ask": {
"include": ["ask-*"],
"agents": [{ "agent": "claude-code", "flags": { "disallowed-tools": "Edit,Write" } }],
},
},
// Agents to evaluate. Strings are shorthand for { "agent": "<name>" }.
"agents": [
"claude-code",
{ "agent": "claude-code", "model": "claude-opus-4-6" },
{ "agent": "codex", "model": "gpt-5-codex" },
{
"agent": "echo", // custom adapter (see "adapters" below)
"command": "./bin/my-agent", // CLI override for custom adapters
"scenarios": ["hello-world"], // restrict this agent to a subset (scenario keys)
"skills": ["./skills/my-conventions"], // per-agent skills
"flags": { "debug": true, "max-turns": "5" },
"name": "echo|control", // stable report/baseline/-a identity, see below
},
// Provider-prefixed models (e.g. OpenRouter) are supported. The raw string
// is passed verbatim to the CLI via --model; the derived agent name slugifies
// the slashes, so this becomes "claude-code|anthropic-claude-3.5-sonnet".
{ "agent": "claude-code", "model": "anthropic/claude-3.5-sonnet" },
],
// Agents used to score runs. When omitted, each agent judges itself
// (it scores its own transcript). Otherwise the first entry whose adapter
// name differs from the run's own agent is picked; if every entry matches,
// the first entry is used. Order is precedence.
"judging": {
"agents": [{ "agent": "claude-code", "model": "claude-opus-4-7" }, "codex"],
},
// MCP servers shared across all agents and all scenarios. Two types:
// stdio (local subprocess) and http (remote endpoint).
"mcp_servers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"env": { "DEBUG": "1" },
},
"search": {
"type": "http",
"url": "https://mcp.example.com/search",
"headers": { "Authorization": "Bearer ${SEARCH_TOKEN}" },
},
},
// Skills shared across all agents. Each entry is one of:
// - local path: "./skills/my-skill"
// - GitHub shorthand: "anthropics/skills"
// - GitHub URL: "https://github.com/anthropics/skills"
"skills": ["./skills/repo-conventions"],
// Glob patterns (relative to each scenario's workspace) of files captured
// into the report after teardown. Merged with per-scenario artifacts.
"artifacts": ["./*.md", "src/**/*.js"],
// Custom adapters. Keys are adapter names referenced by "agents" entries;
// values are paths (relative to this config) to JS/TS modules that export
// an AgentAdapter as default or as `adapter`.
"adapters": { "echo": "./adapters/echo.ts" },
// Extra env vars allowed through to agent processes. PATH, HOME, and the
// default agent API keys (ANTHROPIC_API_KEY, CODEX_API_KEY, GEMINI_API_KEY)
// pass through automatically; entries here are merged on top.
"env": ["GITHUB_TOKEN", "MY_FEATURE_FLAG"],
// Run-wide settings.
"settings": {
"concurrency": 8,
// How many times each scenario/agent pair runs. Defaults to 1. Overridden
// by a scenario's own "runs", and by `--runs` on the CLI. Must be ODD,
// max 19. Raising this does NOT scale limits.run, a whole-run budget.
"runs": 1,
"limits": {
"run": { "time_minutes": 60, "tokens": 5000000 },
"scenario": { "time_minutes": 10, "tokens": 200000 },
},
"remotes": { "maxDepth": 1 },
},
// CLI-only lifecycle hooks. beforeAll runs once before any scenarios start;
// afterAll runs once after every scenario is scored. Scripts execute with
// cwd set to the config directory. afterAll receives AXIS_REPORT_DIR,
// AXIS_TOTAL, AXIS_COMPLETED, AXIS_FAILED, AXIS_DURATION_MS as env vars.
// These are not fired by the programmatic run() API.
"beforeAll": [{ "action": "run_script", "command": "npm install --silent" }],
"afterAll": [{ "action": "run_script", "command": "echo Report at $AXIS_REPORT_DIR" }],
}
Built-in adapters
claude-code: Anthropic Claude Code CLI. RequiresANTHROPIC_API_KEY.codex: OpenAI Codex CLI. RequiresCODEX_API_KEY.gemini: Google Gemini CLI. RequiresGEMINI_API_KEY.
Any other name in agents[].agent must be declared in the adapters map and point to a module that exports an AgentAdapter.
Profiles
A profile is a named overlay on the config, selected with axis run --profile <name>. Reach for one when a single repo has to run scenarios under materially different conditions, such as a read-only "ask" pass and a full write pass, where the same agent and model need two different flag sets.
Without profiles, listing the same agent plus model twice produces two entries named agent|model and agent|model-2, so identity depends on array order. Profiles keep each variant in its own overlay and every suite reports under stable names.
{
"scenarios": "./scenarios",
"exclude": ["ask-*"],
"agents": [{ "agent": "claude-code", "model": "opus" }],
"profiles": {
"ask": {
"include": ["ask-*"],
"agents": [{ "agent": "claude-code", "model": "opus", "flags": { "disallowed-tools": "Edit,Write" } }]
}
}
}
axis run runs everything except ask-*. axis run --profile ask runs only ask-*, with the read-only flags.
Merge rules:
- Plain objects deep-merge, so a profile setting only
settings.limitskeeps the basesettings.concurrency. - Arrays and scalars replace wholesale, so a profile's
agentsis its own matrix, not an addition to the base one. includeandexcludemerge as a pair: a profile that sets either one replaces both and starts from the full scenario pool. Without this rule a baseexclude: ["ask-*"]would cancel a profile'sinclude: ["ask-*"].- Profiles cannot nest.
- An unknown
--profilename is an error, never a fallback to the default suite.
Selection order per agent: walk scenarios, apply the active suite's include then exclude, narrow to that agent's own scenarios list, then apply the CLI --scenario filter. An agent filter or -s narrows within the active suite; neither can reach a scenario the suite excluded.
Once a config defines profiles, a scenario excluded from the default suite that no profile includes can never run, and AXIS fails the run rather than silently covering less than the suite defines.
A function-style config receives the selected profile in its context argument, for anything the declarative merge cannot express. If it returns no profiles map, AXIS assumes the factory handled the profile and merges nothing.
export default ({ profile }) => ({
scenarios: "./scenarios",
agents: profile === "ask" ? askAgents : createAgents,
});
Stable agent names
An agent entry is identified by {agent}|{model}, with -2, -3 appended when several entries derive the same name. That suffix is positional, so inserting an entry above renames the ones below it and orphans their report history. Set name explicitly when two entries share an agent and model but differ only in flags:
{ "agent": "claude-code", "model": "sonnet", "name": "claude-code|sonnet-control", "flags": { "effort": "low" } }
The name is used verbatim in report paths, baselines, and -a filters. It must be unique and must not contain / or \.
Judging precedence
For each run, AXIS finds a judge by scanning judging.agents in order and picking the first entry whose adapter name differs from the agent being scored. If every entry matches, the first entry is used. When judging is omitted, the run's own agent judges itself.
Repeated runs
Agents are stochastic, so a scenario's score moves between identical runs. Set runs above 1 to sample a pair several times and let AXIS report a representative result plus the spread around it.
Precedence, most specific first: --runs <n> on the CLI, then a scenario's or variant's runs, then settings.runs, then 1. Must be a positive odd integer (1, 3, 5, …), capped at 19.
Odd is enforced, and it is worth being able to explain why if a user asks for runs: 2. An even sample has no middle run, so the median of the composites falls between two runs and the reported number belongs to none of them. At exactly two runs it is worse: both runs sit at identical distance from the per-dimension medians, so the tie-break returns run 1 whether run 1 was the good one or the bad one. Steer users to 3.
{
"scenarios": "./scenarios",
"agents": ["claude-code"],
"settings": { "runs": 3 }
}
What to tell the user when you configure this:
- It is not free. Each extra run is a full agent execution plus its four judge calls, so
runs: 3costs roughly 3x the tokens and 3x the wall clock. Default it off and raise it where score stability matters: baselines, CI gates, and scenarios known to be volatile. - Scope it narrowly first. Prefer
runson the two or three scenarios that actually swing oversettings.runsacross a whole suite. - Run limits do not scale with it.
settings.limits.runis a budget for the entire run, so three runs per pair exhaust it about three times sooner. Raise it alongsiderunsor the run aborts partway through. - Shared external state needs namespacing. Repeats get isolated workspaces but not isolated cloud resources. If
setupprovisions a real site, database, or queue, include$AXIS_RUN_INDEXin its name:
{
"setup": [
{
"action": "run_script",
"command": "netlify sites:create --name \"axis-$AXIS_SCENARIO-$AXIS_RUN_INDEX\""
}
],
"teardown": [
{
"action": "run_script",
"command": "netlify sites:delete --name \"axis-$AXIS_SCENARIO-$AXIS_RUN_INDEX\" --force"
}
]
}
AXIS_RUN_INDEX and AXIS_RUN_COUNT are always set, reading 1 and 1 for an unrepeated pair, so a script never has to branch on whether repeats are configured.
Recipes
Minimal scenario
{
"name": "Hello world",
"prompt": "Say hello.",
"judge": "The agent should say hello."
}
Realistic scenario with setup, weighted judge, and teardown
{
"name": "Debug and fix a broken script",
"setup": [
{
"action": "run_script",
"command": "mkdir -p /tmp/demo && echo 'function add(a, b) { return a - b; }' > /tmp/demo/add.js"
}
],
"prompt": "There is a JavaScript file at /tmp/demo/add.js with a bug. Run it, fix it, and verify the fix.",
"judge": [
{ "check": "Agent ran the script and observed the wrong output", "weight": 0.25 },
{ "check": "Agent identified the subtraction-instead-of-addition bug", "weight": 0.25 },
{ "check": "Agent fixed the bug in the file", "weight": 0.25 },
{ "check": "Agent re-ran the script and confirmed correct output", "weight": 0.25 }
],
"teardown": [{ "action": "run_script", "command": "rm -rf /tmp/demo" }]
}
Multi-agent comparison config
{
"scenarios": "./scenarios",
"agents": [
{ "agent": "claude-code", "model": "claude-sonnet-4-6" },
{ "agent": "claude-code", "model": "claude-opus-4-6" },
"codex"
],
"judging": {
"agents": [{ "agent": "claude-code", "model": "claude-opus-4-7" }]
}
}
One repo, several suites (profiles)
The same scenarios directory split into a default suite and two named ones. Scenario files stay profile-agnostic; membership is declared here, by key glob.
{
"scenarios": "./scenarios",
"exclude": ["ask-*", "questions-*"],
"agents": [{ "agent": "claude-code", "model": "claude-opus-5", "flags": { "permission-mode": "bypassPermissions" } }],
"profiles": {
"create": {},
"ask": {
"include": ["ask-*"],
"agents": [
{
"agent": "claude-code",
"model": "claude-opus-5",
"flags": { "disallowed-tools": "Edit,Write,NotebookEdit" }
},
{ "agent": "codex", "model": "gpt-5.6-sol", "flags": { "sandbox": "read-only" } }
]
},
"questions": {
"include": ["questions-*"],
"agents": [{ "agent": "claude-code", "model": "claude-opus-5" }]
}
}
}
axis run runs the create suite. axis run --profile ask runs only ask-*, read-only. The empty create profile is an alias, so CI can pass all three names uniformly: axis run --profile "$MODE".
Two entries sharing an agent and model but differing in flags belong in separate profiles. If they must coexist in one agents array, give at least one an explicit name, or the second silently becomes agent|model-2 and its identity depends on array order.
Custom adapter wiring
Module adapters/echo.ts:
import { createAgentAdapter } from "@netlify/axis";
export default createAgentAdapter<{ stdout: string }>({
name: "echo",
resolveCommand: () => ({ command: "echo", prefixArgs: [] }),
buildArgs: (input) => [input.prompt],
initialState: () => ({ stdout: "" }),
streamConfig: {
mode: "aggregate",
onChunk: (chunk, ctx) => {
ctx.state.stdout += chunk;
},
},
getResult: (ctx) => ({ result: ctx.state.stdout.trim() || null }),
});
axis.config.json:
{
"adapters": { "echo": "./adapters/echo.ts" },
"scenarios": "./scenarios",
"agents": [{ "agent": "echo" }]
}
Reading AXIS reports
For interpreting reports, comparing runs, finding regressions, or explaining scores, use the companion skill using-axis. It covers the report file layout, dimension semantics, calibration, and citation rules in depth. This skill stays focused on authoring.
Rules you must follow
- Refer to the score as the AXIS Result, never "AXIS Score" (which reads as "score score"). The acronym is Agent Experience Index Score, never "eXperience".
- Do not use em dashes in any prose, comment, or judge check you author. Use a comma, semicolon, colon, parenthesis, or a new sentence instead.
- Do not invent fields. The authoritative schemas live at
src/types/scenario.tsandsrc/types/config.tsin the netlify/axis repo. If you are unsure whether a field exists, omit it and tell the user where to look. - Judge checks must be specific and verifiable. Prefer "Agent wrote a file named X with property Y" over "Agent did well at the task".
- The default per-scenario time limit is 15 minutes. Only override it when the task warrants a different ceiling.
- For
skillsentries, pick the simplest form that works: prefer a local path during development, GitHub shorthand (owner/repo) for public skills, full URLs only when needed. - When
judgeis a weighted array, sum your weights to 1.0 (or leave some unweighted to split the remainder; do not exceed 1.0). - Variant names match
/^[a-zA-Z0-9_-]+$/. Scenario keys are derived from the file path; do not setkeyfor file-based scenarios. beforeAllandafterAllonly fire from the CLI. Do not rely on them when the user runs AXIS programmatically viarun().- In an isolated AXIS scenario workspace, do NOT try to verify your authored file by executing it, importing it, or cross-checking it against an installed copy of
@netlify/axis. The workspace is intentionally minimal: nonode_modules, no git history. That means: notsc, nonode -e "require(...)", nogit difforgit status, nonpm install, and no reading or grepping the globally-installed@netlify/axispackage outside the workspace (paths like/usr/local/lib/node_modules/@netlify/axis,/opt/homebrew/.../node_modules/@netlify/axis, or anynode_modules/@netlify/axisyou did not put there yourself). Every such command fails or wastes interactions and tanks the environment and agent dimensions. Write the file once, correctly, against the schema you already know from this skill. The AXIS judge inspects your output directly; you do not need to prove it works first. - When asked to make a targeted edit (add a field, fix a single bug), edit ONLY what the prompt specifies. Do not reorganize, reformat, or add unrelated fields. Preserve every field the prompt did not name. The judge often checks "original X and Y fields are preserved unchanged".
- Minimize unnecessary tool calls. Every tool call is evaluated as an agent decision; redundant
ls, repeatedcatof the same file, exploratoryfindthat you do not act on, all tank the agent dimension via thenecessitysub-dimension. Read each file you need once. Write each edit once. Stop when the task is done. - Profiles are config-side only. A scenario never declares which profile it belongs to: there is no
profilesfield on a scenario, and inventing one is the most common mistake here. Suite membership is declared in the config withinclude/excludekey globs, which keeps scenario files portable across repos that clone them. Useskip: trueto disable a scenario everywhere; useexcludeto hold it out of the default suite so a profile can claim it. Excluding a scenario that no profile includes fails the run. runsmust be odd. If a user asks for 2 or 4, do not silently write 3 or 5; tell them why even counts are rejected (no middle run, and at 2 the representative is positional) and let them pick.- Do not reach for
runsto express different configurations.runsrepeats one configuration to measure volatility;variantsexpress deliberately different configurations that each deserve their own score. If the user wants "the same task with and without MCP", that is two variants, not two runs. - Field-name discipline. AXIS uses snake_case in all JSON config fields:
mcp_serversnotmcpServers,time_minutesnottimeMinutes,run_scriptnotrunScriptorshell. The deprecated aliasrubricexists for backwards compat; preferjudge. Other commonly-invented names that are WRONG:criteria,success_criteria,expected,tasks,evaluators,models,timeout,maxTokens,tokenLimit,timeoutMinutes.
Validation
After authoring or editing, ask the user to run:
npx @netlify/axis run --help
to confirm the CLI is installed, then:
npx @netlify/axis run
The config loader validates the file on load and prints actionable errors (missing required fields, unknown adapter names, malformed limits, invalid skill sources, etc.). Fix any reported errors before declaring the work done.
Reference
- Documentation site: https://axis.run
- Scenario schema:
src/types/scenario.tsin the netlify/axis repo - Config schema:
src/types/config.tsin the netlify/axis repo - Validator (source of truth for accepted shapes):
src/config/validator.ts
Installing this skill
To make this skill available to your AI tool in a project, drop it under .claude/skills/:
mkdir -p .claude/skills/configure-axis
curl -fsSL https://raw.githubusercontent.com/netlify/axis/main/skills/configure-axis/SKILL.md \
-o .claude/skills/configure-axis/SKILL.md