rcf-lite 0.16.0 → 0.17.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/blueprints/application-spa/assets/tc-templates/e2e.md +85 -0
  3. package/blueprints/application-spa/blueprint.json +24 -2
  4. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1134.json +24 -0
  5. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1135.json +24 -0
  6. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +69 -0
  7. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +18 -0
  8. package/blueprints/delivery-ci-workflows/blueprint.json +301 -60
  9. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6124.json +28 -0
  10. package/fixtures/canary-manifest.json +9 -9
  11. package/package.json +13 -1
  12. package/rcf/code-nodes/cn-070.json +12 -0
  13. package/rcf/code-nodes/cn-071.json +12 -0
  14. package/rcf/code-nodes/cn-072.json +12 -0
  15. package/rcf/code-nodes/cn-073.json +12 -0
  16. package/rcf/fbs/fbs-020.json +18 -0
  17. package/rcf/fbs/fbs-021.json +18 -0
  18. package/rcf/fbs/fbs-022.json +18 -0
  19. package/rcf/fbs/fbs-023.json +18 -0
  20. package/rcf/prd.json +3 -2
  21. package/rcf/requirements/req-011.json +22 -0
  22. package/rcf/test-suites/ts-030.json +66 -0
  23. package/rcf/test-suites/ts-031.json +59 -0
  24. package/rcf/test-suites/ts-032.json +50 -0
  25. package/rcf/test-suites/ts-033.json +83 -0
  26. package/rcf/user-stories/us-1101.json +51 -0
  27. package/rcf/user-stories/us-1102.json +60 -0
  28. package/rcf/user-stories/us-1103.json +51 -0
  29. package/rcf/user-stories/us-1104.json +60 -0
  30. package/releases/releases.yaml +11 -1
  31. package/src/blueprint/supersede.js +2 -2
  32. package/src/cli/doctor.js +182 -5
  33. package/src/cli/init.js +166 -0
  34. package/src/setup/playwright-checks.js +426 -0
  35. package/src/verify/cli/run.js +28 -0
  36. package/src/verify/engine/index.js +24 -4
  37. package/src/verify/engine/launcher.js +41 -10
package/src/cli/doctor.js CHANGED
@@ -47,6 +47,16 @@ import {
47
47
  } from '../setup/managed-gitignore.js';
48
48
  import { identityProfilePath } from '../setup/identity-seed.js';
49
49
  import { knowledgePaths } from '../setup/knowledge-seed.js';
50
+ import {
51
+ checkBrowserPresent,
52
+ checkPlaywrightMcpReachable,
53
+ checkPlaywrightPresent,
54
+ findProjectPlaywrightKey,
55
+ FIX_LINES,
56
+ loadBrowserFacingSources,
57
+ probeClaudeCodeMcp,
58
+ SKIP_LINE_NON_BROWSER_FACING,
59
+ } from '../setup/playwright-checks.js';
50
60
 
