diff --git a/README.md b/README.md
index 296dd4e..54d7b32 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,8 @@ The site defaults to the host root; the docs are under `/docs/`. Pages CI sets
`NEXT_PUBLIC_BASE_PATH=/agent-cli` for its deployment, builds and checks pull
requests, and deploys main. Command schemas and the skill guide are generated
from the CLI with `npm run docs:generate` and before docs builds.
+Agents can fetch the same packaged skill as plain Markdown at
+[SKILL.md](https://bloxwap.github.io/agent-cli/SKILL.md).
[Docs index](docs/index.md), [security](docs/security.md),
[error catalog](docs/errors.md), and [SKILL.md](packages/agent-cli/SKILL.md) remain
diff --git a/apps/docs/README.md b/apps/docs/README.md
index 7c39715..6328149 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -23,11 +23,17 @@ canonical and social URLs (default `https://bloxwap.github.io`). The static prev
reads the base path from the built artifact and needs no Next.js server.
Edit MDX in `content/docs/` and sidebar order in `meta.json`. `commands.mdx` and
-`skill.mdx` are generated from the built CLI and packaged SKILL.md. Run
+`skill.mdx`, plus the plain Markdown `public/SKILL.md`, are generated from the built
+CLI and packaged SKILL.md. Run
`npm run docs:generate` after CLI changes. Dev/build also synchronize them;
`docs:check` rejects stale generated content, checks sidebar/local MDX links,
verifies HIG provenance, and checks Next.js/TypeScript types.
+The canonical skill is https://bloxwap.github.io/agent-cli/SKILL.md
+(`http://localhost:3903/SKILL.md` during local development). It preserves the
+packaged skill's frontmatter and is linked from the skill guide and `llms.txt`.
+The export and browser checks verify that the served file matches the source.
+
Build output lives in `out/`. The postbuild checker validates local HTML links
and assets, base-path containment, social metadata and 1200×630 PNG cards. Search
uses a static `search.json` index in the browser. `llms.txt`, robots, and sitemap
diff --git a/apps/docs/content/docs/skill.mdx b/apps/docs/content/docs/skill.mdx
index a11116d..3adb274 100644
--- a/apps/docs/content/docs/skill.mdx
+++ b/apps/docs/content/docs/skill.mdx
@@ -3,6 +3,10 @@ title: Agent skill guide
description: Install and use the packaged SKILL.md with Codex or Claude Code.
---
+import Link from 'next/link';
+
+Download SKILL.md as plain Markdown, including the skill frontmatter.
+
The npm package ships `SKILL.md` and embeds the same content in the CLI. Install it with:
```sh
diff --git a/apps/docs/public/SKILL.md b/apps/docs/public/SKILL.md
new file mode 100644
index 0000000..f608c5b
--- /dev/null
+++ b/apps/docs/public/SKILL.md
@@ -0,0 +1,158 @@
+---
+name: agent-cli
+description: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery.
+metadata:
+ version: "0.2.0"
+---
+
+Use `agent-cli` for the user's authorized Hyperliquid account operations. Obtain
+command schemas with `agent-cli schema COMMAND`; arguments can be flags or
+`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one
+JSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`.
+Logs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.
+Use `--network testnet` during testing; never silently change to mainnet.
+
+## Onboarding sequence
+
+1. The user creates and owns their own Privy account and app. Configure that
+ app ID and a local secret reference, e.g. `config set privyAppId APP` and
+ `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets the
+ environment secret outside the conversation on their own machine or runner.
+ Bloxwap does not host the app, receive credentials, or custody user keys.
+ Privy manages the trading-agent key in the user's app; master-wallet approval
+ always remains in the user's external wallet. The default Bloxwap builder is
+ `0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (1 bp = 0.01%).
+ Override with `config set builder.address` / `config set builder.fee` or
+ `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`.
+2. Run `init --master-address 0x... --network testnet`. Show the returned approval
+ payload/deeplink/QR. The user signs in their master wallet; submit only the
+ resulting signature with `init --signature 0x...`. Submit builder approval
+ separately with `init --builder-signature 0x...` for the configured 0.01% default. Continue only after `approved:true`.
+3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,
+ origin chain/token, minimum, fees, expected received amount and expiry. On
+ testnet, do not treat the default Arbitrum mainnet chain/token as a test route:
+ use an explicitly supported test chain/token and `originDecimals`, or the
+ credential-gated CI harness with mocked Relay and pre-funded test collateral.
+ If no test route is known, stop funding and report the missing route. The user
+ transfers the quoted origin token. If `signatureSteps` require signatures,
+ show the exact payload and submit each via `deposit` in returned order.
+4. Poll `status` until each active deposit is `settled`, for at most 12 polls
+ or 5 minutes, whichever comes first. Start with 1-second delays, double to a
+ 15-second cap, and stop with an explicit pending report if the budget expires.
+ Settlement requires Relay success and Hyperliquid balance receipt. Stop on
+ failed/refunded deposits and explain their status. Do not place orders while
+ any deposit is awaiting funds or pending.
+5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the
+ last handled sequence; replay after reconnect. An unfilled or rejected order
+ remains incomplete: do not claim the requested trade succeeded. A fill updates the trading
+ plan, an error follows the catalog below, and expiry warnings prompt rotation
+ using external master-wallet approval.
+
+## Completion and approval boundaries
+
+Read this skill and query schemas before acting. Keep a progress record with the
+network, current stage, deposit IDs, last event sequence and order cloid/oid;
+record public IDs only. A missing user-owned Privy app, wallet signature, funding
+route or settled balance is a blocked stage. Report the exact next action without
+requesting any secret in chat or moving to the next stage. Repeat `init` to resume
+persisted approvals rather than creating another agent. An `ok:true` transport
+envelope does not imply exchange acceptance: inspect `data.result` and reconcile
+`orders`/`events` after uncertain mutations. Never retry an order with a new cloid
+until the original intent has been reconciled.
+
+For rotation, verify `swapped:true` before asking the external wallet to sign old
+agent revocation. Require `oldRevoked:true` before claiming rotation complete.
+`revoke` immediately disables local signing; only `exchangeRevoked:true` confirms
+that the signed exchange revocation was submitted. Inspect `status` to verify the
+agent is absent from exchange approvals. Cancel outstanding orders before final
+revocation, and report any cancellation or cleanup failure explicitly.
+
+## Commands and example envelopes
+
+Examples show representative `data`, not live balances or service promises.
+Full inputs/outputs are discoverable with `schema`; amounts/prices are strings.
+
+| Input | Example output |
+| --- | --- |
+| `--help` | `{"ok":true,"data":{"commands":[{"name":"status","description":"Read account status"}],"globalFlags":["--json","--config","--network","--pretty"]}}` |
+| `schema order` | `{"ok":true,"data":{"input":{"type":"object"},"output":{"oneOf":[]}}}` (actual schemas include properties) |
+| `config get network` | `{"ok":true,"data":"testnet"}` |
+| `config set limits.maxLeverage 5` | `{"ok":true,"data":{"version":1,"limits":{"maxLeverage":5}}}` |
+| `config validate` | `{"ok":true,"data":{"valid":true,"version":1}}` |
+| `init --master-address 0x...` | `{"ok":true,"data":{"approved":false,"requiresSignature":true,"approvals":{"agent":{"typedData":{}}},"qrAscii":"..."}}` |
+| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","state":"awaiting_funds","qrAscii":null}}` |
+| `deposit --deposit-id ID` | `{"ok":true,"data":{"depositId":"ID","steps":[{"id":"step:0","requiresSignature":true,"payload":{}}]}}` |
+| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` |
+| `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"2"},"runner":{"pid":"123"}}}` |
+| `balance` | `{"ok":true,"data":{"accountValue":"100","accountValueUnits":"10000000000","withdrawable":"96","marginUsed":"4"}}` |
+| `market --coin BTC` | `{"ok":true,"data":{"coin":"BTC","asset":"0","szDecimals":"5","maxLeverage":"40","mid":"20000"}}` |
+| `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` |
+| `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` |
+| `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","cloid":"0x..."}}` |
+| `order --json '{"coin":"BTC","side":"sell","size":"0.001","type":"market","slippage":0.01,"reduceOnly":true}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":123}}]}}}}}` |
+| `cancel --oid 123` or `cancel --all` | `{"ok":true,"data":{"cancelled":["123"],"result":{"status":"ok"}}}` |
+| `modify --json '{"modifications":[{"oid":123,"price":"20100","size":"0.001"}]}'` | `{"ok":true,"data":{"status":"ok"}}` |
+| `leverage --coin BTC --leverage 5 --mode isolated` | `{"ok":true,"data":{"status":"ok"}}` |
+| `run --detach` | `{"ok":true,"data":{"pid":123,"startedAt":1700000000000,"network":"testnet"}}` |
+| `events --since 12 --types '["fill"]'` | `{"ok":true,"data":[{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}]}` |
+| `events --follow` | `{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}` (one line per event) |
+| `logs --follow --level warn` | `{"level":"warn","requestId":"...","message":"Risk rejected","time":"..."}` (JSON lines) |
+| `rotate` then `rotate --signature 0x...` | `{"ok":true,"data":{"oldAgent":"0x...","newAgent":"0x...","swapped":true,"oldRevoked":false,"revokeApproval":{}}}` |
+| `rotate --revoke-signature 0x...` | `{"ok":true,"data":{"swapped":true,"oldRevoked":true}}` |
+| `revoke --cancel-orders` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":false,"requiresSignature":true,"approval":{}}}` |
+| `revoke --signature 0x...` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":true}}` |
+| `audit --intent ID` | `{"ok":true,"data":[{"intentId":"ID","stage":"decision","data":{"allowed":true},"previousHash":"...","hash":"..."}]}` |
+| `skill install --target codex` | `{"ok":true,"data":{"target":"codex","path":".../agent-cli/SKILL.md","version":"0.2.0"}}` |
+
+Before an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and
+market leverage ceiling. Compute size/notional with decimal strings/bigints,
+using the user-approved limit or the bounded IOC execution price. A valid schema
+alone does not establish that an order is tradable or within the budget.
+
+Use `--intent-id ID --intent "reason"` on trading actions to connect the user's
+reasoning to the audit. Keep cloid stable when retrying a known order intent.
+For TP/SL use `type:"tp"|"sl"` and `triggerPrice` with size/price/reduceOnly;
+market orders use an explicit slippage-bounded IOC, not an unbounded market order.
+
+## Error recovery
+
+| Code | Action |
+| --- | --- |
+| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results |
+| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent |
+| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure |
+| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |
+| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |
+| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |
+| AGENT_EXPIRED | Rotate with master-wallet approval |
+| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |
+| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |
+| UNKNOWN_MARKET | Refresh metadata; use an allowed market |
+| ORDER_NOT_FOUND | Refresh orders before changing intent |
+| CONFIG_INVALID | Correct configuration/input; never insert secrets |
+| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |
+
+## Gotchas
+
+HyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and
+bigints. Some deposits require **two distinct signatures**; resume stored steps
+rather than restarting the route. Agent expiry is at most **180 days**. Never
+ask for, paste, log or import a master private key or seed. The managed agent is
+trade-only; local limits include markets, size/notional, leverage, daily loss and
+open orders. Privy sees an L1 action hash, so it cannot independently inspect
+notional/action contents; Hyperliquid agent permissions and local checks enforce
+those boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`.
+
+
+## Verification boundaries
+
+Offline skill tests validate the documented command examples against CLI schemas.
+Mocked lifecycle tests exercise resumable approvals, pending funding, events
+and failure handling. These checks do **not** prove a live agent or
+exchange run. The nightly testnet lifecycle uses only a company-owned disposable
+CI master account and separate test Privy app, real Hyperliquid testnet and Privy,
+and an explicitly mocked Relay route backed by already funded test collateral.
+It never transfers real funds. Missing CI credentials fail the job; a skipped or
+mocked test must never be reported as live validation. Record the workflow run,
+commit, lifecycle report, event and audit artifacts before claiming a testnet pass.
+See `docs/agent-workflow.md` and `docs/testnet.md` in the repository for setup.
diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt
index 652aac2..23a5f7c 100644
--- a/apps/docs/public/llms.txt
+++ b/apps/docs/public/llms.txt
@@ -1,6 +1,6 @@
# Bloxwap Agent CLI
-Hyperliquid trading with Privy custody and Relay funding. Master keys stay external.
+Hyperliquid trading with user-owned Privy apps and Relay funding. Master keys stay external.
- [Overview and installation](https://bloxwap.github.io/docs/)
- [quickstart](https://bloxwap.github.io/docs/quickstart/)
@@ -15,6 +15,7 @@ Hyperliquid trading with Privy custody and Relay funding. Master keys stay exter
- [skill](https://bloxwap.github.io/docs/skill/)
- [audit](https://bloxwap.github.io/docs/audit/)
+- [SKILL.md — agent instructions](https://bloxwap.github.io/SKILL.md)
- [Sitemap](https://bloxwap.github.io/sitemap.xml)
- [Static search index](https://bloxwap.github.io/search.json)
-- [Source and packaged SKILL.md](https://github.com/bloxwap/agent-cli/tree/main/packages/agent-cli)
+- [SKILL.md source](https://github.com/bloxwap/agent-cli/blob/main/packages/agent-cli/SKILL.md)
diff --git a/apps/docs/scripts/check-export.mjs b/apps/docs/scripts/check-export.mjs
index 4597c40..ba29da1 100644
--- a/apps/docs/scripts/check-export.mjs
+++ b/apps/docs/scripts/check-export.mjs
@@ -14,13 +14,20 @@ async function walk(path) {
}
for (const required of [
'index.html', '404.html', 'docs/index.html', 'search.json', 'icon.svg',
- 'llms.txt', 'robots.txt', '404.md',
+ 'llms.txt', 'robots.txt', '404.md', 'SKILL.md',
]) {
if (!await exists(join(root, required))) failures.push(`Missing ${required}`);
}
// Agent-readiness contract: every machine-readable file the site advertises must exist
// and behave the way llms.txt and the 404 page claim it does.
+const skillSource = await readFile(new URL('../../../packages/agent-cli/SKILL.md', import.meta.url));
+if (await exists(join(root, 'SKILL.md')) && !(await readFile(join(root, 'SKILL.md'))).equals(skillSource)) {
+ failures.push('SKILL.md must exactly match the packaged skill, including its frontmatter');
+}
+const llms = await readFile(join(root, 'llms.txt'), 'utf8');
+const skillUrl = new URL(`${basePath}/SKILL.md`, siteUrl).href;
+if (!llms.includes(`](${skillUrl})`)) failures.push(`llms.txt: missing canonical skill link ${skillUrl}`);
const robots = await readFile(join(root, 'robots.txt'), 'utf8');
if (!/^sitemap:\s*https:\/\/\S+\/sitemap\.xml\s*$/im.test(robots)) failures.push('robots.txt: missing absolute Sitemap directive');
const notFoundMarkdown = await readFile(join(root, '404.md'), 'utf8');
diff --git a/apps/docs/scripts/generate-content.mjs b/apps/docs/scripts/generate-content.mjs
index 755830c..10fa94d 100644
--- a/apps/docs/scripts/generate-content.mjs
+++ b/apps/docs/scripts/generate-content.mjs
@@ -14,11 +14,16 @@ for (const command of invoke('--help').commands) {
}
const skillSource = readFileSync(new URL('../../../packages/agent-cli/SKILL.md', import.meta.url), 'utf8');
const skill = '---\ntitle: Agent skill guide\ndescription: Install and use the packaged SKILL.md with Codex or Claude Code.\n---\n\n' +
+ 'import Link from \'next/link\';\n\n' +
+ 'Download SKILL.md as plain Markdown, including the skill frontmatter.\n\n' +
'The npm package ships `SKILL.md` and embeds the same content in the CLI. Install it with:\n\n```sh\nagent-cli skill install --target codex\nagent-cli skill install --target claude\n```\n\nCodex uses `$CODEX_HOME/skills/agent-cli/SKILL.md` (default `~/.codex/skills/agent-cli/SKILL.md`). Claude Code uses `~/.claude/skills/agent-cli/SKILL.md`. Reinstalling identical content is idempotent; replacing edited content requires `--force`.\n\nThe following guide comes directly from the packaged skill.\n\n' + skillSource.replace(/^---\n[\s\S]*?\n---\n/, '');
-for (const [name, text] of [['commands.mdx', commands], ['skill.mdx', skill]]) {
- const target = new URL(name, pages);
+for (const [name, target, text] of [
+ ['commands.mdx', new URL('commands.mdx', pages), commands],
+ ['skill.mdx', new URL('skill.mdx', pages), skill],
+ ['public/SKILL.md', new URL('../public/SKILL.md', import.meta.url), skillSource],
+]) {
if (process.argv.includes('--check')) {
if (readFileSync(target, 'utf8') !== text) throw new Error(`${name} is stale: run npm run docs:generate`);
} else writeFileSync(target, text);
}
-console.log('Command schemas and packaged skill documentation synchronized.');
+console.log('Command schemas, skill guide and public SKILL.md synchronized.');
diff --git a/apps/docs/scripts/prepare-public.mjs b/apps/docs/scripts/prepare-public.mjs
index 979480b..c00a7df 100644
--- a/apps/docs/scripts/prepare-public.mjs
+++ b/apps/docs/scripts/prepare-public.mjs
@@ -4,6 +4,6 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://bloxwap.github.io';
const absolute = (path) => new URL(`${basePath}${path}`, siteUrl).href;
const pages = JSON.parse(readFileSync(new URL('../content/docs/meta.json', import.meta.url), 'utf8')).pages;
writeFileSync(new URL('../public/robots.txt', import.meta.url), `User-agent: *\nAllow: /\nSitemap: ${absolute('/sitemap.xml')}\n`);
-writeFileSync(new URL('../public/llms.txt', import.meta.url), '# Bloxwap Agent CLI\n\nHyperliquid trading with Privy custody and Relay funding. Master keys stay external.\n\n' +
+writeFileSync(new URL('../public/llms.txt', import.meta.url), '# Bloxwap Agent CLI\n\nHyperliquid trading with user-owned Privy apps and Relay funding. Master keys stay external.\n\n' +
pages.map((page) => `- [${page === 'index' ? 'Overview and installation' : page}](${absolute(page === 'index' ? '/docs/' : `/docs/${page}/`)})`).join('\n') +
- `\n\n- [Sitemap](${absolute('/sitemap.xml')})\n- [Static search index](${absolute('/search.json')})\n- [Source and packaged SKILL.md](https://github.com/bloxwap/agent-cli/tree/main/packages/agent-cli)\n`);
+ `\n\n- [SKILL.md — agent instructions](${absolute('/SKILL.md')})\n- [Sitemap](${absolute('/sitemap.xml')})\n- [Static search index](${absolute('/search.json')})\n- [SKILL.md source](https://github.com/bloxwap/agent-cli/blob/main/packages/agent-cli/SKILL.md)\n`);
diff --git a/apps/docs/scripts/preview.mjs b/apps/docs/scripts/preview.mjs
index 1948615..8b88ab5 100644
--- a/apps/docs/scripts/preview.mjs
+++ b/apps/docs/scripts/preview.mjs
@@ -7,7 +7,7 @@ const root = fileURLToPath(new URL('../out/', import.meta.url)).replace(/\/$/, '
let basePath;
try { ({ basePath } = JSON.parse(await readFile(join(root, '_docs-config.json'), 'utf8'))); }
catch { throw new Error('Build the docs first with npm run docs:build.'); }
-const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml' };
+const mime = { '.html': 'text/html; charset=utf-8', '.md': 'text/markdown; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml' };
const server = createServer(async (request, response) => {
try {
const path = decodeURIComponent(new URL(request.url ?? '/', 'http://localhost').pathname);
diff --git a/apps/docs/scripts/smoke.mjs b/apps/docs/scripts/smoke.mjs
index 5909b03..ccdff0c 100644
--- a/apps/docs/scripts/smoke.mjs
+++ b/apps/docs/scripts/smoke.mjs
@@ -43,6 +43,22 @@ try {
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
+ await check('canonical SKILL.md serves the packaged Markdown and is discoverable', async () => {
+ const response = await desktop.request.get(url('/SKILL.md'));
+ assert.equal(response.status(), 200);
+ assert.match(response.headers()['content-type'], /^text\/markdown\b/);
+ assert.deepEqual(await response.body(), await readFile(new URL('../../../packages/agent-cli/SKILL.md', import.meta.url)));
+ const llms = await desktop.request.get(url('/llms.txt'));
+ assert.equal(llms.status(), 200);
+ assert.ok((await llms.text()).includes(`${basePath}/SKILL.md`));
+ await page.goto(url('/docs/skill/'));
+ const link = page.getByRole('link', { name: 'Download SKILL.md', exact: true });
+ assert.equal(await link.getAttribute('href'), `${basePath}/SKILL.md`);
+ const download = page.waitForEvent('download');
+ await link.click();
+ assert.equal((await download).suggestedFilename(), 'SKILL.md');
+ });
+
await check('home renders with the base path and no runtime errors', async () => {
const response = await page.goto(url('/'));
assert.equal(response?.status(), 200);
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 9a590d4..e7142d7 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -39,7 +39,7 @@
| #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. |
| #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. |
| #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. |
-| #37 | partial | User-owned Privy guidance, resumable approval/funding stages, exchange reconciliation and complete schema-validated command examples/error table. Offline forward tests pass; an actual coding-agent testnet session still needs dedicated QA credentials and evidence. |
+| #37 | partial | User-owned Privy guidance, resumable approval/funding stages, exchange reconciliation and complete schema-validated command examples/error table. Canonical /SKILL.md is generated from the packaged source, linked from llms.txt/the skill guide and checked for byte parity. Offline tests pass; an actual coding-agent testnet session still needs dedicated QA credentials and evidence. |
| #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. |
| #39 | partial | Full mocked CLI/SDK/RPC lifecycle and fail-closed testnet harness, with builder approval, exchange outcome reconciliation, runner/events, rotation/revoke and audit assertions. Nightly live execution and journals need a disposable funded QA account and QA Privy app; missing credentials cannot count as a pass. |
| #40 | partial | Script-free tarball global install and actual installed bunx help/schema smoke pass locally; executable/skill/license checks and Node 22.12/24 macOS/Linux CI. Public registry installation awaits publication. |