jorgex-stack 1.9.28 → 1.9.30

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
@@ -56,7 +56,7 @@ pnpm dlx jorgex-stack uninstall # remove managed files; keep Engram data i
56
56
 
57
57
  For development from a clone, run the same commands through `pnpm cli <command>` (see [Development](#development)).
58
58
 
59
- Every command supports `--dry-run`, `--yes`, and `--target-dir <dir>` for testing without touching the real config. Writes create automatic backups and verify idempotency; merges into user config are surgical (marked markdown sections, JSON/TOML upserts), so user-owned content is never touched.
59
+ Every command supports `--dry-run`, `--yes`, and `--target-dir <dir>` for testing without touching the real config. Writes create automatic backups and verify idempotency; merges into user config are surgical (marked markdown sections, JSON/TOML upserts), so user-owned content is never touched. `--yes` does not authorize downloading missing Engram; use `--engram` for that explicit consent. The interactive install asks before installing it, while dry-run and target-dir never download it.
60
60
 
61
61
  Runtime defaults are documented in [docs/references/permissions.md](docs/references/permissions.md) for permissions and [docs/references/models.md](docs/references/models.md) for the Sol primary default, field-level ownership and independent subagent routing. The quality policy and `jorgex.quality.receipt` contract are documented in [docs/references/quality-receipt.md](docs/references/quality-receipt.md). OpenCode remains provider-agnostic for subagents; its primary defaults to the OpenAI OAuth model `openai/gpt-5.6-sol` unless the user replaces it.
62
62
 
@@ -120,7 +120,7 @@ pnpm dlx jorgex-stack@1.9.7 sync --agents pi
120
120
  pnpm dlx jorgex-stack@1.9.7 uninstall --agents pi
121
121
  ```
122
122
 
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. Chrome DevTools MCP and Context7 remain 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`. Both receipts are scope-bound and fail closed for manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state.
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.
124
124
 
125
125
  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
126
 
@@ -132,7 +132,7 @@ Install, sync and uninstall back up every managed file before changing it and ar
132
132
 
133
133
  The package owns Pi's native primary-model projection: `openai-codex/gpt-5.6-sol`, with a local `contextWindow` request of 872K. It merges only missing compatible fields, records field ownership in `PI_CODING_AGENT_DIR/jorgex-pi/sol-lifecycle.v1.json`, and cleanup removes only still-owned canonical values. Stack does not duplicate that package-owned settings/models logic. The 872K value is local OAuth metadata until a real long-context smoke test confirms backend acceptance; it is not the API context limit.
134
134
 
135
- Engram remains mandatory and user-owned. An existing binary is preserved. Interactive install may offer the native `brew`/`go`/release channel with explicit confirmation; `--yes` and non-TTY installs fail with a remedy when Engram is absent. The database and memories are never updated or deleted, and uninstall never deletes the Engram binary. Under `--target-dir`, Stack accepts only `<target>/bin/engram`, isolates Pi/Home/XDG/AppData/temp/npm-cache paths inside the target, and never consults the host Engram or Pi configuration.
135
+ Engram remains mandatory and user-owned. An existing binary is preserved. `install` resolves or installs the verified official v1.20.0 release before configuring any selected runtime; use `--engram` to authorize that download in non-interactive flows. The release installer writes only `~/.local/bin/engram` (or the platform equivalent), checks the pinned size and SHA-256, and does not use Brew or Go. `sync`, dry-run and `--target-dir` never download it. The database and memories are never updated or deleted, and uninstall never deletes the Engram binary. Under `--target-dir`, Stack accepts only `<target>/bin/engram`, isolates Pi/Home/XDG/AppData/temp/npm-cache paths inside the target, and never consults the host Engram or Pi configuration.
136
136
 
137
137
  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
138
 
@@ -159,6 +159,8 @@ pnpm dlx jorgex-stack install --yes --playwright
159
159
  # Enable Chrome DevTools MCP explicitly per runtime.
160
160
  pnpm dlx jorgex-stack install --devtools
161
161
  pnpm dlx jorgex-stack install --no-devtools
162
+ pnpm dlx jorgex-stack install --agents pi --devtools
163
+ pnpm dlx jorgex-stack sync --agents pi --no-devtools
162
164
  ```
163
165
 
164
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.
package/dist/cli.d.ts CHANGED
@@ -21,6 +21,7 @@ interface Flags {
21
21
  list: boolean;
22
22
  check: boolean;
23
23
  removeEngram: boolean;
24
+ engram: boolean;
24
25
  playwright: boolean;
25
26
  removePlaywright: boolean;
26
27
  devtools: boolean;
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  // src/cli.ts
17
17
  import * as p6 from "@clack/prompts";
18
- import fs24 from "fs";
18
+ import fs25 from "fs";
19
19
  import { pathToFileURL as pathToFileURL2 } from "url";
20
20
 
21
21
  // src/install.ts
@@ -2154,7 +2154,7 @@ var PLAYWRIGHT_CLI = {
2154
2154
  version: "0.1.18",
2155
2155
  browserInstallAction: "install-browser"
2156
2156
  };
2157
- var PNPM_GLOBAL_BIN_REMEDY = "Ejecuta 'pnpm setup', abre una terminal nueva y reintenta. El stack no modifica PNPM_HOME ni PATH.";
2157
+ var PNPM_GLOBAL_BIN_REMEDY = "Ejecuta 'pnpm setup', abre una terminal nueva y reintenta. Sin consentimiento, el stack no modifica la configuraci\xF3n de la shell.";
2158
2158
  function resolvePnpmFailureRemedy(reason) {
2159
2159
  switch (reason) {
2160
2160
  case "pnpm-unavailable":
@@ -2197,6 +2197,34 @@ function resolvePnpmBin() {
2197
2197
  const pnpmCmd = lookPath("pnpm.cmd");
2198
2198
  return pnpmCmd !== null && !pnpmCmd.toLowerCase().endsWith(".ps1") ? pnpmCmd : null;
2199
2199
  }
2200
+ function resolvePnpmHome(env = process.env, platform = process.platform, homeDir = os.homedir()) {
2201
+ const paths = platform === "win32" ? path18.win32 : path18.posix;
2202
+ if (env.PNPM_HOME) return env.PNPM_HOME;
2203
+ if (env.XDG_DATA_HOME) return paths.join(env.XDG_DATA_HOME, "pnpm");
2204
+ if (platform === "darwin") return paths.join(homeDir, "Library", "pnpm");
2205
+ if (platform === "win32") return env.LOCALAPPDATA ? paths.join(env.LOCALAPPDATA, "pnpm") : paths.join(homeDir, ".pnpm");
2206
+ return paths.join(homeDir, ".local", "share", "pnpm");
2207
+ }
2208
+ function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform, homeDir = os.homedir()) {
2209
+ const pnpmHome = resolvePnpmHome(env, platform, homeDir);
2210
+ const paths = platform === "win32" ? path18.win32 : path18.posix;
2211
+ if (!paths.isAbsolute(pnpmHome)) return { ok: false, reason: "PNPM_HOME debe ser una ruta absoluta; corrige la configuraci\xF3n antes de reintentar." };
2212
+ const pathEntries = [pnpmHome, paths.join(pnpmHome, "bin")];
2213
+ if (env.PATH !== void 0) pathEntries.push(env.PATH);
2214
+ const childEnv = {
2215
+ ...env,
2216
+ PNPM_HOME: pnpmHome,
2217
+ PATH: pathEntries.join(platform === "win32" ? ";" : ":")
2218
+ };
2219
+ const invocation = planDetectedBinCommand(pnpmBin, ["setup"]);
2220
+ if (invocation === null) return { ok: false, reason: "No se pudo preparar la invocaci\xF3n de pnpm setup." };
2221
+ try {
2222
+ execFileSync2(invocation.command, invocation.args, { stdio: "inherit", env: childEnv });
2223
+ return { ok: true, env: childEnv };
2224
+ } catch (error) {
2225
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
2226
+ }
2227
+ }
2200
2228
  function isPlaywrightBrowserReady(env = process.env, platform = process.platform, homeDir = os.homedir()) {
2201
2229
  const configuredPath = env.PLAYWRIGHT_BROWSERS_PATH;
2202
2230
  const cacheDir = configuredPath ?? (platform === "win32" ? path18.join(env.LOCALAPPDATA ?? path18.join(homeDir, "AppData", "Local"), "ms-playwright") : platform === "darwin" ? path18.join(homeDir, "Library", "Caches", "ms-playwright") : path18.join(env.XDG_CACHE_HOME ?? path18.join(homeDir, ".cache"), "ms-playwright"));
@@ -2223,13 +2251,16 @@ function planPlaywrightCliCommand(action, pnpmBin) {
2223
2251
  return { command: pnpmBin, args: ["dlx", pinnedPackage, PLAYWRIGHT_CLI.browserInstallAction] };
2224
2252
  }
2225
2253
  }
2226
- function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin()) {
2254
+ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
2227
2255
  if (pnpmBin === null) return { ok: false, reason: "pnpm-unavailable" };
2228
2256
  if (action !== "install-browser") {
2229
2257
  const preflight = planDetectedBinCommand(pnpmBin, ["bin", "--global"]);
2230
2258
  if (preflight === null) return { ok: false, reason: "pnpm-command" };
2231
2259
  try {
2232
- execFileSync2(preflight.command, preflight.args, { stdio: "inherit" });
2260
+ execFileSync2(preflight.command, preflight.args, {
2261
+ stdio: "inherit",
2262
+ ...env === void 0 ? {} : { env }
2263
+ });
2233
2264
  } catch (error) {
2234
2265
  return { ok: false, reason: hasNonzeroProcessStatus(error) ? "pnpm-global-bin" : "pnpm-command" };
2235
2266
  }
@@ -2238,7 +2269,10 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin()) {
2238
2269
  const invocation = planDetectedBinCommand(command.command, command.args);
2239
2270
  if (invocation === null) return { ok: false, reason: "pnpm-command" };
2240
2271
  try {
2241
- execFileSync2(invocation.command, invocation.args, { stdio: "inherit" });
2272
+ execFileSync2(invocation.command, invocation.args, {
2273
+ stdio: "inherit",
2274
+ ...env === void 0 ? {} : { env }
2275
+ });
2242
2276
  return { ok: true };
2243
2277
  } catch {
2244
2278
  return { ok: false, reason: "action-failed" };
@@ -2307,7 +2341,7 @@ function parseDevtoolsMcpState(raw) {
2307
2341
  if (!isRecord2(value) || value.version !== DEVTOOLS_MCP_PREFERENCE_VERSION || !isRecord2(value.enabled) || !isRecord2(value.owned)) return null;
2308
2342
  const enabled = {};
2309
2343
  for (const [runtime, selected] of Object.entries(value.enabled)) {
2310
- if (!isRuntimeId(runtime) || typeof selected !== "boolean") return null;
2344
+ if (!isRuntimeId(runtime) && runtime !== "pi" || typeof selected !== "boolean") return null;
2311
2345
  enabled[runtime] = selected;
2312
2346
  }
2313
2347
  const owned = {};
@@ -2439,8 +2473,8 @@ var ADAPTERS = {
2439
2473
  "claude-code": claudeCodeAdapter,
2440
2474
  codex: codexAdapter
2441
2475
  };
2442
- function executePlaywrightToolAction2(action) {
2443
- return executePlaywrightToolAction(action, resolvePnpmBin());
2476
+ function executePlaywrightToolAction2(action, pnpmBin = resolvePnpmBin(), env) {
2477
+ return executePlaywrightToolAction(action, pnpmBin, env);
2444
2478
  }
2445
2479
  function resolvePlaywrightToolPlan(consent) {
2446
2480
  if (consent.command !== "install" || consent.targetDir) return { actions: [] };
@@ -2560,15 +2594,16 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
2560
2594
  return { targets, complete, warnings };
2561
2595
  }
2562
2596
  async function runInstall(opts) {
2563
- p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
2597
+ const showSummary = opts.showSummary !== false;
2598
+ if (showSummary) p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
2564
2599
  const stackDir = stackRoot();
2565
- const engramBin = detectEngram();
2600
+ const engramBin = opts.engramBin === void 0 ? detectEngram() : opts.engramBin;
2566
2601
  const modePreference = opts.mode === void 0 ? opts.targetDir === void 0 ? loadInstallModePreference() : DEFAULT_INSTALL_MODE_PREFERENCE : normalizeInstallModePreference(opts.mode);
2567
2602
  const useManifest = opts.targetDir === void 0;
2568
2603
  const preferenceErrors = useManifest ? [...browserPreferenceErrors(), primaryModelOwnershipError()].filter((error) => error !== null) : [];
2569
2604
  if (preferenceErrors.length > 0) {
2570
2605
  for (const error of preferenceErrors) p.log.error(error);
2571
- p.outro("Install cancelado: corrige el estado de configuraci\xF3n indicado arriba antes de reintentar.");
2606
+ if (showSummary) p.outro("Install cancelado: corrige el estado de configuraci\xF3n indicado arriba antes de reintentar.");
2572
2607
  return 1;
2573
2608
  }
2574
2609
  const toolPlan = opts.playwrightToolConsent === void 0 ? null : resolvePlaywrightToolPlan({
@@ -2708,9 +2743,41 @@ async function runInstall(opts) {
2708
2743
  if (opts.dryRun) {
2709
2744
  p.log.info("Playwright CLI: instalaci\xF3n global y navegador previstos (dry-run; no se ejecutan).");
2710
2745
  } else if (exitCode === 0) {
2711
- const result = await runPlaywrightToolPlan(toolPlan, opts.playwrightToolDeps ?? {
2712
- run: async (action) => executePlaywrightToolAction2(action),
2713
- persistEnabled: (enabled) => savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled)
2746
+ const baseDeps = opts.playwrightToolDeps ?? {
2747
+ run: async (action, env) => {
2748
+ const pnpmBin = resolvePnpmBin();
2749
+ return executePlaywrightToolAction2(action, pnpmBin, env);
2750
+ },
2751
+ persistEnabled: (enabled) => savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled),
2752
+ setupPnpm: (pnpmBin) => setupPnpmGlobal(pnpmBin)
2753
+ };
2754
+ let setupAttempted = false;
2755
+ let preparedEnv;
2756
+ const result = await runPlaywrightToolPlan(toolPlan, {
2757
+ ...baseDeps,
2758
+ run: async (action) => {
2759
+ const first = await baseDeps.run(action, preparedEnv);
2760
+ if (first === true || first === false || first.ok || first.reason !== "pnpm-global-bin") return first;
2761
+ if (setupAttempted || opts.dryRun || opts.targetDir !== void 0) return first;
2762
+ const consent = opts.playwrightToolConsent;
2763
+ const canAskForSetup = consent?.interactive === true && !opts.yes;
2764
+ if (!canAskForSetup) return first;
2765
+ setupAttempted = true;
2766
+ const accepted = await p.confirm({
2767
+ message: "pnpm no tiene directorio global. \xBFEjecutar 'pnpm setup' ahora? Esto modificar\xE1 la configuraci\xF3n de tu shell.",
2768
+ initialValue: false
2769
+ });
2770
+ if (p.isCancel(accepted) || !accepted) return first;
2771
+ const pnpmBin = resolvePnpmBin();
2772
+ if (pnpmBin === null) return first;
2773
+ const setup = baseDeps.setupPnpm?.(pnpmBin) ?? setupPnpmGlobal(pnpmBin);
2774
+ if (!setup.ok) {
2775
+ p.log.error(`pnpm setup fall\xF3: ${setup.reason}`);
2776
+ return first;
2777
+ }
2778
+ preparedEnv = setup.env;
2779
+ return baseDeps.run(action, preparedEnv);
2780
+ }
2714
2781
  });
2715
2782
  if (!result.ok) {
2716
2783
  const pnpmRemedy = result.reason === void 0 ? null : resolvePnpmFailureRemedy(result.reason);
@@ -2763,7 +2830,9 @@ async function runInstall(opts) {
2763
2830
  if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
2764
2831
  saveInstallModePreference(installModePreferenceFile(), modePreference);
2765
2832
  }
2766
- p.outro(opts.dryRun ? exitCode === 0 ? "Dry-run: no se ha escrito nada." : "Dry-run completado con errores (revisa arriba)." : exitCode === 0 ? "Hecho." : "Install completado con errores (revisa arriba).");
2833
+ if (showSummary) {
2834
+ p.outro(opts.dryRun ? exitCode === 0 ? "Dry-run: no se ha escrito nada." : "Dry-run completado con errores (revisa arriba)." : exitCode === 0 ? "Hecho." : "Install completado con errores (revisa arriba).");
2835
+ }
2767
2836
  return exitCode;
2768
2837
  }
2769
2838
 
@@ -2775,6 +2844,7 @@ import * as p2 from "@clack/prompts";
2775
2844
  // src/lib/pi-projection-lifecycle.ts
2776
2845
  import fs16 from "fs";
2777
2846
  import path23 from "path";
2847
+ import { createHash } from "crypto";
2778
2848
 
2779
2849
  // src/adapters/pi.ts
2780
2850
  import path21 from "path";
@@ -3238,14 +3308,33 @@ function projectionPlan(input, scope) {
3238
3308
  engramBin: input.engramBin,
3239
3309
  models: DEFAULT_MODEL_MAP.codex,
3240
3310
  warnings: [],
3241
- playwrightCliEnabled: input.playwrightCliEnabled
3311
+ playwrightCliEnabled: input.playwrightCliEnabled,
3312
+ enabledMcpServers: input.devtoolsMcpEnabled ? /* @__PURE__ */ new Set([DEVTOOLS_MCP_SERVER]) : void 0
3242
3313
  };
3243
3314
  const adapter = projectedPiAdapter(scope);
3244
- return [
3315
+ const actions = [
3245
3316
  ...planSystemPrompt(adapter, ctx),
3246
3317
  ...planSkills(adapter, ctx),
3247
3318
  ...planCommands(adapter, ctx)
3248
3319
  ];
3320
+ if (input.devtoolsMcpEnabled && input.pnpmBin) {
3321
+ const server = loadCanonicalMcp(input.stackDir).servers[DEVTOOLS_MCP_SERVER];
3322
+ if (server?.transport !== "stdio" || !Array.isArray(server.args)) throw new Error("Falta la configuraci\xF3n can\xF3nica de DevTools.");
3323
+ actions.push({ kind: "write", target: devtoolsPath(scope), content: `${JSON.stringify({
3324
+ schemaVersion: 1,
3325
+ enabled: true,
3326
+ command: input.pnpmBin,
3327
+ args: server.args
3328
+ }, null, 2)}
3329
+ ` });
3330
+ }
3331
+ return actions;
3332
+ }
3333
+ function devtoolsPath(scope) {
3334
+ return path23.resolve(scope.codingAgentDir, "jorgex-pi", "devtools.v1.json");
3335
+ }
3336
+ function contentHash(content) {
3337
+ return createHash("sha256").update(content).digest("hex");
3249
3338
  }
3250
3339
  function canonicalActionContent(action) {
3251
3340
  return action.kind === "write" ? action.content : fs16.readFileSync(action.source, "utf8");
@@ -3253,8 +3342,15 @@ function canonicalActionContent(action) {
3253
3342
  function hasActionDrift(action, deps) {
3254
3343
  return deps.readText(action.target) !== canonicalActionContent(action);
3255
3344
  }
3256
- function applyActions(actions, deps) {
3345
+ function applyActions(actions, deps, handoff) {
3257
3346
  for (const action of actions) {
3347
+ if (handoff && path23.resolve(action.target) === handoff.file) {
3348
+ try {
3349
+ if (deps.readText(handoff.file) !== handoff.previous) return devtoolsConflict(handoff.file);
3350
+ } catch {
3351
+ return devtoolsConflict(handoff.file);
3352
+ }
3353
+ }
3258
3354
  if (action.kind === "write") deps.writeText(action.target, action.content);
3259
3355
  else deps.copyFile(action.source, action.target);
3260
3356
  }
@@ -3271,6 +3367,7 @@ function receiptFor(plan, scope) {
3271
3367
  const owned = [...new Set(
3272
3368
  plan.map((action) => path23.resolve(action.target)).filter((target) => target !== systemPrompt)
3273
3369
  )];
3370
+ const handoff = plan.find((action) => path23.resolve(action.target) === devtoolsPath(scope));
3274
3371
  return {
3275
3372
  schemaVersion: 1,
3276
3373
  scope: {
@@ -3279,7 +3376,8 @@ function receiptFor(plan, scope) {
3279
3376
  codingAgentDir: scope.codingAgentDir,
3280
3377
  receiptFile: scope.receiptFile
3281
3378
  },
3282
- owned
3379
+ owned,
3380
+ ...handoff ? { devtools: { sha256: contentHash(canonicalActionContent(handoff)) } } : {}
3283
3381
  };
3284
3382
  }
3285
3383
  function receiptContent(receipt) {
@@ -3298,16 +3396,25 @@ function parseReceipt2(raw, expected) {
3298
3396
  const version = Reflect.get(receipt, "schemaVersion");
3299
3397
  const receivedScope = Reflect.get(receipt, "scope");
3300
3398
  const owned = Reflect.get(receipt, "owned");
3301
- if (version !== expected.schemaVersion || !hasExactKeys(receipt, ["schemaVersion", "scope", "owned"]) || receivedScope === null || typeof receivedScope !== "object" || Array.isArray(receivedScope) || !hasExactKeys(receivedScope, ["kind", "home", "codingAgentDir", "receiptFile"])) {
3302
- return null;
3303
- }
3304
- 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) || owned.length !== expected.owned.length) {
3399
+ const devtools = Reflect.get(receipt, "devtools");
3400
+ const hasDevtools = Object.hasOwn(receipt, "devtools");
3401
+ 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"])) {
3305
3402
  return null;
3306
3403
  }
3307
- if (!owned.every((file, index) => typeof file === "string" && file === expected.owned[index])) {
3404
+ 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)) {
3308
3405
  return null;
3309
3406
  }
3310
- return expected;
3407
+ 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;
3408
+ const handoff = devtoolsPath(expected.scope);
3409
+ const expectedOwned = expected.owned.filter((file) => file !== handoff);
3410
+ if (hasDevtools) expectedOwned.push(handoff);
3411
+ if (owned.length !== expectedOwned.length || !owned.every((file, index) => typeof file === "string" && file === expectedOwned[index])) return null;
3412
+ return {
3413
+ schemaVersion: 1,
3414
+ scope: expected.scope,
3415
+ owned: expectedOwned,
3416
+ ...hasDevtools ? { devtools: { sha256: Reflect.get(devtools, "sha256") } } : {}
3417
+ };
3311
3418
  } catch {
3312
3419
  return null;
3313
3420
  }
@@ -3433,6 +3540,35 @@ function readProjectionReceipt(receiptFile, deps) {
3433
3540
  return { kind: "unreadable" };
3434
3541
  }
3435
3542
  }
3543
+ function devtoolsConflict(file) {
3544
+ return blocked2(
3545
+ "projection-devtools-conflict",
3546
+ [file],
3547
+ "El handoff de DevTools es ajeno o fue modificado. Cons\xE9rvalo y revisa su propiedad antes de reintentar; Stack no lo sobrescribe ni elimina."
3548
+ );
3549
+ }
3550
+ function inspectDevtools(input, scope, expected, deps) {
3551
+ const file = devtoolsPath(scope);
3552
+ const read = readProjectionReceipt(scope.receiptFile, deps);
3553
+ if (read.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3554
+ const previous = read.kind === "present" ? parseReceipt2(read.content, expected) : null;
3555
+ let content;
3556
+ try {
3557
+ content = deps.readText(file);
3558
+ } catch {
3559
+ return devtoolsConflict(file);
3560
+ }
3561
+ let recordedDevtools = false;
3562
+ if (read.kind === "present") {
3563
+ try {
3564
+ recordedDevtools = Object.hasOwn(JSON.parse(read.content), "devtools");
3565
+ } catch {
3566
+ }
3567
+ if (!previous && (input.devtoolsMcpEnabled || content !== null || recordedDevtools)) return receiptInvalid(scope.receiptFile);
3568
+ }
3569
+ if (content !== null && (!previous?.devtools || contentHash(content) !== previous.devtools.sha256)) return devtoolsConflict(file);
3570
+ return { kind: "checked", previous, content };
3571
+ }
3436
3572
  function preparePiProjectionUninstall(input, deps) {
3437
3573
  if (input.operation !== "uninstall") {
3438
3574
  return cleanupFailure([]);
@@ -3451,11 +3587,13 @@ function preparePiProjectionUninstall(input, deps) {
3451
3587
  const receiptRead = readProjectionReceipt(scope.receiptFile, deps);
3452
3588
  if (receiptRead.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3453
3589
  let ownedToRemove = [];
3590
+ let ownedReceipt = null;
3454
3591
  let receiptFile = null;
3455
3592
  let backupTargets = promptUpdate === null ? [] : [promptUpdate.file];
3456
3593
  if (receiptRead.kind === "present") {
3457
3594
  const receipt = parseReceipt2(receiptRead.content, expectedReceipt2);
3458
3595
  if (receipt === null) return receiptInvalid(scope.receiptFile);
3596
+ ownedReceipt = receipt;
3459
3597
  let retained;
3460
3598
  try {
3461
3599
  retained = manifestOwned(deps.readManifest());
@@ -3466,13 +3604,22 @@ function preparePiProjectionUninstall(input, deps) {
3466
3604
  receiptFile = path23.resolve(scope.receiptFile);
3467
3605
  backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
3468
3606
  }
3607
+ const handoff = devtoolsPath(scope);
3608
+ let handoffContent;
3609
+ try {
3610
+ handoffContent = deps.readText(handoff);
3611
+ } catch {
3612
+ return devtoolsConflict(handoff);
3613
+ }
3614
+ if (handoffContent !== null && (!ownedReceipt?.devtools || contentHash(handoffContent) !== ownedReceipt.devtools.sha256)) return devtoolsConflict(handoff);
3469
3615
  const backup = backupExisting(backupTargets, deps);
3470
3616
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3471
3617
  const token = {};
3472
3618
  preparedPiProjectionUninstalls.set(token, {
3473
3619
  prompt: promptUpdate,
3474
3620
  ownedToRemove,
3475
- receiptFile
3621
+ receiptFile,
3622
+ ...ownedReceipt?.devtools ? { devtools: { file: handoff, sha256: ownedReceipt.devtools.sha256 } } : {}
3476
3623
  });
3477
3624
  return { kind: "prepared", plan: token };
3478
3625
  }
@@ -3484,6 +3631,15 @@ function completePiProjectionUninstall(plan, deps) {
3484
3631
  if (prepared === void 0) {
3485
3632
  return cleanupFailure([]);
3486
3633
  }
3634
+ if (prepared.devtools) {
3635
+ let current;
3636
+ try {
3637
+ current = deps.readText(prepared.devtools.file);
3638
+ } catch {
3639
+ return devtoolsConflict(prepared.devtools.file);
3640
+ }
3641
+ if (current !== null && contentHash(current) !== prepared.devtools.sha256) return devtoolsConflict(prepared.devtools.file);
3642
+ }
3487
3643
  if (prepared.prompt !== null) {
3488
3644
  try {
3489
3645
  deps.writeText(prepared.prompt.file, prepared.prompt.content);
@@ -3493,6 +3649,10 @@ function completePiProjectionUninstall(plan, deps) {
3493
3649
  }
3494
3650
  for (const owned of prepared.ownedToRemove) {
3495
3651
  try {
3652
+ if (prepared.devtools?.file === owned) {
3653
+ const current = deps.readText(owned);
3654
+ if (current !== null && contentHash(current) !== prepared.devtools.sha256) return devtoolsConflict(owned);
3655
+ }
3496
3656
  deps.removeFile(owned);
3497
3657
  } catch {
3498
3658
  return cleanupFailure([owned]);
@@ -3513,13 +3673,23 @@ function runPiProjectionLifecycle(input, deps) {
3513
3673
  const prepared = preparePiProjectionUninstall(input, deps);
3514
3674
  return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
3515
3675
  }
3676
+ if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path23.isAbsolute(input.pnpmBin))) {
3677
+ return blocked2("projection-devtools-command", [devtoolsPath(scope)], "DevTools requiere un ejecutable pnpm absoluto disponible en PATH.");
3678
+ }
3516
3679
  const plan = projectionPlan(input, scope);
3517
3680
  assertPlanContained(plan, scope);
3518
3681
  const receipt = receiptFor(plan, scope);
3682
+ const devtools = inspectDevtools(input, scope, receipt, deps);
3683
+ if (devtools.kind === "blocked") return input.operation === "doctor" ? { kind: "drift", paths: devtools.paths } : devtools;
3684
+ const removeHandoff = !input.devtoolsMcpEnabled && devtools.previous?.devtools !== void 0 && devtools.content !== null;
3519
3685
  const expectedReceipt2 = receiptContent(receipt);
3520
3686
  const drifted = plan.filter((action) => hasActionDrift(action, deps));
3687
+ if (input.devtoolsMcpEnabled && devtools.previous?.devtools?.sha256 !== receipt.devtools?.sha256 && !drifted.some((action) => path23.resolve(action.target) === devtoolsPath(scope))) {
3688
+ return input.operation === "doctor" ? { kind: "drift", paths: [devtoolsPath(scope)] } : devtoolsConflict(devtoolsPath(scope));
3689
+ }
3521
3690
  if (input.operation === "doctor") {
3522
3691
  const paths = drifted.map((action) => path23.resolve(action.target));
3692
+ if (removeHandoff) paths.push(devtoolsPath(scope));
3523
3693
  const currentSettings2 = deps.readText(scope.settingsFile);
3524
3694
  if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
3525
3695
  paths.push(scope.settingsFile);
@@ -3535,19 +3705,30 @@ function runPiProjectionLifecycle(input, deps) {
3535
3705
  }
3536
3706
  const packageWillChange = filteredSettings !== null && filteredSettings !== currentSettings;
3537
3707
  const receiptChanged = deps.readText(scope.receiptFile) !== expectedReceipt2;
3538
- if (drifted.length > 0 || packageWillChange || receiptChanged) {
3708
+ if (drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff) {
3539
3709
  const backup = backupExisting([
3540
3710
  ...drifted.map((action) => action.target),
3711
+ ...removeHandoff ? [devtoolsPath(scope)] : [],
3541
3712
  ...packageWillChange ? [scope.settingsFile] : [],
3542
3713
  ...receiptChanged ? [scope.receiptFile] : []
3543
3714
  ], deps);
3544
3715
  if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3545
3716
  }
3546
- applyActions(drifted, deps);
3717
+ if (removeHandoff) {
3718
+ try {
3719
+ const current = deps.readText(devtoolsPath(scope));
3720
+ if (current !== null && contentHash(current) !== devtools.previous?.devtools?.sha256) return devtoolsConflict(devtoolsPath(scope));
3721
+ deps.removeFile(devtoolsPath(scope));
3722
+ } catch {
3723
+ return cleanupFailure([devtoolsPath(scope)]);
3724
+ }
3725
+ }
3726
+ const writeResult = applyActions(drifted, deps, input.devtoolsMcpEnabled ? { file: devtoolsPath(scope), previous: devtools.content } : void 0);
3727
+ if (writeResult) return writeResult;
3547
3728
  if (packageWillChange && filteredSettings !== null) deps.writeText(scope.settingsFile, filteredSettings);
3548
3729
  if (receiptChanged) deps.writeText(scope.receiptFile, expectedReceipt2);
3549
3730
  if (input.operation === "install") return { kind: "installed", receipt };
3550
- return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged };
3731
+ return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removeHandoff };
3551
3732
  }
3552
3733
  function systemProjectionLifecycle(input) {
3553
3734
  const targetRoot = input.targetDir === void 0 ? null : path23.resolve(input.targetDir);
@@ -3564,7 +3745,9 @@ function systemProjectionLifecycle(input) {
3564
3745
  packageSource: input.packageSource,
3565
3746
  stackDir: stackRoot(),
3566
3747
  engramBin: input.engramBin ?? "",
3567
- playwrightCliEnabled: input.playwrightCliEnabled
3748
+ playwrightCliEnabled: input.playwrightCliEnabled,
3749
+ devtoolsMcpEnabled: input.devtoolsMcpEnabled,
3750
+ pnpmBin: input.pnpmBin
3568
3751
  },
3569
3752
  deps: {
3570
3753
  readText: readTextOnlyIfMissing,
@@ -5149,22 +5332,22 @@ import os4 from "os";
5149
5332
  import path31 from "path";
5150
5333
  import fs23 from "fs";
5151
5334
  import { spawnSync } from "child_process";
5152
- import { createHash } from "crypto";
5335
+ import { createHash as createHash2 } from "crypto";
5153
5336
 
5154
5337
  // src/lib/pi-runtime-pin.json
5155
5338
  var pi_runtime_pin_default = {
5156
5339
  package: {
5157
5340
  name: "jorgex-pi",
5158
- version: "0.8.12",
5159
- source: "npm:jorgex-pi@0.8.12"
5341
+ version: "0.8.13",
5342
+ source: "npm:jorgex-pi@0.8.13"
5160
5343
  },
5161
5344
  provenance: {
5162
- commit: "a041e8f290021b5a6bb161db77e03663f37c82cb"
5345
+ commit: "e28a677da0a112925251cf11269b70cb5c8aa15e"
5163
5346
  },
5164
5347
  tarball: {
5165
- bytes: 89141968,
5166
- sha256: "eb7739852504bf4cd8cdd9f260c67da979728378a756db7f1ff6b0c1d60368c6",
5167
- sha512: "a37b36e5c846e1694db3c7bd7e9acf23f39b8ba45d348bf039fd218f90f041a6d95268af2e3b6248fc4e759dfd6441df1c44490411c1e13c2b0cedbc0ef526b2"
5348
+ bytes: 89143404,
5349
+ sha256: "6bbe877249b900af582c8881e0dd7993951ea1cef6e7252522c626c078de7478",
5350
+ sha512: "bb527d1f3812406f2184f778b22cca00ead33692fecac0012cf8cdcdc469b7cccf3c789c35b4327dd599d06542162bd07eefeeaaa40de19662ec5fc422b14af8"
5168
5351
  }
5169
5352
  };
5170
5353
 
@@ -5186,6 +5369,7 @@ var PI_RUNTIME_CANDIDATE = {
5186
5369
  "goal-continuation-v1",
5187
5370
  "mcp-adapter-v1",
5188
5371
  "engram-runtime-tools-v1",
5372
+ "chrome-devtools-handoff-v1",
5189
5373
  "runner-json-v1",
5190
5374
  "tui-branding-v1",
5191
5375
  "managed-primary-model-v1",
@@ -5538,8 +5722,8 @@ function readOptional(file, fallback) {
5538
5722
  }
5539
5723
  function hashPiTarball(file) {
5540
5724
  const descriptor = fs23.openSync(file, "r");
5541
- const sha2562 = createHash("sha256");
5542
- const sha512 = createHash("sha512");
5725
+ const sha2562 = createHash2("sha256");
5726
+ const sha512 = createHash2("sha512");
5543
5727
  const buffer = Buffer.allocUnsafe(1024 * 1024);
5544
5728
  let bytes = 0;
5545
5729
  try {
@@ -5757,36 +5941,44 @@ function managedPackageResult(result) {
5757
5941
  return result;
5758
5942
  }
5759
5943
  async function runManagedPiSystem(input) {
5944
+ const { devtoolsMcpEnabled: explicitDevtools, ...runtimeInput } = input;
5945
+ const devtoolsMcpEnabled = explicitDevtools ?? (input.targetDir === void 0 && loadDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi"));
5760
5946
  const playwrightCliEnabled = input.targetDir === void 0 && loadPlaywrightCliPreference() === true;
5761
5947
  const projectionInput = {
5762
5948
  targetDir: input.targetDir,
5763
5949
  packageSource: PI_RUNTIME_CANDIDATE.package.source,
5764
5950
  engramBin: input.engramBin,
5765
- playwrightCliEnabled
5951
+ playwrightCliEnabled,
5952
+ devtoolsMcpEnabled,
5953
+ pnpmBin: devtoolsMcpEnabled && input.operation !== "uninstall" ? resolvePnpmBin() : null
5766
5954
  };
5767
- return runManagedPiOperation(input.operation, {
5955
+ const result = await runManagedPiOperation(input.operation, {
5768
5956
  async runPackage(operation) {
5769
- return managedPackageResult(await runPiRuntimeSystem({ ...input, operation }));
5957
+ return managedPackageResult(await runPiRuntimeSystem({ ...runtimeInput, operation }));
5770
5958
  },
5771
5959
  runProjection(operation) {
5772
- const result = runPiProjectionLifecycleSystem({
5960
+ const result2 = runPiProjectionLifecycleSystem({
5773
5961
  operation,
5774
5962
  ...projectionInput
5775
5963
  });
5776
- return Promise.resolve(result.kind === "drift" ? {
5964
+ return Promise.resolve(result2.kind === "drift" ? {
5777
5965
  kind: "drift",
5778
- paths: result.paths,
5966
+ paths: result2.paths,
5779
5967
  remedy: "Ejecuta sync --agents pi para reparar la proyecci\xF3n de Pi."
5780
- } : result);
5968
+ } : result2);
5781
5969
  },
5782
5970
  prepareProjectionUninstall() {
5783
- const result = preparePiProjectionUninstallSystem({ operation: "uninstall", ...projectionInput });
5784
- return Promise.resolve(result.kind === "prepared" ? { kind: "prepared", token: result.plan } : result);
5971
+ const result2 = preparePiProjectionUninstallSystem({ operation: "uninstall", ...projectionInput });
5972
+ return Promise.resolve(result2.kind === "prepared" ? { kind: "prepared", token: result2.plan } : result2);
5785
5973
  },
5786
5974
  completeProjectionUninstall(token) {
5787
5975
  return Promise.resolve(completePiProjectionUninstallSystem(token, { operation: "uninstall", ...projectionInput }));
5788
5976
  }
5789
5977
  });
5978
+ if (input.targetDir === void 0 && explicitDevtools !== void 0 && (input.operation === "install" || input.operation === "sync") && result.kind !== "blocked") {
5979
+ saveDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi", explicitDevtools);
5980
+ }
5981
+ return result;
5790
5982
  }
5791
5983
 
5792
5984
  // src/lib/quality-runner.ts
@@ -6167,6 +6359,112 @@ async function runQualityPlan(input) {
6167
6359
  return { evaluation, receipt };
6168
6360
  }
6169
6361
 
6362
+ // src/lib/engram-install.ts
6363
+ import fs24 from "fs";
6364
+ import os5 from "os";
6365
+ import path33 from "path";
6366
+ import { execFileSync as execFileSync7 } from "child_process";
6367
+ import { createHash as createHash3 } from "crypto";
6368
+
6369
+ // src/lib/engram-release.json
6370
+ var engram_release_default = {
6371
+ assets: {
6372
+ darwin_arm64: {
6373
+ name: "engram_1.20.0_darwin_arm64.tar.gz",
6374
+ sha256: "2363d5012f23e58878f86c3ddcc1f63cfe9dcf3eec7f413e70eaafcbb9d394cc",
6375
+ size: 6949031
6376
+ },
6377
+ darwin_x64: {
6378
+ name: "engram_1.20.0_darwin_amd64.tar.gz",
6379
+ sha256: "3b9015dcfcdd9f823eb7ad0b590b1dbc4c25bb5d81dda3f84e623709815afe80",
6380
+ size: 7367005
6381
+ },
6382
+ linux_arm64: {
6383
+ name: "engram_1.20.0_linux_arm64.tar.gz",
6384
+ sha256: "7eb815910a76ae6cfa9a5d0161d3701e293dcca71f7743cffa62e236e5af59af",
6385
+ size: 6673857
6386
+ },
6387
+ linux_x64: {
6388
+ name: "engram_1.20.0_linux_amd64.tar.gz",
6389
+ sha256: "7dc3003318e303bee269a4772144f3ce01c8ec700bfd524aaec76770acd389ca",
6390
+ size: 7236400
6391
+ },
6392
+ windows_arm64: {
6393
+ name: "engram_1.20.0_windows_arm64.zip",
6394
+ sha256: "eb93c6d7d5a4020ddcf1ff956909f3e90694f6447519433316b10f964f8f37b5",
6395
+ size: 6723366
6396
+ },
6397
+ windows_x64: {
6398
+ name: "engram_1.20.0_windows_amd64.zip",
6399
+ sha256: "c945c47d58367f943642cb8451fa45f208174407839f1dda33a975a05d6e465b",
6400
+ size: 7416778
6401
+ }
6402
+ },
6403
+ version: "1.20.0"
6404
+ };
6405
+
6406
+ // src/lib/engram-install.ts
6407
+ async function installMissingEngram(options = {}) {
6408
+ const platform = options.platform ?? process.platform;
6409
+ const homeDir = options.homeDir ?? os5.homedir();
6410
+ if (!path33.isAbsolute(homeDir)) return { ok: false, reason: "El directorio personal debe ser absoluto." };
6411
+ const binaryName = platform === "win32" ? "engram.exe" : "engram";
6412
+ const bin = path33.join(homeDir, ".local", "bin", binaryName);
6413
+ let staging;
6414
+ try {
6415
+ const existing = fs24.lstatSync(bin, { throwIfNoEntry: false });
6416
+ if (existing) {
6417
+ if (!fs24.statSync(bin).isFile()) throw new Error(`El destino Engram no es un archivo: ${bin}`);
6418
+ fs24.accessSync(bin, platform === "win32" ? fs24.constants.F_OK : fs24.constants.X_OK);
6419
+ return { ok: true, bin };
6420
+ }
6421
+ const key = `${platform === "win32" ? "windows" : platform}_${options.arch ?? process.arch}`;
6422
+ const asset = engram_release_default.assets[key];
6423
+ if (!asset) throw new Error(`Engram no dispone de un binario aprobado para ${key}.`);
6424
+ const response = await (options.fetch ?? globalThis.fetch)(
6425
+ `https://github.com/Gentleman-Programming/engram/releases/download/v${engram_release_default.version}/${asset.name}`,
6426
+ { signal: AbortSignal.timeout(12e4) }
6427
+ );
6428
+ if (!response.ok || !response.body) throw new Error(`Descarga Engram fallida: HTTP ${response.status}.`);
6429
+ const reader = response.body.getReader();
6430
+ const chunks = [];
6431
+ let size = 0;
6432
+ try {
6433
+ while (true) {
6434
+ const chunk = await reader.read();
6435
+ if (chunk.done) break;
6436
+ size += chunk.value.byteLength;
6437
+ if (size > asset.size) throw new Error("El tama\xF1o descargado de Engram supera el aprobado.");
6438
+ chunks.push(chunk.value);
6439
+ }
6440
+ } finally {
6441
+ await reader.cancel();
6442
+ }
6443
+ const archive = Buffer.concat(chunks);
6444
+ if (size !== asset.size || createHash3("sha256").update(archive).digest("hex") !== asset.sha256) {
6445
+ throw new Error("Engram: tama\xF1o o hash SHA-256 no coincide con el artefacto aprobado.");
6446
+ }
6447
+ fs24.mkdirSync(path33.dirname(bin), { recursive: true });
6448
+ staging = fs24.mkdtempSync(path33.join(path33.dirname(bin), ".engram-install-"));
6449
+ const archivePath = path33.join(staging, asset.name);
6450
+ fs24.writeFileSync(archivePath, archive, { flag: "wx", mode: 384 });
6451
+ const binary = execFileSync7(resolveTarBin(), ["-xOf", archivePath, binaryName], {
6452
+ maxBuffer: 64 * 1024 * 1024,
6453
+ timeout: 3e4,
6454
+ stdio: ["ignore", "pipe", "pipe"]
6455
+ });
6456
+ if (binary.length === 0) throw new Error("El artefacto Engram no contiene un binario v\xE1lido.");
6457
+ const stagedBin = path33.join(staging, binaryName);
6458
+ fs24.writeFileSync(stagedBin, binary, { flag: "wx", mode: 493 });
6459
+ fs24.linkSync(stagedBin, bin);
6460
+ return { ok: true, bin };
6461
+ } catch (error) {
6462
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
6463
+ } finally {
6464
+ if (staging) fs24.rmSync(staging, { recursive: true, force: true });
6465
+ }
6466
+ }
6467
+
6170
6468
  // src/cli.ts