51
61
  const OPTION_SPEC = {
52
62
  fix: { type: 'boolean' },
@@ -57,7 +67,24 @@ const OPTION_SPEC = {
57
67
  help: { type: 'boolean' },
58
68
  };
59
69
 
60
- const KNOWN_CHECKS = /** @type {const} */ (['agent-instructions', 'gitignore', 'knowledge', 'identity']);
70
+ const KNOWN_CHECKS = /** @type {const} */ ([
71
+ 'agent-instructions',
72
+ 'gitignore',
73
+ 'knowledge',
74
+ 'identity',
75
+ 'playwright-present',
76
+ 'browser-present',
77
+ 'playwright-mcp-reachable',
78
+ 'playwright-mcp-redundant',
79
+ ]);
80
+
81
+ /** The four Playwright-related checks doctor runs conditionally for
82
+ * browser-facing projects (spec 2026-09-03, section 3). */
83
+ const PLAYWRIGHT_CHECKS = /** @type {const} */ ([
84
+ 'playwright-present',
85
+ 'browser-present',
86
+ 'playwright-mcp-reachable',
87
+ ]);
61
88
 
62
89
  export const HELP = `Usage: rcf doctor [--fix] [--check <check>[,check]] [--json] [--quiet] [--help]
63
90
 
@@ -73,8 +100,10 @@ Options:
73
100
  removing the corrupted region.
74
101
  --check <check>[,check] Run only the named checks. Default: all.
75
102
  Values: agent-instructions, gitignore,
76
- knowledge, identity.
77
- --json Emit machine-readable envelope: { ok, drift[] }.
103
+ knowledge, identity, playwright-present,
104
+ browser-present, playwright-mcp-reachable,
105
+ playwright-mcp-redundant.
106
+ --json Emit machine-readable envelope: { ok, drift, writes, notices }.
78
107
  --quiet Only summary line + first 3 drift items.
79
108
  --force Accept a legacy-markers --fix on hand-edited
80
109
  content that a non-interactive run would
@@ -155,12 +184,48 @@ export async function main(argv, deps = {}) {
155
184
  force: Boolean(flags.force),
156
185
  // isTty is deps-injectable for tests; falls back to real stdout.
157
186
  isTty: deps.isTty ?? Boolean(stdout.isTTY),
187
+ // Doctor's Playwright-check seams (spec 2026-09-03, section 3). Every
188
+ // probe is injectable so the unit suite runs with none of these tools
189
+ // installed on the runner.
190
+ checkPlaywrightPresentImpl: deps.checkPlaywrightPresent ?? checkPlaywrightPresent,
191
+ checkBrowserPresentImpl: deps.checkBrowserPresent ?? checkBrowserPresent,
192
+ checkPlaywrightMcpReachableImpl:
193
+ deps.checkPlaywrightMcpReachable ?? checkPlaywrightMcpReachable,
194
+ probeClaudeCodeMcpImpl: deps.probeClaudeCodeMcp ?? probeClaudeCodeMcp,
195
+ loadBrowserFacingSourcesImpl:
196
+ deps.loadBrowserFacingSources ?? loadBrowserFacingSources,
197
+ readMcpJsonImpl: deps.readMcpJson ?? defaultReadMcpJson,
158
198
  };
159
199
 
200
+ // Section 3.1: browser-facing projection. Computed once; every Playwright
201
+ // check reads the result from ctx rather than re-walking the manifest.
202
+ const browserFacingResult = await ctx.loadBrowserFacingSourcesImpl(cwd);
203
+ ctx.browserFacing = Boolean(browserFacingResult.browserFacing);
204
+ ctx.browserFacingSources = browserFacingResult.sources ?? [];
205
+
160
206
  /** @type {Array<{check: string, item: string, file: string, message: string, refusedByFix: boolean}>} */
161
207
  const drift = [];
162
208
  /** @type {Array<{file: string, action: string}>} */
163
209
  const writes = [];
210
+ /** @type {string[]} */
211
+ const notices = [];
212
+
213
+ // Section 3.4: skip line for the three Playwright checks on non-browser-
214
+ // facing projects. Emitted when at least one of the three is enabled but
215
+ // the project is not browser-facing AND the operator did not explicitly ask
216
+ // for that check by name (spec 3.5). We honour the --check filter by
217
+ // detecting whether the operator explicitly named any playwright check.
218
+ const explicitlyPickedPlaywrightChecks = new Set(
219
+ enabled.filter((c) => PLAYWRIGHT_CHECKS.includes(c)),
220
+ );
221
+ const anyPlaywrightCheckEnabled = explicitlyPickedPlaywrightChecks.size > 0;
222
+ const operatorAskedByName =
223
+ typeof flags.check === 'string'
224
+ && flags.check.length > 0
225
+ && explicitlyPickedPlaywrightChecks.size > 0;
226
+ if (anyPlaywrightCheckEnabled && !ctx.browserFacing && !operatorAskedByName) {
227
+ notices.push(SKIP_LINE_NON_BROWSER_FACING);
228
+ }
164
229
 
165
230
  for (const check of enabled) {
166
231
  let result;
@@ -168,7 +233,21 @@ export async function main(argv, deps = {}) {
168
233
  else if (check === 'gitignore') result = await runGitignoreCheck(ctx);
169
234
  else if (check === 'knowledge') result = await runKnowledgeCheck(ctx);
170
235
  else if (check === 'identity') result = await runIdentityCheck(ctx);
171
- else continue;
236
+ else if (PLAYWRIGHT_CHECKS.includes(check)) {
237
+ // Skip the check on non-browser-facing projects unless the operator
238
+ // explicitly asked for this check by name (--check filter): the skip
239
+ // line above is the ground-truth diagnostic in that case (spec 3.4/3.5).
240
+ if (!ctx.browserFacing && !operatorAskedByName) continue;
241
+ if (check === 'playwright-present') result = await runPlaywrightPresentCheck(ctx);
242
+ else if (check === 'browser-present') result = await runBrowserPresentCheck(ctx);
243
+ else if (check === 'playwright-mcp-reachable') result = await runPlaywrightMcpReachableCheck(ctx);
244
+ else continue;
245
+ } else if (check === 'playwright-mcp-redundant') {
246
+ // Fires only on browser-facing projects (spec 4.5). Never runs on an
247
+ // API-only project even under an explicit --check ask.
248
+ if (!ctx.browserFacing) continue;
249
+ result = await runPlaywrightMcpRedundantCheck(ctx);
250
+ } else continue;
172
251
  for (const d of result.drift) drift.push({ check, ...d });
173
252
  for (const w of result.writes) writes.push(w);
174
253
  }
@@ -183,10 +262,15 @@ export async function main(argv, deps = {}) {
183
262
  const exitCode = ok ? 0 : 3;
184
263
 
185
264
  if (flags.json) {
186
- stdout.write(`${JSON.stringify({ ok, drift, writes }, null, 2)}\n`);
265
+ stdout.write(`${JSON.stringify({ ok, drift, writes, notices }, null, 2)}\n`);
187
266
  return exitCode;
188
267
  }
189
268
 
269
+ // Notices (spec 3.4 skip line) are diagnostic ground truth. Emitted BEFORE
270
+ // the summary so an operator scanning the top of the output sees why the
271
+ // three checks did not fire on a non-browser-facing project. Not
272
+ // suppressed by --quiet.
273
+ for (const notice of notices) stdout.write(`${notice}\n`);
190
274
  writeHumanSummary({ stdout, ok, drift, writes, fixed: ctx.fix, quiet: Boolean(flags.quiet) });
191
275
  return exitCode;
192
276
  }
@@ -546,3 +630,96 @@ async function isPathIgnored(projectRoot) {
546
630
 
547
631
  // Silence unused-import warnings for values consumed only via names.
548
632
  void composeGitignoreInner;
633
+
634
+ /* ------------------------------------------------------------------ */
635
+ /* Check: playwright-present (spec 3.3) */
636
+ /* ------------------------------------------------------------------ */
637
+
638
+ async function runPlaywrightPresentCheck(ctx) {
639
+ const drift = [];
640
+ const result = ctx.checkPlaywrightPresentImpl(ctx.projectRoot);
641
+ if (!result.ok) {
642
+ drift.push({
643
+ item: 'missing-peer',
644
+ file: 'package.json',
645
+ message: FIX_LINES['playwright-present'],
646
+ refusedByFix: true,
647
+ });
648
+ }
649
+ return { drift, writes: [] };
650
+ }
651
+
652
+ /* ------------------------------------------------------------------ */
653
+ /* Check: browser-present (spec 3.3) */
654
+ /* ------------------------------------------------------------------ */
655
+
656
+ async function runBrowserPresentCheck(ctx) {
657
+ const drift = [];
658
+ const result = await ctx.checkBrowserPresentImpl();
659
+ if (!result.ok) {
660
+ drift.push({
661
+ item: 'no-browser',
662
+ file: '(system)',
663
+ message: FIX_LINES['browser-present'],
664
+ refusedByFix: true,
665
+ });
666
+ }
667
+ return { drift, writes: [] };
668
+ }
669
+
670
+ /* ------------------------------------------------------------------ */
671
+ /* Check: playwright-mcp-reachable (spec 3.3) */
672
+ /* ------------------------------------------------------------------ */
673
+
674
+ async function runPlaywrightMcpReachableCheck(ctx) {
675
+ const drift = [];
676
+ const result = await ctx.checkPlaywrightMcpReachableImpl();
677
+ if (!result.ok) {
678
+ drift.push({
679
+ item: result.timedOut ? 'unreachable-timeout' : 'unreachable',
680
+ file: '(npx @playwright/mcp)',
681
+ message: FIX_LINES['playwright-mcp-reachable'],
682
+ refusedByFix: true,
683
+ });
684
+ }
685
+ return { drift, writes: [] };
686
+ }
687
+
688
+ /* ------------------------------------------------------------------ */
689
+ /* Check: playwright-mcp-redundant (spec 4.5) */
690
+ /* ------------------------------------------------------------------ */
691
+
692
+ async function runPlaywrightMcpRedundantCheck(ctx) {
693
+ const drift = [];
694
+ const mcpJson = await ctx.readMcpJsonImpl(ctx.projectRoot);
695
+ const projectKey = mcpJson ? findProjectPlaywrightKey(mcpJson) : null;
696
+ if (!projectKey) return { drift, writes: [] };
697
+ const probeResult = await ctx.probeClaudeCodeMcpImpl();
698
+ if (probeResult.kind !== 'found') return { drift, writes: [] };
699
+ drift.push({
700
+ item: 'redundant-entry',
701
+ file: '.mcp.json',
702
+ message: `project-scope .mcp.json carries a Playwright MCP entry ('${projectKey}') that is also declared at ${probeResult.scope} 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 '${projectKey}' entry from .mcp.json by hand.`,
703
+ refusedByFix: true,
704
+ });
705
+ return { drift, writes: [] };
706
+ }
707
+
708
+ /**
709
+ * Default reader for the project-root .mcp.json body. Returns the parsed
710
+ * object, or null on missing / unparseable (doctor treats an unparseable
711
+ * .mcp.json as "no signature findable" rather than a hard refusal here; the
712
+ * merge path in agent-setup already refuses unparseable with exit 2 on write).
713
+ *
714
+ * @param {string} projectRoot
715
+ * @returns {Promise<object|null>}
716
+ */
717
+ async function defaultReadMcpJson(projectRoot) {
718
+ const file = join(projectRoot, '.mcp.json');
719
+ try {
720
+ const raw = await readFile(file, 'utf8');
721
+ return JSON.parse(raw);
722
+ } catch {
723
+ return null;
724
+ }
725
+ }
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
+