jorgex-stack 1.9.32 → 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.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/cli.js +140 -84
  3. package/package.json +1 -1
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 global Playwright package and Chromium cache are shared by the machine; `--playwright-runtimes` controls which runtime receives the guide. Pi is selectable only when the adopted package declares `playwright-handoff-v1`; otherwise Stack reports that it can install the global capability but cannot project it into Pi yet. 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
 
@@ -240,6 +248,8 @@ La integración de este App pertenece al release del repositorio Stack. No conce
240
248
 
241
249
  Requirements: Node >= 22.5 and pnpm (never npm).
242
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
+
243
253
  ```
244
254
  pnpm install
245
255
  pnpm build # tsup -> dist/
package/dist/cli.js CHANGED
@@ -2826,7 +2826,11 @@ async function runInstall(opts) {
2826
2826
  const pnpmBin = resolvePnpmBin();
2827
2827
  return executePlaywrightToolAction2(action, pnpmBin, env);
2828
2828
  },
2829
- persistEnabled: (enabled) => savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled, opts.playwrightToolConsent?.runtimeSelection),
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
+ },
2830
2834
  setupPnpm: (pnpmBin) => setupPnpmGlobal(pnpmBin)
2831
2835
  };
2832
2836
  let setupAttempted = false;
@@ -3357,6 +3361,10 @@ function runPiPackageManagedOperation(input, deps) {
3357
3361
  }
3358
3362
 
3359
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
+ }
3360
3368
  var preparedPiProjectionUninstalls = /* @__PURE__ */ new WeakMap();
3361
3369
  function projectionScope(scope) {
3362
3370
  return {
@@ -3403,18 +3411,27 @@ function projectionPlan(input, scope) {
3403
3411
  if (input.devtoolsMcpEnabled && input.pnpmBin) {
3404
3412
  const server = loadCanonicalMcp(input.stackDir).servers[DEVTOOLS_MCP_SERVER];
3405
3413
  if (server?.transport !== "stdio" || !Array.isArray(server.args)) throw new Error("Falta la configuraci\xF3n can\xF3nica de DevTools.");
3406
- actions.push({ kind: "write", target: devtoolsPath(scope), content: `${JSON.stringify({
3414
+ actions.push({ kind: "write", target: handoffPath(scope, "devtools"), content: `${JSON.stringify({
3407
3415
  schemaVersion: 1,
3408
3416
  enabled: true,
3409
3417
  command: input.pnpmBin,
3410
3418
  args: server.args
3411
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)}
3412
3429
  ` });
3413
3430
  }
3414
3431
  return actions;
3415
3432
  }
3416
- function devtoolsPath(scope) {
3417
- 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`);
3418
3435
  }
3419
3436
  function contentHash(content) {
3420
3437
  return createHash("sha256").update(content).digest("hex");
@@ -3425,13 +3442,14 @@ function canonicalActionContent(action) {
3425
3442
  function hasActionDrift(action, deps) {
3426
3443
  return deps.readText(action.target) !== canonicalActionContent(action);
3427
3444
  }
3428
- function applyActions(actions, deps, handoff) {
3445
+ function applyActions(actions, deps, handoffs = []) {
3429
3446
  for (const action of actions) {
3430
- if (handoff && path23.resolve(action.target) === handoff.file) {
3447
+ const handoff = handoffs.find((entry) => path23.resolve(action.target) === entry.file);
3448
+ if (handoff) {
3431
3449
  try {
3432
- 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);
3433
3451
  } catch {
3434
- return devtoolsConflict(handoff.file);
3452
+ return handoffConflict(handoff.kind, handoff.file);
3435
3453
  }
3436
3454
  }
3437
3455
  if (action.kind === "write") deps.writeText(action.target, action.content);
@@ -3450,8 +3468,7 @@ function receiptFor(plan, scope) {
3450
3468
  const owned = [...new Set(
3451
3469
  plan.map((action) => path23.resolve(action.target)).filter((target) => target !== systemPrompt)
3452
3470
  )];
3453
- const handoff = plan.find((action) => path23.resolve(action.target) === devtoolsPath(scope));
3454
- return {
3471
+ const receipt = {
3455
3472
  schemaVersion: 1,
3456
3473
  scope: {
3457
3474
  kind: scope.kind,
@@ -3459,9 +3476,13 @@ function receiptFor(plan, scope) {
3459
3476
  codingAgentDir: scope.codingAgentDir,
3460
3477
  receiptFile: scope.receiptFile
3461
3478
  },
3462
- owned,
3463
- ...handoff ? { devtools: { sha256: contentHash(canonicalActionContent(handoff)) } } : {}
3479
+ owned
3464
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;
3465
3486
  }
3466
3487
  function receiptContent(receipt) {
3467
3488
  return `${JSON.stringify(receipt, null, 2)}
@@ -3479,25 +3500,24 @@ function parseReceipt2(raw, expected) {
3479
3500
  const version = Reflect.get(receipt, "schemaVersion");
3480
3501
  const receivedScope = Reflect.get(receipt, "scope");
3481
3502
  const owned = Reflect.get(receipt, "owned");
3482
- const devtools = Reflect.get(receipt, "devtools");
3483
- const hasDevtools = Object.hasOwn(receipt, "devtools");
3484
- 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"])) {
3485
3505
  return null;
3486
3506
  }
3487
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)) {
3488
3508
  return null;
3489
3509
  }
