jorgex-stack 1.9.31 → 1.9.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,14 @@ pnpm dlx jorgex-stack install
42
42
  pnpm dlx jorgex-stack sync
43
43
  ```
44
44
 
45
+ For a fresh Engram installation, always consult the current published Stack and bypass only the `pnpm dlx` cache:
46
+
47
+ ```bash
48
+ pnpm --config.dlx-cache-max-age=0 dlx jorgex-stack@latest install --engram
49
+ ```
50
+
51
+ `dlx-cache-max-age` is separate from the pnpm 11 dependency-age filter: it controls only the cached `dlx` package, while `minimumReleaseAgeExclude` applies only to the named package resolution. An explicit Stack version such as `@1.9.30` does not reuse the cache entry for another version. Do not use `@latest` for Pi; Pi consumption still requires its exact validated pin.
52
+
45
53
  Other important commands:
46
54
 
47
55
  ```bash
@@ -120,7 +128,7 @@ pnpm dlx jorgex-stack@1.9.7 sync --agents pi
120
128
  pnpm dlx jorgex-stack@1.9.7 uninstall --agents pi
121
129
  ```
122
130
 
123
- Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. The historical `0.8.0` tarball was `89128340` bytes; the exact adopted artifact and integrity values are authoritative in `src/lib/pi-runtime-pin.json`, while the lifecycle remains authoritative in `src/lib/pi-runtime.ts`. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. When the managed Playwright preference is active, the projection also adds or removes the marked `jorgex:browser` section dynamically. The Pi-only `install --agents pi --playwright` flow installs and persists that Playwright capability just like the other harnesses. The adopted Pi `0.8.13` candidate also supports the optional Chrome DevTools handoff; Stack persists the selected handoff as part of the Pi projection and records its SHA-256 in the optional `devtools.sha256` field of `~/.jorgex-stack/pi-projection-receipt.json` after a successful operation. Context7 remains outside the Pi scope. The published `1.9.7` entry `{ "source": "npm:jorgex-pi@0.8.4", "skills": [], "prompts": [] }` and the `0.8.5` snapshot reference are historical; they do not describe the adopted pin. Filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Package receipts reject manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state. Projection cleanup requires an exact scope-bound ownership receipt; DevTools conflicts preserve the handoff for review.
131
+ Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. The historical `0.8.0` tarball was `89128340` bytes; the exact adopted artifact and integrity values are authoritative in `src/lib/pi-runtime-pin.json`, while the lifecycle remains authoritative in `src/lib/pi-runtime.ts`. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. When the managed Playwright preference is active, the projection also adds or removes the marked `jorgex:browser` section dynamically. The global Playwright package and Chromium cache are shared by the machine; `--playwright-runtimes` controls which runtime receives the guide. The pin vigente declara `playwright-handoff-v1`, por lo que Pi puede seleccionarse y recibe su handoff de Playwright además de la guía. El paquete Pi fijado también soporta el handoff opcional de Chrome DevTools; Stack persiste cada handoff como parte de la proyección y registra su SHA-256 en el receipt correspondiente tras una operación correcta. Context7 remains outside the Pi scope. The published `1.9.7` entry `{ "source": "npm:jorgex-pi@0.8.4", "skills": [], "prompts": [] }` and the `0.8.5` snapshot reference are historical; they do not describe the adopted pin. Filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Package receipts reject manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state. Projection cleanup requires an exact scope-bound ownership receipt; DevTools conflicts preserve the handoff for review.
124
132
 
125
133
  Historically, the published Pi 0.8.0 direct-package snapshot added `work-audit`: the snapshot grew from **17 to 18 skill trees** (96 to 97 files), and the active runtime allowlist grew from **16 to 17 skills**. The historical Pi 0.8.5 snapshot reference records 18 skill trees and 98 files; it does not describe the adopted pin. Reference F2-A is included while the private F1 skills remain preserved. `playwright-cli` remains in the snapshot but inactive because browser automation is a separate opt-in integration.
126
134
 
@@ -136,7 +144,7 @@ Engram remains mandatory and user-owned. An existing binary is preserved. `insta
136
144
 
