@rolepod/uiproof 0.5.0 → 0.6.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.
@@ -10,8 +10,8 @@
10
10
  {
11
11
  "name": "rolepod-uiproof",
12
12
  "source": "./",
13
- "description": "26 MCP tools (21 atomic browser/mobile primitives + 5 composite workflows) + 5 user-invocable skills. v0.5 completes the UI verification surface console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page, gated JS eval replacing chrome-devtools-mcp and playwright-mcp for UI testing. Web production-ready via Playwright; mobile (iOS/Android) via Appium scaffolded — see `rolepod-uiproof doctor` for readiness.",
14
- "version": "0.5.0",
13
+ "description": "26 MCP tools (21 atomic browser/mobile primitives + 5 composite workflows) + 5 user-invocable skills. v0.6 adds Extension Protocol v1 support — works standalone today, becomes the verify-phase UI provider when installed alongside the `rolepod` parent plugin (evidence routes to `.rolepod/evidence/` with `manifest.json`). Replaces chrome-devtools-mcp and playwright-mcp for UI testing. Web production-ready via Playwright; mobile (iOS/Android) via Appium scaffolded — see `rolepod-uiproof doctor` for readiness.",
14
+ "version": "0.6.0",
15
15
  "author": {
16
16
  "name": "nuttaruj"
17
17
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rolepod-uiproof",
3
- "version": "0.5.0",
4
- "description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.5 completes the UI verification surface: console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page support, gated JS eval — replacing chrome-devtools-mcp and playwright-mcp for UI testing.",
3
+ "version": "0.6.0",
4
+ "description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. Works standalone OR with the `rolepod` parent plugin: when ROLEPOD_PARENT=1 is set, evidence routes to `.rolepod/evidence/` with a `manifest.json` per Extension Protocol v1, so parent's `check-work` skill can aggregate UI verify results into its phase report. v0.5 completed the UI verification surface (console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page, gated JS eval).",
5
5
  "author": {
6
6
  "name": "nuttaruj",
7
7
  "url": "https://github.com/nuttaruj"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rolepod-uiproof",
3
- "version": "0.5.0",
4
- "description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.5 completes the UI verification surface: console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page, gated JS eval.",
3
+ "version": "0.6.0",
4
+ "description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.6 adds Extension Protocol v1 works standalone today, becomes the verify-phase UI provider when paired with the `rolepod` parent plugin.",
5
5
  "author": {
6
6
  "name": "nuttaruj",
7
7
  "url": "https://github.com/nuttaruj"
@@ -25,7 +25,7 @@
25
25
  "interface": {
26
26
  "displayName": "Rolepod UIProof",
27
27
  "shortDescription": "UI verification, a11y audits, visual diff, e2e scaffolding — for AI coding agents.",
28
- "longDescription": "rolepod-uiproof ships an MCP server with 26 tools (21 atomic + 5 composite) and 5 user-invocable skills (/verify-ui, /audit-a11y, /visual-diff, /scaffold-e2e, /check-errors). Web is fully supported via Playwright; mobile (iOS/Android via Appium) supports basic input. v0.5 completes the UI verification surface console logs, network requests, HAR/video/trace capture, runtime emulation, multi-page popups.",
28
+ "longDescription": "rolepod-uiproof ships an MCP server with 26 tools (21 atomic + 5 composite) and 5 user-invocable skills (/verify-ui, /audit-a11y, /visual-diff, /scaffold-e2e, /check-errors). Web is fully supported via Playwright; mobile (iOS/Android via Appium) supports basic input. v0.6: pair with the `rolepod` parent plugin (v2.7+) and uiproof becomes the verify-phase UI providerevidence routes to `.rolepod/evidence/` with a `manifest.json` per Extension Protocol v1.",
29
29
  "developerName": "nuttaruj",
30
30
  "category": "Productivity",
31
31
  "capabilities": ["Read", "Write", "Bash"],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "rolepod-uiproof",
3
3
  "displayName": "Rolepod UIProof",
4
- "version": "0.5.0",
5
- "description": "Multi-platform UI / mobile automation MCP server + 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) for AI coding agents. v0.5 completes the UI verification surface: console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page, gated JS eval replacing chrome-devtools-mcp and playwright-mcp.",
4
+ "version": "0.6.0",
5
+ "description": "Multi-platform UI / mobile automation MCP server + 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) for AI coding agents. v0.6 adds Extension Protocol v1 — works standalone today, becomes the verify-phase UI provider when paired with the `rolepod` parent plugin (evidence routes to `.rolepod/evidence/` with `manifest.json`). Replaces chrome-devtools-mcp and playwright-mcp.",
6
6
  "author": {
7
7
  "name": "nuttaruj"
8
8
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ release.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] — 2026-05-27
11
+
12
+ **Extension Protocol v1 — `uiproof` becomes parent-aware. Standalone
13
+ behavior unchanged.**
14
+
15
+ When the parent `rolepod` plugin (v2.7+) sets `ROLEPOD_PARENT=1` via
16
+ its SessionStart hook, uiproof routes evidence to the shared
17
+ `.rolepod/evidence/` tree and emits a `manifest.json` per spec so the
18
+ parent's `check-work` skill can aggregate UI verify results into its
19
+ phase report. With no parent installed the v0.5 behavior is preserved
20
+ exactly — same artifact path, same tool output, plus a `manifest.json`
21
+ in each run dir as a bonus.
22
+
23
+ ### Added
24
+
25
+ - **Env-aware evidence path** in `ArtifactStore`. Detected at
26
+ construction from `process.env.ROLEPOD_PARENT === "1"`.
27
+ - standalone: `.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
28
+ - with-parent: `.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
29
+ - **`manifest.json`** written by every composite that starts a run
30
+ (`verify_ui_flow`, `audit_a11y`, `visual_diff`, `scaffold_e2e`).
31
+ Schema follows Extension Protocol v1: `protocol`, `plugin`, `skill`,
32
+ `phase`, `status`, `summary`, `started_at`, `finished_at`,
33
+ `artifacts: [{type, path}]`, `metadata`. Best-effort: any IO failure
34
+ is logged but never thrown.
35
+ - **Graduated a11y status**. `audit_a11y` manifest carries `status`:
36
+ `critical/serious > 0 → fail`, `moderate/minor > 0 → warn`, no
37
+ issues → `pass`. Keeps the `warn` signal a strict pass/fail would
38
+ discard.
39
+ - **Protocol version check**. When `ROLEPOD_PROTOCOL` is set but
40
+ does not equal `v1`, `buildServer()` logs a one-shot warning. Does
41
+ not block; manifest is still written in v1 shape.
42
+ - **`/check-errors` evidence routing doc** alongside the other 4
43
+ skills.
44
+
45
+ ### Changed
46
+
47
+ - `ArtifactStore.startRun(prefix, opts?)` — `opts.skill` is new and
48
+ optional. Provides the canonical skill name for both the
49
+ with-parent dirname and the manifest's `skill` field. Return shape
50
+ extended with `skill` and `mode` (back-compat: existing destructuring
51
+ of `{ runId, runDir }` keeps working).
52
+ - `buildServer()` log line surfaces `protocol: "v1"` and
53
+ `mode: "standalone" | "with-parent"` alongside the existing version
54
+ + tools list.
55
+ - All 5 shipped skills' SKILL.md gained an "Evidence routing" section
56
+ between "Process" / "Outputs" and "If the tool is unavailable".
57
+ Mirrored to `plugins/rolepod-uiproof/skills/`.
58
+ - README "Standalone vs Combined" section added explaining the two
59
+ modes.
60
+
61
+ ### Behavior
62
+
63
+ - **Standalone:** unchanged. Evidence still written to
64
+ `.rolepod-uiproof/artifacts/`. New: a `manifest.json` appears in each
65
+ run dir. Tool return values gain an optional `manifest: "<path>"`
66
+ field; everything else is byte-for-byte identical.
67
+ - **With rolepod parent:** evidence written to
68
+ `.rolepod/evidence/<ts>-rolepod-uiproof-<skill>/` with `manifest.json`
69
+ per protocol spec. Visual baselines stay in
70
+ `.rolepod-uiproof/baselines/` regardless of mode.
71
+
72
+ ### Non-goals (kept out of v0.6)
73
+
74
+ - Dynamic capabilities registry (`.claude-plugin/capabilities.json`)
75
+ - Protocol version negotiation beyond a single warn
76
+ - Cross-child coordination (uiproof ↔ wplab handoff inside one run)
77
+ - Mobile platform support stays at the v0.5 partial level
78
+
10
79
  ## [0.5.0] — 2026-05-27
11
80
 
12
81
  **Complete UI verification surface — one MCP replaces chrome-devtools-mcp
package/README.md CHANGED
@@ -27,6 +27,21 @@ One MCP server, one tool surface, five skills you invoke from chat. Web is produ
27
27
 
28
28
  Every skill is **single-backend** (D-024) — it calls the rolepod-uiproof server and only the rolepod-uiproof server. If the server is unavailable, the skill fails with a clear diagnostic. Multi-backend routing belongs in the parent [`rolepod`](https://github.com/nuttaruj/rolepod) plugin's phase skills, not here.
29
29
 
30
+ ## Standalone vs Combined
31
+
32
+ `rolepod-uiproof` works either as a **standalone** browser MCP for any project, or **combined** with the [`rolepod`](https://github.com/nuttaruj/rolepod) parent plugin (v2.7+) where it becomes the Verify phase provider for UI artifacts.
33
+
34
+ **Standalone** (default): use the 5 skills directly as atomic browser tools. Evidence saved under `./.rolepod-uiproof/artifacts/<run>/` with a `manifest.json` per Extension Protocol v1.
35
+
36
+ **Combined with rolepod parent**: when the parent's SessionStart hook sets `ROLEPOD_PARENT=1`, uiproof writes evidence to `./.rolepod/evidence/<ts>-rolepod-uiproof-<skill>/` instead, where parent's `check-work` skill auto-aggregates manifests into the verify report. No skill changes — same 26 tools, same 5 skills, smarter routing.
37
+
38
+ | Install | Unlocks |
39
+ |---|---|
40
+ | uiproof alone | Browser test, a11y audit, visual diff, e2e scaffold, error gate |
41
+ | uiproof + rolepod parent | + verify-phase aggregation, evidence handoff to `check-work` |
42
+
43
+ The `manifest.json` is written in BOTH modes, so installing the parent later still lets historic artifacts get picked up. Baselines for `/visual-diff` always live in `./.rolepod-uiproof/baselines/` regardless of mode — they are user-curated configuration, not per-run evidence.
44
+
30
45
  ## Install
31
46
 
32
47
  Pick your CLI. All install paths share the same MCP server (`@rolepod/uiproof` on npm) and the same skill set.
@@ -175,7 +175,7 @@ function runInstallMobile() {
175
175
 
176
176
  // src/cli/replay.ts
177
177
  import { readFile } from "fs/promises";
178
- import { resolve as resolve3 } from "path";
178
+ import { resolve as resolve4 } from "path";
179
179
 
180
180
  // src/artifact/ArtifactStore.ts
181
181
  import { randomUUID } from "crypto";
@@ -204,16 +204,49 @@ var log = {
204
204
  // src/artifact/ArtifactStore.ts
205
205
  var ArtifactStore = class {
206
206
  rootDir;
207
+ mode;
208
+ baselineRoot;
207
209
  constructor(opts = {}) {
208
- this.rootDir = opts.rootDir ?? resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
210
+ const detectedParent = process.env.ROLEPOD_PARENT === "1";
211
+ this.mode = opts.mode ?? (detectedParent ? "with-parent" : "standalone");
212
+ if (opts.rootDir !== void 0) {
213
+ this.rootDir = opts.rootDir;
214
+ } else if (this.mode === "with-parent") {
215
+ this.rootDir = resolve2(process.cwd(), ".rolepod", "evidence");
216
+ } else {
217
+ this.rootDir = resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
218
+ }
219
+ this.baselineRoot = resolve2(process.cwd(), ".rolepod-uiproof", "baselines");
209
220
  }
210
- /** Allocate a fresh run id and ensure its directory exists. */
211
- async startRun(prefix = "run") {
212
- const runId = `${prefix}_${this.timestampSlug()}_${randomUUID().slice(0, 8)}`;
221
+ /**
222
+ * Allocate a fresh run dir and ensure it exists.
223
+ *
224
+ * - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
225
+ * - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
226
+ *
227
+ * `prefix` is preserved for back-compat with v0.5 callers; new callers
228
+ * should also pass `opts.skill` so the with-parent path can be derived
229
+ * unambiguously and the manifest can be emitted with the canonical
230
+ * skill name.
231
+ */
232
+ async startRun(prefix = "run", opts = {}) {
233
+ const ts = this.timestampSlug();
234
+ const skill = opts.skill ?? prefix;
235
+ let runId;
236
+ if (this.mode === "with-parent") {
237
+ runId = `${ts}-rolepod-uiproof-${skill}`;
238
+ } else {
239
+ runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
240
+ }
213
241
  const runDir = resolve2(this.rootDir, runId);
214
242
  await mkdir(runDir, { recursive: true });
215
- log.debug("artifact run started", { run_id: runId, dir: runDir });
216
- return { runId, runDir };
243
+ log.debug("artifact run started", {
244
+ run_id: runId,
245
+ dir: runDir,
246
+ mode: this.mode,
247
+ skill
248
+ });
249
+ return { runId, runDir, skill, mode: this.mode };
217
250
  }
218
251
  async writeScreenshot(runDir, buf, name) {
219
252
  const path = resolve2(runDir, `${name}.png`);
@@ -241,7 +274,7 @@ var ArtifactStore = class {
241
274
  }
242
275
  /** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
243
276
  get baselineDir() {
244
- return resolve2(this.rootDir, "..", "baselines");
277
+ return this.baselineRoot;
245
278
  }
246
279
  timestampSlug() {
247
280
  const d = /* @__PURE__ */ new Date();
@@ -1170,21 +1203,21 @@ var PlaywrightEngine = class {
1170
1203
  if (s.dialogArming) {
1171
1204
  s.dialogArming.resolve(false);
1172
1205
  }
1173
- return new Promise((resolve6) => {
1206
+ return new Promise((resolve7) => {
1174
1207
  const arming = {
1175
1208
  action: opts.action,
1176
1209
  text: opts.text,
1177
1210
  expiresAt,
1178
1211
  resolve: (handled) => {
1179
1212
  s.dialogArming = null;
1180
- resolve6({ handled });
1213
+ resolve7({ handled });
1181
1214
  }
1182
1215
  };
1183
1216
  s.dialogArming = arming;
1184
1217
  const timer = setTimeout(() => {
1185
1218
  if (s.dialogArming === arming) {
1186
1219
  s.dialogArming = null;
1187
- resolve6({ handled: false });
1220
+ resolve7({ handled: false });
1188
1221
  }
1189
1222
  }, timeoutMs);
1190
1223
  timer.unref?.();
@@ -2035,6 +2068,37 @@ async function ddmin(input, reproduces) {
2035
2068
  return current;
2036
2069
  }
2037
2070
 
2071
+ // src/util/manifest.ts
2072
+ import { writeFile as writeFile2 } from "fs/promises";
2073
+ import { resolve as resolve3 } from "path";
2074
+ var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
2075
+ async function writeManifest(input) {
2076
+ const manifest = {
2077
+ protocol: ROLEPOD_PROTOCOL_VERSION,
2078
+ plugin: "rolepod-uiproof",
2079
+ skill: input.skill,
2080
+ phase: input.phase,
2081
+ status: input.status,
2082
+ summary: input.summary,
2083
+ started_at: input.startedAt,
2084
+ finished_at: input.finishedAt,
2085
+ artifacts: input.artifacts,
2086
+ metadata: input.metadata ?? {}
2087
+ };
2088
+ const path = resolve3(input.runDir, "manifest.json");
2089
+ try {
2090
+ await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
2091
+ return path;
2092
+ } catch (err) {
2093
+ log.warn("manifest write failed", {
2094
+ run_dir: input.runDir,
2095
+ skill: input.skill,
2096
+ err: String(err)
2097
+ });
2098
+ return void 0;
2099
+ }
2100
+ }
2101
+
2038
2102
  // src/tools/result.ts
2039
2103
  function ok(value) {
2040
2104
  return {
@@ -2076,7 +2140,11 @@ var verifyUiFlowTool = {
2076
2140
  inputShape: verifyUiFlowShape,
2077
2141
  build(ctx) {
2078
2142
  return safeHandler(async (args) => {
2079
- const { runId, runDir } = await ctx.store.startRun("verify");
2143
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2144
+ const { runId, runDir, skill } = await ctx.store.startRun(
2145
+ "verify",
2146
+ { skill: "verify-ui" }
2147
+ );
2080
2148
  const initial = await runFlow(ctx, args, args.steps, runDir, {
2081
2149
  captureEvidence: true,
2082
2150
  bundleName: "replay.json"
@@ -2101,10 +2169,49 @@ var verifyUiFlowTool = {
2101
2169
  attempts: min.attempts
2102
2170
  };
2103
2171
  }
2172
+ const manifestPath = await writeManifest({
2173
+ runDir,
2174
+ skill,
2175
+ phase: "verify",
2176
+ status: initial.passed ? "pass" : "fail",
2177
+ summary: buildVerifySummary(args, initial),
2178
+ startedAt,
2179
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2180
+ artifacts: flattenVerifyEvidence(initial.evidence),
2181
+ metadata: {
2182
+ mode: args.mode,
2183
+ step_count: args.steps.length,
2184
+ expect_count: args.expect.length,
2185
+ ...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
2186
+ }
2187
+ });
2188
+ if (manifestPath) result.manifest = manifestPath;
2104
2189
  return ok(result);
2105
2190
  });
2106
2191
  }
2107
2192
  };
2193
+ function buildVerifySummary(args, outcome) {
2194
+ const stepCount = args.steps.length;
2195
+ const expectCount = args.expect.length;
2196
+ if (outcome.passed) {
2197
+ return `${stepCount} step(s), ${expectCount} expect(s) passed`;
2198
+ }
2199
+ if (outcome.failedAtStep !== void 0) {
2200
+ return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
2201
+ }
2202
+ return `failed: ${outcome.failureReason ?? "unknown"}`;
2203
+ }
2204
+ function flattenVerifyEvidence(ev) {
2205
+ const out = [];
2206
+ for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
2207
+ if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
2208
+ if (ev.console) out.push({ type: "console", path: ev.console });
2209
+ if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
2210
+ if (ev.har) out.push({ type: "har", path: ev.har });
2211
+ if (ev.trace) out.push({ type: "trace", path: ev.trace });
2212
+ if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
2213
+ return out;
2214
+ }
2108
2215
  function buildCaptureOptions(captures, runDir) {
2109
2216
  const cap = {};
2110
2217
  if (captures.has("har")) {
@@ -2518,7 +2625,7 @@ function treeHasText(tree, text) {
2518
2625
 
2519
2626
  // src/cli/replay.ts
2520
2627
  async function runReplay(bundlePath) {
2521
- const abs = resolve3(bundlePath);
2628
+ const abs = resolve4(bundlePath);
2522
2629
  const raw = await readFile(abs, "utf8");
2523
2630
  const bundle = JSON.parse(raw);
2524
2631
  if (bundle.version !== 1) {
@@ -3042,7 +3149,11 @@ var auditA11yTool = {
3042
3149
  inputShape: auditA11yShape,
3043
3150
  build(ctx) {
3044
3151
  return safeHandler(async (args) => {
3045
- const { runId, runDir } = await ctx.store.startRun("audit");
3152
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3153
+ const { runId, runDir, skill } = await ctx.store.startRun(
3154
+ "audit",
3155
+ { skill: "audit-a11y" }
3156
+ );
3046
3157
  const session = await ctx.registry.open(args.open);
3047
3158
  const engine = ctx.registry.engineFor(session.id);
3048
3159
  if (!(engine instanceof PlaywrightEngine)) {
@@ -3110,15 +3221,45 @@ var auditA11yTool = {
3110
3221
  await ctx.registry.close(session).catch(() => void 0);
3111
3222
  }
3112
3223
  }
3224
+ const counts = countBySeverity(issues);
3225
+ const status = a11yStatus(counts);
3226
+ const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
3227
+ const manifestPath = await writeManifest({
3228
+ runDir,
3229
+ skill,
3230
+ phase: "verify",
3231
+ status,
3232
+ summary: buildAuditSummary(args.level, counts, status),
3233
+ startedAt,
3234
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3235
+ artifacts,
3236
+ metadata: {
3237
+ level: args.level,
3238
+ scope: args.scope,
3239
+ counts,
3240
+ report_format: args.report_format
3241
+ }
3242
+ });
3113
3243
  return ok({
3114
3244
  run_id: runId,
3115
- counts: countBySeverity(issues),
3245
+ counts,
3116
3246
  issues,
3117
- report_path: reportPath
3247
+ report_path: reportPath,
3248
+ ...manifestPath ? { manifest: manifestPath } : {}
3118
3249
  });
3119
3250
  });
3120
3251
  }
3121
3252
  };
3253
+ function a11yStatus(counts) {
3254
+ if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
3255
+ if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
3256
+ return "pass";
3257
+ }
3258
+ function buildAuditSummary(level, counts, status) {
3259
+ const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
3260
+ if (status === "pass") return `${level}: 0 issues`;
3261
+ return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
3262
+ }
3122
3263
  function pickWcagRef(tags) {
3123
3264
  return tags.find((t) => /^wcag\d/.test(t));
3124
3265
  }
@@ -3235,14 +3376,18 @@ function scoreTree(root, tokens) {
3235
3376
 
3236
3377
  // src/tools/composite/scaffold_e2e.ts
3237
3378
  import { readFile as readFile2 } from "fs/promises";
3238
- import { resolve as resolve4 } from "path";
3379
+ import { resolve as resolve5 } from "path";
3239
3380
  var scaffoldE2eTool = {
3240
3381
  name: ToolNames.scaffoldE2e,
3241
3382
  description: "Generate a runnable e2e test file (playwright-test, vitest+playwright, or pytest+selenium) from a scenario description and optional replay bundle from a prior verify_ui_flow run.",
3242
3383
  inputShape: scaffoldE2eShape,
3243
3384
  build(ctx) {
3244
3385
  return safeHandler(async (args) => {
3245
- const { runId, runDir } = await ctx.store.startRun("scaffold");
3386
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3387
+ const { runId, runDir, skill } = await ctx.store.startRun(
3388
+ "scaffold",
3389
+ { skill: "scaffold-e2e" }
3390
+ );
3246
3391
  const slug = slugify(args.scenario_nl);
3247
3392
  const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
3248
3393
  const ctxObj = { args, slug, bundle };
@@ -3280,19 +3425,35 @@ var scaffoldE2eTool = {
3280
3425
  );
3281
3426
  }
3282
3427
  const path = await ctx.store.writeReport(runDir, filename, body);
3428
+ const manifestPath = await writeManifest({
3429
+ runDir,
3430
+ skill,
3431
+ phase: "build",
3432
+ status: "pass",
3433
+ summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
3434
+ startedAt,
3435
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3436
+ artifacts: [{ type: "test_file", path }],
3437
+ metadata: {
3438
+ framework: args.framework,
3439
+ language,
3440
+ from_replay_bundle: Boolean(bundle)
3441
+ }
3442
+ });
3283
3443
  return ok({
3284
3444
  run_id: runId,
3285
3445
  test_file_path: path,
3286
3446
  language,
3287
3447
  dependencies,
3288
3448
  setup_notes: setupNotes,
3289
- from_replay_bundle: Boolean(bundle)
3449
+ from_replay_bundle: Boolean(bundle),
3450
+ ...manifestPath ? { manifest: manifestPath } : {}
3290
3451
  });
3291
3452
  });
3292
3453
  }
3293
3454
  };
3294
3455
  async function loadReplay(bundlePath) {
3295
- const raw = await readFile2(resolve4(bundlePath), "utf8");
3456
+ const raw = await readFile2(resolve5(bundlePath), "utf8");
3296
3457
  return JSON.parse(raw);
3297
3458
  }
3298
3459
  function slugify(s) {
@@ -3581,7 +3742,7 @@ function indent(block, n) {
3581
3742
  // src/tools/composite/visual_diff.ts
3582
3743
  import { existsSync as existsSync2 } from "fs";
3583
3744
  import { readFile as readFile3 } from "fs/promises";
3584
- import { resolve as resolve5 } from "path";
3745
+ import { resolve as resolve6 } from "path";
3585
3746
  import pixelmatch from "pixelmatch";
3586
3747
  import { PNG } from "pngjs";
3587
3748
  var visualDiffTool = {
@@ -3590,7 +3751,11 @@ var visualDiffTool = {
3590
3751
  inputShape: visualDiffShape,
3591
3752
  build(ctx) {
3592
3753
  return safeHandler(async (args) => {
3593
- const { runId, runDir } = await ctx.store.startRun("vdiff");
3754
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3755
+ const { runId, runDir, skill } = await ctx.store.startRun(
3756
+ "vdiff",
3757
+ { skill: "visual-diff" }
3758
+ );
3594
3759
  const session = await ctx.registry.open({
3595
3760
  ...args.open,
3596
3761
  ...args.viewport ? { viewport: args.viewport } : {}
@@ -3609,7 +3774,7 @@ var visualDiffTool = {
3609
3774
  );
3610
3775
  const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
3611
3776
  await ctx.store.ensureDir(ctx.store.baselineDir);
3612
- const baselinePath = resolve5(
3777
+ const baselinePath = resolve6(
3613
3778
  ctx.store.baselineDir,
3614
3779
  `${args.baseline_id}.png`
3615
3780
  );
@@ -3619,6 +3784,20 @@ var visualDiffTool = {
3619
3784
  `${args.baseline_id}.png`,
3620
3785
  buf
3621
3786
  );
3787
+ const manifestPath2 = await writeManifest({
3788
+ runDir,
3789
+ skill,
3790
+ phase: "verify",
3791
+ status: "pass",
3792
+ summary: `baseline "${args.baseline_id}" seeded from current capture`,
3793
+ startedAt,
3794
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3795
+ artifacts: [
3796
+ { type: "baseline", path: baselinePath },
3797
+ { type: "screenshot", path: currentPath }
3798
+ ],
3799
+ metadata: { baseline_id: args.baseline_id, seeded: true }
3800
+ });
3622
3801
  return ok({
3623
3802
  run_id: runId,
3624
3803
  baseline_id: args.baseline_id,
@@ -3626,6 +3805,7 @@ var visualDiffTool = {
3626
3805
  passed: true,
3627
3806
  baseline_path: baselinePath,
3628
3807
  current_path: currentPath,
3808
+ ...manifestPath2 ? { manifest: manifestPath2 } : {},
3629
3809
  note: "Baseline did not exist \u2014 current capture saved as the new baseline."
3630
3810
  });
3631
3811
  }
@@ -3656,21 +3836,45 @@ var visualDiffTool = {
3656
3836
  );
3657
3837
  const total = baseline.width * baseline.height;
3658
3838
  const diffPct = diffPixels / total;
3839
+ const passed = diffPct <= args.threshold_pct;
3659
3840
  const diffImagePath = await ctx.store.writeBytes(
3660
3841
  runDir,
3661
3842
  "diff.png",
3662
3843
  PNG.sync.write(diff)
3663
3844
  );
3845
+ const artifacts = [
3846
+ { type: "baseline", path: baselinePath },
3847
+ { type: "screenshot", path: currentPath },
3848
+ { type: "diff", path: diffImagePath }
3849
+ ];
3850
+ const manifestPath = await writeManifest({
3851
+ runDir,
3852
+ skill,
3853
+ phase: "verify",
3854
+ status: passed ? "pass" : "fail",
3855
+ summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
3856
+ startedAt,
3857
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3858
+ artifacts,
3859
+ metadata: {
3860
+ baseline_id: args.baseline_id,
3861
+ diff_pct: Number(diffPct.toFixed(6)),
3862
+ diff_pixels: diffPixels,
3863
+ total_pixels: total,
3864
+ threshold_pct: args.threshold_pct
3865
+ }
3866
+ });
3664
3867
  return ok({
3665
3868
  run_id: runId,
3666
3869
  baseline_id: args.baseline_id,
3667
3870
  diff_pct: Number(diffPct.toFixed(6)),
3668
3871
  diff_pixels: diffPixels,
3669
3872
  total_pixels: total,
3670
- passed: diffPct <= args.threshold_pct,
3873
+ passed,
3671
3874
  baseline_path: baselinePath,
3672
3875
  current_path: currentPath,
3673
- diff_image_path: diffImagePath
3876
+ diff_image_path: diffImagePath,
3877
+ ...manifestPath ? { manifest: manifestPath } : {}
3674
3878
  });
3675
3879
  } finally {
3676
3880
  if (args.close_on_finish) {
@@ -3939,8 +4143,19 @@ var toolMetadata = {
3939
4143
 
3940
4144
  // src/server.ts
3941
4145
  var SERVER_NAME = "rolepod-uiproof";
3942
- var SERVER_VERSION = "0.5.0";
4146
+ var SERVER_VERSION = "0.6.0";
4147
+ var SUPPORTED_PROTOCOL = "v1";
4148
+ function checkProtocolCompat() {
4149
+ const requested = process.env.ROLEPOD_PROTOCOL;
4150
+ if (!requested) return;
4151
+ if (requested !== SUPPORTED_PROTOCOL) {
4152
+ console.warn(
4153
+ `rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${requested}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
4154
+ );
4155
+ }
4156
+ }
3943
4157
  function buildServer(opts = {}) {
4158
+ checkProtocolCompat();
3944
4159
  const webEngine = createWebEngine();
3945
4160
  const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
3946
4161
  registry.register("web", webEngine);
@@ -4001,6 +4216,8 @@ function buildServer(opts = {}) {
4001
4216
  }
4002
4217
  log.info("rolepod-uiproof server built", {
4003
4218
  version: SERVER_VERSION,
4219
+ protocol: SUPPORTED_PROTOCOL,
4220
+ mode: store.mode,
4004
4221
  tools: tools.map((t) => t.name)
4005
4222
  });
4006
4223
  return {