3490
- 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;
3491
- const handoff = devtoolsPath(expected.scope);
3492
- const expectedOwned = expected.owned.filter((file) => file !== handoff);
3493
- 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));
3494
3519
  if (owned.length !== expectedOwned.length || !owned.every((file, index) => typeof file === "string" && file === expectedOwned[index])) return null;
3495
- return {
3496
- schemaVersion: 1,
3497
- scope: expected.scope,
3498
- owned: expectedOwned,
3499
- ...hasDevtools ? { devtools: { sha256: Reflect.get(devtools, "sha256") } } : {}
3500
- };
3520
+ return { schemaVersion: 1, scope: expected.scope, owned: expectedOwned, ...digests };
3501
3521
  } catch {
3502
3522
  return null;
3503
3523
  }
@@ -3623,34 +3643,39 @@ function readProjectionReceipt(receiptFile, deps) {
3623
3643
  return { kind: "unreadable" };
3624
3644
  }
3625
3645
  }
3626
- function devtoolsConflict(file) {
3646
+ function handoffConflict(kind, file) {
3647
+ const label = kind === "devtools" ? "DevTools" : "Playwright";
3627
3648
  return blocked2(
3628
- "projection-devtools-conflict",
3649
+ `projection-${kind}-conflict`,
3629
3650
  [file],
3630
- "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.`
3631
3652
  );
3632
3653
  }
3633
- function inspectDevtools(input, scope, expected, deps) {
3634
- const file = devtoolsPath(scope);
3654
+ function inspectHandoffs(input, scope, expected, deps) {
3635
3655
  const read = readProjectionReceipt(scope.receiptFile, deps);
3636
3656
  if (read.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3637
3657
  const previous = read.kind === "present" ? parseReceipt2(read.content, expected) : null;
3638
- let content;
3639
- try {
3640
- content = deps.readText(file);
3641
- } catch {
3642
- return devtoolsConflict(file);
3643
- }
3644
- let recordedDevtools = false;
3645
- if (read.kind === "present") {
3658
+ const handoffs = [];
3659
+ for (const kind of HANDOFF_KINDS) {
3660
+ const file = handoffPath(scope, kind);
3661
+ let content;
3646
3662
  try {
3647
- recordedDevtools = Object.hasOwn(JSON.parse(read.content), "devtools");
3663
+ content = deps.readText(file);
3648
3664
  } catch {
3665
+ return handoffConflict(kind, file);
3649
3666
  }
3650
- 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 });
3651
3677
  }
3652
- if (content !== null && (!previous?.devtools || contentHash(content) !== previous.devtools.sha256)) return devtoolsConflict(file);
3653
- return { kind: "checked", previous, content };
3678
+ return { kind: "checked", previous, handoffs };
3654
3679
  }
3655
3680
  function preparePiProjectionUninstall(input, deps) {
3656
3681
  if (input.operation !== "uninstall") {
@@ -3687,14 +3712,19 @@ function preparePiProjectionUninstall(input, deps) {
3687
3712
  receiptFile = path23.resolve(scope.receiptFile);
3688
3713
  backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
3689
3714
  }
3690
- const handoff = devtoolsPath(scope);
3691
- let handoffContent;
3692
- try {
3693
- handoffContent = deps.readText(handoff);
3694
- } catch {
3695
- 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 });
3696
3727
  }
3697
- if (handoffContent !== null && (!ownedReceipt?.devtools || contentHash(handoffContent) !== ownedReceipt.devtools.sha256)) return devtoolsConflict(handoff);
3698
3728
  const backup = backupExisting(backupTargets, deps);
3699
3729
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3700
3730
  const token = {};
@@ -3702,7 +3732,7 @@ function preparePiProjectionUninstall(input, deps) {
3702
3732
  prompt: promptUpdate,
3703
3733
  ownedToRemove,
3704
3734
  receiptFile,
3705
- ...ownedReceipt?.devtools ? { devtools: { file: handoff, sha256: ownedReceipt.devtools.sha256 } } : {}
3735
+ handoffs
3706
3736
  });
3707
3737
  return { kind: "prepared", plan: token };
3708
3738
  }
