rcf-lite 0.16.0 → 0.18.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/CHANGELOG.md +36 -0
- package/blueprints/application-api-rest/README.md +5 -1
- package/blueprints/application-api-rest/blueprint.json +1 -1
- package/blueprints/application-api-rest/contributions/requirements/application-api-rest-req-006.json +4 -4
- package/blueprints/application-api-rest/contributions/tacs/tac-306-application-api-rest-operability.json +9 -8
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2103.json +3 -3
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2108.json +27 -27
- package/blueprints/application-api-rest/docs/topics.md +2 -2
- package/blueprints/application-api-rest/guide/application-api-rest.md +5 -1
- package/blueprints/application-spa/assets/tc-templates/e2e.md +85 -0
- package/blueprints/application-spa/blueprint.json +24 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1134.json +24 -0
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1135.json +24 -0
- package/blueprints/application-spa/docs/topics.md +2 -2
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +69 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +18 -0
- package/blueprints/delivery-ci-workflows/blueprint.json +301 -60
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6124.json +28 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +2 -2
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
- package/blueprints/email-smtp-resend/docs/topics.md +2 -2
- package/blueprints/observability-essentials/README.md +6 -2
- package/blueprints/observability-essentials/blueprint.json +133 -33
- package/blueprints/observability-essentials/contributions/adrs/adr-801-observability-essentials-health-probes.json +4 -4
- package/blueprints/observability-essentials/contributions/adrs/adr-802-observability-essentials-readiness-semantics.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-001.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-002.json +4 -4
- package/blueprints/observability-essentials/contributions/tacs/tac-801-observability-essentials-liveness-probe.json +10 -9
- package/blueprints/observability-essentials/contributions/tacs/tac-802-observability-essentials-readiness-probe.json +17 -11
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7101.json +12 -3
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7102.json +12 -3
- package/blueprints/observability-essentials/docs/topics.md +15 -8
- package/blueprints/observability-essentials/guide/observability-essentials.md +9 -3
- package/blueprints/observability-probe-endpoints/README.md +5 -1
- package/blueprints/observability-probe-endpoints/blueprint.json +116 -24
- package/blueprints/observability-probe-endpoints/contributions/adrs/adr-1503-observability-probe-endpoints-kubernetes-default.json +5 -5
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14102.json +11 -2
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14107.json +11 -2
- package/blueprints/observability-probe-endpoints/docs/topics.md +6 -6
- package/blueprints/observability-probe-endpoints/guide/observability-probe-endpoints.md +10 -0
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-sqlite/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/docs/topics.md +2 -2
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/docs/topics.md +2 -2
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +13 -1
- package/rcf/code-nodes/cn-070.json +12 -0
- package/rcf/code-nodes/cn-071.json +12 -0
- package/rcf/code-nodes/cn-072.json +12 -0
- package/rcf/code-nodes/cn-073.json +12 -0
- package/rcf/code-nodes/cn-074.json +19 -0
- package/rcf/code-nodes/cn-075.json +15 -0
- package/rcf/code-nodes/cn-076.json +14 -0
- package/rcf/fbs/fbs-020.json +18 -0
- package/rcf/fbs/fbs-021.json +18 -0
- package/rcf/fbs/fbs-022.json +18 -0
- package/rcf/fbs/fbs-023.json +18 -0
- package/rcf/fbs/fbs-024.json +24 -0
- package/rcf/fbs/fbs-025.json +25 -0
- package/rcf/fbs/fbs-026.json +27 -0
- package/rcf/fbs/fbs-027.json +25 -0
- package/rcf/prd.json +3 -2
- package/rcf/requirements/req-011.json +22 -0
- package/rcf/requirements/req-012.json +22 -0
- package/rcf/test-suites/ts-030.json +66 -0
- package/rcf/test-suites/ts-031.json +59 -0
- package/rcf/test-suites/ts-032.json +50 -0
- package/rcf/test-suites/ts-033.json +83 -0
- package/rcf/test-suites/ts-034.json +32 -0
- package/rcf/test-suites/ts-035.json +23 -0
- package/rcf/test-suites/ts-036.json +65 -0
- package/rcf/test-suites/ts-037.json +55 -0
- package/rcf/user-stories/us-1101.json +51 -0
- package/rcf/user-stories/us-1102.json +60 -0
- package/rcf/user-stories/us-1103.json +51 -0
- package/rcf/user-stories/us-1104.json +60 -0
- package/rcf/user-stories/us-1201.json +34 -0
- package/rcf/user-stories/us-1202.json +25 -0
- package/rcf/user-stories/us-1203.json +43 -0
- package/rcf/user-stories/us-1204.json +25 -0
- package/releases/releases.yaml +21 -1
- package/src/blueprint/index.js +1 -0
- package/src/blueprint/remove-resolution.js +104 -0
- package/src/blueprint/supersede.js +2 -2
- package/src/cli/blueprint.js +41 -0
- package/src/cli/doctor.js +257 -5
- package/src/cli/init.js +166 -0
- package/src/setup/playwright-checks.js +426 -0
- package/src/verify/cli/run.js +28 -0
- package/src/verify/engine/index.js +24 -4
- package/src/verify/engine/launcher.js +41 -10
package/src/cli/init.js
CHANGED
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
writeAgentInstructions,
|
|
32
32
|
writeMcpConfig,
|
|
33
33
|
} from '../setup/agent-setup.js';
|
|
34
|
+
import {
|
|
35
|
+
findProjectPlaywrightKey,
|
|
36
|
+
probeClaudeCodeMcp,
|
|
37
|
+
} from '../setup/playwright-checks.js';
|
|
38
|
+
import { PLAYWRIGHT_MCP_VERSION } from '../verify/engine/launcher.js';
|
|
34
39
|
import { writeKnowledgeSeed } from '../setup/knowledge-seed.js';
|
|
35
40
|
import { writeIdentityTemplate } from '../setup/identity-seed.js';
|
|
36
41
|
import {
|
|
@@ -46,6 +51,7 @@ const OPTION_SPEC = {
|
|
|
46
51
|
'project-name': { type: 'string' },
|
|
47
52
|
'non-interactive': { type: 'boolean' },
|
|
48
53
|
'no-agent-setup': { type: 'boolean' },
|
|
54
|
+
'no-playwright-mcp': { type: 'boolean' },
|
|
49
55
|
quiet: { type: 'boolean' },
|
|
50
56
|
help: { type: 'boolean' },
|
|
51
57
|
};
|
|
@@ -70,6 +76,11 @@ Options:
|
|
|
70
76
|
not on a TTY or when piped)
|
|
71
77
|
--no-agent-setup Scaffold the tree only; print the manual
|
|
72
78
|
harness-wiring instructions instead
|
|
79
|
+
--no-playwright-mcp Skip the Playwright MCP entry step. The probe
|
|
80
|
+
still runs so the print-out remains honest, but
|
|
81
|
+
init writes no Playwright entry and touches no
|
|
82
|
+
existing one. Use when a user-scope Playwright
|
|
83
|
+
entry is declared in a harness init cannot probe.
|
|
73
84
|
--quiet Suppress non-error stdout
|
|
74
85
|
--help Print this help
|
|
75
86
|
`;
|
|
@@ -172,6 +183,26 @@ export async function main(argv, deps = {}) {
|
|
|
172
183
|
return 2;
|
|
173
184
|
}
|
|
174
185
|
|
|
186
|
+
// Step 1a: Playwright MCP entry (spec 2026-09-03, section 4). Writes a
|
|
187
|
+
// distinctly named 'playwright-rcf' entry ONLY when init can prove no
|
|
188
|
+
// Playwright entry exists at any scope the harness can report. Detection
|
|
189
|
+
// is by command tail (spec 4.1). Cross-scope probe of the Claude Code
|
|
190
|
+
// harness via `claude mcp list` (spec 4.2). Where the harness cannot be
|
|
191
|
+
// probed non-interactively, init falls back to the distinctly-named entry
|
|
192
|
+
// with a notice (spec 4.3). --no-playwright-mcp suppresses the write
|
|
193
|
+
// entirely; the probe still runs so the print-out remains honest.
|
|
194
|
+
const playwrightPass = await runPlaywrightMcpPass({
|
|
195
|
+
projectRoot: cwd,
|
|
196
|
+
stdout,
|
|
197
|
+
quiet: Boolean(flags.quiet),
|
|
198
|
+
optOut: Boolean(flags['no-playwright-mcp']),
|
|
199
|
+
probeClaudeCodeMcp: deps.probeClaudeCodeMcp ?? probeClaudeCodeMcp,
|
|
200
|
+
});
|
|
201
|
+
if (playwrightPass && 'kind' in playwrightPass && 'message' in playwrightPass) {
|
|
202
|
+
stderr.write(`[error] ${playwrightPass.kind} ${playwrightPass.message}\n`);
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
|
|
175
206
|
// Step 2: agent-instructions managed block inside rcf markers (idempotent).
|
|
176
207
|
// Source is the 0.6.0 canonical asset, not the harness-template fenced
|
|
177
208
|
// fragment (which is now regenerated from the same canonical text at
|
|
@@ -297,3 +328,138 @@ function countLocal(haystack, needle) {
|
|
|
297
328
|
i = at + needle.length;
|
|
298
329
|
}
|
|
299
330
|
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Init's Playwright MCP pass (spec 2026-09-03, section 4). Decision tree:
|
|
334
|
+
*
|
|
335
|
+
* 1. If --no-playwright-mcp: probe still runs so the print-out is honest,
|
|
336
|
+
* but no .mcp.json entry is written or touched.
|
|
337
|
+
* 2. If the project-scope .mcp.json carries a Playwright signature under
|
|
338
|
+
* any key (spec 4.1): print the 'left alone' line and return.
|
|
339
|
+
* 3. Probe the Claude Code harness (spec 4.2):
|
|
340
|
+
* - 'found': print the 'already registered at <scope> scope' line and
|
|
341
|
+
* return (no shadowing).
|
|
342
|
+
* - 'none': write the distinctly-named 'playwright-rcf' entry.
|
|
343
|
+
* - 'inconclusive' (claude absent, non-zero, or unparseable): write the
|
|
344
|
+
* distinctly-named 'playwright-rcf' entry with the 'could not probe'
|
|
345
|
+
* notice.
|
|
346
|
+
*
|
|
347
|
+
* A distinct project-scope entry keeps init's "never write outside the
|
|
348
|
+
* project root" discipline; a coexisting user-scope entry the operator has
|
|
349
|
+
* that init could not see is not shadowed by naming convention.
|
|
350
|
+
*
|
|
351
|
+
* @param {object} args
|
|
352
|
+
* @param {string} args.projectRoot
|
|
353
|
+
* @param {NodeJS.WritableStream} args.stdout
|
|
354
|
+
* @param {boolean} args.quiet
|
|
355
|
+
* @param {boolean} args.optOut
|
|
356
|
+
* @param {import('../setup/playwright-checks.js').probeClaudeCodeMcp} args.probeClaudeCodeMcp
|
|
357
|
+
* @returns {Promise<{ action: 'left-alone-project'|'left-alone-harness'|'written'|'skipped-opt-out', name?: string, scope?: string } | import('../core/errors/index.js').RcfError>}
|
|
358
|
+
*/
|
|
359
|
+
async function runPlaywrightMcpPass({ projectRoot, stdout, quiet, optOut, probeClaudeCodeMcp: probeImpl }) {
|
|
360
|
+
const mcpPath = join(projectRoot, '.mcp.json');
|
|
361
|
+
let mcpJson = null;
|
|
362
|
+
try {
|
|
363
|
+
const raw = await readFile(mcpPath, 'utf8');
|
|
364
|
+
try {
|
|
365
|
+
mcpJson = JSON.parse(raw);
|
|
366
|
+
} catch {
|
|
367
|
+
// writeMcpConfig already refused unparseable with a distinct error and
|
|
368
|
+
// returned before us; if we somehow reach here with a corrupt file,
|
|
369
|
+
// stay out and let doctor surface it. Return silently.
|
|
370
|
+
return { action: 'skipped-opt-out' };
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err.code !== 'ENOENT') throw err;
|
|
374
|
+
// .mcp.json does not exist yet (rcf mcp write above would have created
|
|
375
|
+
// it; if that step ran we should not hit ENOENT). Treat as no project
|
|
376
|
+
// entry and continue to the probe.
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const projectKey = mcpJson ? findProjectPlaywrightKey(mcpJson) : null;
|
|
380
|
+
// Gate finding 8 (2026-09-03): when --no-playwright-mcp is set, print the
|
|
381
|
+
// opt-out notice on EVERY terminal branch of this pass, in addition to the
|
|
382
|
+
// honest probe line. The probe still runs (spec 4.4) so the print-out is
|
|
383
|
+
// always honest about what init could see; the notice makes the reason a
|
|
384
|
+
// write did not happen unambiguous on the branches that would otherwise
|
|
385
|
+
// have written.
|
|
386
|
+
const optOutLine = 'Playwright MCP: --no-playwright-mcp set; nothing written.\n';
|
|
387
|
+
|
|
388
|
+
if (projectKey) {
|
|
389
|
+
if (!quiet) {
|
|
390
|
+
stdout.write(
|
|
391
|
+
`Playwright MCP: already registered in .mcp.json under key '${projectKey}' at project scope, left alone.\n`,
|
|
392
|
+
);
|
|
393
|
+
if (optOut) stdout.write(optOutLine);
|
|
394
|
+
}
|
|
395
|
+
return { action: 'left-alone-project', name: projectKey };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Probe the harness even when --no-playwright-mcp so the print-out remains
|
|
399
|
+
// honest about what init can see.
|
|
400
|
+
const probeResult = await probeImpl();
|
|
401
|
+
if (probeResult.kind === 'found') {
|
|
402
|
+
if (!quiet) {
|
|
403
|
+
stdout.write(
|
|
404
|
+
`Playwright MCP: already registered under key '${probeResult.name}' at ${probeResult.scope} scope in Claude Code, left alone.\n`,
|
|
405
|
+
);
|
|
406
|
+
if (optOut) stdout.write(optOutLine);
|
|
407
|
+
}
|
|
408
|
+
return { action: 'left-alone-harness', name: probeResult.name, scope: probeResult.scope };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (optOut) {
|
|
412
|
+
if (!quiet) {
|
|
413
|
+
// The honest probe line first, then the opt-out notice.
|
|
414
|
+
if (probeResult.kind === 'inconclusive') {
|
|
415
|
+
stdout.write(
|
|
416
|
+
`Playwright MCP: could not probe user-scope entries (${probeResult.reason}).\n`,
|
|
417
|
+
);
|
|
418
|
+
} else {
|
|
419
|
+
stdout.write(
|
|
420
|
+
'Playwright MCP: no ambient Playwright entry found at any scope this init could probe.\n',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
stdout.write(optOutLine);
|
|
424
|
+
}
|
|
425
|
+
return { action: 'skipped-opt-out' };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Compose the distinctly-named entry. Distinct from 'playwright' (the
|
|
429
|
+
// common name a user-scope entry uses) and from any name Claude Code
|
|
430
|
+
// writes by default. If init could not see a user-scope entry that
|
|
431
|
+
// actually exists, both coexist and no shadowing has occurred; verify
|
|
432
|
+
// provisions its OWN MCP config anyway.
|
|
433
|
+
const distinctEntry = {
|
|
434
|
+
type: 'stdio',
|
|
435
|
+
command: 'npx',
|
|
436
|
+
args: ['-y', `@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}`],
|
|
437
|
+
env: {},
|
|
438
|
+
};
|
|
439
|
+
const nextBody = mcpJson ?? {};
|
|
440
|
+
const servers = (nextBody.mcpServers && typeof nextBody.mcpServers === 'object' && !Array.isArray(nextBody.mcpServers))
|
|
441
|
+
? nextBody.mcpServers
|
|
442
|
+
: {};
|
|
443
|
+
const nextConfig = {
|
|
444
|
+
...nextBody,
|
|
445
|
+
mcpServers: {
|
|
446
|
+
...servers,
|
|
447
|
+
'playwright-rcf': distinctEntry,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
await writeFile(mcpPath, `${JSON.stringify(nextConfig, null, 2)}\n`, 'utf8');
|
|
451
|
+
|
|
452
|
+
if (!quiet) {
|
|
453
|
+
if (probeResult.kind === 'inconclusive') {
|
|
454
|
+
stdout.write(
|
|
455
|
+
`Playwright MCP: could not probe user-scope entries (${probeResult.reason}). Wrote 'playwright-rcf' at project scope; remove with \`rcf init --no-playwright-mcp\` or delete the entry by hand.\n`,
|
|
456
|
+
);
|
|
457
|
+
} else {
|
|
458
|
+
stdout.write(
|
|
459
|
+
"Playwright MCP: wrote 'playwright-rcf' at project scope (no ambient Playwright entry found at any scope this init could probe). Remove with `rcf init --no-playwright-mcp` or delete the entry by hand.\n",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { action: 'written' };
|
|
464
|
+
}
|
|
465
|
+
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// Doctor's browser-facing project checks (spec 2026-09-03, section 3) plus
|
|
2
|
+
// init-time Playwright signature detection and the Claude Code cross-scope
|
|
3
|
+
// probe (spec sections 4.1-4.3). All probes are injectable via a `deps`
|
|
4
|
+
// argument so the unit suite runs with none of these tools installed.
|
|
5
|
+
//
|
|
6
|
+
// A project is browser-facing exactly when its rcf/manifest.json carries at
|
|
7
|
+
// least one blueprint in manifest.blueprints[] whose loaded blueprint.json
|
|
8
|
+
// declares `browserSurface`. Detection is a manifest fact, not string grammar:
|
|
9
|
+
// doctor loads the manifest, walks manifest.blueprints[].source, reads each
|
|
10
|
+
// blueprint.json, and evaluates the flag. Blueprints not shipping the field
|
|
11
|
+
// are treated as not-browser-facing, and no per-slug allowlist exists.
|
|
12
|
+
//
|
|
13
|
+
// The 15-second cap on the reachability probe is a diagnostic ceiling; on a
|
|
14
|
+
// warm cache the probe returns in well under a second, and the ceiling only
|
|
15
|
+
// matters where npx would be doing a fresh network fetch it should not be
|
|
16
|
+
// doing on a diagnostic run.
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { basename, isAbsolute, join, resolve as pathResolve } from 'node:path';
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
import { PLAYWRIGHT_MCP_VERSION } from '../verify/engine/launcher.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The three fix lines emitted per check, verbatim from the spec's fix-line
|
|
29
|
+
* table. Tests assert the strings; changing them requires a spec amendment.
|
|
30
|
+
*/
|
|
31
|
+
export const FIX_LINES = Object.freeze({
|
|
32
|
+
'playwright-present':
|
|
33
|
+
'Install the peer dependency: npm i -D playwright@^1.50.0 (or the pnpm/yarn equivalent for your project).',
|
|
34
|
+
'browser-present':
|
|
35
|
+
'Install a browser: npx @playwright/mcp install-browser chromium (Playwright-managed) or install system Google Chrome (used by @playwright/mcp by default).',
|
|
36
|
+
'playwright-mcp-reachable':
|
|
37
|
+
`Install @playwright/mcp: npm i -D @playwright/mcp@${PLAYWRIGHT_MCP_VERSION} (the pinned version rcf verify runs).`,
|
|
38
|
+
'playwright-mcp-redundant':
|
|
39
|
+
"project-scope .mcp.json carries a Playwright MCP entry that is also declared at user scope. The project entry shadows the user entry. Remove the project entry with `rcf init --no-playwright-mcp` (which re-runs init without writing it), or delete the entry from .mcp.json by hand.",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The single skip line for non-browser-facing projects. Not suppressed by
|
|
44
|
+
* --quiet (spec 3.4).
|
|
45
|
+
*/
|
|
46
|
+
export const SKIP_LINE_NON_BROWSER_FACING =
|
|
47
|
+
'rcf doctor: skipping playwright-present, browser-present, playwright-mcp-reachable (no applied blueprint declares a browser surface).';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Is the manifest a browser-facing project per spec 3.1?
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectRoot
|
|
53
|
+
* @param {object} [deps]
|
|
54
|
+
* @param {(source: string) => Promise<object|null>} [deps.readBlueprintManifest] - test seam
|
|
55
|
+
* @returns {Promise<{ browserFacing: boolean, sources: string[] }>}
|
|
56
|
+
*/
|
|
57
|
+
export async function loadBrowserFacingSources(projectRoot, deps = {}) {
|
|
58
|
+
const readManifest = deps.readBlueprintManifest ?? defaultReadBlueprintManifest;
|
|
59
|
+
const rootManifestPath = join(projectRoot, 'rcf', 'manifest.json');
|
|
60
|
+
let manifest = null;
|
|
61
|
+
try {
|
|
62
|
+
manifest = JSON.parse(await readFile(rootManifestPath, 'utf8'));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.code === 'ENOENT') return { browserFacing: false, sources: [] };
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const applied = Array.isArray(manifest?.blueprints) ? manifest.blueprints : [];
|
|
68
|
+
const sources = [];
|
|
69
|
+
for (const record of applied) {
|
|
70
|
+
if (!record || typeof record.source !== 'string') continue;
|
|
71
|
+
const resolved = isAbsolute(record.source)
|
|
72
|
+
? record.source
|
|
73
|
+
: pathResolve(projectRoot, record.source);
|
|
74
|
+
let bp;
|
|
75
|
+
try {
|
|
76
|
+
bp = await readManifest(resolved);
|
|
77
|
+
} catch {
|
|
78
|
+
bp = null;
|
|
79
|
+
}
|
|
80
|
+
if (bp && bp.browserSurface && bp.browserSurface.declared === true) {
|
|
81
|
+
sources.push(resolved);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { browserFacing: sources.length > 0, sources };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function defaultReadBlueprintManifest(source) {
|
|
88
|
+
const metaPath = join(source, 'blueprint.json');
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(await readFile(metaPath, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check that `playwright` is resolvable from the project root.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectRoot
|
|
100
|
+
* @returns {{ ok: boolean, resolvedFrom: (string|null) }}
|
|
101
|
+
*/
|
|
102
|
+
export function checkPlaywrightPresent(projectRoot) {
|
|
103
|
+
const req = createRequire(join(projectRoot, '__rcf-require-anchor__'));
|
|
104
|
+
try {
|
|
105
|
+
const resolvedFrom = req.resolve('playwright');
|
|
106
|
+
return { ok: true, resolvedFrom };
|
|
107
|
+
} catch {
|
|
108
|
+
return { ok: false, resolvedFrom: null };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check that some Chromium is available to @playwright/mcp: system Chrome on
|
|
114
|
+
* PATH first, then a Playwright-managed cache directory, then the tool's own
|
|
115
|
+
* first-party status probe.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} [deps]
|
|
118
|
+
* @param {(name: string) => boolean} [deps.pathHas]
|
|
119
|
+
* @param {(path: string) => boolean} [deps.dirExists]
|
|
120
|
+
* @param {() => Promise<{ ok: boolean, source: string }>} [deps.probeMcpBrowserStatus]
|
|
121
|
+
* @param {() => string} [deps.homedir]
|
|
122
|
+
* @returns {Promise<{ ok: boolean, source: (string|null) }>}
|
|
123
|
+
*/
|
|
124
|
+
export async function checkBrowserPresent(deps = {}) {
|
|
125
|
+
const pathHas = deps.pathHas ?? defaultPathHas;
|
|
126
|
+
const dirExists = deps.dirExists ?? existsSync;
|
|
127
|
+
const home = deps.homedir ?? homedir;
|
|
128
|
+
for (const bin of ['google-chrome-stable', 'chromium', 'chrome', 'Google Chrome']) {
|
|
129
|
+
if (pathHas(bin)) return { ok: true, source: `path:${bin}` };
|
|
130
|
+
}
|
|
131
|
+
const playwrightCache = join(home(), '.cache', 'ms-playwright');
|
|
132
|
+
if (dirExists(playwrightCache)) {
|
|
133
|
+
try {
|
|
134
|
+
const entries = await readdir(playwrightCache);
|
|
135
|
+
if (entries.some((e) => /^chromium/i.test(e))) {
|
|
136
|
+
return { ok: true, source: `cache:${playwrightCache}` };
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// fall through to first-party probe
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const probe = deps.probeMcpBrowserStatus ?? defaultProbeMcpBrowserStatus;
|
|
143
|
+
try {
|
|
144
|
+
const res = await probe();
|
|
145
|
+
if (res && res.ok) return { ok: true, source: `mcp:${res.source ?? 'ok'}` };
|
|
146
|
+
} catch {
|
|
147
|
+
// treat any probe failure as no browser
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, source: null };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function defaultPathHas(binName) {
|
|
153
|
+
const path = process.env.PATH ?? '';
|
|
154
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
155
|
+
for (const dir of path.split(sep)) {
|
|
156
|
+
if (!dir) continue;
|
|
157
|
+
try {
|
|
158
|
+
const p = join(dir, binName);
|
|
159
|
+
if (existsSync(p)) return true;
|
|
160
|
+
} catch {
|
|
161
|
+
/* ignore */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function defaultProbeMcpBrowserStatus() {
|
|
168
|
+
return runNpxProbe(['--no-install', '@playwright/mcp', 'browser-status', '--json'], 15000)
|
|
169
|
+
.then((r) => (r.exitCode === 0 ? { ok: true, source: 'browser-status' } : { ok: false }))
|
|
170
|
+
.catch(() => ({ ok: false }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check that `npx --no-install @playwright/mcp --help` runs and exits 0 in
|
|
175
|
+
* under 15 seconds. `--no-install` so the check does not silently pull the
|
|
176
|
+
* package during a diagnostic run.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} [deps]
|
|
179
|
+
* @param {(args: string[], timeoutMs: number) => Promise<{ exitCode: number, timedOut: boolean }>} [deps.runNpx]
|
|
180
|
+
* @param {number} [deps.timeoutMs]
|
|
181
|
+
* @returns {Promise<{ ok: boolean, exitCode: (number|null), timedOut: boolean }>}
|
|
182
|
+
*/
|
|
183
|
+
export async function checkPlaywrightMcpReachable(deps = {}) {
|
|
184
|
+
const runNpx = deps.runNpx ?? runNpxProbe;
|
|
185
|
+
const timeoutMs = deps.timeoutMs ?? 15000;
|
|
186
|
+
try {
|
|
187
|
+
const res = await runNpx(['--no-install', '@playwright/mcp', '--help'], timeoutMs);
|
|
188
|
+
return {
|
|
189
|
+
ok: res.exitCode === 0,
|
|
190
|
+
exitCode: res.exitCode ?? null,
|
|
191
|
+
timedOut: Boolean(res.timedOut),
|
|
192
|
+
};
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return { ok: false, exitCode: null, timedOut: false, error: err.message };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function runNpxProbe(args, timeoutMs) {
|
|
199
|
+
return new Promise((resolvePromise) => {
|
|
200
|
+
let child;
|
|
201
|
+
try {
|
|
202
|
+
child = spawn('npx', args, { stdio: 'ignore' });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
resolvePromise({ exitCode: null, timedOut: false, error: err.message });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
let done = false;
|
|
208
|
+
const timer = setTimeout(() => {
|
|
209
|
+
done = true;
|
|
210
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
211
|
+
resolvePromise({ exitCode: null, timedOut: true });
|
|
212
|
+
}, timeoutMs);
|
|
213
|
+
child.on('close', (code) => {
|
|
214
|
+
if (done) return;
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
done = true;
|
|
217
|
+
resolvePromise({ exitCode: code ?? null, timedOut: false });
|
|
218
|
+
});
|
|
219
|
+
child.on('error', (err) => {
|
|
220
|
+
if (done) return;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
done = true;
|
|
223
|
+
resolvePromise({ exitCode: null, timedOut: false, error: err.message });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ================================================================== */
|
|
229
|
+
/* Playwright signature detection (spec 4.1) + probe (spec 4.2/4.3). */
|
|
230
|
+
/* ================================================================== */
|
|
231
|
+
|
|
232
|
+
/** Regex naming a @playwright/mcp args token (any pinning suffix accepted). */
|
|
233
|
+
const PLAYWRIGHT_MCP_TOKEN = /^@playwright\/mcp(?:@|$)/;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* True iff a parsed mcpServers[<name>] entry has a Playwright signature by
|
|
237
|
+
* command tail: any `args` string matching /^@playwright\/mcp(@|$)/, or a
|
|
238
|
+
* `command` path whose basename is `mcp` and lives under a @playwright/mcp
|
|
239
|
+
* package directory.
|
|
240
|
+
*
|
|
241
|
+
* @param {unknown} entry
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
export function hasPlaywrightSignature(entry) {
|
|
245
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
246
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
247
|
+
if (args.some((a) => typeof a === 'string' && PLAYWRIGHT_MCP_TOKEN.test(a))) return true;
|
|
248
|
+
if (typeof entry.command === 'string') {
|
|
249
|
+
const cmd = entry.command;
|
|
250
|
+
if (basename(cmd) === 'mcp' && /@playwright\/mcp/.test(cmd)) return true;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Look up the first project-scope entry key carrying a Playwright signature
|
|
257
|
+
* in a parsed .mcp.json body. Returns null when no entry matches.
|
|
258
|
+
*
|
|
259
|
+
* @param {object|null|undefined} mcpJson - the parsed .mcp.json body
|
|
260
|
+
* @returns {string|null}
|
|
261
|
+
*/
|
|
262
|
+
export function findProjectPlaywrightKey(mcpJson) {
|
|
263
|
+
const servers = mcpJson?.mcpServers;
|
|
264
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) return null;
|
|
265
|
+
for (const [key, entry] of Object.entries(servers)) {
|
|
266
|
+
if (hasPlaywrightSignature(entry)) return key;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Probe the Claude Code harness for a Playwright entry at any scope by
|
|
273
|
+
* shelling out to `claude mcp list` and parsing its text output. `list`
|
|
274
|
+
* itself does not name the scope column (as of Claude Code 1.x); when the
|
|
275
|
+
* list surfaces a Playwright entry, follow up with `claude mcp get <name>`
|
|
276
|
+
* to read the scope. Result shapes:
|
|
277
|
+
*
|
|
278
|
+
* - { kind: 'found', name, scope } - list matched signature and get named scope
|
|
279
|
+
* - { kind: 'none' } - claude ran cleanly and reported no Playwright entry
|
|
280
|
+
* - { kind: 'inconclusive', reason } - probe could not prove either way
|
|
281
|
+
*
|
|
282
|
+
* @param {object} [deps]
|
|
283
|
+
* @param {(cmd: string, args: string[], timeoutMs: number) => Promise<{ exitCode: number, stdout: string, timedOut: boolean, error?: string }>} [deps.runProbe]
|
|
284
|
+
* @param {number} [deps.timeoutMs]
|
|
285
|
+
* @returns {Promise<{ kind: 'found', name: string, scope: string }
|
|
286
|
+
* | { kind: 'none' } | { kind: 'inconclusive', reason: string }>}
|
|
287
|
+
*/
|
|
288
|
+
export async function probeClaudeCodeMcp(deps = {}) {
|
|
289
|
+
const run = deps.runProbe ?? defaultRunProbeText;
|
|
290
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
291
|
+
let res;
|
|
292
|
+
try {
|
|
293
|
+
res = await run('claude', ['mcp', 'list'], timeoutMs);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return { kind: 'inconclusive', reason: `claude probe threw: ${err.message}` };
|
|
296
|
+
}
|
|
297
|
+
if (res.timedOut) return { kind: 'inconclusive', reason: 'claude mcp list timed out' };
|
|
298
|
+
if (res.error) return { kind: 'inconclusive', reason: `claude probe error: ${res.error}` };
|
|
299
|
+
if (res.exitCode !== 0) return { kind: 'inconclusive', reason: `claude mcp list exited ${res.exitCode}` };
|
|
300
|
+
const parsed = parseClaudeMcpListOutput(res.stdout ?? '');
|
|
301
|
+
if (parsed.kind === 'unparseable') {
|
|
302
|
+
return { kind: 'inconclusive', reason: 'claude mcp list output did not parse' };
|
|
303
|
+
}
|
|
304
|
+
if (parsed.kind === 'none') return { kind: 'none' };
|
|
305
|
+
// parsed.kind === 'found': list has a Playwright line but the shipping
|
|
306
|
+
// `claude mcp list` format does not carry an explicit scope column. Ask
|
|
307
|
+
// for the per-server detail so we can name the scope on the print-out.
|
|
308
|
+
let scope = parsed.scope;
|
|
309
|
+
if (!scope || scope === 'unknown') {
|
|
310
|
+
try {
|
|
311
|
+
const detail = await run('claude', ['mcp', 'get', parsed.name], timeoutMs);
|
|
312
|
+
if (detail.exitCode === 0 && typeof detail.stdout === 'string') {
|
|
313
|
+
scope = parseClaudeMcpGetScope(detail.stdout) ?? 'unknown';
|
|
314
|
+
} else {
|
|
315
|
+
scope = 'unknown';
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
scope = 'unknown';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { kind: 'found', name: parsed.name, scope };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Read the Scope line out of `claude mcp get <name>` output. Format observed
|
|
326
|
+
* on Claude Code 1.x: ` Scope: User config (available in all your projects)`
|
|
327
|
+
* for user, ` Scope: Local config` / ` Scope: Project config` otherwise.
|
|
328
|
+
* Returns 'user' | 'project' | 'local', or null when the format is not
|
|
329
|
+
* recognised (the caller then reports scope as 'unknown').
|
|
330
|
+
*
|
|
331
|
+
* @param {string} text
|
|
332
|
+
* @returns {'user'|'project'|'local'|null}
|
|
333
|
+
*/
|
|
334
|
+
export function parseClaudeMcpGetScope(text) {
|
|
335
|
+
if (typeof text !== 'string') return null;
|
|
336
|
+
const m = text.match(/Scope:\s*(User|Project|Local)/i);
|
|
337
|
+
if (!m) return null;
|
|
338
|
+
return m[1].toLowerCase();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Parse `claude mcp list` text output for a Playwright signature. Claude
|
|
343
|
+
* Code 1.x prints each server on its own line as
|
|
344
|
+
* `<name>: <command line> - <status>`; there is no scope column on the
|
|
345
|
+
* list output (per-server scope is available via `claude mcp get <name>`),
|
|
346
|
+
* so this parser returns the name and leaves the scope as 'unknown' for
|
|
347
|
+
* the caller to resolve if it wants one.
|
|
348
|
+
*
|
|
349
|
+
* Accepted line shapes:
|
|
350
|
+
* - `<name>: <command line> - <status>` (Claude Code 1.x)
|
|
351
|
+
* - `<name> <scope> <command line>` (older columnar form, kept for
|
|
352
|
+
* tolerance)
|
|
353
|
+
*
|
|
354
|
+
* @param {string} text
|
|
355
|
+
* @returns {{ kind: 'found', name: string, scope: string } | { kind: 'none' } | { kind: 'unparseable' }}
|
|
356
|
+
*/
|
|
357
|
+
export function parseClaudeMcpListOutput(text) {
|
|
358
|
+
if (typeof text !== 'string') return { kind: 'unparseable' };
|
|
359
|
+
const lines = text.split('\n');
|
|
360
|
+
let sawAnyEntry = false;
|
|
361
|
+
for (const raw of lines) {
|
|
362
|
+
const line = raw.trim();
|
|
363
|
+
if (line.length === 0) continue;
|
|
364
|
+
if (/^(No |No MCP servers|Usage:|Error:|Checking |name\b)/i.test(line)) continue;
|
|
365
|
+
// Recognise a non-Playwright entry so we can distinguish "parseable but
|
|
366
|
+
// no Playwright" from "unparseable output".
|
|
367
|
+
if (/^[A-Za-z0-9_.\/@-]+:\s+/.test(line) || /\b(user|project|local)\b/.test(line)) {
|
|
368
|
+
sawAnyEntry = true;
|
|
369
|
+
}
|
|
370
|
+
if (!/@playwright\/mcp/.test(line)) continue;
|
|
371
|
+
|
|
372
|
+
// Extract the name. Both accepted shapes start with the name; the
|
|
373
|
+
// 1.x shape ends the name with a colon, the older shape is space-
|
|
374
|
+
// separated.
|
|
375
|
+
let name;
|
|
376
|
+
const colonForm = line.match(/^([A-Za-z0-9_.\/@-]+):\s+/);
|
|
377
|
+
if (colonForm) {
|
|
378
|
+
name = colonForm[1];
|
|
379
|
+
} else {
|
|
380
|
+
name = line.split(/\s+/)[0];
|
|
381
|
+
}
|
|
382
|
+
if (!name) continue;
|
|
383
|
+
// Older columnar shape may name the scope inline; keep that when present.
|
|
384
|
+
const scopeMatch = line.match(/\b(user|project|local)\b/);
|
|
385
|
+
const scope = scopeMatch ? scopeMatch[1] : 'unknown';
|
|
386
|
+
return { kind: 'found', name, scope };
|
|
387
|
+
}
|
|
388
|
+
if (sawAnyEntry) return { kind: 'none' };
|
|
389
|
+
if (lines.every((l) => l.trim().length === 0)) return { kind: 'none' };
|
|
390
|
+
return { kind: 'none' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function defaultRunProbeText(cmd, args, timeoutMs) {
|
|
394
|
+
return new Promise((resolvePromise) => {
|
|
395
|
+
let child;
|
|
396
|
+
try {
|
|
397
|
+
child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
398
|
+
} catch (err) {
|
|
399
|
+
resolvePromise({ exitCode: null, stdout: '', timedOut: false, error: err.message });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
let stdout = '';
|
|
403
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
404
|
+
let done = false;
|
|
405
|
+
const timer = setTimeout(() => {
|
|
406
|
+
done = true;
|
|
407
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
408
|
+
resolvePromise({ exitCode: null, stdout, timedOut: true });
|
|
409
|
+
}, timeoutMs);
|
|
410
|
+
child.on('close', (code) => {
|
|
411
|
+
if (done) return;
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
done = true;
|
|
414
|
+
resolvePromise({ exitCode: code ?? null, stdout, timedOut: false });
|
|
415
|
+
});
|
|
416
|
+
child.on('error', (err) => {
|
|
417
|
+
if (done) return;
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
done = true;
|
|
420
|
+
resolvePromise({ exitCode: null, stdout, timedOut: false, error: err.message });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Silence unused import warnings for values consumed only via names.
|
|
426
|
+
void stat;
|
package/src/verify/cli/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import { parseArgs } from 'node:util';
|
|
|
12
12
|
import { formatError, isRcfError } from '#core/errors';
|
|
13
13
|
|
|
14
14
|
import { runVerification } from '../engine/index.js';
|
|
15
|
+
import { isSemverString, PLAYWRIGHT_MCP_VERSION } from '../engine/launcher.js';
|
|
15
16
|
import { serialiseReport } from '../report/index.js';
|
|
16
17
|
import { gateTripped, FINDING_SEVERITIES } from '../verdict/index.js';
|
|
17
18
|
|
|
@@ -26,6 +27,7 @@ const OPTION_SPEC = {
|
|
|
26
27
|
'severity-gate': { type: 'string' },
|
|
27
28
|
'provision-mode': { type: 'string' },
|
|
28
29
|
persona: { type: 'string' },
|
|
30
|
+
'playwright-mcp-version': { type: 'string' },
|
|
29
31
|
help: { type: 'boolean' },
|
|
30
32
|
};
|
|
31
33
|
|
|
@@ -51,6 +53,12 @@ Optional:
|
|
|
51
53
|
PASS | COSMETIC | DEGRADED | BROKEN
|
|
52
54
|
--provision-mode <m> run | skip (default: run)
|
|
53
55
|
--persona <name> Adversarial persona flavour (default: generic-sceptic)
|
|
56
|
+
--playwright-mcp-version <semver>
|
|
57
|
+
Override the pinned Playwright MCP version for
|
|
58
|
+
one run (emergency use only; a loud stderr notice
|
|
59
|
+
fires and the report records the overridden pin
|
|
60
|
+
as runStats.playwrightMcpVersion). Must be an
|
|
61
|
+
exact semver X.Y.Z.
|
|
54
62
|
--help Print this help
|
|
55
63
|
|
|
56
64
|
Exit codes:
|
|
@@ -119,6 +127,25 @@ export async function main(argv, deps = {}) {
|
|
|
119
127
|
return 2;
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
// --playwright-mcp-version override (spec 2026-09-03, section 1.4). Semver
|
|
131
|
+
// only; anything else refuses exit 2 with the spec-named message. When set,
|
|
132
|
+
// the effective pin fires a loud stderr override notice on preflight; when
|
|
133
|
+
// unset, preflight prints the pinned default.
|
|
134
|
+
const overrideRaw = flags['playwright-mcp-version'];
|
|
135
|
+
if (overrideRaw !== undefined && !isSemverString(overrideRaw)) {
|
|
136
|
+
// Spec sections 1.4 and 6 name the message without a prefix. The
|
|
137
|
+
// module's other refusals prefix with `[error] usage`, but this one
|
|
138
|
+
// is the spec-verbatim line and stands on its own.
|
|
139
|
+
stderr.write(`--playwright-mcp-version expects a semver string, got '${overrideRaw}'\n`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
if (overrideRaw !== undefined) {
|
|
143
|
+
// Loud override notice on stderr (spec 1.4: not silenceable with quiet).
|
|
144
|
+
stderr.write(`Playwright MCP: OVERRIDE @playwright/mcp@${overrideRaw} (pinned default: ${PLAYWRIGHT_MCP_VERSION})\n`);
|
|
145
|
+
} else {
|
|
146
|
+
stderr.write(`Playwright MCP: pinned to @playwright/mcp@${PLAYWRIGHT_MCP_VERSION}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
const result = await runVerification({
|
|
123
150
|
repo: flags.repo,
|
|
124
151
|
chainRef: flags.chain,
|
|
@@ -129,6 +156,7 @@ export async function main(argv, deps = {}) {
|
|
|
129
156
|
provisionMode,
|
|
130
157
|
persona: flags.persona,
|
|
131
158
|
severityGate: gate,
|
|
159
|
+
playwrightMcpVersion: overrideRaw,
|
|
132
160
|
}, deps);
|
|
133
161
|
|
|
134
162
|
if (isRcfError(result)) {
|