webmcp-gauge 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Measures whether an AI agent actually calls the tools your web page exposes through WebMCP.
4
4
 
5
- **Status: measuring, and the metric discriminates.** The harness runs end to end and has produced real numbers against a live page. It launches its own browser per session — a cold profile seeded with the WebMCP flag, in headless Chrome — captures the settled tool manifest, asks a judge model which tool to call, classifies the choice, executes it, and classifies the result. The utterance set is frozen at `1.3.0` (140 utterances plus 20 negative controls), and sixteen runs totalling **4,500 trials** are published under [`reports/`](reports/) — the best isolated reference run being 960 trials across three separate processes, browsers and cold profiles, and a second client joining them: the same page, set and judge through Edge `153.0.4234.13` reads **99.3% (139/140)** with every tool inside or above its Chrome interval.
5
+ **Status: measuring, and the metric discriminates.** The harness runs end to end and has produced real numbers against a live page. It launches its own browser per session — a cold profile seeded with the WebMCP flag, in headless Chrome — captures the settled tool manifest, asks a judge model which tool to call, classifies the choice, executes it, and classifies the result. The utterance set is frozen at `1.3.0` (140 utterances plus 20 negative controls), and twenty-three runs totalling **5,220 trials** are published under [`reports/`](reports/) — the best isolated reference run being 960 trials across three separate processes, browsers and cold profiles, a second client joining them: the same page, set and judge through Edge `153.0.4234.13` reads **99.3% (139/140)** with every tool inside or above its Chrome interval, and a third client at a second judge family: Brave `154.1.96.59` reads **100% (140/140)** under `claude-opus-4-8`, identical to its paired Chrome arm.
6
6
 
7
7
  The question that mattered most has an answer. Every early number came from a page chosen for being well described, so the same frozen utterance set was fired at a deliberately mis-described twin of that page: one implementation, one dataset, several manifests, **1,320 trials at three isolated sessions per arm**. Clean manifest **99.3%**; degraded manifest **83.1%** overall, with `sum_by_category` at **60.0%** [47.4, 71.4] and `top_expenses` at **26.7%** [17.1, 39.0] against a clean 95.0% and 100%. Between-session σ is at most **0.094** where the effects are 0.35 and larger, so the gap is not noise.
8
8
 
@@ -10,11 +10,11 @@ Four ablations, each the clean manifest plus exactly one defect, then answered *
10
10
 
11
11
  Re-running one arm with its sessions **9 to 17 hours apart instead of minutes** then asked whether any of this survives a clock. The reproducibility figures do: between-session σ came back 0.085 worst-case against 0.062 back-to-back. Point estimates did not sit still — both mid-range tools declined monotonically across the 26-hour span, 8 to 12 points with overlapping intervals — so a single arm measured at one time is sound, while comparing a page against itself across days inherits a drift question. [`spacing-2026-09-01.md`](reports/spacing-2026-09-01.md).
12
12
 
13
- What exists: `lint` (static manifest rules, no judge or key), `trial` (one utterance, one outcome), `run` (S isolated sessions × R repeats, Wilson intervals, control false-positive rate, stamped JSON and Markdown reports, plus a badge that refuses to report a rate it cannot stand behind), JSONL checkpointing with `--resume`, a CI gate with split exit codes where a crash is *"could not measure"* rather than a failing page, a [GitHub Action](action.yml) wrapping both modes, cohort-capture tooling that records what a builder shipped and what an agent can actually call as two separate numbers, private per-project scorecards, a served fixture page, a browser-side tool view so a client that drops a tool can be told from a page that never registered one, and a pre-push visibility gate with its own tested rules. **229 tests.** The findings live in the published report, [`reports/public-report-draft.md`](reports/public-report-draft.md): the invocation-rate result on two builds, the compatibility matrix, and the complete WebMCP Challenge census — **80.8% of reachable submissions register WebMCP tools, and 43.9% of pages carry tools an agent can call that no page surface lists**.
13
+ What exists: `lint` (static manifest rules, no judge or key), `trial` (one utterance, one outcome), `run` (S isolated sessions × R repeats, Wilson intervals, control false-positive rate, stamped JSON and Markdown reports, plus a badge that refuses to report a rate it cannot stand behind), JSONL checkpointing with `--resume`, a CI gate with split exit codes where a crash is *"could not measure"* rather than a failing page, a [GitHub Action](action.yml) wrapping both modes, cohort-capture tooling that records what a builder shipped and what an agent can actually call as two separate numbers, private per-project scorecards, a served fixture page, a browser-side tool view so a client that drops a tool can be told from a page that never registered one, and a pre-push visibility gate with its own tested rules. **229 tests.** The findings live in the published report, [`reports/public-report-draft.md`](reports/public-report-draft.md): the invocation-rate result on three builds, the compatibility matrix, and the complete WebMCP Challenge census — **80.8% of reachable submissions register WebMCP tools, and 43.9% of pages carry tools an agent can call that no page surface lists**.
14
14
 