@@ -3714,14 +3744,14 @@ function completePiProjectionUninstall(plan, deps) {
3714
3744
  if (prepared === void 0) {
3715
3745
  return cleanupFailure([]);
3716
3746
  }
3717
- if (prepared.devtools) {
3747
+ for (const handoff of prepared.handoffs) {
3718
3748
  let current;
3719
3749
  try {
3720
- current = deps.readText(prepared.devtools.file);
3750
+ current = deps.readText(handoff.file);
3721
3751
  } catch {
3722
- return devtoolsConflict(prepared.devtools.file);
3752
+ return handoffConflict(handoff.kind, handoff.file);
3723
3753
  }
3724
- 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);
3725
3755
  }
3726
3756
  if (prepared.prompt !== null) {
3727
3757
  try {
@@ -3732,9 +3762,10 @@ function completePiProjectionUninstall(plan, deps) {
3732
3762
  }
3733
3763
  for (const owned of prepared.ownedToRemove) {
3734
3764
  try {
3735
- if (prepared.devtools?.file === owned) {
3765
+ const handoff = prepared.handoffs.find((entry) => entry.file === owned);
3766
+ if (handoff) {
3736
3767
  const current = deps.readText(owned);
3737
- if (current !== null && contentHash(current) !== prepared.devtools.sha256) return devtoolsConflict(owned);
3768
+ if (current !== null && contentHash(current) !== handoff.sha256) return handoffConflict(handoff.kind, owned);
3738
3769
  }
3739
3770
  deps.removeFile(owned);
3740
3771
  } catch {
@@ -3757,22 +3788,27 @@ function runPiProjectionLifecycle(input, deps) {
3757
3788
  return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
3758
3789
  }
3759
3790
  if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path23.isAbsolute(input.pnpmBin))) {
3760
- 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.");
3761
3795
  }
3762
3796
  const plan = projectionPlan(input, scope);
3763
3797
  assertPlanContained(plan, scope);
3764
3798
  const receipt = receiptFor(plan, scope);
3765
- const devtools = inspectDevtools(input, scope, receipt, deps);
3766
- if (devtools.kind === "blocked") return input.operation === "doctor" ? { kind: "drift", paths: devtools.paths } : devtools;
3767
- 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);
3768
3802
  const expectedReceipt2 = receiptContent(receipt);
3769
3803
  const drifted = plan.filter((action) => hasActionDrift(action, deps));
3770
- if (input.devtoolsMcpEnabled && devtools.previous?.devtools?.sha256 !== receipt.devtools?.sha256 && !drifted.some((action) => path23.resolve(action.target) === devtoolsPath(scope))) {
3771
- 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
+ }
3772
3808
  }
3773
3809
  if (input.operation === "doctor") {
3774
3810
  const paths = drifted.map((action) => path23.resolve(action.target));
3775
- if (removeHandoff) paths.push(devtoolsPath(scope));
3811
+ paths.push(...removedHandoffs.map(({ file }) => file));
3776
3812
  const currentSettings2 = deps.readText(scope.settingsFile);
3777
3813
  if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
3778
3814
  paths.push(scope.settingsFile);
@@ -3788,30 +3824,38 @@ function runPiProjectionLifecycle(input, deps) {
3788
3824
  }
3789
3825
  const packageWillChange = filteredSettings !== null && filteredSettings !== currentSettings;
3790
3826
  const receiptChanged = deps.readText(scope.receiptFile) !== expectedReceipt2;
3791
- if (drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff) {
3827
+ if (drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0) {
3792
3828
  const backup = backupExisting([
3793
3829
  ...drifted.map((action) => action.target),
3794
- ...removeHandoff ? [devtoolsPath(scope)] : [],
3830
+ ...removedHandoffs.map(({ file }) => file),
3795
3831
  ...packageWillChange ? [scope.settingsFile] : [],
3796
3832
  ...receiptChanged ? [scope.receiptFile] : []
3797
3833
  ], deps);
3798
3834
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3799
3835
  }
3800
- if (removeHandoff) {
3836
+ for (const { kind, file } of removedHandoffs) {
3837
+ try {
3838
+ const current = deps.readText(file);
3839
+ if (current !== null && contentHash(current) !== checked.previous?.[kind]?.sha256) return handoffConflict(kind, file);
3840
+ } catch {
3841
+ return handoffConflict(kind, file);
3842
+ }
3843
+ }
3844
+ for (const { kind, file } of removedHandoffs) {
3801
3845
  try {
3802
- const current = deps.readText(devtoolsPath(scope));
3803
- if (current !== null && contentHash(current) !== devtools.previous?.devtools?.sha256) return devtoolsConflict(devtoolsPath(scope));
3804
- deps.removeFile(devtoolsPath(scope));
3846
+ const current = deps.readText(file);
3847
+ if (current !== null && contentHash(current) !== checked.previous?.[kind]?.sha256) return handoffConflict(kind, file);
3848
+ deps.removeFile(file);
3805
3849
  } catch {
3806
- return cleanupFailure([devtoolsPath(scope)]);
3850
+ return cleanupFailure([file]);
3807
3851
  }
3808
3852
  }
3809
- const writeResult = applyActions(drifted, deps, input.devtoolsMcpEnabled ? { file: devtoolsPath(scope), previous: devtools.content } : void 0);
3853
+ const writeResult = applyActions(drifted, deps, checked.handoffs.filter(({ kind }) => handoffEnabled(input, kind)).map(({ kind, file, content }) => ({ kind, file, previous: content })));
3810
3854
  if (writeResult) return writeResult;
3811
3855
  if (packageWillChange && filteredSettings !== null) deps.writeText(scope.settingsFile, filteredSettings);
3812
3856
  if (receiptChanged) deps.writeText(scope.receiptFile, expectedReceipt2);
3813
3857
  if (input.operation === "install") return { kind: "installed", receipt };
3814
- return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff };
3858
+ return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0 };
3815
3859
  }
3816
3860
  function systemProjectionLifecycle(input) {
3817
3861
  const targetRoot = input.targetDir === void 0 ? null : path23.resolve(input.targetDir);
@@ -3830,7 +3874,9 @@ function systemProjectionLifecycle(input) {
3830
3874
  engramBin: input.engramBin ?? "",
3831
3875
  playwrightCliEnabled: input.playwrightCliEnabled,
3832
3876
  devtoolsMcpEnabled: input.devtoolsMcpEnabled,
3833
- pnpmBin: input.pnpmBin
3877
+ pnpmBin: input.pnpmBin,
3878
+ playwrightHandoffEnabled: input.playwrightHandoffEnabled,
3879
+ playwrightCliCommand: input.playwrightCliCommand
3834
3880
  },
3835
3881
  deps: {
3836
3882
  readText: readTextOnlyIfMissing,
@@ -5425,16 +5471,16 @@ import { createHash as createHash2 } from "crypto";
5425
5471
  var pi_runtime_pin_default = {
5426
5472
  package: {
5427
5473
  name: "jorgex-pi",
5428
- version: "0.8.13",
5429
- source: "npm:jorgex-pi@0.8.13"
5474
+ version: "0.8.15",
5475
+ source: "npm:jorgex-pi@0.8.15"
5430
5476
  },
5431
5477
  provenance: {
5432
- commit: "e28a677da0a112925251cf11269b70cb5c8aa15e"
5478
+ commit: "9d619e205685df0b8b122aa24b81e8694182e3ca"
5433
5479
  },
5434
5480
  tarball: {
5435
- bytes: 89143404,
5436
- sha256: "6bbe877249b900af582c8881e0dd7993951ea1cef6e7252522c626c078de7478",
5437
- sha512: "bb527d1f3812406f2184f778b22cca00ead33692fecac0012cf8cdcdc469b7cccf3c789c35b4327dd599d06542162bd07eefeeaaa40de19662ec5fc422b14af8"
5481
+ bytes: 89145074,
5482
+ sha256: "a866da924e0d7ad2b5a5ef285671c06b742d5b64cd62d463a8a39d8e7b5d4453",
5483
+ sha512: "ac5a70bcf336bd28eccb06360a8874c2a3bb2887fe420b6f97a0b61ca1b1c97d2db26fd28033fa296ab273423b1b5884d123dfc56ad1d6e274aefd828d9d25c6"
5438
5484
  }
5439
5485
  };
5440
5486
 
@@ -5457,6 +5503,7 @@ var PI_RUNTIME_CANDIDATE = {
5457
5503
  "mcp-adapter-v1",
5458
5504
  "engram-runtime-tools-v1",
5459
5505
  "chrome-devtools-handoff-v1",
5506
+ "playwright-handoff-v1",
5460
5507
  "runner-json-v1",
5461
5508
  "tui-branding-v1",
5462
5509
  "managed-primary-model-v1",
@@ -6031,14 +6078,18 @@ function managedPackageResult(result) {
6031
6078
  return result;
6032
6079
  }
6033
6080
  async function runManagedPiSystem(input) {
6034
- const { devtoolsMcpEnabled: explicitDevtools, ...runtimeInput } = input;
6081
+ const { devtoolsMcpEnabled: explicitDevtools, playwrightCliEnabled: explicitPlaywright, ...runtimeInput } = input;
6035
6082
  const devtoolsMcpEnabled = explicitDevtools ?? (input.targetDir === void 0 && loadDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi"));
6036
- const playwrightCliEnabled = input.targetDir === void 0 && loadPlaywrightCliPreference(void 0, "pi") === 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;
6037
6086
  const projectionInput = {
6038
6087
  targetDir: input.targetDir,
6039
6088
  packageSource: PI_RUNTIME_CANDIDATE.package.source,
6040
6089
  engramBin: input.engramBin,
6041
6090
  playwrightCliEnabled,
6091
+ playwrightHandoffEnabled: playwrightCliEnabled,
6092
+ playwrightCliCommand,
6042
6093
  devtoolsMcpEnabled,
6043
6094
  pnpmBin: devtoolsMcpEnabled && input.operation !== "uninstall" ? resolvePnpmBin() : null
6044
6095
  };
@@ -6068,6 +6119,9 @@ async function runManagedPiSystem(input) {
6068
6119
  if (input.targetDir === void 0 && explicitDevtools !== void 0 && (input.operation === "install" || input.operation === "sync") && result.kind !== "blocked") {
6069
6120
  saveDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi", explicitDevtools);
6070
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
+ }
6071
6125
  return result;
6072
6126
  }
6073
6127
 
@@ -6875,7 +6929,7 @@ async function resolveHostEngramForInstall(flags) {
6875
6929
  return { ok: false, message: `Engram: ${error instanceof Error ? error.message : String(error)}` };
6876
6930
  }
6877
6931
  }
6878
- async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled) {
6932
+ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled, playwrightCliEnabled) {
6879
6933
  if (targetDir === void 0 && operation !== "models") {
6880
6934
  const preferenceErrors = browserPreferenceErrors();
6881
6935
  if (preferenceErrors.length > 0) {
@@ -6918,7 +6972,8 @@ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBi
6918
6972
  targetDir,
6919
6973
  detected: { executable: detected.executable, version: detected.version },
6920
6974
  engramBin,
6921
- ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled }
6975
+ ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled },
6976
+ ...playwrightCliEnabled === void 0 ? {} : { playwrightCliEnabled }
6922
6977
  });
6923
6978
  if (result.kind === "blocked") {
6924
6979
  const paths = "paths" in result ? `: ${result.paths.join(", ")}` : "";
@@ -7121,7 +7176,8 @@ Flags disponibles: jorgex-stack --help`
7121
7176
  flags.targetDir,
7122
7177
  flags.yes,
7123
7178
  flags.targetDir === void 0 ? engramBin : void 0,
7124
- devtoolsMcpSelection.pi
7179
+ devtoolsMcpSelection.pi,
7180
+ flags.targetDir === void 0 && exitCode === 0 && resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0 ? playwrightToolConsent.runtimeSelection?.pi : void 0
7125
7181
  ));
7126
7182
  }
7127
7183
  completed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.32",
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",