agent-readiness 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Veldin Salcinovic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # agent-ready
2
+
3
+ [![agent-ready: 100/100](https://img.shields.io/badge/agent--ready-100%2F100-brightgreen)](https://agent-ready-web.vercel.app)
4
+ [![npm version](https://img.shields.io/npm/v/agent-readiness?color=cb3837&logo=npm)](https://www.npmjs.com/package/agent-readiness)
5
+ [![CI](https://github.com/VeldinS/agent-ready/actions/workflows/ci.yml/badge.svg)](https://github.com/VeldinS/agent-ready/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+
8
+ **Make any website agent-ready in one command.** Scan a site, get an agent-readiness score, and auto-generate the two things the agentic web now expects — an `llms.txt` and a **WebMCP** tool scaffold — so AI agents can *understand and act on* your site instead of blindly scraping it.
9
+
10
+ ```bash
11
+ npx agent-readiness https://yoursite.com
12
+ ```
13
+
14
+ > **No install?** Scan any site in your browser at **[agent-ready-web.vercel.app](https://agent-ready-web.vercel.app)** — paste a URL, get the score + per-check breakdown, and download the generated `llms.txt` + WebMCP scaffold.
15
+
16
+ ```
17
+ agent-ready — scanning https://yoursite.com
18
+
19
+ ✗ llms.txt present (0/22)
20
+ → Add /llms.txt so agents get a clean, token-cheap map of your site (generated below).
21
+ ✗ WebMCP tools (document.modelContext) (0/22)
22
+ → Expose your key actions as WebMCP tools so agents can ACT, not just scrape (scaffold generated below).
23
+ ✔ Structured data (JSON-LD / OpenGraph) (+13/13)
24
+ ✔ Semantic landmarks + an H1 (+13/13)
25
+ ✔ Title + meta description (+10/10)
26
+ ✗ robots.txt + sitemap.xml (0/8)
27
+ ✔ Canonical URL (<link rel="canonical">) (+4/4)
28
+ ✔ Document language (<html lang>) (+4/4)
29
+ ✔ Image alt-text coverage (18/20, 90%) (+4/4)
30
+
31
+ Agent-Readiness: 48/100
32
+ pages: 17 • forms: 2 • inferred actions: 3
33
+ Generated → ./agent-ready-out/llms.txt, ./agent-ready-out/webmcp.tools.js, ./agent-ready-out/structured-data.html, ./agent-ready-out/agent-ready.json
34
+ ```
35
+
36
+ ## Why this exists
37
+
38
+ Chrome's Lighthouse now ships an **"Agentic Browsing"** audit *in the default config* (13.3, May 2026), and **WebMCP** is a W3C draft in origin trial. Millions of sites are about to see a failing agent-readiness grade with no obvious way to fix it. `agent-ready` is the fix: it doesn't just *diagnose* (every scanner does that) — it **generates the artifacts** and is built to run in CI.
39
+
40
+ - **`llms.txt`** — a clean, machine-readable map of your pages (with one-line summaries) and actions, built from a multi-page crawl (the emerging standard agents read first).
41
+ - **`webmcp.tools.js`** — a [WebMCP](https://webmachinelearning.github.io/webmcp/) scaffold that registers each `<form>` and inferred action (search / login / signup / cart / checkout / contact / subscribe) as a callable agent tool via `document.modelContext.registerTool(...)`, so an agent can *complete the signup / search / booking* instead of guessing at your DOM.
42
+ - **`structured-data.html`** — a JSON-LD + OpenGraph snippet to drop into your `<head>` so machines get explicit, structured meaning.
43
+
44
+ ## One command, one PR (the fixer)
45
+
46
+ Every other tool *scans*. `agent-ready` also **fixes** — and opens the fix as a pull request:
47
+
48
+ ```bash
49
+ npx agent-readiness fix ./my-site --pr
50
+ ```
51
+
52
+ It detects your framework (Next.js App/Pages Router, Vite, or plain static HTML), writes `llms.txt` + `webmcp.tools.js` into the right place, **injects** the `<script>` tag and the missing `<html lang>` / meta description / JSON-LD + OpenGraph into your entry HTML or layout, then branches, commits, and opens a PR with a clear **before → after** score:
53
+
54
+ ```
55
+ agent-ready fix — ./my-site
56
+
57
+ Framework: Static / plain HTML
58
+ Score: 17/100 → 88/100
59
+
60
+ Files:
61
+ + llms.txt (token-cheap site map for agents)
62
+ + webmcp.tools.js (WebMCP tool scaffold)
63
+ inject → index.html (html-lang, meta-description, structured-data, webmcp-script)
64
+
65
+ ✔ Opened pull request: https://github.com/you/my-site/pull/42
66
+ ```
67
+
68
+ - `--dry-run` — print the plan (and the exact git/gh commands) without touching anything.
69
+ - `--url https://yoursite.com` — scan the live site for a richer `llms.txt` (multi-page) and to fill real URLs + a canonical tag.
70
+ - `--base <branch>` / `--branch <name>` — control the PR target and head branch.
71
+ - Injection is **idempotent** — re-running never duplicates a tag. `--pr` needs a git repo with an `origin` remote and the [`gh`](https://cli.github.com/) CLI authenticated.
72
+
73
+ Prefer to apply locally without a PR? `agent-ready fix ./my-site` writes + injects in place; `agent-ready <url> --apply ./my-site` just drops the files into `public/`.
74
+
75
+ ## Use it in CI (GitHub Action)
76
+
77
+ ```yaml
78
+ permissions:
79
+ contents: read
80
+ pull-requests: write # so it can comment the score on the PR
81
+ steps:
82
+ - uses: VeldinS/agent-ready@v0
83
+ with:
84
+ url: https://yoursite.com
85
+ comment: true # post a sticky agent-readiness comment on the PR (default)
86
+ min-score: 60 # optional: fail the job if the score drops below this
87
+ ```
88
+
89
+ On every pull request it posts (and updates) a single comment with the score and per-check breakdown. Add the badge to your README once you're green:
90
+
91
+ ```md
92
+ ![agent-ready](https://img.shields.io/badge/agent--ready-100%2F100-brightgreen)
93
+ ```
94
+
95
+ ## Use it from your agent (MCP server)
96
+
97
+ Run *"make my site agent-ready"* right inside Claude Code / Cursor / Claude Desktop. The [`@veldins/agent-ready-mcp`](mcp/) server exposes two tools — `scan_site` (score + per-check breakdown) and `generate_fixes` (the `llms.txt` + WebMCP scaffold + structured-data contents) — over stdio, sharing the same scanner core.
98
+
99
+ ```bash
100
+ claude mcp add agent-ready -- npx -y @veldins/agent-ready-mcp
101
+ ```
102
+
103
+ ```json
104
+ { "mcpServers": { "agent-ready": { "command": "npx", "args": ["-y", "@veldins/agent-ready-mcp"] } } }
105
+ ```
106
+
107
+ ## What it checks
108
+
109
+ | Check | Weight |
110
+ |-------|:--:|
111
+ | `llms.txt` present | 22 |
112
+ | WebMCP tools (`document.modelContext`) | 22 |
113
+ | Structured data (JSON-LD / OpenGraph) | 13 |
114
+ | Semantic landmarks + an `<h1>` | 13 |
115
+ | Title + meta description | 10 |
116
+ | `robots.txt` + `sitemap.xml` | 8 |
117
+ | Canonical URL (`<link rel="canonical">`) | 4 |
118
+ | Document language (`<html lang>`) | 4 |
119
+ | Image alt-text coverage (≥80%) | 4 |
120
+
121
+ > The score reflects the **server-rendered HTML** an agent first sees; heavily client-rendered SPAs will under-report content checks until they ship meaningful HTML.
122
+
123
+ ## Roadmap
124
+
125
+ - [x] `fix` mode: write + inject the artifacts and open a PR with a before → after score
126
+ - [x] Framework adapters (Next.js App/Pages Router, Vite, static HTML)
127
+ - [x] GitHub Action comments the score on every PR
128
+ - [x] [Web scanner page](https://agent-ready-web.vercel.app): paste a URL → scored report + downloadable fixes (no install)
129
+ - [x] MCP server so Claude Code / Cursor can run "make my site agent-ready" (`@veldins/agent-ready-mcp`)
130
+ - [ ] Re-grade against live Lighthouse Agentic Browsing on each PR
131
+
132
+ ## Local dev
133
+
134
+ ```bash
135
+ npm install
136
+ node bin/agent-ready.mjs examples/sample-site.html
137
+ npm test # node --test — fixtures, generators, crawl, CLI
138
+ ```
139
+
140
+ One runtime dependency ([`node-html-parser`](https://www.npmjs.com/package/node-html-parser)); Node ≥20.19; ESM; no build step.
141
+
142
+ MIT.
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { writeFile, mkdir } from 'node:fs/promises'
4
+ import { join, relative } from 'node:path'
5
+ import { crawl, crawlLocal, analyze, score, badgeUrl, writeArtifacts, buildLlmsTxt, buildWebMcp, buildStructuredData } from '../lib/core.mjs'
6
+ import { detectFramework, buildFixPlan, applyFix, gitInfo, ghReady, openPullRequest } from '../lib/fix.mjs'
7
+ import { prBody, commentBody, COMMENT_MARKER } from '../lib/report.mjs'
8
+ import { ghContext, upsertPrComment } from '../lib/github.mjs'
9
+
10
+ const C = { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' }
11
+ const colorFor = (n) => (n >= 80 ? C.g : n >= 50 ? C.y : C.r)
12
+
13
+ const USAGE = `usage:
14
+ agent-ready <url | local.html> [--apply <dir>] [--comment] [--min-score <n>]
15
+ agent-ready fix <repo-dir> [--url <url>] [--pr] [--dry-run] [--base <branch>] [--branch <name>]`
16
+
17
+ // Tiny flag parser. Supports both `--flag value` and `--flag=value`. For a value
18
+ // flag with no value (next token missing or another --flag), the value is null —
19
+ // matching the old --apply guard.
20
+ function parseArgs(args, valueFlags = []) {
21
+ const flags = {}
22
+ const positional = []
23
+ for (let i = 0; i < args.length; i++) {
24
+ const a = args[i]
25
+ if (a.startsWith('--')) {
26
+ const eq = a.indexOf('=')
27
+ const name = eq === -1 ? a.slice(2) : a.slice(2, eq)
28
+ if (eq !== -1) {
29
+ flags[name] = a.slice(eq + 1)
30
+ } else if (valueFlags.includes(name)) {
31
+ const next = args[i + 1]
32
+ flags[name] = next === undefined || next.startsWith('--') ? null : args[++i]
33
+ } else {
34
+ flags[name] = true
35
+ }
36
+ } else {
37
+ positional.push(a)
38
+ }
39
+ }
40
+ return { flags, positional }
41
+ }
42
+
43
+ function stamp() {
44
+ const d = new Date()
45
+ const p = (n) => String(n).padStart(2, '0')
46
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
47
+ }
48
+
49
+ function printChecks(sc) {
50
+ for (const c of sc.checks) {
51
+ const mark = c.pass ? `${C.g}✔${C.x}` : `${C.r}✗${C.x}`
52
+ console.log(` ${mark} ${c.label} ${C.d}(${c.pass ? '+' + c.weight : 0}/${c.weight})${C.x}`)
53
+ if (!c.pass) console.log(` ${C.d}→ ${c.fix}${C.x}`)
54
+ }
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // scan (default command) — unchanged behavior + --comment / --min-score
59
+ // ---------------------------------------------------------------------------
60
+
61
+ async function runScan(args) {
62
+ const { flags, positional } = parseArgs(args, ['apply', 'min-score'])
63
+ const arg = positional[0]
64
+ if (!arg) {
65
+ console.error(USAGE)
66
+ process.exit(1)
67
+ }
68
+ if ('apply' in flags && !flags.apply) {
69
+ console.error('--apply requires a <project-dir>, e.g. agent-ready example.com --apply ./my-site')
70
+ process.exit(1)
71
+ }
72
+
73
+ const isLocal = existsSync(arg)
74
+ const target = isLocal ? arg : arg.startsWith('http') ? arg : 'https://' + arg
75
+ console.log(`\n${C.b}agent-ready${C.x} ${C.d}— scanning ${target}${C.x}\n`)
76
+
77
+ let raw
78
+ try {
79
+ raw = isLocal ? await crawlLocal(target) : await crawl(target)
80
+ } catch (err) {
81
+ console.error(`${C.r}✗ ${err.message}${C.x}`)
82
+ process.exit(1)
83
+ }
84
+
85
+ const a = analyze(raw)
86
+ const sc = score(a)
87
+ printChecks(sc)
88
+
89
+ console.log(`\n ${C.b}Agent-Readiness:${C.x} ${colorFor(sc.total)}${C.b}${sc.total}/100${C.x}`)
90
+ console.log(` ${C.d}badge: ${badgeUrl(sc.total)}${C.x}`)
91
+ console.log(` ${C.d}pages: ${a.pages.length} • forms: ${a.forms.length} • inferred actions: ${a.actions.length}${C.x}`)
92
+
93
+ const out = './agent-ready-out'
94
+ await writeArtifacts(out, a, sc)
95
+ console.log(`\n ${C.b}Generated${C.x} → ${out}/llms.txt, ${out}/webmcp.tools.js, ${out}/structured-data.html, ${out}/agent-ready.json`)
96
+
97
+ const applyDir = flags.apply
98
+ if (applyDir) {
99
+ const dest = existsSync(join(applyDir, 'public')) ? join(applyDir, 'public') : applyDir
100
+ await mkdir(dest, { recursive: true })
101
+ await Promise.all([
102
+ writeFile(join(dest, 'llms.txt'), buildLlmsTxt(a)),
103
+ writeFile(join(dest, 'webmcp.tools.js'), buildWebMcp(a)),
104
+ writeFile(join(dest, 'structured-data.html'), buildStructuredData(a))
105
+ ])
106
+ console.log(`\n ${C.g}${C.b}✔ Applied the fix${C.x} → wrote llms.txt + webmcp.tools.js + structured-data.html into ${C.b}${dest}/${C.x}`)
107
+ console.log(` ${C.d}Tip: ${C.x}agent-ready fix <repo-dir>${C.d} injects the <script> tag + meta for you and can open a PR.${C.x}`)
108
+ } else {
109
+ console.log(` ${C.d}Re-run with ${C.x}--apply <project-dir>${C.d}, or ${C.x}agent-ready fix <repo-dir>${C.d} to inject + open a PR.${C.x}`)
110
+ }
111
+
112
+ if (flags.comment) await maybeComment(target, a, sc)
113
+
114
+ if (flags['min-score'] != null) {
115
+ const min = Number(flags['min-score'])
116
+ if (Number.isFinite(min) && sc.total < min) {
117
+ console.error(`\n${C.r}✗ agent-readiness ${sc.total}/100 is below --min-score ${min}${C.x}`)
118
+ process.exit(1)
119
+ }
120
+ }
121
+ }
122
+
123
+ async function maybeComment(target, a, sc) {
124
+ const ctx = ghContext()
125
+ if (!ctx) {
126
+ console.log(` ${C.d}(--comment: no GitHub PR context/token — skipping)${C.x}`)
127
+ return
128
+ }
129
+ const body = commentBody({
130
+ url: target,
131
+ total: sc.total,
132
+ checks: sc.checks,
133
+ counts: { pages: a.pages.length, forms: a.forms.length, actions: a.actions.length }
134
+ })
135
+ try {
136
+ const r = await upsertPrComment(ctx, body, COMMENT_MARKER)
137
+ console.log(` ${C.g}✔ ${r.updated ? 'Updated' : 'Posted'} the agent-ready PR comment${C.x}`)
138
+ } catch (err) {
139
+ console.log(` ${C.d}(--comment: could not post — ${err.message})${C.x}`)
140
+ }
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // fix (the one-click fixer)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ async function runFix(args) {
148
+ const { flags, positional } = parseArgs(args, ['url', 'base', 'branch'])
149
+ const repoDir = positional[0]
150
+ if (!repoDir) {
151
+ console.error(USAGE)
152
+ process.exit(1)
153
+ }
154
+ if (!existsSync(repoDir)) {
155
+ console.error(`${C.r}✗ fix: directory not found: ${repoDir}${C.x}`)
156
+ process.exit(1)
157
+ }
158
+
159
+ const fw = detectFramework(repoDir)
160
+
161
+ let analysis
162
+ let baseUrl = null
163
+ if (flags.url) {
164
+ const url = flags.url.startsWith('http') ? flags.url : 'https://' + flags.url
165
+ console.log(`\n${C.b}agent-ready fix${C.x} ${C.d}— scanning ${url} to generate artifacts${C.x}`)
166
+ try {
167
+ analysis = analyze(await crawl(url))
168
+ baseUrl = analysis.base
169
+ } catch (err) {
170
+ console.error(`${C.r}✗ fix: could not scan ${url}: ${err.message}${C.x}`)
171
+ process.exit(1)
172
+ }
173
+ } else if (fw.entry && existsSync(fw.entry)) {
174
+ analysis = analyze({ base: '', home: readFileSync(fw.entry, 'utf8') })
175
+ } else {
176
+ analysis = analyze({ base: '', home: '' })
177
+ }
178
+
179
+ const plan = buildFixPlan({ framework: fw, analysis, baseUrl })
180
+
181
+ console.log(`\n${C.b}agent-ready fix${C.x} ${C.d}— ${fw.root}${C.x}\n`)
182
+ console.log(` ${C.b}Framework:${C.x} ${fw.label}`)
183
+ console.log(` ${C.d}files dir: ${plan.framework.publicDir}${C.x}`)
184
+ console.log(` ${C.d}entry: ${fw.entry || '(none detected)'}${C.x}`)
185
+ console.log(
186
+ `\n ${C.b}Score:${C.x} ${plan.before.total}/100 → ${colorFor(plan.after.total)}${C.b}${plan.after.total}/100${C.x}${plan.after.projected ? C.d + ' (projected after deploy)' + C.x : ''}\n`
187
+ )
188
+ console.log(` ${C.b}Files:${C.x}`)
189
+ for (const w of plan.writes) console.log(` ${w.exists ? '~' : C.g + '+' + C.x} ${w.rel} ${C.d}(${w.label})${C.x}`)
190
+ if (plan.injection) console.log(` ${C.d}inject → ${plan.injection.entryRel} (${plan.injection.applied.join(', ') || 'no changes needed'})${C.x}`)
191
+ if (plan.notes.length) {
192
+ console.log(`\n ${C.b}Notes:${C.x}`)
193
+ for (const n of plan.notes) console.log(` ${C.d}• ${n}${C.x}`)
194
+ }
195
+
196
+ if (flags['dry-run']) {
197
+ console.log(`\n ${C.y}dry run — no files written.${C.x}`)
198
+ if (flags.pr) {
199
+ const gi = gitInfo(repoDir)
200
+ const base = resolveBase(gi, flags) || 'main'
201
+ const branch = flags.branch || `agent-ready/fix-${stamp()}`
202
+ const filesRel = changedRel(gi, plan)
203
+ const pr = openPullRequest({ root: gi && gi.root, branch, base, files: filesRel, title: prTitle(plan), body: '', dryRun: true })
204
+ console.log(`\n ${C.b}Would open a PR with:${C.x}`)
205
+ for (const cmd of pr.commands) console.log(` ${C.d}$ ${cmd}${C.x}`)
206
+ }
207
+ return
208
+ }
209
+
210
+ let gi = null
211
+ let base = null
212
+ if (flags.pr) {
213
+ gi = gitInfo(repoDir)
214
+ if (!gi) {
215
+ console.error(`\n${C.r}✗ fix --pr: not a git repository. Run \`git init\` and add a GitHub remote first.${C.x}`)
216
+ process.exit(1)
217
+ }
218
+ if (!gi.hasOrigin) {
219
+ console.error(`\n${C.r}✗ fix --pr: no \`origin\` remote — push this repo to GitHub first.${C.x}`)
220
+ process.exit(1)
221
+ }
222
+ if (!ghReady(gi.root)) {
223
+ console.error(`\n${C.r}✗ fix --pr: GitHub CLI (gh) is not installed or not authenticated. Install gh and run \`gh auth login\`.${C.x}`)
224
+ process.exit(1)
225
+ }
226
+ base = resolveBase(gi, flags)
227
+ if (!base) {
228
+ console.error(`\n${C.r}✗ fix --pr: detached HEAD and no default branch found — pass --base <branch>.${C.x}`)
229
+ process.exit(1)
230
+ }
231
+ }
232
+
233
+ const changed = await applyFix(plan)
234
+ console.log(`\n ${C.g}${C.b}✔ Applied${C.x} — wrote/updated ${changed.length} file(s).`)
235
+
236
+ if (flags.pr) {
237
+ const branch = flags.branch || `agent-ready/fix-${stamp()}`
238
+ const filesRel = changed.map((abs) => relative(gi.root, abs))
239
+ const title = prTitle(plan)
240
+ const body = prBody({ before: plan.before, after: plan.after, framework: fw, writes: plan.writes, injection: plan.injection, notes: plan.notes })
241
+ try {
242
+ const r = openPullRequest({ root: gi.root, branch, base, files: filesRel, title, body, dryRun: false })
243
+ if (r.noChanges) {
244
+ console.log(`\n ${C.g}${C.b}✔ Already agent-ready${C.x} — the artifacts match what's committed; no PR needed.`)
245
+ } else {
246
+ console.log(`\n ${C.g}${C.b}✔ Opened pull request:${C.x} ${r.url}`)
247
+ }
248
+ } catch (err) {
249
+ console.error(`\n${C.r}✗ Could not open the PR: ${err.message}${C.x}`)
250
+ console.error(`${C.d} ${recoveryHint(err)}${C.x}`)
251
+ process.exit(1)
252
+ }
253
+ } else {
254
+ console.log(` ${C.d}Re-run with ${C.x}--pr${C.d} to branch, commit, and open a pull request.${C.x}`)
255
+ }
256
+ }
257
+
258
+ // PR base = explicit --base, else the repo's default branch, else the current
259
+ // branch (unless detached). null means "couldn't determine — ask the user".
260
+ function resolveBase(gi, flags) {
261
+ if (flags.base) return flags.base
262
+ if (!gi) return null
263
+ if (gi.defaultBranch) return gi.defaultBranch
264
+ return gi.detached ? null : gi.branch
265
+ }
266
+
267
+ // Describe the repo's true state for the stage the PR flow failed at.
268
+ function recoveryHint(err) {
269
+ switch (err.agentReadyStage) {
270
+ case 'pushed':
271
+ return `branch "${err.branch}" was committed and pushed to origin — open the PR from GitHub, or delete the branch and retry.`
272
+ case 'committed':
273
+ return `changes were committed locally on "${err.branch}" but not pushed.`
274
+ default:
275
+ return 'the fix files were written to your working tree; commit + open the PR manually.'
276
+ }
277
+ }
278
+
279
+ function prTitle(plan) {
280
+ return `agent-ready: add llms.txt + WebMCP scaffold (score ${plan.before.total}→${plan.after.total})`
281
+ }
282
+
283
+ function changedRel(gi, plan) {
284
+ const abs = [...plan.writes.map((w) => w.abs)]
285
+ if (plan.injection && plan.injection.after !== plan.injection.before) abs.push(plan.injection.entryAbs)
286
+ return gi ? abs.map((p) => relative(gi.root, p)) : abs
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+
291
+ const argv = process.argv.slice(2)
292
+ if (argv[0] === 'fix') {
293
+ await runFix(argv.slice(1))
294
+ } else {
295
+ await runScan(argv)
296
+ }