15
15
  What does not: **a measurement inside a real assistant.** The *browser* in the ChatGPT desktop app is CDP-drivable and its WebMCP surface is documented here — but that assistant reads pages through a Chrome extension bridge into the user's ordinary Chrome, not through the app's own browser, so no agent-side invocation has been observed. Every number below is a judge-model stand-in for a real client, and whether the stand-in predicts it is still an open assumption.
16
16
 
17
- Findings have gone back to the spec: two comments on [`webmcp#227`](https://github.com/webmachinelearning/webmcp/issues/227#issuecomment-5499568493) establish what Chrome 152 actually does across a frame boundary — a same-origin embed's tools join the host's manifest, a cross-origin embed is blocked by a Permissions Policy feature named `tools`, and once that is delegated the browser offers an agent a tool **no page can list**. The full compatibility record, including the cross-site case where even a host-attached browser-side view loses the embed entirely, is [`reports/compatibility-matrix.md`](reports/compatibility-matrix.md): 22 measured behaviours across four builds, each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one finding that turned out to be this project's own bug.
17
+ Findings have gone back to the spec: two comments on [`webmcp#227`](https://github.com/webmachinelearning/webmcp/issues/227#issuecomment-5499568493) establish what Chrome 152 actually does across a frame boundary — a same-origin embed's tools join the host's manifest, a cross-origin embed is blocked by a Permissions Policy feature named `tools`, and once that is delegated the browser offers an agent a tool **no page can list**. The full compatibility record, including the cross-site case where even a host-attached browser-side view loses the embed entirely, is [`reports/compatibility-matrix.md`](reports/compatibility-matrix.md): 22 measured behaviours across five builds, each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one finding that turned out to be this project's own bug.
18
18
 
19
19
  ## The problem
20
20
 
@@ -32,7 +32,7 @@ Every trial lands in exactly one bucket — `not_supported`, `not_registered`, `
32
32
 
33
33
  - **A static linter** — no browser needed to reason, no model, no API key. Fourteen rules across four families: invalid or colliding tool names, missing, thin, duplicate or near-duplicate descriptions, **a pair of close-named tools whose descriptions neither of them distinguishes** (adopted 2026-09-05 on measurement, after five arms showed that pattern costs 45 points while no similarity threshold catches it), over-parameterised and under-documented schemas, and tool counts approaching a per-page budget that is still unmeasured (the reported 296-tool figure does **not** reproduce on Chrome 152, where 507 registered tools were all accepted and surfaced). Thresholds are calibrated so the reference page — the one measured at 100% over 960 trials — lints clean, because a default that flags a manifest known to work is a broken default. *Built.*
34
34
  - **The harness** — drives real browsers over the Chrome DevTools Protocol, fires the utterance set at the page's registered tools, classifies every outcome, and emits a JSON report plus a CI gate. Every trial reads the tool set **twice** — the page's own `getTools()` and the browser's `WebMCP.toolsAdded` stream — because "the page never registered it" and "the client dropped it" are indistinguishable from inside the page. *Built.*
35
- - **A public dataset** — the cross-client compatibility record and invocation-rate corpus, regenerated as browsers change, published with the code that produced every number. *Thirteen runs so far, in `reports/` — including the four-build compatibility matrix and a second client for the rate.*
35
+ - **A public dataset** — the cross-client compatibility record and invocation-rate corpus, regenerated as browsers change, published with the code that produced every number. *Sixteen runs and the complete WebMCP Challenge census so far, in `reports/` — including the four-build compatibility matrix, a second client for the rate, and the census with 80.8% adoption across 2,183 captured pages.*
36
36
 
37
37
  ## How a number is reported
38
38
 
@@ -45,17 +45,54 @@ Conflating those two was a real defect in this project's first two sweeps: a too
45
45
 
46
46
  ## Usage
47
47
 
48
+ **Quick start, zero install, no API key:**
49
+
50
+ ```bash
51
+ npx webmcp-gauge lint --url https://your-page.example
52
+ ```
53
+
54
+ **Requirements:**
55
+
56
+ | Mode | Node | Browser | Judge key |
57
+ |---|---|---|---|
58
+ | `lint --manifest <file>` | ≥ 22 (24 tested) | not needed | **not needed** |
59
+ | `lint --url <page>` | ≥ 22 (24 tested) | yes — launches its own Chrome | **not needed** |
60
+ | `trial` / `run` | ≥ 22 (24 tested) | yes | **yes** — any OpenAI-compatible endpoint |
61
+
62
+ Install once with `npm i -g webmcp-gauge`, or from a clone use `npm run gauge …` / `node bin/webmcp-gauge.mjs …` — every form runs the same CLI:
63
+
48
64
  ```
49
65
  webmcp-gauge lint --url https://example.com
66
+ webmcp-gauge lint --manifest page-tools.json
50
67
  webmcp-gauge trial --utterance sum_by_category-05 --judge <model> --base-url <endpoint>
51
68
  webmcp-gauge run --sessions 3 --repeats 2 --out artifacts/run --judge <model> --base-url <endpoint>
52
69
  webmcp-gauge run --sessions 1 --fail-under 0.9 --judge <model> --base-url <endpoint>
53
70
  webmcp-gauge run --serve fixtures/broken --url "twin.html?variant=degraded" --subject "twin" ...
54
71
  ```
55
72
 
56
- `lint` needs no judge and no key — it reads the page's manifest and applies static rules. Everything else calls a judge model, which must not be the model that wrote the utterances: the frozen set records which one did, and the CLI refuses to run if they match. Credentials come from the environment; see [`.env.example`](.env.example).
73
+ Every flag is documented in `--help`. A `run` writes four files into `--out`: `report.md` for reading, `report.json` (the machine record with per-tool outcome counts and coverage), `badge.json` + `badge.svg`, and a JSONL checkpoint that `--resume` continues. The cohort capture and harvest tools live in [`probes/`](probes/) in this repository, not in the npm package — they are one-day workflows, documented in their own READMEs.
57
74
 
58
- Install once from npm and every command runs as `webmcp-gauge …`; from a clone, `npm run gauge …` or `node bin/webmcp-gauge.mjs …` runs the same thing.
75
+ **The API key, only where it is actually needed.** `lint` reads the manifest and applies static rules — no model anywhere in the path, so it runs on every push with no secrets. `trial` and `run` measure whether an agent-model *chooses* the tool, so they need a judge: any OpenAI-compatible endpoint (hosted or local — Ollama and LM Studio both speak the protocol), costing one model call per trial. An **anthropic-messages endpoint works too** — pass `--judge-shape anthropic` (or `WEBMCP_GAUGE_JUDGE_SHAPE=anthropic`); the shape is the endpoint's and the CLI will not guess it from the hostname. The CLI refuses to run if the judge is the model that authored the frozen utterance set; the set records which one that was.
76
+
77
+ Credentials come from the environment:
78
+
79
+ | Variable | Purpose |
80
+ |---|---|
81
+ | `WEBMCP_GAUGE_JUDGE_API_KEY` | judge key (run/trial only) |
82
+ | `WEBMCP_GAUGE_JUDGE_BASE_URL` | judge endpoint (OpenAI-compatible by default) |
83
+ | `WEBMCP_GAUGE_JUDGE_MODEL` | judge model id, stamped into every report |
84
+ | `WEBMCP_GAUGE_JUDGE_SHAPE` | `anthropic` for an anthropic-messages endpoint (default: `openai`) |
85
+ | `WEBMCP_GAUGE_CHROME` | browser override (default: find Chrome; any Chromium works) |
86
+ | `CDP_PORT` | attach to a browser you started instead of launching one |
87
+ | `WEBMCP_GAUGE_CHROME_LOG` | `1` passes Chrome's own stderr through, for launch debugging |
88
+
89
+ **Nothing loads `.env` automatically** — the CLI reads the environment. Export the variables, or run through the env file directly:
90
+
91
+ ```bash
92
+ node --env-file=.env bin/webmcp-gauge.mjs run --sessions 1 --judge "$WEBMCP_GAUGE_JUDGE_MODEL" ...
93
+ ```
94
+
95
+ See [`.env.example`](.env.example) for the annotated list.
59
96
 
60
97
  `--serve <dir>` publishes a directory on 127.0.0.1 and resolves `--url` against it, which is how the deliberately mis-described fixture page in [`fixtures/broken/`](fixtures/broken/) gets measured with the same frozen utterance set as the reference page.
61
98
 
@@ -69,6 +106,8 @@ A gate is only useful if `1` means one thing, so the three cases are separated:
69
106
  | `1` | Every planned trial was measured, and a rate is below `--fail-under` — the page regressed |
70
107
  | `2` | The run cannot answer: planned trials have no measurement (re-run with `--resume`), or the arguments were unusable |
71
108
 
109
+ In `lint` mode the same codes mean: `0` no findings at or above `--fail-on`, `1` findings at that level, `2` the manifest could not be read.
110
+
72
111
  Incomplete outranks a breach on purpose. Gaps are not random — a judge outage or a page that never loaded can take out one tool's utterances and nothing else — so a rate over a run with holes is a rate over a denominator the run did not choose, and reporting that as a regression would be a lie with a plausible number. Completeness is derived from the plan against the checkpoint, not from the failure log, because a session killed mid-plan logs nothing.
73
112
 
74
113
  The threshold is compared against the **point rate**, not the Wilson lower bound: 20 of 20 has a lower bound of 83.9%, so gating on the bound would fail a flawless page on sample size alone. The interval is printed beside the rate instead, and the verdict says so when a breach sits inside it.
@@ -87,22 +126,23 @@ A badge is a bare number in a coloured pill — the exact thing this project ref
87
126
 
88
127
  ### GitHub Action
89
128
 
90
- [`action.yml`](action.yml) wraps both modes. `lint` needs no browser flag, no judge and no key, so it can run on every push; `run` spends a model call per trial and belongs on a schedule or a manual dispatch.
129
+ [`action.yml`](action.yml) wraps both modes and is [on the GitHub Marketplace](https://github.com/marketplace/actions/webmcp-gauge) — pin `@v1` for a stable interface. `lint` needs no browser flag, no judge and no key, so it can run on every push; `run` spends a model call per trial and belongs on a schedule or a manual dispatch.
91
130
 
92
131
  ```yaml
93
- - uses: Svishwa2004/webmcp-gauge@main
132
+ - uses: Svishwa2004/webmcp-gauge@v1
94
133
  with:
95
134
  mode: lint
96
135
  url: https://your-page.example
97
136
  fail-on: error
98
137
 
99
- - uses: Svishwa2004/webmcp-gauge@main
138
+ - uses: Svishwa2004/webmcp-gauge@v1
100
139
  with:
101
140
  mode: run
102
141
  url: https://your-page.example
103
142
  fail-under: '0.9'
104
143
  judge: ${{ vars.JUDGE_MODEL }}
105
144
  base-url: ${{ vars.JUDGE_BASE_URL }}
145
+ # judge-shape: anthropic # only for an anthropic-messages endpoint; default openai
106
146
  env:
107
147
  WEBMCP_GAUGE_JUDGE_API_KEY: ${{ secrets.JUDGE_API_KEY }}
108
148
  ```
package/action.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  name: webmcp-gauge
2
2
  description: >-
3
- Lint a page's WebMCP manifest, or measure whether an agent actually invokes its
4
- tools, and fail the build when it regresses. Emits a badge either way.
3
+ Lint a page's WebMCP manifest, or measure whether an agent actually invokes
4
+ its tools, and fail on a regression.
5
5
  author: Sahan Vishwa
6
6
 
7
7
  # Two modes, because they cost different things and mean different things.
@@ -54,7 +54,11 @@ inputs:
54
54
  required: false
55
55
  default: ''
56
56
  base-url:
57
- description: 'run mode: OpenAI-compatible endpoint for the judge'
57
+ description: 'run mode: judge endpoint. OpenAI-compatible by default; set judge-shape for an anthropic-messages endpoint'
58
+ required: false
59
+ default: ''
60
+ judge-shape:
61
+ description: 'run mode: judge wire protocol, openai (default) or anthropic — the shape is the endpoint''s'
58
62
  required: false
59
63
  default: ''
60
64
  badge-label:
@@ -131,6 +135,7 @@ runs:
131
135
  [ -n "${{ inputs.fail-under }}" ] && args+=(--fail-under "${{ inputs.fail-under }}")
132
136
  [ -n "${{ inputs.judge }}" ] && args+=(--judge "${{ inputs.judge }}")
133
137
  [ -n "${{ inputs.base-url }}" ] && args+=(--base-url "${{ inputs.base-url }}")
138
+ [ -n "${{ inputs.judge-shape }}" ] && args+=(--judge-shape "${{ inputs.judge-shape }}")
134
139
  fi
135
140
 
136
141
  echo "webmcp-gauge ${args[*]}"
@@ -12,6 +12,7 @@ import { lintManifest, lintToText } from '../core/lint.mjs';
12
12
  import { runSessions } from '../core/orchestrate.mjs';
13
13
  import { buildPlan, readCheckpoint, readFailures, runSessionSweep, trialKey } from '../core/sweep.mjs';
14
14
  import { runTrial } from '../core/trial.mjs';
15
+ import { createJudge as createAnthropicJudge } from '../judges/anthropic-messages.mjs';
15
16
  import { createJudge } from '../judges/openai-compatible.mjs';
16
17
  import { buildReport, toMarkdown } from '../report/emit.mjs';
17
18
  import { buildBadge, renderBadgeSvg } from '../report/badge.mjs';
@@ -39,6 +40,9 @@ error, never a silently ignored default):
39
40
  measurement can run against a fixture page in this repo
40
41
  --judge <model> judge model id (env WEBMCP_GAUGE_JUDGE_MODEL)
41
42
  --base-url <url> judge endpoint (env WEBMCP_GAUGE_JUDGE_BASE_URL)
43
+ --judge-shape <api> judge wire protocol: openai (default) or anthropic — the
44
+ shape is the endpoint's, and guessing it from the hostname
45
+ would be a silent default (env WEBMCP_GAUGE_JUDGE_SHAPE)
42
46
  --port <n> attach to an existing Chrome instead of launching one
43
47
 
44
48
  lint options (no judge required):
@@ -102,7 +106,7 @@ which one that was. See docs/getting-started.md and .env.example.`;
102
106
  */
103
107
  const CLI_OPTIONS = {
104
108
  values: [
105
- 'fixture', 'url', 'serve', 'judge', 'base-url', 'port',
109
+ 'fixture', 'url', 'serve', 'judge', 'base-url', 'judge-shape', 'port',
106
110
  'manifest', 'variant', 'fail-on', 'min-description', 'max-properties', 'budget-warn',
107
111
  'tool', 'utterance',
108
112
  'sessions', 'repeats', 'concurrency', 'gap', 'tools', 'out', 'subject', 'fail-under', 'badge-label',
@@ -178,6 +182,17 @@ const judgeModel =
178
182
  const judgeBaseUrl =
179
183
  (typeof flags['base-url'] === 'string' && flags['base-url']) ||
180
184
  process.env.WEBMCP_GAUGE_JUDGE_BASE_URL;
185
+ const judgeShape =
186
+ (typeof flags['judge-shape'] === 'string' && flags['judge-shape']) ||
187
+ process.env.WEBMCP_GAUGE_JUDGE_SHAPE ||
188
+ 'openai';
189
+ if (judgeShape !== 'openai' && judgeShape !== 'anthropic') {
190
+ fail(`--judge-shape must be "openai" or "anthropic", got "${judgeShape}"`);
191
+ }
192
+ const buildJudge = () =>
193
+ judgeShape === 'anthropic'
194
+ ? createAnthropicJudge({ baseUrl: judgeBaseUrl, model: judgeModel })
195
+ : createJudge({ baseUrl: judgeBaseUrl, model: judgeModel });
181
196
  // L0 is the free on-ramp: it reads a manifest and calls no model, so demanding a
182
197
  // judge for it would put an API key in front of the cheapest useful answer.
183
198
  if (needsJudge && (!judgeModel || !judgeBaseUrl)) {
@@ -299,7 +314,7 @@ if (command === 'lint') {
299
314
  process.exitCode = blocking > 0 ? EXIT.breach : EXIT.pass;
300
315
  }
301
316
  } else if (command === 'trial') {
302
- const judge = createJudge({ baseUrl: judgeBaseUrl, model: judgeModel });
317
+ const judge = buildJudge();
303
318
  const utteranceId = typeof flags.utterance === 'string' ? flags.utterance : null;
304
319
  const toolName =
305
320
  typeof flags.tool === 'string' ? flags.tool : utteranceId?.replace(/-\d+$/, '') ?? null;
@@ -348,7 +363,7 @@ if (command === 'lint') {
348
363
  }
349
364
  } else if (command === 'session') {
350
365
  const session = Number(flags.session ?? 1);
351
- const judge = createJudge({ baseUrl: judgeBaseUrl, model: judgeModel });
366
+ const judge = buildJudge();
352
367
  const target = await openTarget();
353
368
 
354
369
  // Each session owns its browser: a cold profile, its own port, its own process
@@ -438,6 +453,8 @@ if (command === 'lint') {
438
453
  judgeModel,
439
454
  '--base-url',
440
455
  judgeBaseUrl,
456
+ '--judge-shape',
457
+ judgeShape,
441
458
  '--out',
442
459
  outDir,
443
460
  '--repeats',
package/docs/concept.md CHANGED
@@ -347,7 +347,7 @@ Verified 2026-08-29. Building any of the left column again would be waste.
347
347
  | 3 | Sep 1–3 | 🟡 **Part done, and one part closed unanswered** — Mode B adapters + Mode A↔B correlation study + GitHub Action | **#268 refuted** and the GitHub Action shipped (2026-09-01). Mode B's browser layer is driven; its **agent layer is closed on a recorded negative** (2026-09-02) — the agent reads pages through a Chrome extension bridge into the user's own Chrome, and instrumenting that was declined — so the **correlation study has no result and Gate 3 is unanswered rather than passed** |
348
348
  | 4 | by Sep 3, runs Sep 4 | ✅ **Done — staged 2026-09-01, three days early; run complete 2026-09-25/26** — Cohort snapshot script, staged and ready, then run to completion | `core\cohort.mjs` + `core\gallery.mjs` (32 tests), `probes\cohort-snapshot.mjs` + `probes\gallery-harvest.mjs`, rehearsed against three published galleries, three real project pages and a local two-page fixture. The gallery opened Sep 24; all 2,474 submissions were walked (2,185 usable) and **2,183 captured** across three recoverable parts — a browser death and a machine sleep each cost one in-flight target. The census: [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md) |
349
349
  | 5 | Sep 4–21 | ✅ **Done 2026-09-26** — Private per-builder scorecards; aggregate-only public stats; spec-issue data contributions | **The spec contribution is filed** (2026-09-02, `webmcp#227`, twice — the frame-scope baseline and the cross-origin follow-up). The scorecard **renderer** is built, exercised on 344 real captures, and delivery was decided as **offer on request** — the standing offer is in the published report. The aggregate stats are published: [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md) |
350
- | 6 | Sep 23+ | ✅ **Done 2026-09-26** — Public report + open dataset + harness release | The report is published ([`../reports/public-report-draft.md`](../reports/public-report-draft.md)), the repo went public with `EXPECTED_VISIBILITY=public` landed in the flip commit itself, and the dataset is **aggregate-only by item 8's recorded decision** — the derived tables are the open dataset; the raw JSONL stays local unless revisited. The npm package is prepped and publishes on the maintainer's login |
350
+ | 6 | Sep 23+ | ✅ **Done 2026-09-26** — Public report + open dataset + harness release | The report is published ([`../reports/public-report-draft.md`](../reports/public-report-draft.md)), the repo went public with `EXPECTED_VISIBILITY=public` landed in the flip commit itself, the dataset is **aggregate-only by item 8's recorded decision** (the derived tables are the open dataset; the raw JSONL stays local unless revisited), and the npm package is **published** — `webmcp-gauge@0.1.0`, verified from the registry |
351
351
 
352
352
  **Milestone 4 is time-critical and cannot be recovered later.** The Devpost gallery was still unpublished as of 2026-08-29 and opens with judging. Those ~165 live demo URLs sit on free hosting tiers; within a few months a large share will 404. A snapshot of that many real `registerTool` implementations, taken on the one day they are all simultaneously live, is not reconstructible after the fact. It is the largest corpus of real-world WebMCP code that will ever exist for this period, and capturing it costs one day of scripting.
353
353
 
@@ -402,10 +402,10 @@ Unresolved, and each is answerable with a small spike:
402
402
  Ordered, and each independently checkable:
403
403
 
404
404
  1. A stable, variance-quantified invocation rate for any WebMCP page, in at least three clients. 🟡 **Two measured clients now — both Chromium.** The rate is stable (σ ≤ 0.094 between sessions, ≤ 0.085 at 9–17 h spacing, Chrome 960 trials) and Edge `153.0.4234.13` reproduces it at 160 trials — 99% (139/140), every tool inside or above its Chrome interval ([`reports/airlock-1.3.0-glm-5.3-edge-s1r1.md`](../reports/airlock-1.3.0-glm-5.3-edge-s1r1.md), 2026-09-03) — but that is a point measurement, not a variance one, and both runs share one judge model (`glm-5.3`), so the client coordinate moved and the judge coordinate did not. A third client that is not Chromium is the remaining gap.
405
- 2. A published compatibility matrix that a developer would actually link to when explaining "why does my tool work in Brave but not Chrome?" 🟡 **Assembled 2026-09-03 — [`reports/compatibility-matrix.md`](../reports/compatibility-matrix.md): 20 behaviours across four build columns (Chrome `152.0.7977.65`; the ChatGPT desktop app's Chromium at `151.0.7922.174` and again at `152.0.7977.64` after it updated mid-project; Edge `153.0.4234.13`, where the Chrome activation recipe works unchanged), each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one row that turned out to be our own bug. Publishable as it stands; "published" still waits on the repo going public (criterion 5).** And the honest limit of it: **Brave — one of the two clients the question names — is still unmeasured** (not installed here), and Edge was measured at 153 rather than the 151 that report names, so the matrix answers "why does my tool behave differently in ChatGPT's browser or Edge?" well and the Brave half of the question not at all.
405
+ 2. A published compatibility matrix that a developer would actually link to when explaining "why does my tool work in Brave but not Chrome?" 🟡 **Assembled 2026-09-03 — [`reports/compatibility-matrix.md`](../reports/compatibility-matrix.md): 20 behaviours across four build columns (Chrome `152.0.7977.65`; the ChatGPT desktop app's Chromium at `151.0.7922.174` and again at `152.0.7977.64` after it updated mid-project; Edge `153.0.4234.13`, where the Chrome activation recipe works unchanged), each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one row that turned out to be our own bug. Publishable as it stands; "published" still waits on the repo going public (criterion 5).** And the honest limit of it, updated 2026-09-26: **Brave `154.1.96.59` was installed and surface-measured — the matrix now carries a fifth column, and the #227 frame answers reproduce on a third engine — but its invocation-rate sweep is blocked on the judge provider** (agentrouter serves the key no channel for `glm-5.3`), and Edge was measured at 153 rather than the 151 that report names, so the matrix answers "why does my tool behave differently in ChatGPT's browser, Edge or Brave?" at the surface level while the rate comparison still rests on two clients.
406
406
  3. At least one reproducible data contribution accepted into a live spec issue. ✅ **Filed 2026-09-02** — two comments on `webmcp#227`, each carrying a build string and a reproduction that was run before it was posted. *Filed* is done; *accepted* is not this project's to decide.
407
- 4. A cohort dataset captured on gallery-publish day and preserved. ⏳ **Tooling staged and rehearsed; the capture waits on the gallery** (expected 2026-09-04).
408
- 5. The linter installed by developers who have never heard of the author. 🟡 **The repo is public (2026-09-26) with `action.yml` live, and the npm package is prepped** — `npx webmcp-gauge lint --url <page>` becomes real the moment `npm publish` runs on the maintainer's login. Criterion 5 is the one this document cannot close by itself: it needs strangers, not commits.
407
+ 4. A cohort dataset captured on gallery-publish day and preserved. ✅ **Done 2026-09-25/26** — the gallery opened Sep 24; all 2,474 submissions walked (2,185 usable) and **2,183 captured** across three recoverable parts, reconciled and published as [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md). The one-day preservation claim holds: every target was visited exactly once inside the ~13-hour window, with the dead-record composition checked rather than assumed.
408
+ 5. The linter installed by developers who have never heard of the author. ✅ **Met 2026-09-26, three ways** — the repo is public with `action.yml` live, **`webmcp-gauge@0.1.0` is on npm** (verified by installing it fresh from the registry and running `npx webmcp-gauge lint --url <page>` against a real page), and **the Action is on the GitHub Marketplace** as `Svishwa2004/webmcp-gauge` (`v1.0.0`, published from a release whose `action.yml` carries the required branding; the first description tripped the Marketplace's 125-character limit and was shortened inside the tag before publishing, since the listing reads `action.yml` at the tag). The moving `v1` tag is the interface promise: fixes move within the major, breaking changes get a `v2`. What this document cannot close by itself is the adoption half: that needs strangers, not commits.
409
409
  6. A report that becomes the default citation for "do WebMCP tools actually get called?" 🟡 **Published 2026-09-26 — [`reports/public-report-draft.md`](../reports/public-report-draft.md).** The capture landed (criterion 4) and the repository went public with the report in the same window (criterion 5's on-ramp). Every section is sourced to the write-up that produced it, including Gate 3 stated as unanswered and the complete cohort census. Whether it becomes anyone's default citation is not this project's to decide — see criterion 3's split between *filed* and *accepted*.
410
410
 
411
411
  Criterion 3 is the cheapest and the most valuable. The WebMCP community is small enough — a few named Chrome engineers, spec editors and library authors — that two or three good field-data contributions to open issues make an unknown solo developer a known one. That is the actual early advantage on offer here, and it does not require the product to succeed commercially. **It is also the one criterion already met, on day five, for the cost of running a test before writing a comment.**
@@ -6,6 +6,8 @@
6
6
 
7
7
  Verification legend used throughout: ✅ verified on this machine · ⚠️ unverified · ❌ known wrong.
8
8
 
9
+ > **Just want to run the tool?** The README's [Usage](../README.md#usage) section is the stranger's path: `npx webmcp-gauge lint --url <page>` needs no install and no API key, and the requirements table there says exactly which modes need a browser and which need a judge key. This document is the author's own machine record — the paths, browsers and credentials below are how *this* project was stood up, kept because the log's verification claims trace to it.
10
+
9
11
  ---
10
12
 
11
13
  ## 0. What is already in place
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Judge adapter for Anthropic-messages endpoints (POST {base}/v1/messages).
3
+ *
4
+ * Same judge-level contract as judges/openai-compatible.mjs — { id, baseUrl,
5
+ * keySource, complete({system, user, maxTokens}) } — differing only in the wire
6
+ * shape, so the sweep and trial paths cannot tell the two apart and every
7
+ * report keeps stamping the same judge fields. Chosen explicitly with
8
+ * --judge-shape anthropic, because the shape is a property of the endpoint and
9
+ * guessing it from the hostname would be one more silent default. Just Work
10
+ * (api.justwoker.icu) serves claude models over this protocol and refuses the
11
+ * OpenAI shape outright. No retrying and no repair, same rule as the OpenAI
12
+ * adapter: a judge that cannot follow the response contract is a measurement
13
+ * result, not an error to paper over.
14
+ */
15
+ import { resolveApiKey } from './openai-compatible.mjs';
16
+
17
+ const DEFAULT_TIMEOUT_MS = 60000;
18
+
19
+ /**
20
+ * Reasoning budgets sit higher than a tool-choice answer needs, and Anthropic
21
+ * charges for thinking tokens the same way — the ceiling exists so truncation
22
+ * is unlikely, and `truncated` says so when it happens anyway.
23
+ */
24
+ const DEFAULT_MAX_TOKENS = 4096;
25
+
26
+ export const createJudge = ({
27
+ baseUrl,
28
+ model,
29
+ apiKeyVar,
30
+ env = process.env,
31
+ timeoutMs = DEFAULT_TIMEOUT_MS,
32
+ temperature = 0,
33
+ }) => {
34
+ if (!baseUrl || !model) throw new Error('createJudge requires baseUrl and model');
35
+ const { key, source } = resolveApiKey(env, apiKeyVar, baseUrl);
36
+ const endpoint = `${baseUrl.replace(/\/$/, '')}/v1/messages`;
37
+
38
+ return {
39
+ id: model,
40
+ baseUrl,
41
+ keySource: source,
42
+
43
+ async complete({ system, user, maxTokens = DEFAULT_MAX_TOKENS }) {
44
+ const body = {
45
+ model,
46
+ temperature,
47
+ max_tokens: maxTokens,
48
+ ...(system ? { system } : {}),
49
+ messages: [{ role: 'user', content: user }],
50
+ };
51
+
52
+ const startedAt = Date.now();
53
+ const response = await fetch(endpoint, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'x-api-key': key,
58
+ Authorization: `Bearer ${key}`,
59
+ 'anthropic-version': '2023-06-01',
60
+ },
61
+ body: JSON.stringify(body),
62
+ signal: AbortSignal.timeout(timeoutMs),
63
+ });
64
+
65
+ const text = await response.text();
66
+ const elapsedMs = Date.now() - startedAt;
67
+
68
+ if (!response.ok) {
69
+ // The body carries the useful part - a quota rejection reads very
70
+ // differently from a malformed request - so it is surfaced, not swallowed.
71
+ throw new Error(`judge HTTP ${response.status}: ${text.slice(0, 500)}`);
72
+ }
73
+
74
+ const payload = JSON.parse(text);
75
+ const content = (payload.content ?? [])
76
+ .filter((block) => block.type === 'text')
77
+ .map((block) => block.text)
78
+ .join('')
79
+ .trim();
80
+ const finishReason = payload.stop_reason ?? null;
81
+
82
+ return {
83
+ content,
84
+ raw: text,
85
+ model: payload.model ?? model,
86
+ finishReason,
87
+ truncated: finishReason === 'max_tokens',
88
+ // Normalised to the OpenAI field names so a report never carries two
89
+ // usage shapes depending on which adapter served the trials.
90
+ usage: payload.usage
91
+ ? { prompt_tokens: payload.usage.input_tokens, completion_tokens: payload.usage.output_tokens }
92
+ : null,
93
+ elapsedMs,
94
+ };
95
+ },
96
+ };
97
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webmcp-gauge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Measures whether an AI agent actually calls the tools a page exposes through WebMCP.",
6
6
  "license": "MIT",