6171
6469
  var VERSION = readPackageVersion();
6172
6470
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall", "quality"];
@@ -6201,6 +6499,7 @@ function parseFlags(args, allowReceipt = false) {
6201
6499
  list: false,
6202
6500
  check: false,
6203
6501
  removeEngram: false,
6502
+ engram: false,
6204
6503
  playwright: false,
6205
6504
  removePlaywright: false,
6206
6505
  devtools: false,
@@ -6263,6 +6562,7 @@ function parseFlags(args, allowReceipt = false) {
6263
6562
  else if (arg === "--list") flags.list = true;
6264
6563
  else if (arg === "--check") flags.check = true;
6265
6564
  else if (arg === "--remove-engram") flags.removeEngram = true;
6565
+ else if (arg === "--engram") flags.engram = true;
6266
6566
  else if (arg === "--playwright") flags.playwright = true;
6267
6567
  else if (arg === "--remove-playwright") flags.removePlaywright = true;
6268
6568
  else if (arg === "--devtools") flags.devtools = true;
@@ -6367,8 +6667,9 @@ async function resolveDevtoolsMcpSelection(command, flags, runtimes) {
6367
6667
  }
6368
6668
  const file = devtoolsMcpPreferenceFile();
6369
6669
  const selected = await p6.multiselect({
6370
- message: "Chrome DevTools MCP avanzado (full: ~29 tools y ~5.8\u20137.7k tokens de schemas). \xBFEn qu\xE9 runtimes activarlo?",
6371
- options: runtimes.map((runtime) => ({ value: runtime, label: ADAPTERS[runtime]?.name ?? runtime })),
6670
+ message: "Chrome DevTools MCP avanzado (opcional). \xBFEn qu\xE9 runtimes activarlo?",
6671
+ required: false,
6672
+ options: runtimes.map((runtime) => ({ value: runtime, label: runtime === "pi" ? "Pi" : ADAPTERS[runtime]?.name ?? runtime })),
6372
6673
  initialValues: runtimes.filter((runtime) => loadDevtoolsMcpPreference(file, runtime))
6373
6674
  });
6374
6675
  if (p6.isCancel(selected)) return null;
@@ -6413,7 +6714,34 @@ async function resolveRuntimes(flags, includeAvailablePi = false) {
6413
6714
  if (p6.isCancel(choice)) return null;
6414
6715
  return choice;
6415
6716
  }
6416
- async function runSelectedPi(operation, targetDir, yes = false) {
6717
+ async function resolveHostEngramForInstall(flags) {
6718
+ if (flags.targetDir !== void 0) return { ok: true, bin: null };
6719
+ const existing = resolvePiEngramBin();
6720
+ if (flags.dryRun) return { ok: true, bin: existing };
6721
+ if (existing !== null) return { ok: true, bin: existing };
6722
+ let accepted = flags.engram;
6723
+ if (!accepted && !flags.yes && process.stdin.isTTY === true && process.stdout.isTTY === true) {
6724
+ const answer = await p6.confirm({
6725
+ message: "Engram no est\xE1 instalado. \xBFDescargar e instalar ahora el binario oficial verificado?",
6726
+ initialValue: false
6727
+ });
6728
+ if (!p6.isCancel(answer)) accepted = answer === true;
6729
+ }
6730
+ if (!accepted) {
6731
+ return {
6732
+ ok: false,
6733
+ message: "Engram no est\xE1 instalado. Usa --engram para autorizar su instalaci\xF3n o inst\xE1lalo antes de reintentar."
6734
+ };
6735
+ }
6736
+ try {
6737
+ const result = await installMissingEngram();
6738
+ if (result.ok) return { ok: true, bin: result.bin };
6739
+ return { ok: false, message: `Engram: ${result.reason}` };
6740
+ } catch (error) {
6741
+ return { ok: false, message: `Engram: ${error instanceof Error ? error.message : String(error)}` };
6742
+ }
6743
+ }
6744
+ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled) {
6417
6745
  if (targetDir === void 0 && operation !== "models") {
6418
6746
  const preferenceErrors = browserPreferenceErrors();
6419
6747
  if (preferenceErrors.length > 0) {
@@ -6430,7 +6758,7 @@ async function runSelectedPi(operation, targetDir, yes = false) {
6430
6758
  console.error("No se pudo verificar la versi\xF3n instalada de Pi sin ejecutarlo; revisa la instalaci\xF3n de Pi.");
6431
6759
  return 1;
6432
6760
  }
6433
- let engramBin = resolvePiEngramBin(targetDir);
6761
+ let engramBin = resolvedEngramBin === void 0 ? resolvePiEngramBin(targetDir) : resolvedEngramBin;
6434
6762
  if (operation === "install" && engramBin === null) {
6435
6763
  const requirement = await resolvePiEngramRequirement({
6436
6764
  targetDir,
@@ -6455,7 +6783,8 @@ async function runSelectedPi(operation, targetDir, yes = false) {
6455
6783
  operation,
6456
6784
  targetDir,
6457
6785
  detected: { executable: detected.executable, version: detected.version },
6458
- engramBin
6786
+ engramBin,
6787
+ ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled }
6459
6788
  });
6460
6789
  if (result.kind === "blocked") {
6461
6790
  const paths = "paths" in result ? `: ${result.paths.join(", ")}` : "";
@@ -6491,6 +6820,7 @@ Opciones:
6491
6820
  --dry-run Muestra el plan sin escribir nada
6492
6821
  --yes, -y No interactivo
6493
6822
  --playwright Autoriza Playwright CLI global y sus navegadores (requerido con --yes/sin TTY)
6823
+ --engram (install) autoriza instalar el binario Engram si falta
6494
6824
  --devtools (install/sync) activa Chrome DevTools MCP para los runtimes destino (opt-in)
6495
6825
  --no-devtools (install/sync) desactiva Chrome DevTools MCP (incompatible con --devtools)
6496
6826
  --remove-engram (uninstall) desregistra Engram de los runtimes;
@@ -6524,6 +6854,11 @@ Flags disponibles: jorgex-stack --help`
6524
6854
  return;
6525
6855
  }
6526
6856
  const { command, flags } = parsed;
6857
+ if (flags.engram && command !== "install") {
6858
+ console.error("--engram solo se admite durante install.");
6859
+ process.exitCode = 1;
6860
+ return;
6861
+ }
6527
6862
  if (command !== "quality" && flags.targetDir !== void 0 && flags.agents.length !== 1) {
6528
6863
  console.error("--target-dir requiere exactamente un runtime en --agents.");
6529
6864
  process.exitCode = 1;
@@ -6531,7 +6866,7 @@ Flags disponibles: jorgex-stack --help`
6531
6866
  }
6532
6867
  switch (command) {
6533
6868
  case "quality": {
6534
- if (flags.targetDir !== void 0 || flags.agents.length > 0 || flags.dryRun || flags.yes || flags.mode !== void 0 || flags.subagentConcurrency !== void 0 || flags.list || flags.check || flags.removeEngram || flags.playwright || flags.removePlaywright || flags.devtools || flags.noDevtools) {
6869
+ if (flags.targetDir !== void 0 || flags.agents.length > 0 || flags.dryRun || flags.yes || flags.mode !== void 0 || flags.subagentConcurrency !== void 0 || flags.list || flags.check || flags.removeEngram || flags.engram || flags.playwright || flags.removePlaywright || flags.devtools || flags.noDevtools) {
6535
6870
  console.error("quality solo admite <plan.json> y, opcionalmente, --receipt <path>.");
6536
6871
  process.exitCode = 1;
6537
6872
  return;
@@ -6547,7 +6882,7 @@ Flags disponibles: jorgex-stack --help`
6547
6882
  return;
6548
6883
  }
6549
6884
  try {
6550
- const plan = JSON.parse(fs24.readFileSync(flags.positional[0], "utf8"));
6885
+ const plan = JSON.parse(fs25.readFileSync(flags.positional[0], "utf8"));
6551
6886
  const result = await runQualityPlan(plan);
6552
6887
  const serialized = serializeQualityReceipt(result.receipt);
6553
6888
  if (flags.receipt === void 0) process.stdout.write(`${serialized}
@@ -6572,33 +6907,55 @@ Flags disponibles: jorgex-stack --help`
6572
6907
  }
6573
6908
  const fileRuntimes = runtimes.filter(isFileManagedRuntime);
6574
6909
  let exitCode = 0;
6575
- if (fileRuntimes.length > 0) {
6576
- const mode = await resolveInstallMode(flags);
6910
+ let completed = false;
6911
+ p6.intro(`jorgex-stack ${command}${flags.dryRun ? " (dry-run)" : ""}`);
6912
+ try {
6913
+ if (flags.targetDir === void 0) {
6914
+ const errors = browserPreferenceErrors();
6915
+ if (errors.length > 0) {
6916
+ for (const error of errors) p6.log.error(error);
6917
+ exitCode = 1;
6918
+ return;
6919
+ }
6920
+ }
6921
+ const mode = fileRuntimes.length > 0 ? await resolveInstallMode(flags) : void 0;
6577
6922
  if (mode === null) return;
6578
- const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, fileRuntimes);
6579
- if (devtoolsMcpSelection === null) return;
6580
- const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
6581
- if (playwrightToolConsent === null) return;
6582
- const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes);
6583
- if (!hasOpenCodeModels) {
6584
- process.exitCode = 1;
6923
+ const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, runtimes);
6924
+ if (devtoolsMcpSelection === null) {
6925
+ exitCode = process.exitCode === 1 ? 1 : 0;
6585
6926
  return;
6586
6927
  }
6587
- exitCode = await runInstall({
6588
- runtimes: fileRuntimes,
6589
- targetDir: flags.targetDir,
6590
- dryRun: flags.dryRun,
6591
- yes: flags.yes,
6592
- mode,
6593
- playwrightToolConsent,
6594
- devtoolsMcpSelection
6595
- });
6596
- }
6597
- let piCanRun = true;
6598
- if (command === "install" && fileRuntimes.length === 0 && runtimes.includes("pi")) {
6599
6928
  const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
6600
6929
  if (playwrightToolConsent === null) return;
6601
- if (resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0) {
6930
+ if (!await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes)) {
6931
+ exitCode = 1;
6932
+ return;
6933
+ }
6934
+ let engramBin;
6935
+ if (command === "install") {
6936
+ const engram = await resolveHostEngramForInstall(flags);
6937
+ if (!engram.ok) {
6938
+ p6.log.error(engram.message);
6939
+ exitCode = 1;
6940
+ return;
6941
+ }
6942
+ engramBin = engram.bin;
6943
+ }
6944
+ if (fileRuntimes.length > 0) {
6945
+ exitCode = await runInstall({
6946
+ runtimes: fileRuntimes,
6947
+ targetDir: flags.targetDir,
6948
+ dryRun: flags.dryRun,
6949
+ yes: flags.yes,
6950
+ mode,
6951
+ playwrightToolConsent,
6952
+ devtoolsMcpSelection,
6953
+ engramBin,
6954
+ showSummary: false
6955
+ });
6956
+ }
6957
+ let piCanRun = true;
6958
+ if (command === "install" && fileRuntimes.length === 0 && runtimes.includes("pi") && resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0) {
6602
6959
  if (flags.dryRun) {
6603
6960
  p6.log.info("Playwright CLI: instalaci\xF3n global y navegador previstos (dry-run; no se ejecutan).");
6604
6961
  } else {
@@ -6607,17 +6964,32 @@ Flags disponibles: jorgex-stack --help`
6607
6964
  targetDir: flags.targetDir,
6608
6965
  dryRun: false,
6609
6966
  yes: flags.yes,
6610
- playwrightToolConsent
6967
+ playwrightToolConsent,
6968
+ engramBin,
6969
+ showSummary: false
6611
6970
  });
6612
6971
  piCanRun = exitCode === 0;
6613
6972
  }
6614
6973
  }
6974
+ if (runtimes.includes("pi") && piCanRun) {
6975
+ if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
6976
+ else exitCode = Math.max(exitCode, await runSelectedPi(
6977
+ command,
6978
+ flags.targetDir,
6979
+ flags.yes,
6980
+ flags.targetDir === void 0 ? engramBin : void 0,
6981
+ devtoolsMcpSelection.pi
6982
+ ));
6983
+ }
6984
+ completed = true;
6985
+ } catch (error) {
6986
+ p6.log.error(error instanceof Error ? error.message : String(error));
6987
+ exitCode = 1;
6988
+ } finally {
6989
+ if (process.exitCode === 1) exitCode = 1;
6990
+ process.exitCode = exitCode;
6991
+ p6.outro(exitCode !== 0 ? `${command} completado con errores (revisa arriba).` : !completed ? `${command} cancelado.` : flags.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
6615
6992
  }
6616
- if (runtimes.includes("pi") && piCanRun) {
6617
- if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
6618
- else exitCode = Math.max(exitCode, await runSelectedPi(command, flags.targetDir, flags.yes));
6619
- }
6620
- process.exitCode = exitCode;
6621
6993
  return;
6622
6994
  }
6623
6995
  case "uninstall": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.28",
3
+ "version": "1.9.30",
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",