137
145
  Históricamente, Stack `1.9.7` reconocía el receipt exacto de Pi `npm:jorgex-pi@0.8.4`. Usa versiones exactas, nunca `latest`, y no edites receipts o hashes ni borres `HOME`, Engram o la proyección de otro runtime para forzar confianza. El pin y los comandos actuales de transición y rollback están en [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
138
146
 
139
- The 24-hour managed-consumption maturity rule applies only to real installation or consumption of the new Pi package; development, PR validation, merge and Stack publication may proceed immediately. Installing it on a real user scope before the maturity window requires Jorge's explicit exception.
147
+ Pi puede consumirse inmediatamente después de publicar y verificar el artefacto adoptado. El pin exacto, la procedencia, los SHA-256/SHA-512, el SRI, la compatibilidad y el procedimiento de rollback siguen siendo obligatorios; no uses `latest` ni una versión aproximada.
140
148
 
141
149
  `update --agents pi` only runs the Pi package lifecycle; it does not enter the global Stack updater. `update --check --agents pi` is a read-only Pi doctor. Uninstall runs package cleanup, backs up Pi's settings before removal, removes only the exact receipt-owned package after verifying absence, and preserves all companion/user state. Full behavior, failure states and troubleshooting are in [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
142
150
 
@@ -156,6 +164,9 @@ pnpm dlx jorgex-stack install
156
164
  # Non-interactive / agent: --playwright authorizes the global install.
157
165
  pnpm dlx jorgex-stack install --yes --playwright
158
166
 
167
+ # Instala el paquete compartido y activa la guía solo en estos runtimes.
168
+ pnpm dlx jorgex-stack install --playwright --playwright-runtimes=opencode,claude-code
169
+
159
170
  # Enable Chrome DevTools MCP explicitly per runtime.
160
171
  pnpm dlx jorgex-stack install --devtools
161
172
  pnpm dlx jorgex-stack install --no-devtools
@@ -163,7 +174,7 @@ pnpm dlx jorgex-stack install --agents pi --devtools
163
174
  pnpm dlx jorgex-stack sync --agents pi --no-devtools
164
175
  ```
165
176
 
166
- Under the hood, `--playwright` runs two `pnpm` argv-only plans back to back: `pnpm add --global @playwright/cli@0.1.18` (the package) and then `pnpm dlx @playwright/cli@0.1.18 install-browser` (the browser binary cache). Removal is `pnpm remove --global @playwright/cli` (no version suffix). If installation fails, the error identifies the failed phase — global package, browser download, or preference persistence — and recommends `jorgex-stack install --playwright`; the preference is not marked enabled unless the complete plan succeeds.
177
+ Under the hood, `--playwright` runs two `pnpm` argv-only plans back to back: `pnpm add --global @playwright/cli@0.1.18` (the package) and then `pnpm dlx @playwright/cli@0.1.18 install-browser chromium` (the Chromium cache). It then verifies that the pinned package launches Chromium headless against `about:blank`. Removal is `pnpm remove --global @playwright/cli` (no version suffix). If installation fails, the error identifies the failed phase — global package, browser download, browser launch, or preference persistence — and recommends `jorgex-stack install --playwright`; the preference is not marked enabled unless the complete plan succeeds.
167
178
 
168
179
  Daily operation:
169
180
 
@@ -179,7 +190,7 @@ Daily operation:
179
190
 
180
191
  1. **Stack** (jorgex-stack): detects whether it is a git clone or a global install, then offers an update with confirmation.
181
192
  2. **Engram** (binary): detects the installed version and offers an update through the **native channel** (brew -> `go install` -> release URL). Nothing needs to be stopped: as in upstream macOS/Linux, live processes keep using the old version until clients restart; on Windows, the in-use `.exe` is rotated by rename before installation. **Automatic DB backup before updating**. The database and memories are never touched.
182
- 3. **Playwright CLI** (only when explicitly enabled): compares the detected binary with the approved bundle pin and offers to realign it with explicit confirmation. The realignment re-applies **both** plans — `pnpm add --global @playwright/cli@0.1.18` (package) and `pnpm dlx @playwright/cli@0.1.18 install-browser` (browser cache) — and fails closed if either step returns non-zero. The error identifies whether the package-update or browser-download phase failed and recommends `jorgex-stack install --playwright` to retry both; a Playwright update does not require `sync`.
193
+ 3. **Playwright CLI** (only when explicitly enabled): compares the detected binary with the approved bundle pin and offers to realign it with explicit confirmation. The realignment re-applies **both** plans — `pnpm add --global @playwright/cli@0.1.18` (package) and `pnpm dlx @playwright/cli@0.1.18 install-browser chromium` (Chromium cache) — and fails closed if either step returns non-zero. The error identifies whether the package-update or browser-download phase failed and recommends `jorgex-stack install --playwright` to retry both; a Playwright update does not require `sync`.
183
194
  4. **Vendored skills** (maintainer only): third-party skills ship **pinned** with the stack version, so the installed package never reaches out to their upstreams. Only when running from a git clone (`pnpm cli update`) does `update` scan the upstreams in `upstreams.json`, download to a temp directory, **show a mandatory diff**, and ask for confirmation. A moved upstream is only a candidate until that review is accepted and a deliberate re-pin is made for a future release; it is never treated as an accepted official update automatically. Skills with local changes (`modified: true`) warn and require double confirmation.
184
195
 
185
196
  Usage:
@@ -237,6 +248,8 @@ La integración de este App pertenece al release del repositorio Stack. No conce
237
248
 
238
249
  Requirements: Node >= 22.5 and pnpm (never npm).
239
250
 
251
+ pnpm 11 dependency resolution uses `minimumReleaseAge=1440` by default, with `minimumReleaseAgeExclude` for `jorgex-stack` and `jorgex-pi`. Keep that exclusion in the user-level pnpm config for `pnpm dlx` launched from HOME and in each repository workspace config for local commands; preserve every unrelated existing setting. This documents the maintainer environment and does not change other users' global configuration.
252
+
240
253
  ```
241
254
  pnpm install
242
255
  pnpm build # tsup -> dist/
package/dist/cli.d.ts CHANGED
@@ -23,6 +23,7 @@ interface Flags {
23
23
  removeEngram: boolean;
24
24
  engram: boolean;
25
25
  playwright: boolean;
26
+ playwrightRuntimes?: SelectableRuntimeId[];
26
27
  removePlaywright: boolean;
27
28
  devtools: boolean;
28
29
  noDevtools: boolean;
package/dist/cli.js CHANGED
@@ -2165,6 +2165,8 @@ function resolvePnpmFailureRemedy(reason) {
2165
2165
  return PNPM_GLOBAL_BIN_REMEDY;
2166
2166
  case "action-failed":
2167
2167
  return null;
2168
+ case "browser-launch":
2169
+ return "Chromium se descarg\xF3, pero no ha podido arrancar. Revisa el error de lanzamiento y las dependencias de tu sistema antes de reintentar.";
2168
2170
  }
2169
2171
  }
2170
2172
  function hasNonzeroProcessStatus(error) {
@@ -2186,6 +2188,18 @@ function resolvePlaywrightCliState({ binPath, versionOutput }) {
2186
2188
  }
2187
2189
  function detectPlaywrightCli() {
2188
2190
  const binPath = lookPath(PLAYWRIGHT_CLI.bin);
2191
+ if (binPath === null) {
2192
+ const pnpmHome = resolvePnpmHome();
2193
+ const name = process.platform === "win32" ? `${PLAYWRIGHT_CLI.bin}.cmd` : PLAYWRIGHT_CLI.bin;
2194
+ const known = [path18.join(pnpmHome, name), path18.join(pnpmHome, "bin", name)].find((file) => {
2195
+ try {
2196
+ return fs13.statSync(file).isFile();
2197
+ } catch {
2198
+ return false;
2199
+ }
2200
+ });
2201
+ if (known) return { status: "not-in-path", binPath: known, detectedVersion: null };
2202
+ }
2189
2203
  return resolvePlaywrightCliState({
2190
2204
  binPath,
2191
2205
  versionOutput: binPath ? runDetectedBin(binPath, ["--version"], 5e3, { NO_UPDATE_NOTIFIER: "1" }) : null
@@ -2248,7 +2262,44 @@ function planPlaywrightCliCommand(action, pnpmBin) {
2248
2262
  case "remove":
2249
2263
  return { command: pnpmBin, args: ["remove", "--global", PLAYWRIGHT_CLI.packageName] };
2250
2264
  case "install-browser":
2251
- return { command: pnpmBin, args: ["dlx", pinnedPackage, PLAYWRIGHT_CLI.browserInstallAction] };
2265
+ return { command: pnpmBin, args: ["dlx", pinnedPackage, PLAYWRIGHT_CLI.browserInstallAction, "chromium"] };
2266
+ }
2267
+ }
2268
+ function verifyPlaywrightBrowser(pnpmBin, env = process.env, cwd) {
2269
+ const rootCommand = planDetectedBinCommand(pnpmBin, ["root", "--global"]);
2270
+ if (rootCommand === null) return false;
2271
+ try {
2272
+ const root = execFileSync2(rootCommand.command, rootCommand.args, {
2273
+ encoding: "utf8",
2274
+ timeout: 5e3,
2275
+ env,
2276
+ cwd,
2277
+ stdio: ["ignore", "pipe", "inherit"]
2278
+ }).trim();
2279
+ if (!path18.isAbsolute(root)) return false;
2280
+ const packageFile = fs13.realpathSync(path18.join(root, "@playwright", "cli", "package.json"));
2281
+ const installed = JSON.parse(fs13.readFileSync(packageFile, "utf8"));
2282
+ if (installed.name !== PLAYWRIGHT_CLI.packageName || installed.version !== PLAYWRIGHT_CLI.version) return false;
2283
+ const probe = `
2284
+ const { createRequire } = require('node:module');
2285
+ const { chromium } = createRequire(process.argv[1])('playwright');
2286
+ (async () => {
2287
+ const browser = await chromium.launch({ headless: true, timeout: 15000 });
2288
+ try {
2289
+ const page = await browser.newPage();
2290
+ await page.goto('about:blank', { timeout: 5000 });
2291
+ } finally { await browser.close(); }
2292
+ })().catch(error => { console.error(error.message); process.exitCode = 1; });
2293
+ `;
2294
+ execFileSync2(process.execPath, ["-e", probe, packageFile], {
2295
+ timeout: 25e3,
2296
+ stdio: "inherit",
2297
+ cwd,
2298
+ env: { ...env, NO_UPDATE_NOTIFIER: "1" }
2299
+ });
2300
+ return true;
2301
+ } catch {
2302
+ return false;
2252
2303
  }
2253
2304
  }
2254
2305
  function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
@@ -2270,14 +2321,22 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
2270
2321
  const command = planPlaywrightCliCommand(action, pnpmBin);
2271
2322
  const invocation = planDetectedBinCommand(command.command, command.args);
2272
2323
  if (invocation === null) return { ok: false, reason: "pnpm-command" };
2324
+ const cwd = action === "install-browser" ? fs13.mkdtempSync(path18.join(os.tmpdir(), "jorgex-playwright-")) : void 0;
2325
+ const childEnv = action === "install-browser" ? { ...env ?? process.env, NO_UPDATE_NOTIFIER: "1" } : env ?? process.env;
2273
2326
  try {
2274
2327
  execFileSync2(invocation.command, invocation.args, {
2275
2328
  stdio: "inherit",
2276
- ...env === void 0 ? {} : { env }
2329
+ env: childEnv,
2330
+ ...cwd === void 0 ? {} : { cwd }
2277
2331
  });
2332
+ if (action === "install-browser" && !verifyPlaywrightBrowser(pnpmBin, childEnv, cwd)) {
2333
+ return { ok: false, reason: "browser-launch" };
2334
+ }
2278
2335
  return { ok: true };
2279
2336
  } catch {
2280
2337
  return { ok: false, reason: "action-failed" };
2338
+ } finally {
2339
+ if (cwd !== void 0) fs13.rmSync(cwd, { recursive: true, force: true });
2281
2340
  }
2282
2341
  }
2283
2342
 
@@ -2287,6 +2346,7 @@ import path19 from "path";
2287
2346
  var PLAYWRIGHT_CLI_PREFERENCE_VERSION = 1;
2288
2347
  var DEVTOOLS_MCP_PREFERENCE_VERSION = 1;
2289
2348
  var PRIMARY_MODEL_OWNERSHIP_VERSION = 1;
2349
+ var PLAYWRIGHT_RUNTIMES = ["opencode", "claude-code", "codex", "pi"];
2290
2350
  function readPreference(file) {
2291
2351
  try {
2292
2352
  return { raw: fs14.readFileSync(file, "utf8"), errorCode: null };
@@ -2301,8 +2361,11 @@ function playwrightCliPreferenceFile(stateDir = dataDir()) {
2301
2361
  function parsePlaywrightCliPreference(raw) {
2302
2362
  try {
2303
2363
  const value = JSON.parse(raw);
2304
- if (value.version !== PLAYWRIGHT_CLI_PREFERENCE_VERSION || typeof value.enabled !== "boolean") return void 0;
2305
- return value.enabled;
2364
+ if (!isRecord2(value)) return void 0;
2365
+ if (value.version === 1 && typeof value.enabled === "boolean") return { version: 1, enabled: value.enabled };
2366
+ if (value.version !== 2 || !isRecord2(value.enabled)) return void 0;
2367
+ if (Object.entries(value.enabled).some(([runtime, enabled]) => !PLAYWRIGHT_RUNTIMES.includes(runtime) || typeof enabled !== "boolean")) return void 0;
2368
+ return { version: 2, enabled: value.enabled };
2306
2369
  } catch {
2307
2370
  return void 0;
2308
2371
  }
@@ -2315,15 +2378,28 @@ function playwrightCliPreferenceError(file = playwrightCliPreferenceFile()) {
2315
2378
  if (raw === null || parsePlaywrightCliPreference(raw) !== void 0) return null;
2316
2379
  return `Playwright CLI: preferencia inv\xE1lida en ${file}. Corrige o borra ese archivo antes de reintentar.`;
2317
2380
  }
2318
- function loadPlaywrightCliPreference(file = playwrightCliPreferenceFile()) {
2381
+ function loadPlaywrightCliPreference(file = playwrightCliPreferenceFile(), runtime) {
2319
2382
  const { raw } = readPreference(file);
2320
- if (raw === null) return void 0;
2321
- return parsePlaywrightCliPreference(raw);
2383
+ const state = raw === null ? void 0 : parsePlaywrightCliPreference(raw);
2384
+ if (state === void 0) return void 0;
2385
+ if (state.version === 1) return state.enabled;
2386
+ return runtime === void 0 ? Object.values(state.enabled).some(Boolean) : state.enabled[runtime] === true;
2322
2387
  }
2323
- function savePlaywrightCliPreference(file, enabled) {
2388
+ function savePlaywrightCliPreference(file, enabled, selection) {
2324
2389
  const error = playwrightCliPreferenceError(file);
2325
2390
  if (error !== null) throw new Error(error);
2326
- writeText(file, JSON.stringify({ version: PLAYWRIGHT_CLI_PREFERENCE_VERSION, enabled }) + "\n");
2391
+ const { raw } = readPreference(file);
2392
+ const previous = raw === null ? void 0 : parsePlaywrightCliPreference(raw);
2393
+ if (selection === void 0 && previous?.version !== 2) {
2394
+ writeText(file, JSON.stringify({ version: PLAYWRIGHT_CLI_PREFERENCE_VERSION, enabled }) + "\n");
2395
+ return;
2396
+ }
2397
+ const inherited = previous?.version === 2 ? previous.enabled : Object.fromEntries(PLAYWRIGHT_RUNTIMES.map((runtime) => [runtime, previous?.enabled === true]));
2398
+ const choices = { ...inherited, ...selection };
2399
+ const state = { version: 2, enabled: enabled ? choices : Object.fromEntries(Object.keys(choices).map((runtime) => [runtime, false])) };
2400
+ const content = JSON.stringify(state) + "\n";
2401
+ if (parsePlaywrightCliPreference(content) === void 0) throw new Error("Playwright CLI: selecci\xF3n de runtimes inv\xE1lida.");
2402
+ writeText(file, content);
2327
2403
  }
2328
2404
  function devtoolsMcpPreferenceFile(stateDir = dataDir()) {
2329
2405
  return path19.join(stateDir, "devtools-mcp.json");
@@ -2536,7 +2612,7 @@ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE,
2536
2612
  models,
2537
2613
  warnings: [],
2538
2614
  enabledMcpServers: enabledMcpServers(adapter.id, void 0, useBrowserPreferences),
2539
- playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference() === true,
2615
+ playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), adapter.id) === true,
2540
2616
  ownedMcpServers: ownedMcpServers(adapter.id, useBrowserPreferences),
2541
2617
  ownedPrimaryModelFields: useBrowserPreferences ? loadPrimaryModelOwnership(primaryModelOwnershipFile(), adapter.id, configDir) : /* @__PURE__ */ new Set()
2542
2618
  };
@@ -2655,7 +2731,7 @@ async function runInstall(opts) {
2655
2731
  models,
2656
2732
  warnings: [],
2657
2733
  enabledMcpServers: enabledMcpServers(id, opts.devtoolsMcpSelection?.[id], useManifest),
2658
- playwrightCliEnabled: projectPlaywrightPrompt || useManifest && loadPlaywrightCliPreference() === true,
2734
+ playwrightCliEnabled: projectPlaywrightPrompt ? opts.playwrightToolConsent?.runtimeSelection?.[id] ?? true : useManifest && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), id) === true,
2659
2735
  ownedMcpServers: ownedMcpServers(id, useManifest),
2660
2736
  ownedPrimaryModelFields: useManifest ? loadPrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir) : /* @__PURE__ */ new Set()
2661
2737
  };
@@ -2750,7 +2826,11 @@ async function runInstall(opts) {
2750
2826
  const pnpmBin = resolvePnpmBin();
2751
2827
  return executePlaywrightToolAction2(action, pnpmBin, env);
2752
2828
  },
2753
- persistEnabled: (enabled) => savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled),
2829
+ persistEnabled: (enabled) => {
2830
+ const selected = opts.playwrightToolConsent?.runtimeSelection;
2831
+ const fileSelection = selected === void 0 ? void 0 : Object.fromEntries(Object.entries(selected).filter(([runtime]) => runtime !== "pi"));
2832
+ savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled, fileSelection);
2833
+ },
2754
2834
  setupPnpm: (pnpmBin) => setupPnpmGlobal(pnpmBin)
2755
2835
  };
2756
2836
  let setupAttempted = false;
@@ -2778,6 +2858,7 @@ async function runInstall(opts) {
2778
2858
  return first;
2779
2859
  }
2780
2860
  preparedEnv = setup.env;
2861
+ p.log.info("pnpm preparado para esta instalaci\xF3n. Abre una terminal nueva al terminar para que otras herramientas y doctor reciban el PATH actualizado.");
2781
2862
  return baseDeps.run(action, preparedEnv);
2782
2863
  }
2783
2864
  });
@@ -2789,7 +2870,11 @@ async function runInstall(opts) {
2789
2870
  } else {
2790
2871
  let promptReconciliationFailed = false;
2791
2872
  for (const { adapter, ctx } of successfulContexts) {
2792
- const browserCtx = { ...ctx, playwrightCliEnabled: true, warnings: [] };
2873
+ const browserCtx = {
2874
+ ...ctx,
2875
+ playwrightCliEnabled: opts.playwrightToolConsent?.runtimeSelection?.[adapter.id] ?? true,
2876
+ warnings: []
2877
+ };
2793
2878
  try {
2794
2879
  const browserChanges = diffPlan(planSystemPrompt(adapter, browserCtx)).filter((change) => change.status !== "unchanged");
2795
2880
  if (browserChanges.length === 0) continue;
@@ -2812,7 +2897,7 @@ async function runInstall(opts) {
2812
2897
  exitCode = 1;
2813
2898
  p.log.error("Playwright CLI y navegador se han instalado y la preferencia est\xE1 activada, pero la gu\xEDa de navegador qued\xF3 en estado parcial. Ejecuta 'jorgex-stack sync' para repararla.");
2814
2899
  } else {
2815
- p.log.success("Playwright CLI y navegador instalados.");
2900
+ p.log.success("Playwright CLI instalado y arranque de Chromium verificado.");
2816
2901
  }
2817
2902
  }
2818
2903
  }
@@ -3276,6 +3361,10 @@ function runPiPackageManagedOperation(input, deps) {
3276
3361
  }
3277
3362
 
3278
3363
  // src/lib/pi-projection-lifecycle.ts
3364
+ var HANDOFF_KINDS = ["devtools", "playwright"];
3365
+ function handoffEnabled(input, kind) {
3366
+ return (kind === "devtools" ? input.devtoolsMcpEnabled : input.playwrightHandoffEnabled) === true;
3367
+ }
3279
3368
  var preparedPiProjectionUninstalls = /* @__PURE__ */ new WeakMap();
3280
3369
  function projectionScope(scope) {
3281
3370
  return {
@@ -3322,18 +3411,27 @@ function projectionPlan(input, scope) {
3322
3411
  if (input.devtoolsMcpEnabled && input.pnpmBin) {
3323
3412
  const server = loadCanonicalMcp(input.stackDir).servers[DEVTOOLS_MCP_SERVER];
3324
3413
  if (server?.transport !== "stdio" || !Array.isArray(server.args)) throw new Error("Falta la configuraci\xF3n can\xF3nica de DevTools.");
3325
- actions.push({ kind: "write", target: devtoolsPath(scope), content: `${JSON.stringify({
3414
+ actions.push({ kind: "write", target: handoffPath(scope, "devtools"), content: `${JSON.stringify({
3326
3415
  schemaVersion: 1,
3327
3416
  enabled: true,
3328
3417
  command: input.pnpmBin,
3329
3418
  args: server.args
3330
3419
  }, null, 2)}
3420
+ ` });
3421
+ }
3422
+ if (input.playwrightHandoffEnabled && input.playwrightCliCommand) {
3423
+ actions.push({ kind: "write", target: handoffPath(scope, "playwright"), content: `${JSON.stringify({
3424
+ schemaVersion: 1,
3425
+ enabled: true,
3426
+ command: input.playwrightCliCommand,
3427
+ version: PLAYWRIGHT_CLI.version
3428
+ }, null, 2)}
3331
3429
  ` });
3332
3430
  }
3333
3431
  return actions;
3334
3432
  }
3335
- function devtoolsPath(scope) {
3336
- return path23.resolve(scope.codingAgentDir, "jorgex-pi", "devtools.v1.json");
3433
+ function handoffPath(scope, kind) {
3434
+ return path23.resolve(scope.codingAgentDir, "jorgex-pi", `${kind}.v1.json`);
3337
3435
  }
3338
3436
  function contentHash(content) {
3339
3437
  return createHash("sha256").update(content).digest("hex");
@@ -3344,13 +3442,14 @@ function canonicalActionContent(action) {
3344
3442
  function hasActionDrift(action, deps) {
3345
3443
  return deps.readText(action.target) !== canonicalActionContent(action);
3346
3444
  }
3347
- function applyActions(actions, deps, handoff) {
3445
+ function applyActions(actions, deps, handoffs = []) {
3348
3446
  for (const action of actions) {
3349
- if (handoff && path23.resolve(action.target) === handoff.file) {
3447
+ const handoff = handoffs.find((entry) => path23.resolve(action.target) === entry.file);
3448
+ if (handoff) {
3350
3449
  try {
3351
- if (deps.readText(handoff.file) !== handoff.previous) return devtoolsConflict(handoff.file);
3450
+ if (deps.readText(handoff.file) !== handoff.previous) return handoffConflict(handoff.kind, handoff.file);
3352
3451
  } catch {
3353
- return devtoolsConflict(handoff.file);
3452
+ return handoffConflict(handoff.kind, handoff.file);
3354
3453
  }
3355
3454
  }
3356
3455
  if (action.kind === "write") deps.writeText(action.target, action.content);
@@ -3369,8 +3468,7 @@ function receiptFor(plan, scope) {
3369
3468
  const owned = [...new Set(
3370
3469
  plan.map((action) => path23.resolve(action.target)).filter((target) => target !== systemPrompt)
3371
3470
  )];
3372
- const handoff = plan.find((action) => path23.resolve(action.target) === devtoolsPath(scope));
3373
- return {
3471
+ const receipt = {
3374
3472
  schemaVersion: 1,
3375
3473
  scope: {
3376
3474
  kind: scope.kind,
@@ -3378,9 +3476,13 @@ function receiptFor(plan, scope) {
3378
3476
  codingAgentDir: scope.codingAgentDir,
3379
3477
  receiptFile: scope.receiptFile
3380
3478
  },
3381
- owned,
3382
- ...handoff ? { devtools: { sha256: contentHash(canonicalActionContent(handoff)) } } : {}
3479
+ owned
3383
3480
  };
3481
+ for (const kind of HANDOFF_KINDS) {
3482
+ const action = plan.find((entry) => path23.resolve(entry.target) === handoffPath(scope, kind));
3483
+ if (action) receipt[kind] = { sha256: contentHash(canonicalActionContent(action)) };
3484
+ }
3485
+ return receipt;
3384
3486
  }
3385
3487
  function receiptContent(receipt) {
3386
3488
  return `${JSON.stringify(receipt, null, 2)}
@@ -3398,25 +3500,24 @@ function parseReceipt2(raw, expected) {
3398
3500
  const version = Reflect.get(receipt, "schemaVersion");
3399
3501
  const receivedScope = Reflect.get(receipt, "scope");
3400
3502
  const owned = Reflect.get(receipt, "owned");
3401
- const devtools = Reflect.get(receipt, "devtools");
3402
- const hasDevtools = Object.hasOwn(receipt, "devtools");
3403
- if (version !== expected.schemaVersion || !hasExactKeys(receipt, ["schemaVersion", "scope", "owned", ...hasDevtools ? ["devtools"] : []]) || receivedScope === null || typeof receivedScope !== "object" || Array.isArray(receivedScope) || !hasExactKeys(receivedScope, ["kind", "home", "codingAgentDir", "receiptFile"])) {
3503
+ const presentKinds = HANDOFF_KINDS.filter((kind) => Object.hasOwn(receipt, kind));
3504
+ if (version !== expected.schemaVersion || !hasExactKeys(receipt, ["schemaVersion", "scope", "owned", ...presentKinds]) || receivedScope === null || typeof receivedScope !== "object" || Array.isArray(receivedScope) || !hasExactKeys(receivedScope, ["kind", "home", "codingAgentDir", "receiptFile"])) {
3404
3505
  return null;
3405
3506
  }
3406
3507
  if (Reflect.get(receivedScope, "kind") !== expected.scope.kind || Reflect.get(receivedScope, "home") !== expected.scope.home || Reflect.get(receivedScope, "codingAgentDir") !== expected.scope.codingAgentDir || Reflect.get(receivedScope, "receiptFile") !== expected.scope.receiptFile || !Array.isArray(owned)) {
3407
3508
  return null;
3408
3509
  }
3409
- if (hasDevtools && (devtools === null || typeof devtools !== "object" || Array.isArray(devtools) || !hasExactKeys(devtools, ["sha256"]) || typeof Reflect.get(devtools, "sha256") !== "string" || !/^[a-f0-9]{64}$/.test(Reflect.get(devtools, "sha256")))) return null;
3410
- const handoff = devtoolsPath(expected.scope);
3411
- const expectedOwned = expected.owned.filter((file) => file !== handoff);
3412
- if (hasDevtools) expectedOwned.push(handoff);
3510
+ const digests = {};
3511
+ for (const kind of presentKinds) {
3512
+ const value = Reflect.get(receipt, kind);
3513
+ if (value === null || typeof value !== "object" || Array.isArray(value) || !hasExactKeys(value, ["sha256"]) || typeof Reflect.get(value, "sha256") !== "string" || !/^[a-f0-9]{64}$/.test(Reflect.get(value, "sha256"))) return null;
3514
+ digests[kind] = { sha256: Reflect.get(value, "sha256") };
3515
+ }
3516
+ const handoffPaths = HANDOFF_KINDS.map((kind) => handoffPath(expected.scope, kind));
3517
+ const expectedOwned = expected.owned.filter((file) => !handoffPaths.includes(file));
3518
+ for (const kind of presentKinds) expectedOwned.push(handoffPath(expected.scope, kind));
3413
3519
  if (owned.length !== expectedOwned.length || !owned.every((file, index) => typeof file === "string" && file === expectedOwned[index])) return null;
3414
- return {
3415
- schemaVersion: 1,
3416
- scope: expected.scope,
3417
- owned: expectedOwned,
3418
- ...hasDevtools ? { devtools: { sha256: Reflect.get(devtools, "sha256") } } : {}
3419
- };
3520
+ return { schemaVersion: 1, scope: expected.scope, owned: expectedOwned, ...digests };
3420
3521
  } catch {
3421
3522
  return null;
3422
3523
  }
@@ -3542,34 +3643,39 @@ function readProjectionReceipt(receiptFile, deps) {
3542
3643
  return { kind: "unreadable" };
3543
3644
  }
3544
3645
  }
3545
- function devtoolsConflict(file) {
3646
+ function handoffConflict(kind, file) {
3647
+ const label = kind === "devtools" ? "DevTools" : "Playwright";
3546
3648
  return blocked2(
3547
- "projection-devtools-conflict",
3649
+ `projection-${kind}-conflict`,
3548
3650
  [file],
3549
- "El handoff de DevTools es ajeno o fue modificado. Cons\xE9rvalo y revisa su propiedad antes de reintentar; Stack no lo sobrescribe ni elimina."
3651
+ `El handoff de ${label} es ajeno o fue modificado. Cons\xE9rvalo y revisa su propiedad antes de reintentar; Stack no lo sobrescribe ni elimina.`
3550
3652
  );
3551
3653
  }
3552
- function inspectDevtools(input, scope, expected, deps) {
3553
- const file = devtoolsPath(scope);
3654
+ function inspectHandoffs(input, scope, expected, deps) {
3554
3655
  const read = readProjectionReceipt(scope.receiptFile, deps);
3555
3656
  if (read.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3556
3657
  const previous = read.kind === "present" ? parseReceipt2(read.content, expected) : null;
3557
- let content;
3558
- try {
3559
- content = deps.readText(file);
3560
- } catch {
3561
- return devtoolsConflict(file);
3562
- }
3563
- let recordedDevtools = false;
3564
- if (read.kind === "present") {
3658
+ const handoffs = [];
3659
+ for (const kind of HANDOFF_KINDS) {
3660
+ const file = handoffPath(scope, kind);
3661
+ let content;
3565
3662
  try {
3566
- recordedDevtools = Object.hasOwn(JSON.parse(read.content), "devtools");
3663
+ content = deps.readText(file);
3567
3664
  } catch {
3665
+ return handoffConflict(kind, file);
3568
3666
  }
3569
- if (!previous && (input.devtoolsMcpEnabled || content !== null || recordedDevtools)) return receiptInvalid(scope.receiptFile);
3667
+ let recorded = false;
3668
+ if (read.kind === "present") {
3669
+ try {
3670
+ recorded = Object.hasOwn(JSON.parse(read.content), kind);
3671
+ } catch {
3672
+ }
3673
+ if (!previous && (handoffEnabled(input, kind) || content !== null || recorded)) return receiptInvalid(scope.receiptFile);
3674
+ }
3675
+ if (content !== null && (!previous?.[kind] || contentHash(content) !== previous[kind].sha256)) return handoffConflict(kind, file);
3676
+ handoffs.push({ kind, file, content });
3570
3677
  }
3571
- if (content !== null && (!previous?.devtools || contentHash(content) !== previous.devtools.sha256)) return devtoolsConflict(file);
3572
- return { kind: "checked", previous, content };
3678
+ return { kind: "checked", previous, handoffs };
3573
3679
  }
3574
3680
  function preparePiProjectionUninstall(input, deps) {
3575
3681
  if (input.operation !== "uninstall") {
@@ -3606,14 +3712,19 @@ function preparePiProjectionUninstall(input, deps) {
3606
3712
  receiptFile = path23.resolve(scope.receiptFile);
3607
3713
  backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
3608
3714
  }
3609
- const handoff = devtoolsPath(scope);
3610
- let handoffContent;
3611
- try {
3612
- handoffContent = deps.readText(handoff);
3613
- } catch {
3614
- return devtoolsConflict(handoff);
3715
+ const handoffs = [];
3716
+ for (const kind of HANDOFF_KINDS) {
3717
+ const file = handoffPath(scope, kind);
3718
+ let content;
3719
+ try {
3720
+ content = deps.readText(file);
3721
+ } catch {
3722
+ return handoffConflict(kind, file);
3723
+ }
3724
+ const digest = ownedReceipt?.[kind]?.sha256;
3725
+ if (content !== null && (digest === void 0 || contentHash(content) !== digest)) return handoffConflict(kind, file);
3726
+ if (digest !== void 0) handoffs.push({ kind, file, sha256: digest });
3615
3727
  }
3616
- if (handoffContent !== null && (!ownedReceipt?.devtools || contentHash(handoffContent) !== ownedReceipt.devtools.sha256)) return devtoolsConflict(handoff);
3617
3728
  const backup = backupExisting(backupTargets, deps);
3618
3729
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3619
3730
  const token = {};
@@ -3621,7 +3732,7 @@ function preparePiProjectionUninstall(input, deps) {
3621
3732
  prompt: promptUpdate,
3622
3733
  ownedToRemove,
3623
3734
  receiptFile,
3624
- ...ownedReceipt?.devtools ? { devtools: { file: handoff, sha256: ownedReceipt.devtools.sha256 } } : {}
3735
+ handoffs
3625
3736
  });
3626
3737
  return { kind: "prepared", plan: token };
3627
3738
  }
@@ -3633,14 +3744,14 @@ function completePiProjectionUninstall(plan, deps) {
3633
3744
  if (prepared === void 0) {
3634
3745
  return cleanupFailure([]);
3635
3746
  }
3636
- if (prepared.devtools) {
3747
+ for (const handoff of prepared.handoffs) {
3637
3748
  let current;
3638
3749
  try {
3639
- current = deps.readText(prepared.devtools.file);
3750
+ current = deps.readText(handoff.file);
3640
3751
  } catch {
3641
- return devtoolsConflict(prepared.devtools.file);
3752
+ return handoffConflict(handoff.kind, handoff.file);
3642
3753
  }
3643
- if (current !== null && contentHash(current) !== prepared.devtools.sha256) return devtoolsConflict(prepared.devtools.file);
3754
+ if (current !== null && contentHash(current) !== handoff.sha256) return handoffConflict(handoff.kind, handoff.file);
3644
3755
  }
3645
3756
  if (prepared.prompt !== null) {
3646
3757
  try {
@@ -3651,9 +3762,10 @@ function completePiProjectionUninstall(plan, deps) {
3651
3762
  }
3652
3763
  for (const owned of prepared.ownedToRemove) {
3653
3764
  try {
3654
- if (prepared.devtools?.file === owned) {
3765
+ const handoff = prepared.handoffs.find((entry) => entry.file === owned);
3766
+ if (handoff) {
3655
3767
  const current = deps.readText(owned);
3656
- if (current !== null && contentHash(current) !== prepared.devtools.sha256) return devtoolsConflict(owned);
3768
+ if (current !== null && contentHash(current) !== handoff.sha256) return handoffConflict(handoff.kind, owned);
3657
3769
  }
3658
3770
  deps.removeFile(owned);
3659
3771
  } catch {
@@ -3676,22 +3788,27 @@ function runPiProjectionLifecycle(input, deps) {
3676
3788
  return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
3677
3789
  }
3678
3790
  if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path23.isAbsolute(input.pnpmBin))) {
3679
- return blocked2("projection-devtools-command", [devtoolsPath(scope)], "DevTools requiere un ejecutable pnpm absoluto disponible en PATH.");
3791
+ return blocked2("projection-devtools-command", [handoffPath(scope, "devtools")], "DevTools requiere un ejecutable pnpm absoluto disponible en PATH.");
3792
+ }
3793
+ if (input.playwrightHandoffEnabled && (!input.playwrightCliCommand || !path23.isAbsolute(input.playwrightCliCommand) || /[\u0000-\u001f\u007f]/.test(input.playwrightCliCommand))) {
3794
+ return blocked2("projection-playwright-command", [handoffPath(scope, "playwright")], "Playwright requiere un ejecutable absoluto verificado. Abre una terminal nueva tras pnpm setup y reintenta install --playwright.");
3680
3795
  }
3681
3796
  const plan = projectionPlan(input, scope);
3682
3797
  assertPlanContained(plan, scope);
3683
3798
  const receipt = receiptFor(plan, scope);
3684
- const devtools = inspectDevtools(input, scope, receipt, deps);
3685
- if (devtools.kind === "blocked") return input.operation === "doctor" ? { kind: "drift", paths: devtools.paths } : devtools;
3686
- const removeHandoff = !input.devtoolsMcpEnabled && devtools.previous?.devtools !== void 0 && devtools.content !== null;
3799
+ const checked = inspectHandoffs(input, scope, receipt, deps);
3800
+ if (checked.kind === "blocked") return input.operation === "doctor" ? { kind: "drift", paths: checked.paths } : checked;
3801
+ const removedHandoffs = checked.handoffs.filter(({ kind, content }) => !handoffEnabled(input, kind) && checked.previous?.[kind] !== void 0 && content !== null);
3687
3802
  const expectedReceipt2 = receiptContent(receipt);
3688
3803
  const drifted = plan.filter((action) => hasActionDrift(action, deps));
3689
- if (input.devtoolsMcpEnabled && devtools.previous?.devtools?.sha256 !== receipt.devtools?.sha256 && !drifted.some((action) => path23.resolve(action.target) === devtoolsPath(scope))) {
3690
- return input.operation === "doctor" ? { kind: "drift", paths: [devtoolsPath(scope)] } : devtoolsConflict(devtoolsPath(scope));
3804
+ for (const { kind, file } of checked.handoffs) {
3805
+ if (handoffEnabled(input, kind) && checked.previous?.[kind]?.sha256 !== receipt[kind]?.sha256 && !drifted.some((action) => path23.resolve(action.target) === file)) {
3806
+ return input.operation === "doctor" ? { kind: "drift", paths: [file] } : handoffConflict(kind, file);
3807
+ }
3691
3808
  }
3692
3809
  if (input.operation === "doctor") {
3693
3810
  const paths = drifted.map((action) => path23.resolve(action.target));
3694
- if (removeHandoff) paths.push(devtoolsPath(scope));
3811
+ paths.push(...removedHandoffs.map(({ file }) => file));
3695
3812
  const currentSettings2 = deps.readText(scope.settingsFile);
3696
3813
  if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
3697
3814
  paths.push(scope.settingsFile);
@@ -3707,30 +3824,38 @@ function runPiProjectionLifecycle(input, deps) {
3707
3824
  }
3708
3825
  const packageWillChange = filteredSettings !== null && filteredSettings !== currentSettings;
3709
3826
  const receiptChanged = deps.readText(scope.receiptFile) !== expectedReceipt2;
3710
- if (drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff) {
3827
+ if (drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0) {
3711
3828
  const backup = backupExisting([
3712
3829
  ...drifted.map((action) => action.target),
3713
- ...removeHandoff ? [devtoolsPath(scope)] : [],
3830
+ ...removedHandoffs.map(({ file }) => file),
3714
3831
  ...packageWillChange ? [scope.settingsFile] : [],
3715
3832
  ...receiptChanged ? [scope.receiptFile] : []
3716
3833
  ], deps);
3717
3834
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3718
3835
  }
3719
- if (removeHandoff) {
3836
+ for (const { kind, file } of removedHandoffs) {
3720
3837
  try {
3721
- const current = deps.readText(devtoolsPath(scope));
3722
- if (current !== null && contentHash(current) !== devtools.previous?.devtools?.sha256) return devtoolsConflict(devtoolsPath(scope));
3723
- deps.removeFile(devtoolsPath(scope));
3838
+ const current = deps.readText(file);
3839
+ if (current !== null && contentHash(current) !== checked.previous?.[kind]?.sha256) return handoffConflict(kind, file);
3724
3840
  } catch {
3725
- return cleanupFailure([devtoolsPath(scope)]);
3841
+ return handoffConflict(kind, file);
3726
3842
  }
3727
3843
  }
3728
- const writeResult = applyActions(drifted, deps, input.devtoolsMcpEnabled ? { file: devtoolsPath(scope), previous: devtools.content } : void 0);
3844
+ for (const { kind, file } of removedHandoffs) {
3845
+ try {
3846
+ const current = deps.readText(file);
3847
+ if (current !== null && contentHash(current) !== checked.previous?.[kind]?.sha256) return handoffConflict(kind, file);
3848
+ deps.removeFile(file);
3849
+ } catch {
3850
+ return cleanupFailure([file]);
3851
+ }
3852
+ }
3853
+ const writeResult = applyActions(drifted, deps, checked.handoffs.filter(({ kind }) => handoffEnabled(input, kind)).map(({ kind, file, content }) => ({ kind, file, previous: content })));
3729
3854
  if (writeResult) return writeResult;
3730
3855
  if (packageWillChange && filteredSettings !== null) deps.writeText(scope.settingsFile, filteredSettings);
3731
3856
  if (receiptChanged) deps.writeText(scope.receiptFile, expectedReceipt2);
3732
3857
  if (input.operation === "install") return { kind: "installed", receipt };
3733
- return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff };
3858
+ return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0 };
3734
3859
  }
3735
3860
  function systemProjectionLifecycle(input) {
3736
3861
  const targetRoot = input.targetDir === void 0 ? null : path23.resolve(input.targetDir);
@@ -3749,7 +3874,9 @@ function systemProjectionLifecycle(input) {
3749
3874
  engramBin: input.engramBin ?? "",
3750
3875
  playwrightCliEnabled: input.playwrightCliEnabled,
3751
3876
  devtoolsMcpEnabled: input.devtoolsMcpEnabled,
3752
- pnpmBin: input.pnpmBin
3877
+ pnpmBin: input.pnpmBin,
3878
+ playwrightHandoffEnabled: input.playwrightHandoffEnabled,
3879
+ playwrightCliCommand: input.playwrightCliCommand
3753
3880
  },
3754
3881
  deps: {
3755
3882
  readText: readTextOnlyIfMissing,
@@ -3954,6 +4081,7 @@ function engramVersion(bin) {
3954
4081
  }
3955
4082
  function resolvePlaywrightDoctorState(input) {
3956
4083
  if (input.enabled !== true) return { status: "disabled" };
4084
+ if (input.cli.status === "not-in-path") return { status: "not-in-path" };
3957
4085
  if (input.cli.status === "absent") return { status: "missing", missing: "package" };
3958
4086
  if (input.cli.status === "broken") return { status: "broken" };
3959
4087
  if (input.cli.status === "outdated") return { status: "outdated" };
@@ -4014,7 +4142,10 @@ async function runDoctor() {
4014
4142
  if (playwright.status === "disabled") {
4015
4143
  p3.log.info("Playwright CLI: deshabilitado (opcional). Usa 'install --playwright' para instalarlo de forma expl\xEDcita.");
4016
4144
  } else if (playwright.status === "healthy") {
4017
- p3.log.success("Playwright CLI: paquete y navegador listos.");
4145
+ p3.log.success("Playwright CLI: paquete y cach\xE9 de Chromium detectados (doctor no prueba el arranque).");
4146
+ } else if (playwright.status === "not-in-path") {
4147
+ p3.log.warn("Playwright CLI: instalado en el directorio de pnpm, pero fuera del PATH de esta terminal. Abre una terminal nueva tras pnpm setup; no hace falta reinstalarlo.");
4148
+ problems++;
4018
4149
  } else if (playwright.status === "missing") {
4019
4150
  const target = playwright.missing === "package" ? "el paquete global" : "el navegador de Playwright";
4020
4151
  p3.log.warn(`Playwright CLI: habilitado, pero falta ${target} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
@@ -5340,16 +5471,16 @@ import { createHash as createHash2 } from "crypto";
5340
5471
  var pi_runtime_pin_default = {
5341
5472
  package: {
5342
5473
  name: "jorgex-pi",
5343
- version: "0.8.13",
5344
- source: "npm:jorgex-pi@0.8.13"
5474
+ version: "0.8.15",
5475
+ source: "npm:jorgex-pi@0.8.15"
5345
5476
  },
5346
5477
  provenance: {
5347
- commit: "e28a677da0a112925251cf11269b70cb5c8aa15e"
5478
+ commit: "9d619e205685df0b8b122aa24b81e8694182e3ca"
5348
5479
  },
5349
5480
  tarball: {
5350
- bytes: 89143404,
5351
- sha256: "6bbe877249b900af582c8881e0dd7993951ea1cef6e7252522c626c078de7478",
5352
- sha512: "bb527d1f3812406f2184f778b22cca00ead33692fecac0012cf8cdcdc469b7cccf3c789c35b4327dd599d06542162bd07eefeeaaa40de19662ec5fc422b14af8"
5481
+ bytes: 89145074,
5482
+ sha256: "a866da924e0d7ad2b5a5ef285671c06b742d5b64cd62d463a8a39d8e7b5d4453",
5483
+ sha512: "ac5a70bcf336bd28eccb06360a8874c2a3bb2887fe420b6f97a0b61ca1b1c97d2db26fd28033fa296ab273423b1b5884d123dfc56ad1d6e274aefd828d9d25c6"
5353
5484
  }
5354
5485
  };
5355
5486
 
@@ -5372,6 +5503,7 @@ var PI_RUNTIME_CANDIDATE = {
5372
5503
  "mcp-adapter-v1",
5373
5504
  "engram-runtime-tools-v1",
5374
5505
  "chrome-devtools-handoff-v1",
5506
+ "playwright-handoff-v1",
5375
5507
  "runner-json-v1",
5376
5508
  "tui-branding-v1",
5377
5509
  "managed-primary-model-v1",
@@ -5480,9 +5612,12 @@ function normalizeInstalledSource(settingsJson, alias, canonical) {
5480
5612
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
5481
5613
  const packages = Reflect.get(parsed, "packages");
5482
5614
  if (!Array.isArray(packages)) return null;
5483
- const hasCanonical = packages.some((entry) => entry === canonical || entry !== null && typeof entry === "object" && !Array.isArray(entry) && Reflect.get(entry, "source") === canonical);
5484
- if (packages.filter((entry) => entry === alias).length !== 1 || hasCanonical) return null;
5485
- Reflect.set(parsed, "packages", packages.map((entry) => entry === alias ? canonical : entry));
5615
+ const sourceOf = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) ? Reflect.get(entry, "source") : entry;
5616
+ if (packages.filter((entry) => sourceOf(entry) === alias).length !== 1 || packages.some((entry) => sourceOf(entry) === canonical)) return null;
5617
+ Reflect.set(parsed, "packages", packages.map((entry) => {
5618
+ if (sourceOf(entry) !== alias) return entry;
5619
+ return typeof entry === "string" ? canonical : { ...entry, source: canonical };
5620
+ }));
5486
5621
  return JSON.stringify(parsed);
5487
5622
  } catch {
5488
5623
  return null;
@@ -5943,14 +6078,18 @@ function managedPackageResult(result) {
5943
6078
  return result;
5944
6079
  }
5945
6080
  async function runManagedPiSystem(input) {
5946
- const { devtoolsMcpEnabled: explicitDevtools, ...runtimeInput } = input;
6081
+ const { devtoolsMcpEnabled: explicitDevtools, playwrightCliEnabled: explicitPlaywright, ...runtimeInput } = input;
5947
6082
  const devtoolsMcpEnabled = explicitDevtools ?? (input.targetDir === void 0 && loadDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi"));
5948
- const playwrightCliEnabled = input.targetDir === void 0 && loadPlaywrightCliPreference() === true;
6083
+ const supportsPlaywright = PI_RUNTIME_CANDIDATE.contract.capabilities.includes("playwright-handoff-v1");
6084
+ const playwrightCliEnabled = input.targetDir === void 0 && supportsPlaywright && (explicitPlaywright ?? loadPlaywrightCliPreference(void 0, "pi") === true);
6085
+ const playwrightCliCommand = playwrightCliEnabled && input.operation !== "uninstall" && input.operation !== "models" ? detectPlaywrightCli().binPath : null;
5949
6086
  const projectionInput = {
5950
6087
  targetDir: input.targetDir,
5951
6088
  packageSource: PI_RUNTIME_CANDIDATE.package.source,
5952
6089
  engramBin: input.engramBin,
5953
6090
  playwrightCliEnabled,
6091
+ playwrightHandoffEnabled: playwrightCliEnabled,
6092
+ playwrightCliCommand,
5954
6093
  devtoolsMcpEnabled,
5955
6094
  pnpmBin: devtoolsMcpEnabled && input.operation !== "uninstall" ? resolvePnpmBin() : null
5956
6095
  };
@@ -5980,6 +6119,9 @@ async function runManagedPiSystem(input) {
5980
6119
  if (input.targetDir === void 0 && explicitDevtools !== void 0 && (input.operation === "install" || input.operation === "sync") && result.kind !== "blocked") {
5981
6120
  saveDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi", explicitDevtools);
5982
6121
  }
6122
+ if (input.targetDir === void 0 && supportsPlaywright && explicitPlaywright !== void 0 && (input.operation === "install" || input.operation === "sync") && result.kind !== "blocked") {
6123
+ savePlaywrightCliPreference(playwrightCliPreferenceFile(), true, { pi: explicitPlaywright });
6124
+ }
5983
6125
  return result;
5984
6126
  }
5985
6127
 
@@ -6472,6 +6614,7 @@ var VERSION = readPackageVersion();
6472
6614
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall", "quality"];
6473
6615
  var QUALITY_REJECTED_VALUE_FLAGS = /* @__PURE__ */ new Set([
6474
6616
  "--agents",
6617
+ "--playwright-runtimes",
6475
6618
  "-a",
6476
6619
  "--target-dir",
6477
6620
  "--mode",
@@ -6557,7 +6700,13 @@ function parseFlags(args, allowReceipt = false) {
6557
6700
  flags.subagentConcurrency = value ?? "";
6558
6701
  i = nextIndex;
6559
6702
  } else if (arg.startsWith("--subagent-concurrency=")) flags.subagentConcurrency = arg.slice(23);
6560
- else if (arg === "--dry-run") flags.dryRun = true;
6703
+ else if (arg === "--playwright-runtimes") {
6704
+ const [value, nextIndex] = readValue(i);
6705
+ flags.playwrightRuntimes = (value ?? "").split(",").filter(Boolean);
6706
+ i = nextIndex;
6707
+ } else if (arg.startsWith("--playwright-runtimes=")) {
6708
+ flags.playwrightRuntimes = arg.slice("--playwright-runtimes=".length).split(",").filter(Boolean);
6709
+ } else if (arg === "--dry-run") flags.dryRun = true;
6561
6710
  else if (arg === "--yes" || arg === "-y") flags.yes = true;
6562
6711
  else if (arg === "--help" || arg === "-h") flags.help = true;
6563
6712
  else if (arg === "--version" || arg === "-v") flags.version = true;
@@ -6635,8 +6784,25 @@ Corrige o borra ${preferenceFile}, o vuelve a ejecutar con --mode human|programm
6635
6784
  subagentConcurrency: concurrency
6636
6785
  };
6637
6786
  }
6638
- async function resolvePlaywrightToolConsent(command, flags) {
6787
+ async function resolvePlaywrightToolConsent(command, flags, runtimes) {
6639
6788
  const interactive = Boolean(process.stdout.isTTY);
6789
+ const supported = runtimes.filter((runtime) => runtime !== "pi" || PI_RUNTIME_CANDIDATE.contract.capabilities.includes("playwright-handoff-v1"));
6790
+ if (flags.playwrightRuntimes !== void 0) {
6791
+ const requested = flags.playwrightRuntimes;
6792
+ let error;
6793
+ if (requested.length === 0) error = "--playwright-runtimes requiere al menos un runtime.";
6794
+ for (const runtime of requested) {
6795
+ if (!["opencode", "claude-code", "codex", "pi"].includes(runtime)) error = `Runtime Playwright desconocido: ${runtime}.`;
6796
+ else if (!runtimes.includes(runtime)) error = `El runtime ${runtime} no est\xE1 en --agents/destinos de esta instalaci\xF3n.`;
6797
+ else if (!supported.includes(runtime)) error = "Pi requiere un candidato con playwright-handoff-v1 para activar Playwright.";
6798
+ if (error) break;
6799
+ }
6800
+ if (error) {
6801
+ console.error(error);
6802
+ process.exitCode = 1;
6803
+ return null;
6804
+ }
6805
+ }
6640
6806
  let confirmed = false;
6641
6807
  if (command === "install" && interactive && !flags.yes && !flags.dryRun && flags.targetDir === void 0) {
6642
6808
  const answer = await p6.confirm({
@@ -6646,13 +6812,33 @@ async function resolvePlaywrightToolConsent(command, flags) {
6646
6812
  if (p6.isCancel(answer)) return null;
6647
6813
  confirmed = answer === true;
6648
6814
  }
6815
+ let runtimeSelection;
6816
+ const approved = interactive && !flags.yes ? confirmed : flags.yes && flags.playwright;
6817
+ if (command === "install" && approved && supported.length > 0) {
6818
+ let selected = flags.playwrightRuntimes ?? supported;
6819
+ if (interactive && !flags.yes && !flags.dryRun && flags.targetDir === void 0 && flags.playwrightRuntimes === void 0) {
6820
+ const answer = await p6.multiselect({
6821
+ message: "\xBFEn qu\xE9 runtimes activar la gu\xEDa de Playwright? (instalaci\xF3n global compartida)",
6822
+ required: false,
6823
+ options: supported.map((runtime) => ({ value: runtime, label: runtime === "pi" ? "Pi" : ADAPTERS[runtime]?.name ?? runtime })),
6824
+ initialValues: supported.filter((runtime) => loadPlaywrightCliPreference(void 0, runtime) === true)
6825
+ });
6826
+ if (p6.isCancel(answer)) return null;
6827
+ selected = answer;
6828
+ }
6829
+ runtimeSelection = Object.fromEntries(supported.map((runtime) => [runtime, selected.includes(runtime)]));
6830
+ }
6831
+ if (command === "install" && approved && runtimes.includes("pi") && !supported.includes("pi")) {
6832
+ p6.log.info("La activaci\xF3n de Playwright en Pi requiere la pr\xF3xima adopci\xF3n del paquete Pi; el binario global s\xED puede instalarse.");
6833
+ }
6649
6834
  return {
6650
6835
  command,
6651
6836
  interactive,
6652
6837
  yes: flags.yes,
6653
6838
  targetDir: flags.targetDir !== void 0,
6654
6839
  explicitToolSelection: flags.playwright,
6655
- confirmed
6840
+ confirmed,
6841
+ ...runtimeSelection === void 0 ? {} : { runtimeSelection }
6656
6842
  };
6657
6843
  }
6658
6844
  async function resolveDevtoolsMcpSelection(command, flags, runtimes) {
@@ -6743,7 +6929,7 @@ async function resolveHostEngramForInstall(flags) {
6743
6929
  return { ok: false, message: `Engram: ${error instanceof Error ? error.message : String(error)}` };
6744
6930
  }
6745
6931
  }
6746
- async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled) {
6932
+ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled, playwrightCliEnabled) {
6747
6933
  if (targetDir === void 0 && operation !== "models") {
6748
6934
  const preferenceErrors = browserPreferenceErrors();
6749
6935
  if (preferenceErrors.length > 0) {
@@ -6786,7 +6972,8 @@ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBi
6786
6972
  targetDir,
6787
6973
  detected: { executable: detected.executable, version: detected.version },
6788
6974
  engramBin,
6789
- ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled }
6975
+ ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled },
6976
+ ...playwrightCliEnabled === void 0 ? {} : { playwrightCliEnabled }
6790
6977
  });
6791
6978
  if (result.kind === "blocked") {
6792
6979
  const paths = "paths" in result ? `: ${result.paths.join(", ")}` : "";
@@ -6827,6 +7014,7 @@ Opciones:
6827
7014
  --no-devtools (install/sync) desactiva Chrome DevTools MCP (incompatible con --devtools)
6828
7015
  --remove-engram (uninstall) desregistra Engram de los runtimes;
6829
7016
  memorias y binario quedan intactos igualmente
7017
+ --playwright-runtimes <csv> Activa su gu\xEDa s\xF3lo en estos runtimes de --agents (con --playwright)
6830
7018
  --remove-playwright (uninstall) retira solo el paquete global de Playwright;
6831
7019
  nunca perfiles, cach\xE9 ni navegadores
6832
7020
  --receipt <path> (quality) escribe el receipt en ese path de forma at\xF3mica
@@ -6861,6 +7049,11 @@ Flags disponibles: jorgex-stack --help`
6861
7049
  process.exitCode = 1;
6862
7050
  return;
6863
7051
  }
7052
+ if (flags.playwrightRuntimes !== void 0 && (command !== "install" || !flags.playwright)) {
7053
+ console.error("--playwright-runtimes requiere install --playwright.");
7054
+ process.exitCode = 1;
7055
+ return;
7056
+ }
6864
7057
  if (command !== "quality" && flags.targetDir !== void 0 && flags.agents.length !== 1) {
6865
7058
  console.error("--target-dir requiere exactamente un runtime en --agents.");
6866
7059
  process.exitCode = 1;
@@ -6927,8 +7120,11 @@ Flags disponibles: jorgex-stack --help`
6927
7120
  exitCode = process.exitCode === 1 ? 1 : 0;
6928
7121
  return;
6929
7122
  }
6930
- const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
6931
- if (playwrightToolConsent === null) return;
7123
+ const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags, runtimes);
7124
+ if (playwrightToolConsent === null) {
7125
+ exitCode = process.exitCode === 1 ? 1 : 0;
7126
+ return;
7127
+ }
6932
7128
  if (!await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes)) {
6933
7129
  exitCode = 1;
6934
7130
  return;
@@ -6980,7 +7176,8 @@ Flags disponibles: jorgex-stack --help`
6980
7176
  flags.targetDir,
6981
7177
  flags.yes,
6982
7178
  flags.targetDir === void 0 ? engramBin : void 0,
6983
- devtoolsMcpSelection.pi
7179
+ devtoolsMcpSelection.pi,
7180
+ flags.targetDir === void 0 && exitCode === 0 && resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0 ? playwrightToolConsent.runtimeSelection?.pi : void 0
6984
7181
  ));
6985
7182
  }
6986
7183
  completed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.31",
3
+ "version": "1.9.33",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",