jorgex-stack 1.9.38 → 1.9.40

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/dist/cli.js CHANGED
@@ -15,12 +15,12 @@ import {
15
15
 
16
16
  // src/cli.ts
17
17
  import * as p6 from "@clack/prompts";
18
- import fs26 from "fs";
18
+ import fs27 from "fs";
19
19
  import { pathToFileURL as pathToFileURL2 } from "url";
20
20
 
21
21
  // src/install.ts
22
- import fs16 from "fs";
23
- import path21 from "path";
22
+ import fs17 from "fs";
23
+ import path22 from "path";
24
24
  import * as p from "@clack/prompts";
25
25
 
26
26
  // src/lib/writing-style.ts
@@ -498,15 +498,81 @@ function applyWritingStyle(plan, dryRun = false) {
498
498
  writeText(plan.sourcePath, plan.installedContent, mode);
499
499
  }
500
500
 
501
+ // src/lib/system-prompt-sections.ts
502
+ import fs4 from "fs";
503
+ import path4 from "path";
504
+ var SYSTEM_PROMPT_SECTIONS = [
505
+ "system-prompt",
506
+ "engram-protocol",
507
+ "context7",
508
+ "playwright",
509
+ "chrome-devtools",
510
+ "browser",
511
+ "writing-style"
512
+ ];
513
+ function assertSystemPromptMarkers(content, target) {
514
+ if (content === null) return;
515
+ const ranges = [];
516
+ for (const section of SYSTEM_PROMPT_SECTIONS) {
517
+ const markers2 = [...content.matchAll(new RegExp(`<!--\\s*\\/?jorgex:${section}(?=[\\s>]|-->|$)`, "g"))];
518
+ if (markers2.length === 0) continue;
519
+ if (markers2.length !== 2 || !hasHealthyManagedMarkdownMarkers(content, section)) {
520
+ throw new Error(`Marcadores jorgex:${section} ambiguos en ${target}. Repara el bloque antes de reintentar; su contenido se conserva.`);
521
+ }
522
+ ranges.push({
523
+ section,
524
+ start: content.indexOf(`<!-- jorgex:${section} -->`),
525
+ end: content.indexOf(`<!-- /jorgex:${section} -->`) + `<!-- /jorgex:${section} -->`.length
526
+ });
527
+ }
528
+ ranges.sort((a, b) => a.start - b.start);
529
+ for (let index = 1; index < ranges.length; index++) {
530
+ if (ranges[index].start < ranges[index - 1].end) {
531
+ throw new Error(`Marcadores jorgex:${ranges[index].section} anidados en ${target}. Separa los bloques antes de reintentar; su contenido se conserva.`);
532
+ }
533
+ }
534
+ }
535
+ function readSystemPromptFile(target, rootDir) {
536
+ if (rootDir !== void 0) {
537
+ const probe = fs4.existsSync(target) ? target : path4.dirname(target);
538
+ if (fs4.existsSync(probe) && fs4.existsSync(rootDir)) {
539
+ const resolved = fs4.realpathSync(probe);
540
+ const root = fs4.realpathSync(rootDir);
541
+ if (resolved !== root && !isContainedIn(resolved, root)) {
542
+ throw new Error(`El prompt ${target} sale del destino aislado.`);
543
+ }
544
+ }
545
+ }
546
+ let bytes;
547
+ try {
548
+ bytes = fs4.readFileSync(target);
549
+ } catch (error) {
550
+ if (error.code === "ENOENT") return null;
551
+ throw new Error(`No se puede leer el prompt ${target}. Revisa los permisos y el tipo de archivo.`, { cause: error });
552
+ }
553
+ try {
554
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
555
+ } catch (error) {
556
+ throw new Error(`El prompt ${target} debe usar UTF-8 v\xE1lido.`, { cause: error });
557
+ }
558
+ }
559
+ function assertSystemPromptFile(target, rootDir) {
560
+ assertSystemPromptMarkers(readSystemPromptFile(target, rootDir), target);
561
+ }
562
+ function removeSystemPromptSections(content) {
563
+ assertSystemPromptMarkers(content, "system prompt");
564
+ return SYSTEM_PROMPT_SECTIONS.reduce((current, section) => removeMarkdownSection(current, section), content);
565
+ }
566
+
501
567
  // src/adapters/opencode.ts
502
- import path8 from "path";
503
- import fs5 from "fs";
568
+ import path9 from "path";
569
+ import fs6 from "fs";
504
570
  import { isDeepStrictEqual } from "util";
505
571
  import { pathToFileURL } from "url";
506
572
 
507
573
  // src/lib/canonical.ts
508
- import path4 from "path";
509
- import fs4 from "fs";
574
+ import path5 from "path";
575
+ import fs5 from "fs";
510
576
  function parseCanonicalAgent(source, file) {
511
577
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
512
578
  if (!match) throw new Error(`Agente sin frontmatter: ${file}`);
@@ -532,9 +598,9 @@ function parseCanonicalAgent(source, file) {
532
598
  };
533
599
  }
534
600
  function loadCanonicalAgents(agentsDir) {
535
- return fs4.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").map((f) => {
536
- const file = path4.join(agentsDir, f);
537
- const source = fs4.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
601
+ return fs5.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").map((f) => {
602
+ const file = path5.join(agentsDir, f);
603
+ const source = fs5.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
538
604
  return parseCanonicalAgent(source, file);
539
605
  });
540
606
  }
@@ -543,21 +609,21 @@ function isCanonicalMcpServerEnabled(name, server, enabledServers) {
543
609
  return !server.optional || server.defaultEnabled === true || enabledServers?.has(name) === true;
544
610
  }
545
611
  function loadCanonicalMcp(stackDir) {
546
- return JSON.parse(fs4.readFileSync(path4.join(stackDir, "mcp", "servers.json"), "utf8"));
612
+ return JSON.parse(fs5.readFileSync(path5.join(stackDir, "mcp", "servers.json"), "utf8"));
547
613
  }
548
614
  function loadCanonicalHooks(stackDir) {
549
- return JSON.parse(fs4.readFileSync(path4.join(stackDir, "hooks", "hooks.json"), "utf8"));
615
+ return JSON.parse(fs5.readFileSync(path5.join(stackDir, "hooks", "hooks.json"), "utf8"));
550
616
  }
551
617
  function loadCanonicalDefaults(stackDir) {
552
- const file = path4.join(stackDir, "config", "defaults.json");
553
- if (!fs4.existsSync(file)) return {};
554
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
618
+ const file = path5.join(stackDir, "config", "defaults.json");
619
+ if (!fs5.existsSync(file)) return {};
620
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
555
621
  delete parsed["$comment"];
556
622
  return parsed;
557
623
  }
558
624
 
559
625
  // src/lib/model-map.ts
560
- import path5 from "path";
626
+ import path6 from "path";
561
627
  import { existsSync } from "fs";
562
628
  function resolveAgentModel(models, agentName, tier) {
563
629
  const base = models[tier];
@@ -584,7 +650,7 @@ var DEFAULT_MODEL_MAP = {
584
650
  }
585
651
  };
586
652
  function modelMapFile() {
587
- return path5.join(dataDir(), "model-map.json");
653
+ return path6.join(dataDir(), "model-map.json");
588
654
  }
589
655
  function loadModelMap() {
590
656
  const file = modelMapFile();
@@ -612,15 +678,15 @@ function ensureModelMapFile() {
612
678
  }
613
679
 
614
680
  // src/lib/detect.ts
615
- import path6 from "path";
681
+ import path7 from "path";
616
682
  import { existsSync as existsSync2, statSync } from "fs";
617
683
  import { execFileSync } from "child_process";
618
- function lookPath(cmd) {
684
+ function lookPath(cmd, env = process.env) {
619
685
  const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
620
- for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
686
+ for (const dir of (env.PATH ?? "").split(path7.delimiter)) {
621
687
  if (!dir) continue;
622
688
  for (const ext of exts) {
623
- const candidate = path6.join(dir, cmd + ext);
689
+ const candidate = path7.join(dir, cmd + ext);
624
690
  if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
625
691
  }
626
692
  }
@@ -650,7 +716,7 @@ function runDetectedBin(bin, args, timeoutMs, envOverrides) {
650
716
  }
651
717
  }
652
718
  function detectOpenCode() {
653
- const configDir = process.env.OPENCODE_CONFIG_DIR ?? path6.join(HOME, ".config", "opencode");
719
+ const configDir = process.env.OPENCODE_CONFIG_DIR ?? path7.join(HOME, ".config", "opencode");
654
720
  const binPath = lookPath("opencode");
655
721
  return {
656
722
  id: "opencode",
@@ -661,7 +727,7 @@ function detectOpenCode() {
661
727
  };
662
728
  }
663
729
  function detectClaudeCode() {
664
- const configDir = path6.join(HOME, ".claude");
730
+ const configDir = path7.join(HOME, ".claude");
665
731
  const binPath = lookPath("claude");
666
732
  return {
667
733
  id: "claude-code",
@@ -672,7 +738,7 @@ function detectClaudeCode() {
672
738
  };
673
739
  }
674
740
  function detectCodex() {
675
- const configDir = process.env.CODEX_HOME ?? path6.join(HOME, ".codex");
741
+ const configDir = process.env.CODEX_HOME ?? path7.join(HOME, ".codex");
676
742
  const binPath = lookPath("codex");
677
743
  return {
678
744
  id: "codex",
@@ -688,19 +754,19 @@ function detectEngram() {
688
754
  const onPath = lookPath("engram");
689
755
  if (onPath) return onPath;
690
756
  const candidates = [
691
- path6.join(HOME, "go", "bin", "engram.exe"),
692
- path6.join(HOME, "go", "bin", "engram"),
693
- path6.join(HOME, ".local", "bin", "engram.exe"),
694
- path6.join(HOME, ".local", "bin", "engram")
757
+ path7.join(HOME, "go", "bin", "engram.exe"),
758
+ path7.join(HOME, "go", "bin", "engram"),
759
+ path7.join(HOME, ".local", "bin", "engram.exe"),
760
+ path7.join(HOME, ".local", "bin", "engram")
695
761
  ];
696
762
  for (const c of candidates) if (existsSync2(c)) return c;
697
763
  return null;
698
764
  }
699
765
 
700
766
  // src/lib/hooks-format.ts
701
- import path7 from "path";
767
+ import path8 from "path";
702
768
  function hookScriptNames(canonical) {
703
- return Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path7.basename(s));
769
+ return Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path8.basename(s));
704
770
  }
705
771
  function plainHookCommands(canonical) {
706
772
  return new Set(
@@ -747,7 +813,7 @@ function upsertNativeHooks(existing, canonical, scriptsDir, mapMatcher = (m) =>
747
813
  ...h.timeout !== void 0 ? { timeout: h.timeout } : {}
748
814
  }))
749
815
  };
750
- const scriptNames = entry.hooks.map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path7.basename(s));
816
+ const scriptNames = entry.hooks.map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path8.basename(s));
751
817
  const plainCommands = entry.hooks.map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"));
752
818
  const index = list.findIndex((existingEntry) => {
753
819
  const e = existingEntry;
@@ -891,7 +957,7 @@ function pruneEmpty(parent, key) {
891
957
  if (value !== null && Object.keys(value).length === 0) delete parent[key];
892
958
  }
893
959
  function hasOpenCodeManualApproval(configDir) {
894
- const content = readTextIfExists(path8.join(configDir, "opencode.json"));
960
+ const content = readTextIfExists(path9.join(configDir, "opencode.json"));
895
961
  if (content === null) return false;
896
962
  try {
897
963
  const root = JSON.parse(content);
@@ -907,7 +973,7 @@ var opencodeAdapter = {
907
973
  name: "OpenCode",
908
974
  detect: detectOpenCode,
909
975
  reportCapabilities(configDir) {
910
- const prompt = readTextIfExists(path8.join(configDir, "AGENTS.md"));
976
+ const prompt = readTextIfExists(path9.join(configDir, "AGENTS.md"));
911
977
  return createLocalCapabilityReport("opencode", [
912
978
  ...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
913
979
  id: "policy-guidance",
@@ -929,16 +995,16 @@ var opencodeAdapter = {
929
995
  paths(configDir) {
930
996
  const isRealConfigDir = samePath(
931
997
  configDir,
932
- process.env.OPENCODE_CONFIG_DIR ?? path8.join(HOME, ".config", "opencode")
998
+ process.env.OPENCODE_CONFIG_DIR ?? path9.join(HOME, ".config", "opencode")
933
999
  );
934
- const agentsHome = isRealConfigDir ? HOME : path8.dirname(configDir);
1000
+ const agentsHome = isRealConfigDir ? HOME : path9.dirname(configDir);
935
1001
  return {
936
- systemPromptFile: path8.join(configDir, "AGENTS.md"),
937
- agentsDir: path8.join(configDir, "agents"),
938
- skillsDir: path8.join(agentsHome, ".agents", "skills"),
939
- commandsDir: path8.join(configDir, "commands"),
940
- pluginsDir: path8.join(configDir, "plugins"),
941
- scriptsDir: path8.join(configDir, "scripts"),
1002
+ systemPromptFile: path9.join(configDir, "AGENTS.md"),
1003
+ agentsDir: path9.join(configDir, "agents"),
1004
+ skillsDir: path9.join(agentsHome, ".agents", "skills"),
1005
+ commandsDir: path9.join(configDir, "commands"),
1006
+ pluginsDir: path9.join(configDir, "plugins"),
1007
+ scriptsDir: path9.join(configDir, "scripts"),
942
1008
  outputStylesDir: null,
943
1009
  profilesDir: null
944
1010
  };
@@ -996,12 +1062,12 @@ ${agent.body}`,
996
1062
  ctx.warnings.push(`opencode: hook sin {{SCRIPTS_DIR}} no traducible: ${hook.command}`);
997
1063
  continue;
998
1064
  }
999
- const script = `scripts/${path8.basename(match[1])}`;
1065
+ const script = `scripts/${path9.basename(match[1])}`;
1000
1066
  (bashEntries[includes] ??= []).push(script);
1001
1067
  }
1002
1068
  }
1003
1069
  }
1004
- const hooksFile = path8.join(ctx.configDir, "hooks.json");
1070
+ const hooksFile = path9.join(ctx.configDir, "hooks.json");
1005
1071
  const content = upsertJson(readTextIfExists(hooksFile), (root) => {
1006
1072
  const afterValue = root["tool.execute.after"] ??= {};
1007
1073
  if (afterValue === null || typeof afterValue !== "object" || Array.isArray(afterValue)) {
@@ -1036,16 +1102,16 @@ ${agent.body}`,
1036
1102
  }
1037
1103
  });
1038
1104
  actions.push({ kind: "write", target: hooksFile, content });
1039
- const scriptsSource = path8.join(ctx.stackDir, "scripts");
1040
- if (fs5.existsSync(scriptsSource)) {
1041
- for (const f of fs5.readdirSync(scriptsSource)) {
1042
- actions.push({ kind: "copy", source: path8.join(scriptsSource, f), target: path8.join(scriptsDir, f) });
1105
+ const scriptsSource = path9.join(ctx.stackDir, "scripts");
1106
+ if (fs6.existsSync(scriptsSource)) {
1107
+ for (const f of fs6.readdirSync(scriptsSource)) {
1108
+ actions.push({ kind: "copy", source: path9.join(scriptsSource, f), target: path9.join(scriptsDir, f) });
1043
1109
  }
1044
1110
  }
1045
1111
  return actions;
1046
1112
  },
1047
1113
  planMainConfig(canonical, ctx) {
1048
- const file = path8.join(ctx.configDir, "opencode.json");
1114
+ const file = path9.join(ctx.configDir, "opencode.json");
1049
1115
  const { pluginsDir } = this.paths(ctx.configDir);
1050
1116
  const original = readTextIfExists(file);
1051
1117
  const contentSource = original === null || original.trim() === "" ? null : original;
@@ -1152,13 +1218,10 @@ ${agent.body}`,
1152
1218
  const { systemPromptFile, pluginsDir } = this.paths(ctx.configDir);
1153
1219
  const prompt = readTextIfExists(systemPromptFile);
1154
1220
  if (prompt !== null) {
1155
- let content = removeMarkdownSection(prompt, "system-prompt");
1156
- content = removeMarkdownSection(content, "engram-protocol");
1157
- content = removeMarkdownSection(content, "browser");
1158
- content = removeMarkdownSection(content, "writing-style");
1221
+ const content = removeSystemPromptSections(prompt);
1159
1222
  actions.push({ kind: "write", target: systemPromptFile, content });
1160
1223
  }
1161
- const configFile = path8.join(ctx.configDir, "opencode.json");
1224
+ const configFile = path9.join(ctx.configDir, "opencode.json");
1162
1225
  const config = readTextIfExists(configFile);
1163
1226
  if (config !== null) {
1164
1227
  const mcpOwnership = [];
@@ -1226,7 +1289,7 @@ ${agent.body}`,
1226
1289
  ...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
1227
1290
  });
1228
1291
  }
1229
- const hooksFile = path8.join(ctx.configDir, "hooks.json");
1292
+ const hooksFile = path9.join(ctx.configDir, "hooks.json");
1230
1293
  const hooksJson = readTextIfExists(hooksFile);
1231
1294
  if (hooksJson !== null) {
1232
1295
  const ourScripts = hookScriptNames(hooks);
@@ -1252,8 +1315,8 @@ ${agent.body}`,
1252
1315
  };
1253
1316
 
1254
1317
  // src/adapters/claude-code.ts
1255
- import path9 from "path";
1256
- import fs6 from "fs";
1318
+ import path10 from "path";
1319
+ import fs7 from "fs";
1257
1320
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
1258
1321
  function yamlString2(value) {
1259
1322
  return JSON.stringify(value);
@@ -1269,7 +1332,7 @@ function isRecord(value) {
1269
1332
  return value !== null && typeof value === "object" && !Array.isArray(value);
1270
1333
  }
1271
1334
  function hasClaudeManualApproval(configDir) {
1272
- const content = readTextIfExists(path9.join(configDir, "settings.json"));
1335
+ const content = readTextIfExists(path10.join(configDir, "settings.json"));
1273
1336
  if (content === null) return false;
1274
1337
  try {
1275
1338
  const root = JSON.parse(content);
@@ -1289,8 +1352,8 @@ function toolsFor(agent) {
1289
1352
  return tools.join(", ");
1290
1353
  }
1291
1354
  function hasEngramPlugin(configDir) {
1292
- if (fs6.existsSync(path9.join(configDir, "plugins", "marketplaces", "engram"))) return true;
1293
- const registry = readTextIfExists(path9.join(configDir, "plugins", "installed_plugins.json"));
1355
+ if (fs7.existsSync(path10.join(configDir, "plugins", "marketplaces", "engram"))) return true;
1356
+ const registry = readTextIfExists(path10.join(configDir, "plugins", "installed_plugins.json"));
1294
1357
  if (registry === null) return false;
1295
1358
  try {
1296
1359
  const parsed = JSON.parse(registry);
@@ -1313,7 +1376,7 @@ var claudeCodeAdapter = {
1313
1376
  name: "Claude Code",
1314
1377
  detect: detectClaudeCode,
1315
1378
  reportCapabilities(configDir) {
1316
- const prompt = readTextIfExists(path9.join(configDir, "CLAUDE.md"));
1379
+ const prompt = readTextIfExists(path10.join(configDir, "CLAUDE.md"));
1317
1380
  return createLocalCapabilityReport("claude-code", [
1318
1381
  ...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
1319
1382
  id: "policy-guidance",
@@ -1334,13 +1397,13 @@ var claudeCodeAdapter = {
1334
1397
  },
1335
1398
  paths(configDir) {
1336
1399
  return {
1337
- systemPromptFile: path9.join(configDir, "CLAUDE.md"),
1338
- agentsDir: path9.join(configDir, "agents"),
1339
- skillsDir: path9.join(configDir, "skills"),
1340
- commandsDir: path9.join(configDir, "commands"),
1400
+ systemPromptFile: path10.join(configDir, "CLAUDE.md"),
1401
+ agentsDir: path10.join(configDir, "agents"),
1402
+ skillsDir: path10.join(configDir, "skills"),
1403
+ commandsDir: path10.join(configDir, "commands"),
1341
1404
  pluginsDir: null,
1342
- scriptsDir: path9.join(configDir, "scripts"),
1343
- outputStylesDir: path9.join(configDir, "output-styles"),
1405
+ scriptsDir: path10.join(configDir, "scripts"),
1406
+ outputStylesDir: path10.join(configDir, "output-styles"),
1344
1407
  profilesDir: null
1345
1408
  };
1346
1409
  },
@@ -1386,9 +1449,9 @@ ${agent.body}`,
1386
1449
  planHooks(canonical, ctx) {
1387
1450
  const actions = [];
1388
1451
  const { scriptsDir } = this.paths(ctx.configDir);
1389
- const original = readTextIfExists(path9.join(ctx.configDir, "settings.json"));
1452
+ const original = readTextIfExists(path10.join(ctx.configDir, "settings.json"));
1390
1453
  const contentSource = original === null || original.trim() === "" ? null : original;
1391
- const settingsFile = path9.join(ctx.configDir, "settings.json");
1454
+ const settingsFile = path10.join(ctx.configDir, "settings.json");
1392
1455
  let content = upsertNativeHooks(contentSource, canonical, scriptsDir);
1393
1456
  const defaults = loadCanonicalDefaults(ctx.stackDir)["claude-code"];
1394
1457
  if (contentSource === null && defaults?.["permissions"] !== void 0) {
@@ -1402,16 +1465,16 @@ ${agent.body}`,
1402
1465
  });
1403
1466
  }
1404
1467
  actions.push({ kind: "write", target: settingsFile, content });
1405
- const scriptsSource = path9.join(ctx.stackDir, "scripts");
1406
- if (fs6.existsSync(scriptsSource)) {
1407
- for (const f of fs6.readdirSync(scriptsSource)) {
1408
- actions.push({ kind: "copy", source: path9.join(scriptsSource, f), target: path9.join(scriptsDir, f) });
1468
+ const scriptsSource = path10.join(ctx.stackDir, "scripts");
1469
+ if (fs7.existsSync(scriptsSource)) {
1470
+ for (const f of fs7.readdirSync(scriptsSource)) {
1471
+ actions.push({ kind: "copy", source: path10.join(scriptsSource, f), target: path10.join(scriptsDir, f) });
1409
1472
  }
1410
1473
  }
1411
1474
  return actions;
1412
1475
  },
1413
1476
  planMainConfig(canonical, ctx) {
1414
- const file = path9.join(path9.dirname(ctx.configDir), `${path9.basename(ctx.configDir)}.json`);
1477
+ const file = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
1415
1478
  const mcpOwnership = [];
1416
1479
  const content = upsertJson(readTextIfExists(file), (root) => {
1417
1480
  const servers = root["mcpServers"] ??= {};
@@ -1471,13 +1534,10 @@ ${agent.body}`,
1471
1534
  const { systemPromptFile } = this.paths(ctx.configDir);
1472
1535
  const prompt = readTextIfExists(systemPromptFile);
1473
1536
  if (prompt !== null) {
1474
- let content = removeMarkdownSection(prompt, "system-prompt");
1475
- content = removeMarkdownSection(content, "engram-protocol");
1476
- content = removeMarkdownSection(content, "browser");
1477
- content = removeMarkdownSection(content, "writing-style");
1537
+ const content = removeSystemPromptSections(prompt);
1478
1538
  actions.push({ kind: "write", target: systemPromptFile, content });
1479
1539
  }
1480
- const settingsFile = path9.join(ctx.configDir, "settings.json");
1540
+ const settingsFile = path10.join(ctx.configDir, "settings.json");
1481
1541
  const settings = readTextIfExists(settingsFile);
1482
1542
  if (settings !== null) {
1483
1543
  const content = removeNativeHooks(settings, hooks);
@@ -1485,7 +1545,7 @@ ${agent.body}`,
1485
1545
  actions.push({ kind: "write", target: settingsFile, content: content.trim() === "{}" ? "" : content });
1486
1546
  }
1487
1547
  }
1488
- const mainFile = path9.join(path9.dirname(ctx.configDir), `${path9.basename(ctx.configDir)}.json`);
1548
+ const mainFile = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
1489
1549
  const main2 = readTextIfExists(mainFile);
1490
1550
  if (main2 !== null) {
1491
1551
  const mcpOwnership = [];
@@ -1511,8 +1571,8 @@ ${agent.body}`,
1511
1571
  };
1512
1572
 
1513
1573
  // src/adapters/codex.ts
1514
- import path10 from "path";
1515
- import fs7 from "fs";
1574
+ import path11 from "path";
1575
+ import fs8 from "fs";
1516
1576
  function tomlString(value) {
1517
1577
  return JSON.stringify(value);
1518
1578
  }
@@ -1535,15 +1595,15 @@ function isManagedOptionalStdioServer3(server, section) {
1535
1595
  return server.optional === true && server.transport === "stdio" && section?.trim() === stdioMcpSection(server);
1536
1596
  }
1537
1597
  function hasActiveEngramPlugin(configDir) {
1538
- const config = readTextIfExists(path10.join(configDir, "config.toml"));
1598
+ const config = readTextIfExists(path11.join(configDir, "config.toml"));
1539
1599
  if (config === null) return false;
1540
1600
  const match = /\[plugins\."engram@[^"]*"\]([^[]*)/.exec(config);
1541
1601
  return match !== null && !/enabled\s*=\s*false/.test(match[1]);
1542
1602
  }
1543
1603
  function hasEngramProtocol(configDir) {
1544
1604
  if (hasActiveEngramPlugin(configDir)) return true;
1545
- if (fs7.existsSync(path10.join(configDir, "engram-instructions.md"))) return true;
1546
- const config = readTextIfExists(path10.join(configDir, "config.toml"));
1605
+ if (fs8.existsSync(path11.join(configDir, "engram-instructions.md"))) return true;
1606
+ const config = readTextIfExists(path11.join(configDir, "config.toml"));
1547
1607
  return config !== null && /engram-instructions\.md/.test(config);
1548
1608
  }
1549
1609
  var CODEX_JSON_STRING = String.raw`"(?:\\.|[^"\\\r\n])*"`;
@@ -1668,7 +1728,7 @@ function hasCanonicalCodexPermissionProfile(scan) {
1668
1728
  return true;
1669
1729
  }
1670
1730
  function hasCodexManualApproval(configDir) {
1671
- const config = readTextIfExists(path10.join(configDir, "config.toml"));
1731
+ const config = readTextIfExists(path11.join(configDir, "config.toml"));
1672
1732
  if (config === null) return false;
1673
1733
  const scan = scanCodexToml(config);
1674
1734
  if (scan === null) return false;
@@ -1686,7 +1746,7 @@ var codexAdapter = {
1686
1746
  name: "Codex CLI",
1687
1747
  detect: detectCodex,
1688
1748
  reportCapabilities(configDir) {
1689
- const prompt = readTextIfExists(path10.join(configDir, "AGENTS.md"));
1749
+ const prompt = readTextIfExists(path11.join(configDir, "AGENTS.md"));
1690
1750
  return createLocalCapabilityReport("codex", [
1691
1751
  ...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
1692
1752
  id: "policy-guidance",
@@ -1711,18 +1771,18 @@ var codexAdapter = {
1711
1771
  return !hasEngramProtocol(ctx.configDir);
1712
1772
  },
1713
1773
  paths(configDir) {
1714
- const isRealConfigDir = samePath(configDir, process.env.CODEX_HOME ?? path10.join(HOME, ".codex"));
1715
- const agentsHome = isRealConfigDir ? HOME : path10.dirname(configDir);
1716
- const skillsDir = path10.join(agentsHome, ".agents", "skills");
1774
+ const isRealConfigDir = samePath(configDir, process.env.CODEX_HOME ?? path11.join(HOME, ".codex"));
1775
+ const agentsHome = isRealConfigDir ? HOME : path11.dirname(configDir);
1776
+ const skillsDir = path11.join(agentsHome, ".agents", "skills");
1717
1777
  return {
1718
- systemPromptFile: path10.join(configDir, "AGENTS.md"),
1719
- agentsDir: path10.join(configDir, "agents"),
1778
+ systemPromptFile: path11.join(configDir, "AGENTS.md"),
1779
+ agentsDir: path11.join(configDir, "agents"),
1720
1780
  skillsDir,
1721
1781
  // Los custom prompts de Codex están deprecados: los commands se
1722
1782
  // instalan como skills (renderCommand produce <nombre>/SKILL.md).
1723
1783
  commandsDir: skillsDir,
1724
1784
  pluginsDir: null,
1725
- scriptsDir: path10.join(configDir, "scripts"),
1785
+ scriptsDir: path11.join(configDir, "scripts"),
1726
1786
  outputStylesDir: null,
1727
1787
  profilesDir: configDir
1728
1788
  };
@@ -1770,7 +1830,7 @@ ${body}`
1770
1830
  planHooks(canonical, ctx) {
1771
1831
  const actions = [];
1772
1832
  const { scriptsDir } = this.paths(ctx.configDir);
1773
- const hooksFile = path10.join(ctx.configDir, "hooks.json");
1833
+ const hooksFile = path11.join(ctx.configDir, "hooks.json");
1774
1834
  const content = upsertNativeHooks(
1775
1835
  readTextIfExists(hooksFile),
1776
1836
  canonical,
@@ -1781,16 +1841,16 @@ ${body}`
1781
1841
  ctx.warnings.push(
1782
1842
  "Codex: los hooks no-managed requieren aprobaci\xF3n manual \u2014 ejecuta /hooks dentro de codex para activarlos."
1783
1843
  );
1784
- const scriptsSource = path10.join(ctx.stackDir, "scripts");
1785
- if (fs7.existsSync(scriptsSource)) {
1786
- for (const f of fs7.readdirSync(scriptsSource)) {
1787
- actions.push({ kind: "copy", source: path10.join(scriptsSource, f), target: path10.join(scriptsDir, f) });
1844
+ const scriptsSource = path11.join(ctx.stackDir, "scripts");
1845
+ if (fs8.existsSync(scriptsSource)) {
1846
+ for (const f of fs8.readdirSync(scriptsSource)) {
1847
+ actions.push({ kind: "copy", source: path11.join(scriptsSource, f), target: path11.join(scriptsDir, f) });
1788
1848
  }
1789
1849
  }
1790
1850
  return actions;
1791
1851
  },
1792
1852
  planMainConfig(canonical, ctx) {
1793
- const file = path10.join(ctx.configDir, "config.toml");
1853
+ const file = path11.join(ctx.configDir, "config.toml");
1794
1854
  const original = readTextIfExists(file);
1795
1855
  const contentSource = original === null || original.trim() === "" ? null : original;
1796
1856
  let content = contentSource;
@@ -1904,13 +1964,10 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1904
1964
  const { systemPromptFile } = this.paths(ctx.configDir);
1905
1965
  const prompt = readTextIfExists(systemPromptFile);
1906
1966
  if (prompt !== null) {
1907
- let content = removeMarkdownSection(prompt, "system-prompt");
1908
- content = removeMarkdownSection(content, "engram-protocol");
1909
- content = removeMarkdownSection(content, "browser");
1910
- content = removeMarkdownSection(content, "writing-style");
1967
+ const content = removeSystemPromptSections(prompt);
1911
1968
  actions.push({ kind: "write", target: systemPromptFile, content });
1912
1969
  }
1913
- const configFile = path10.join(ctx.configDir, "config.toml");
1970
+ const configFile = path11.join(ctx.configDir, "config.toml");
1914
1971
  const config = readTextIfExists(configFile);
1915
1972
  if (config !== null) {
1916
1973
  let content = config;
@@ -1945,7 +2002,7 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1945
2002
  ...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
1946
2003
  });
1947
2004
  }
1948
- const hooksFile = path10.join(ctx.configDir, "hooks.json");
2005
+ const hooksFile = path11.join(ctx.configDir, "hooks.json");
1949
2006
  const hooksJson = readTextIfExists(hooksFile);
1950
2007
  if (hooksJson !== null) {
1951
2008
  const content = removeNativeHooks(hooksJson, hooks);
@@ -1958,8 +2015,8 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1958
2015
  };
1959
2016
 
1960
2017
  // src/lib/install-mode.ts
1961
- import fs8 from "fs";
1962
- import path11 from "path";
2018
+ import fs9 from "fs";
2019
+ import path12 from "path";
1963
2020
  var INSTALL_MODES = /* @__PURE__ */ new Set(["human", "programmatic"]);
1964
2021
  var SUBAGENT_CONCURRENCIES = /* @__PURE__ */ new Set(["serial", "parallel"]);
1965
2022
  var DEFAULT_INSTALL_MODE_PREFERENCE = {
@@ -1967,10 +2024,10 @@ var DEFAULT_INSTALL_MODE_PREFERENCE = {
1967
2024
  subagentConcurrency: "serial"
1968
2025
  };
1969
2026
  function installModePreferenceFile() {
1970
- return path11.join(dataDir(), "install-mode.json");
2027
+ return path12.join(dataDir(), "install-mode.json");
1971
2028
  }
1972
2029
  function hasInstallModePreference(file = installModePreferenceFile()) {
1973
- return fs8.existsSync(file);
2030
+ return fs9.existsSync(file);
1974
2031
  }
1975
2032
  function isInstallMode(value) {
1976
2033
  return INSTALL_MODES.has(value);
@@ -2001,9 +2058,9 @@ function normalizeInstallModePreference(value) {
2001
2058
  throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
2002
2059
  }
2003
2060
  function loadInstallModePreference(file = installModePreferenceFile()) {
2004
- if (!fs8.existsSync(file)) return DEFAULT_INSTALL_MODE_PREFERENCE;
2061
+ if (!fs9.existsSync(file)) return DEFAULT_INSTALL_MODE_PREFERENCE;
2005
2062
  try {
2006
- const raw = fs8.readFileSync(file, "utf8");
2063
+ const raw = fs9.readFileSync(file, "utf8");
2007
2064
  return normalizeInstallModePreference(JSON.parse(raw));
2008
2065
  } catch (error) {
2009
2066
  const message = error instanceof Error ? error.message : String(error);
@@ -2041,10 +2098,10 @@ function parseInstallModePreferenceFlags(mode, subagentConcurrency) {
2041
2098
  }
2042
2099
 
2043
2100
  // src/lib/manifest.ts
2044
- import fs9 from "fs";
2045
- import path12 from "path";
2101
+ import fs10 from "fs";
2102
+ import path13 from "path";
2046
2103
  function manifestFile() {
2047
- return path12.join(dataDir(), "manifest.json");
2104
+ return path13.join(dataDir(), "manifest.json");
2048
2105
  }
2049
2106
  function readManifest(file = manifestFile()) {
2050
2107
  const raw = readTextIfExists(file);
@@ -2068,24 +2125,24 @@ function removeRuntimeManifest(id, file = manifestFile()) {
2068
2125
  writeText(file, JSON.stringify(manifest, null, 2) + "\n");
2069
2126
  }
2070
2127
  function findOrphans(prevOwned, currentTargets, root = HOME) {
2071
- return prevOwned.map((f) => path12.resolve(f)).filter(
2072
- (f) => !currentTargets.has(f) && path12.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs9.existsSync(f)
2128
+ return prevOwned.map((f) => path13.resolve(f)).filter(
2129
+ (f) => !currentTargets.has(f) && path13.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs10.existsSync(f)
2073
2130
  );
2074
2131
  }
2075
2132
 
2076
2133
  // src/components/system-prompt.ts
2077
- import path14 from "path";
2078
- import fs11 from "fs";
2134
+ import path15 from "path";
2135
+ import fs12 from "fs";
2079
2136
 
2080
2137
  // src/lib/mode-composition.ts
2081
- import fs10 from "fs";
2082
- import path13 from "path";
2138
+ import fs11 from "fs";
2139
+ import path14 from "path";
2083
2140
  var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
2084
2141
  var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
2085
2142
  var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
2086
2143
  var normalize = (value) => value.replace(/\r\n/g, "\n");
2087
2144
  function loadProgrammaticAddendum(stackDir, fileName) {
2088
- return normalize(fs10.readFileSync(path13.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
2145
+ return normalize(fs11.readFileSync(path14.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
2089
2146
  }
2090
2147
  function appendAddendum(base, addendum) {
2091
2148
  const normalizedBase = normalize(base);
@@ -2128,30 +2185,30 @@ function composeProgrammaticAgentBody(stackDir, agent, mode, concurrency) {
2128
2185
  var normalize2 = (s) => s.replace(/\r\n/g, "\n");
2129
2186
  function planSystemPrompt(adapter, ctx) {
2130
2187
  const target = adapter.paths(ctx.configDir).systemPromptFile;
2131
- const agentsMd = normalize2(fs11.readFileSync(path14.join(ctx.stackDir, "system-prompt", "AGENTS.md"), "utf8"));
2132
- const protocol = stripLeadingHtmlComments(
2133
- normalize2(fs11.readFileSync(path14.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
2188
+ const existing = readSystemPromptFile(target);
2189
+ assertSystemPromptMarkers(existing, target);
2190
+ const readModule = (file) => stripLeadingHtmlComments(
2191
+ normalize2(fs12.readFileSync(path15.join(ctx.stackDir, "system-prompt", file), "utf8"))
2134
2192
  );
2135
- const composedAgentsMd = composeProgrammaticSystemPrompt(ctx.stackDir, agentsMd, ctx.mode);
2136
- const browser = [
2137
- ctx.playwrightCliEnabled ? "browser-playwright.md" : null,
2138
- ctx.enabledMcpServers?.has(DEVTOOLS_MCP_SERVER) ? "browser-chrome-devtools.md" : null
2139
- ].filter((file) => file !== null).map((file) => normalize2(fs11.readFileSync(path14.join(ctx.stackDir, "system-prompt", file), "utf8"))).join("\n\n");
2140
- let content = readTextIfExists(target);
2141
- content = upsertMarkdownSection(content, "system-prompt", composedAgentsMd);
2142
- if (adapter.injectEngramProtocol(ctx)) {
2143
- content = upsertMarkdownSection(content, "engram-protocol", protocol);
2144
- } else {
2145
- content = removeMarkdownSection(content, "engram-protocol");
2193
+ const modules = {
2194
+ "system-prompt": composeProgrammaticSystemPrompt(ctx.stackDir, readModule("AGENTS.md"), ctx.mode),
2195
+ "engram-protocol": adapter.injectEngramProtocol(ctx) ? readModule("engram-protocol.md") : void 0,
2196
+ context7: readModule("context7.md"),
2197
+ playwright: ctx.playwrightCliEnabled ? readModule("browser-playwright.md") : void 0,
2198
+ "chrome-devtools": ctx.enabledMcpServers?.has(DEVTOOLS_MCP_SERVER) ? readModule("browser-chrome-devtools.md") : void 0,
2199
+ "writing-style": ctx.mode === "programmatic" ? void 0 : ctx.writingStyle?.content ?? void 0
2200
+ };
2201
+ const sections = adapter.adaptSystemPromptSections?.(modules) ?? modules;
2202
+ let content = existing ?? "";
2203
+ for (const section of SYSTEM_PROMPT_SECTIONS) {
2204
+ const body = sections[section];
2205
+ content = body ? upsertMarkdownSection(content, section, body) : removeMarkdownSection(content, section);
2146
2206
  }
2147
- content = browser === "" ? removeMarkdownSection(content, "browser") : upsertMarkdownSection(content, "browser", browser);
2148
- const style = ctx.mode === "programmatic" ? null : ctx.writingStyle?.content;
2149
- content = style ? upsertMarkdownSection(content, "writing-style", style) : removeMarkdownSection(content, "writing-style");
2150
2207
  return [{ kind: "write", target, content }];
2151
2208
  }
2152
2209
 
2153
2210
  // src/components/agents.ts
2154
- import path15 from "path";
2211
+ import path16 from "path";
2155
2212
  function planAgents(adapter, ctx) {
2156
2213
  const { agentsDir, commandsDir, outputStylesDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
2157
2214
  const dirFor = {
@@ -2161,7 +2218,7 @@ function planAgents(adapter, ctx) {
2161
2218
  profile: profilesDir
2162
2219
  };
2163
2220
  const scriptsBase = scriptsDir.replace(/\\/g, "/");
2164
- return loadCanonicalAgents(path15.join(ctx.stackDir, "agents")).flatMap((agent) => {
2221
+ return loadCanonicalAgents(path16.join(ctx.stackDir, "agents")).flatMap((agent) => {
2165
2222
  const composedAgent = {
2166
2223
  ...agent,
2167
2224
  body: composeProgrammaticAgentBody(ctx.stackDir, agent, ctx.mode, ctx.subagentConcurrency)
@@ -2173,42 +2230,42 @@ function planAgents(adapter, ctx) {
2173
2230
  return [];
2174
2231
  }
2175
2232
  const content = rendered.content.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsBase);
2176
- return [{ kind: "write", target: path15.join(dir, rendered.file), content }];
2233
+ return [{ kind: "write", target: path16.join(dir, rendered.file), content }];
2177
2234
  });
2178
2235
  });
2179
2236
  }
2180
2237
 
2181
2238
  // src/components/skills.ts
2182
- import path16 from "path";
2239
+ import path17 from "path";
2183
2240
  function planSkills(adapter, ctx) {
2184
2241
  const { skillsDir } = adapter.paths(ctx.configDir);
2185
- const source = path16.join(ctx.stackDir, "skills");
2242
+ const source = path17.join(ctx.stackDir, "skills");
2186
2243
  return listFilesRecursive(source).map((file) => {
2187
- const relative = path16.relative(source, file);
2188
- return { kind: "copy", source: file, target: path16.join(skillsDir, relative) };
2244
+ const relative = path17.relative(source, file);
2245
+ return { kind: "copy", source: file, target: path17.join(skillsDir, relative) };
2189
2246
  });
2190
2247
  }
2191
2248
 
2192
2249
  // src/components/commands.ts
2193
- import path17 from "path";
2194
- import fs12 from "fs";
2250
+ import path18 from "path";
2251
+ import fs13 from "fs";
2195
2252
  function planCommands(adapter, ctx) {
2196
2253
  const { commandsDir } = adapter.paths(ctx.configDir);
2197
- const source = path17.join(ctx.stackDir, "commands");
2198
- if (!fs12.existsSync(source)) return [];
2254
+ const source = path18.join(ctx.stackDir, "commands");
2255
+ if (!fs13.existsSync(source)) return [];
2199
2256
  const commandFiles = [
2200
2257
  ...listMarkdownFiles(source),
2201
- ...listMarkdownFiles(path17.join(source, adapter.id))
2258
+ ...listMarkdownFiles(path18.join(source, adapter.id))
2202
2259
  ];
2203
2260
  return commandFiles.map(({ file, fullPath }) => {
2204
- const raw = fs12.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
2261
+ const raw = fs13.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
2205
2262
  const rendered = adapter.renderCommand(file, raw);
2206
- return { kind: "write", target: path17.join(commandsDir, rendered.file), content: rendered.content };
2263
+ return { kind: "write", target: path18.join(commandsDir, rendered.file), content: rendered.content };
2207
2264
  });
2208
2265
  }
2209
2266
  function listMarkdownFiles(dir) {
2210
- if (!fs12.existsSync(dir)) return [];
2211
- return fs12.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path17.join(dir, entry.name) }));
2267
+ if (!fs13.existsSync(dir)) return [];
2268
+ return fs13.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path18.join(dir, entry.name) }));
2212
2269
  }
2213
2270
 
2214
2271
  // src/components/hooks.ts
@@ -2222,23 +2279,23 @@ function planMcp(adapter, ctx) {
2222
2279
  }
2223
2280
 
2224
2281
  // src/components/plugins.ts
2225
- import path18 from "path";
2226
- import fs13 from "fs";
2282
+ import path19 from "path";
2283
+ import fs14 from "fs";
2227
2284
  function planPlugins(adapter, ctx) {
2228
2285
  const { pluginsDir } = adapter.paths(ctx.configDir);
2229
2286
  if (pluginsDir === null) return [];
2230
- const source = path18.join(ctx.stackDir, "plugins", adapter.id);
2231
- if (!fs13.existsSync(source)) return [];
2287
+ const source = path19.join(ctx.stackDir, "plugins", adapter.id);
2288
+ if (!fs14.existsSync(source)) return [];
2232
2289
  return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).map((sourceFile) => {
2233
- const target = path18.join(pluginsDir, path18.relative(source, sourceFile));
2234
- const raw = fs13.readFileSync(sourceFile, "utf8");
2290
+ const target = path19.join(pluginsDir, path19.relative(source, sourceFile));
2291
+ const raw = fs14.readFileSync(sourceFile, "utf8");
2235
2292
  let content = raw;
2236
2293
  if (content.includes('"{{ENGRAM_BIN}}"')) {
2237
2294
  content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
2238
2295
  }
2239
2296
  if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
2240
2297
  const protocol = stripLeadingHtmlComments(
2241
- fs13.readFileSync(path18.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
2298
+ fs14.readFileSync(path19.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
2242
2299
  );
2243
2300
  content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
2244
2301
  }
@@ -2249,9 +2306,9 @@ function planPlugins(adapter, ctx) {
2249
2306
  }
2250
2307
 
2251
2308
  // src/lib/external-tools.ts
2252
- import fs14 from "fs";
2309
+ import fs15 from "fs";
2253
2310
  import os from "os";
2254
- import path19 from "path";
2311
+ import path20 from "path";
2255
2312
  import { execFileSync as execFileSync2 } from "child_process";
2256
2313
  var PLAYWRIGHT_CLI = {
2257
2314
  packageName: "@playwright/cli",
@@ -2291,14 +2348,15 @@ function resolvePlaywrightCliState({ binPath, versionOutput }) {
2291
2348
  detectedVersion
2292
2349
  };
2293
2350
  }
2294
- function detectPlaywrightCli() {
2295
- const binPath = lookPath(PLAYWRIGHT_CLI.bin);
2351
+ function detectPlaywrightCli(env) {
2352
+ const actualEnv = env ?? process.env;
2353
+ const binPath = env === void 0 ? lookPath(PLAYWRIGHT_CLI.bin) : lookPath(PLAYWRIGHT_CLI.bin, env);
2296
2354
  if (binPath === null) {
2297
- const pnpmHome = resolvePnpmHome();
2355
+ const pnpmHome = resolvePnpmHome(actualEnv);
2298
2356
  const name = process.platform === "win32" ? `${PLAYWRIGHT_CLI.bin}.cmd` : PLAYWRIGHT_CLI.bin;
2299
- const known = [path19.join(pnpmHome, name), path19.join(pnpmHome, "bin", name)].find((file) => {
2357
+ const known = [path20.join(pnpmHome, name), path20.join(pnpmHome, "bin", name)].find((file) => {
2300
2358
  try {
2301
- return fs14.statSync(file).isFile();
2359
+ return fs15.statSync(file).isFile();
2302
2360
  } catch {
2303
2361
  return false;
2304
2362
  }
@@ -2307,17 +2365,17 @@ function detectPlaywrightCli() {
2307
2365
  }
2308
2366
  return resolvePlaywrightCliState({
2309
2367
  binPath,
2310
- versionOutput: binPath ? runDetectedBin(binPath, ["--version"], 5e3, { NO_UPDATE_NOTIFIER: "1" }) : null
2368
+ versionOutput: binPath ? runDetectedBin(binPath, ["--version"], 5e3, { ...actualEnv, NO_UPDATE_NOTIFIER: "1" }) : null
2311
2369
  });
2312
2370
  }
2313
- function resolvePnpmBin() {
2314
- const pnpm = lookPath("pnpm");
2371
+ function resolvePnpmBin(env) {
2372
+ const pnpm = env === void 0 ? lookPath("pnpm") : lookPath("pnpm", env);
2315
2373
  if (pnpm !== null && !pnpm.toLowerCase().endsWith(".ps1")) return pnpm;
2316
- const pnpmCmd = lookPath("pnpm.cmd");
2374
+ const pnpmCmd = env === void 0 ? lookPath("pnpm.cmd") : lookPath("pnpm.cmd", env);
2317
2375
  return pnpmCmd !== null && !pnpmCmd.toLowerCase().endsWith(".ps1") ? pnpmCmd : null;
2318
2376
  }
2319
2377
  function resolvePnpmHome(env = process.env, platform = process.platform, homeDir = os.homedir()) {
2320
- const paths = platform === "win32" ? path19.win32 : path19.posix;
2378
+ const paths = platform === "win32" ? path20.win32 : path20.posix;
2321
2379
  if (env.PNPM_HOME) return env.PNPM_HOME;
2322
2380
  if (env.XDG_DATA_HOME) return paths.join(env.XDG_DATA_HOME, "pnpm");
2323
2381
  if (platform === "darwin") return paths.join(homeDir, "Library", "pnpm");
@@ -2326,7 +2384,7 @@ function resolvePnpmHome(env = process.env, platform = process.platform, homeDir
2326
2384
  }
2327
2385
  function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform, homeDir = os.homedir()) {
2328
2386
  const pnpmHome = resolvePnpmHome(env, platform, homeDir);
2329
- const paths = platform === "win32" ? path19.win32 : path19.posix;
2387
+ const paths = platform === "win32" ? path20.win32 : path20.posix;
2330
2388
  if (!paths.isAbsolute(pnpmHome)) return { ok: false, reason: "PNPM_HOME debe ser una ruta absoluta; corrige la configuraci\xF3n antes de reintentar." };
2331
2389
  const pathEntries = [pnpmHome, paths.join(pnpmHome, "bin")];
2332
2390
  if (env.PATH !== void 0) pathEntries.push(env.PATH);
@@ -2346,10 +2404,10 @@ function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform
2346
2404
  }
2347
2405
  function isPlaywrightBrowserReady(env = process.env, platform = process.platform, homeDir = os.homedir()) {
2348
2406
  const configuredPath = env.PLAYWRIGHT_BROWSERS_PATH;
2349
- const cacheDir = configuredPath ?? (platform === "win32" ? path19.join(env.LOCALAPPDATA ?? path19.join(homeDir, "AppData", "Local"), "ms-playwright") : platform === "darwin" ? path19.join(homeDir, "Library", "Caches", "ms-playwright") : path19.join(env.XDG_CACHE_HOME ?? path19.join(homeDir, ".cache"), "ms-playwright"));
2407
+ const cacheDir = configuredPath ?? (platform === "win32" ? path20.join(env.LOCALAPPDATA ?? path20.join(homeDir, "AppData", "Local"), "ms-playwright") : platform === "darwin" ? path20.join(homeDir, "Library", "Caches", "ms-playwright") : path20.join(env.XDG_CACHE_HOME ?? path20.join(homeDir, ".cache"), "ms-playwright"));
2350
2408
  if (configuredPath === "0") return { status: "missing", path: cacheDir, errorCode: "DISABLED" };
2351
2409
  try {
2352
- const ready = fs14.readdirSync(cacheDir, { withFileTypes: true }).some(
2410
+ const ready = fs15.readdirSync(cacheDir, { withFileTypes: true }).some(
2353
2411
  (entry) => entry.isDirectory() && /^chromium(?:_headless_shell)?-/.test(entry.name)
2354
2412
  );
2355
2413
  return ready ? { status: "ready", path: cacheDir } : { status: "missing", path: cacheDir };
@@ -2381,9 +2439,9 @@ function verifyPlaywrightBrowser(pnpmBin, env = process.env, cwd) {
2381
2439
  cwd,
2382
2440
  stdio: ["ignore", "pipe", "inherit"]
2383
2441
  }).trim();
2384
- if (!path19.isAbsolute(root)) return false;
2385
- const packageFile = fs14.realpathSync(path19.join(root, "@playwright", "cli", "package.json"));
2386
- const installed = JSON.parse(fs14.readFileSync(packageFile, "utf8"));
2442
+ if (!path20.isAbsolute(root)) return false;
2443
+ const packageFile = fs15.realpathSync(path20.join(root, "@playwright", "cli", "package.json"));
2444
+ const installed = JSON.parse(fs15.readFileSync(packageFile, "utf8"));
2387
2445
  if (installed.name !== PLAYWRIGHT_CLI.packageName || installed.version !== PLAYWRIGHT_CLI.version) return false;
2388
2446
  const probe = `
2389
2447
  const { createRequire } = require('node:module');
@@ -2426,7 +2484,7 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
2426
2484
  const command = planPlaywrightCliCommand(action, pnpmBin);
2427
2485
  const invocation = planDetectedBinCommand(command.command, command.args);
2428
2486
  if (invocation === null) return { ok: false, reason: "pnpm-command" };
2429
- const cwd = action === "install-browser" ? fs14.mkdtempSync(path19.join(os.tmpdir(), "jorgex-playwright-")) : void 0;
2487
+ const cwd = action === "install-browser" ? fs15.mkdtempSync(path20.join(os.tmpdir(), "jorgex-playwright-")) : void 0;
2430
2488
  const childEnv = action === "install-browser" ? { ...env ?? process.env, NO_UPDATE_NOTIFIER: "1" } : env ?? process.env;
2431
2489
  try {
2432
2490
  execFileSync2(invocation.command, invocation.args, {
@@ -2441,27 +2499,51 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
2441
2499
  } catch {
2442
2500
  return { ok: false, reason: "action-failed" };
2443
2501
  } finally {
2444
- if (cwd !== void 0) fs14.rmSync(cwd, { recursive: true, force: true });
2502
+ if (cwd !== void 0) fs15.rmSync(cwd, { recursive: true, force: true });
2503
+ }
2504
+ }
2505
+
2506
+ // src/lib/playwright-capability.ts
2507
+ function inspectPlaywrightCapability(options = {}) {
2508
+ const env = options.env ?? process.env;
2509
+ const cli = options.env === void 0 ? detectPlaywrightCli() : detectPlaywrightCli(env);
2510
+ const browserCache = options.env === void 0 ? isPlaywrightBrowserReady() : isPlaywrightBrowserReady(env);
2511
+ let browserVerified = false;
2512
+ if (cli.status === "current" && browserCache.status === "ready") {
2513
+ if (options.browserVerified === true) {
2514
+ browserVerified = true;
2515
+ } else {
2516
+ const pnpmBin = options.env === void 0 ? resolvePnpmBin() : resolvePnpmBin(env);
2517
+ if (pnpmBin !== null) {
2518
+ try {
2519
+ browserVerified = verifyPlaywrightBrowser(pnpmBin, env);
2520
+ } catch {
2521
+ browserVerified = false;
2522
+ }
2523
+ }
2524
+ }
2445
2525
  }
2526
+ const effective = cli.status === "current" && cli.binPath !== null && (options.browserVerified === true || browserCache.status === "ready") && browserVerified;
2527
+ return { cli, browserCache, browserVerified, effective };
2446
2528
  }
2447
2529
 
2448
2530
  // src/lib/tool-preferences.ts
2449
- import fs15 from "fs";
2450
- import path20 from "path";
2531
+ import fs16 from "fs";
2532
+ import path21 from "path";
2451
2533
  var PLAYWRIGHT_CLI_PREFERENCE_VERSION = 1;
2452
2534
  var DEVTOOLS_MCP_PREFERENCE_VERSION = 1;
2453
2535
  var PRIMARY_MODEL_OWNERSHIP_VERSION = 1;
2454
2536
  var PLAYWRIGHT_RUNTIMES = ["opencode", "claude-code", "codex", "pi"];
2455
2537
  function readPreference(file) {
2456
2538
  try {
2457
- return { raw: fs15.readFileSync(file, "utf8"), errorCode: null };
2539
+ return { raw: fs16.readFileSync(file, "utf8"), errorCode: null };
2458
2540
  } catch (error) {
2459
2541
  const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
2460
2542
  return code === "ENOENT" ? { raw: null, errorCode: null } : { raw: null, errorCode: code };
2461
2543
  }
2462
2544
  }
2463
2545
  function playwrightCliPreferenceFile(stateDir = dataDir()) {
2464
- return path20.join(stateDir, "playwright-cli.json");
2546
+ return path21.join(stateDir, "playwright-cli.json");
2465
2547
  }
2466
2548
  function parsePlaywrightCliPreference(raw) {
2467
2549
  try {
@@ -2507,10 +2589,10 @@ function savePlaywrightCliPreference(file, enabled, selection) {
2507
2589
  writeText(file, content);
2508
2590
  }
2509
2591
  function devtoolsMcpPreferenceFile(stateDir = dataDir()) {
2510
- return path20.join(stateDir, "devtools-mcp.json");
2592
+ return path21.join(stateDir, "devtools-mcp.json");
2511
2593
  }
2512
2594
  function primaryModelOwnershipFile(stateDir = dataDir()) {
2513
- return path20.join(stateDir, "primary-model.json");
2595
+ return path21.join(stateDir, "primary-model.json");
2514
2596
  }
2515
2597
  function isRecord2(value) {
2516
2598
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -2620,7 +2702,7 @@ function primaryModelOwnershipError(file = primaryModelOwnershipFile()) {
2620
2702
  return `Primary model: ownership inv\xE1lido en ${file}. Corrige o borra ese archivo antes de reintentar.`;
2621
2703
  }
2622
2704
  function primaryModelConfigKey(configDir) {
2623
- const resolved = path20.resolve(configDir);
2705
+ const resolved = path21.resolve(configDir);
2624
2706
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
2625
2707
  }
2626
2708
  function loadPrimaryModelOwnership(file, runtime, configDir) {
@@ -2705,7 +2787,7 @@ function persistConfigurationOwnershipChanges(runtime, configDir, plan) {
2705
2787
  const primaryFile = primaryModelOwnershipFile();
2706
2788
  for (const [field, owned] of primary) savePrimaryModelOwnership(primaryFile, runtime, configDir, field, owned);
2707
2789
  }
2708
- function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE, useBrowserPreferences = true) {
2790
+ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE, useBrowserPreferences = true, playwrightCapability) {
2709
2791
  const models = loadModelMap()[adapter.id];
2710
2792
  if (!models) return null;
2711
2793
  return {
@@ -2717,7 +2799,7 @@ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE,
2717
2799
  models,
2718
2800
  warnings: [],
2719
2801
  enabledMcpServers: enabledMcpServers(adapter.id, void 0, useBrowserPreferences),
2720
- playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), adapter.id) === true,
2802
+ playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), adapter.id) === true && (playwrightCapability ?? true),
2721
2803
  ownedMcpServers: ownedMcpServers(adapter.id, useBrowserPreferences),
2722
2804
  ownedPrimaryModelFields: useBrowserPreferences ? loadPrimaryModelOwnership(primaryModelOwnershipFile(), adapter.id, configDir) : /* @__PURE__ */ new Set()
2723
2805
  };
@@ -2740,7 +2822,7 @@ function diffPlan(plan) {
2740
2822
  if (current === null) return { action, status: "create" };
2741
2823
  return { action, status: current === action.content ? "unchanged" : "update" };
2742
2824
  }
2743
- if (!fs16.existsSync(action.target)) return { action, status: "create" };
2825
+ if (!fs17.existsSync(action.target)) return { action, status: "create" };
2744
2826
  return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
2745
2827
  });
2746
2828
  }
@@ -2752,21 +2834,21 @@ function applyChanges(changes, onOwnershipWritten) {
2752
2834
  } else copyFile(action.source, action.target);
2753
2835
  }
2754
2836
  }
2755
- function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
2837
+ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE, playwrightCapability) {
2756
2838
  const targets = /* @__PURE__ */ new Set();
2757
2839
  let complete = true;
2758
2840
  const warnings = [];
2759
2841
  for (const adapter of Object.values(ADAPTERS)) {
2760
2842
  const detection = adapter.detect();
2761
2843
  if (!detection.installed) continue;
2762
- const ctx = makeContext(adapter, detection.configDir, mode);
2844
+ const ctx = makeContext(adapter, detection.configDir, mode, true, playwrightCapability);
2763
2845
  if (!ctx) {
2764
2846
  complete = false;
2765
2847
  warnings.push(`${adapter.name}: limpieza de hu\xE9rfanos deshabilitada \u2014 falta contexto/model-map instalable para este runtime.`);
2766
2848
  continue;
2767
2849
  }
2768
2850
  try {
2769
- for (const action of buildPlan(adapter, ctx)) targets.add(path21.resolve(action.target));
2851
+ for (const action of buildPlan(adapter, ctx)) targets.add(path22.resolve(action.target));
2770
2852
  } catch (error) {
2771
2853
  complete = false;
2772
2854
  warnings.push(
@@ -2779,6 +2861,18 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
2779
2861
  async function runInstall(opts) {
2780
2862
  const showSummary = opts.showSummary !== false;
2781
2863
  if (showSummary) p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
2864
+ try {
2865
+ for (const id of opts.runtimes) {
2866
+ const adapter = ADAPTERS[id];
2867
+ if (!adapter) continue;
2868
+ const detection = adapter.detect();
2869
+ if (opts.targetDir === void 0 && !detection.installed) continue;
2870
+ assertSystemPromptFile(adapter.paths(opts.targetDir ?? detection.configDir).systemPromptFile, opts.targetDir);
2871
+ }
2872
+ } catch (error) {
2873
+ p.log.error(error instanceof Error ? error.message : String(error));
2874
+ return 1;
2875
+ }
2782
2876
  let writingStyle;
2783
2877
  let preparedStyle;
2784
2878
  try {
@@ -2804,6 +2898,10 @@ async function runInstall(opts) {
2804
2898
  });
2805
2899
  const projectPlaywrightPrompt = opts.dryRun && toolPlan?.persistEnabledOnSuccess === true;
2806
2900
  const hasFileRuntimes = opts.runtimes.length > 0;
2901
+ const shouldInspectPlaywright = useManifest && !opts.dryRun && (toolPlan === null || toolPlan.actions.length === 0) && loadPlaywrightCliPreference() === true;
2902
+ const playwrightCapability = opts.dryRun || !useManifest ? void 0 : opts.playwrightCapability ?? (shouldInspectPlaywright ? inspectPlaywrightCapability() : void 0);
2903
+ const effectivePlaywright = playwrightCapability?.effective;
2904
+ const plannedPlaywright = effectivePlaywright ?? (toolPlan !== null && toolPlan.actions.length > 0 ? false : void 0);
2807
2905
  const modelMap = hasFileRuntimes ? loadModelMap() : {};
2808
2906
  if (preparedStyle !== void 0) {
2809
2907
  try {
@@ -2816,7 +2914,7 @@ async function runInstall(opts) {
2816
2914
  }
2817
2915
  if (hasFileRuntimes && useManifest && !opts.dryRun) ensureModelMapFile();
2818
2916
  p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
2819
- const current = hasFileRuntimes && useManifest ? collectAllCurrentTargets(modePreference) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
2917
+ const current = hasFileRuntimes && useManifest ? collectAllCurrentTargets(modePreference, plannedPlaywright) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
2820
2918
  const canOrphan = hasFileRuntimes && useManifest && current.complete;
2821
2919
  const canonicalMcp = loadCanonicalMcp(stackDir);
2822
2920
  const canonicalHooks = loadCanonicalHooks(stackDir);
@@ -2855,7 +2953,7 @@ async function runInstall(opts) {
2855
2953
  models,
2856
2954
  warnings: [],
2857
2955
  enabledMcpServers: enabledMcpServers(id, opts.devtoolsMcpSelection?.[id], useManifest),
2858
- playwrightCliEnabled: projectPlaywrightPrompt ? opts.playwrightToolConsent?.runtimeSelection?.[id] ?? true : useManifest && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), id) === true,
2956
+ playwrightCliEnabled: projectPlaywrightPrompt ? opts.playwrightToolConsent?.runtimeSelection?.[id] ?? true : useManifest && loadPlaywrightCliPreference(playwrightCliPreferenceFile(), id) === true && (plannedPlaywright ?? true),
2859
2957
  ownedMcpServers: ownedMcpServers(id, useManifest),
2860
2958
  ownedPrimaryModelFields: useManifest ? loadPrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir) : /* @__PURE__ */ new Set()
2861
2959
  };
@@ -2889,10 +2987,10 @@ async function runInstall(opts) {
2889
2987
  }
2890
2988
  const writeManifest = () => {
2891
2989
  if (!useManifest) return;
2892
- const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path21.resolve(a.target)));
2990
+ const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path22.resolve(a.target)));
2893
2991
  const keepTarget = (target) => !unmergeTargets.has(target);
2894
- const liveOwned = plan.map((a) => path21.resolve(a.target)).filter(keepTarget);
2895
- const previousOwned = (prevManifest?.owned ?? []).map((target) => path21.resolve(target)).filter(keepTarget);
2992
+ const liveOwned = plan.map((a) => path22.resolve(a.target)).filter(keepTarget);
2993
+ const previousOwned = (prevManifest?.owned ?? []).map((target) => path22.resolve(target)).filter(keepTarget);
2896
2994
  const owned = canOrphan ? liveOwned : [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
2897
2995
  writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2898
2996
  };
@@ -2921,9 +3019,9 @@ async function runInstall(opts) {
2921
3019
  const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
2922
3020
  if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
2923
3021
  applyChanges(changes, useManifest ? (action) => persistConfigurationOwnershipChanges(id, configDir, [action]) : void 0);
2924
- const pruneRoot = useManifest ? HOME : path21.dirname(configDir);
3022
+ const pruneRoot = useManifest ? HOME : path22.dirname(configDir);
2925
3023
  for (const orphan of orphans) {
2926
- fs16.rmSync(orphan, { force: true });
3024
+ fs17.rmSync(orphan, { force: true });
2927
3025
  pruneEmptyDirs(orphan, pruneRoot);
2928
3026
  }
2929
3027
  const verifyCtx = { ...ctx, warnings: [] };
@@ -3021,21 +3119,23 @@ async function runInstall(opts) {
3021
3119
  exitCode = 1;
3022
3120
  p.log.error("Playwright CLI y navegador se han instalado y la preferencia est\xE1 activada, pero la gu\xEDa de navegador qued\xF3 en estado parcial. Ejecuta 'jorgex-stack sync' para repararla.");
3023
3121
  } else {
3122
+ const verified = preparedEnv === void 0 ? inspectPlaywrightCapability({ browserVerified: true }) : inspectPlaywrightCapability({ browserVerified: true, env: preparedEnv });
3123
+ if (verified.effective && verified.cli.status === "current" && verified.cli.binPath !== null && verified.cli.detectedVersion !== null && verified.browserCache.status === "ready") {
3124
+ opts.onPlaywrightCapability?.(verified);
3125
+ }
3024
3126
  p.log.success("Playwright CLI instalado y arranque de Chromium verificado.");
3025
3127
  }
3026
3128
  }
3027
3129
  }
3028
- } else if (useManifest && opts.playwrightToolConsent?.command === "sync" && loadPlaywrightCliPreference() === true) {
3029
- const cli = detectPlaywrightCli();
3030
- if (cli.status !== "current") {
3130
+ } else if (playwrightCapability !== void 0 && !playwrightCapability.effective) {
3131
+ if (playwrightCapability.cli.status !== "current") {
3031
3132
  p.log.warn("Playwright CLI sigue habilitado, pero el paquete no est\xE1 listo; sync no instala herramientas. Ejecuta 'jorgex-stack install --playwright'.");
3133
+ } else if (playwrightCapability.browserCache.status === "unreadable") {
3134
+ p.log.warn(`Playwright CLI sigue habilitado, pero no se puede leer la cach\xE9 de navegadores en ${playwrightCapability.browserCache.path} (${playwrightCapability.browserCache.errorCode}). Revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
3135
+ } else if (playwrightCapability.browserCache.status === "missing") {
3136
+ p.log.warn("Playwright CLI sigue habilitado, pero falta el navegador; sync no descarga navegadores. Ejecuta 'jorgex-stack install --playwright'.");
3032
3137
  } else {
3033
- const browserCache = isPlaywrightBrowserReady();
3034
- if (browserCache.status === "unreadable") {
3035
- p.log.warn(`Playwright CLI sigue habilitado, pero no se puede leer la cach\xE9 de navegadores en ${browserCache.path} (${browserCache.errorCode}). Revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
3036
- } else if (browserCache.status === "missing") {
3037
- p.log.warn("Playwright CLI sigue habilitado, pero falta el navegador; sync no descarga navegadores. Ejecuta 'jorgex-stack install --playwright'.");
3038
- }
3138
+ p.log.warn("Playwright CLI sigue habilitado, pero Chromium no arranca; se ha retirado la gu\xEDa. Ejecuta 'jorgex-stack install --playwright' para repararlo.");
3039
3139
  }
3040
3140
  }
3041
3141
  if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
@@ -3048,29 +3148,33 @@ async function runInstall(opts) {
3048
3148
  }
3049
3149
 
3050
3150
  // src/uninstall.ts
3051
- import fs18 from "fs";
3052
- import path25 from "path";
3151
+ import fs19 from "fs";
3152
+ import path26 from "path";
3053
3153
  import * as p2 from "@clack/prompts";
3054
3154
 
3055
3155
  // src/lib/pi-projection-lifecycle.ts
3056
- import fs17 from "fs";
3057
- import path24 from "path";
3156
+ import fs18 from "fs";
3157
+ import path25 from "path";
3058
3158
  import { createHash } from "crypto";
3059
3159
 
3060
3160
  // src/adapters/pi.ts
3061
- import path22 from "path";
3161
+ import path23 from "path";
3162
+ function piSystemPromptFile(targetDir) {
3163
+ const configDir = targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path23.join(HOME, ".pi", "agent") : path23.join(path23.resolve(targetDir), "pi-agent");
3164
+ return path23.join(configDir, "AGENTS.md");
3165
+ }
3062
3166
  var piAdapter = {
3063
3167
  id: "pi",
3064
3168
  paths(configDir) {
3065
- const piConfigDir = process.env.PI_CODING_AGENT_DIR ?? path22.join(HOME, ".pi", "agent");
3066
- const agentsHome = samePath(configDir, piConfigDir) ? HOME : path22.join(path22.dirname(configDir), "home");
3169
+ const piConfigDir = path23.dirname(piSystemPromptFile());
3170
+ const agentsHome = samePath(configDir, piConfigDir) ? HOME : path23.join(path23.dirname(configDir), "home");
3067
3171
  return {
3068
- systemPromptFile: path22.join(configDir, "AGENTS.md"),
3069
- agentsDir: path22.join(configDir, "agents"),
3070
- skillsDir: path22.join(agentsHome, ".agents", "skills"),
3071
- commandsDir: path22.join(configDir, "prompts"),
3172
+ systemPromptFile: path23.join(configDir, "AGENTS.md"),
3173
+ agentsDir: path23.join(configDir, "agents"),
3174
+ skillsDir: path23.join(agentsHome, ".agents", "skills"),
3175
+ commandsDir: path23.join(configDir, "prompts"),
3072
3176
  pluginsDir: null,
3073
- scriptsDir: path22.join(configDir, "scripts"),
3177
+ scriptsDir: path23.join(configDir, "scripts"),
3074
3178
  outputStylesDir: null,
3075
3179
  profilesDir: null
3076
3180
  };
@@ -3080,11 +3184,20 @@ var piAdapter = {
3080
3184
  },
3081
3185
  injectEngramProtocol() {
3082
3186
  return true;
3187
+ },
3188
+ // Pi 0.8.18 recompone estos marcadores desde su snapshot: no conoce los nuevos.
3189
+ adaptSystemPromptSections(sections) {
3190
+ const { context7, playwright, "chrome-devtools": devtools, ...legacy } = sections;
3191
+ return {
3192
+ ...legacy,
3193
+ "system-prompt": [legacy["system-prompt"], context7].filter(Boolean).join("\n\n"),
3194
+ browser: [playwright, devtools].filter(Boolean).join("\n\n")
3195
+ };
3083
3196
  }
3084
3197
  };
3085
3198
 
3086
3199
  // src/lib/pi-package-lifecycle.ts
3087
- import path23 from "path";
3200
+ import path24 from "path";
3088
3201
  var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
3089
3202
  "foundation-contract-v1",
3090
3203
  "runner-json-v1",
@@ -3216,7 +3329,7 @@ function parseReceiptShape(receiptJson) {
3216
3329
  return null;
3217
3330
  }
3218
3331
  if (engram === void 0) return "upgrade-required";
3219
- if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path23.isAbsolute(Reflect.get(engram, "binary"))) {
3332
+ if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path24.isAbsolute(Reflect.get(engram, "binary"))) {
3220
3333
  return null;
3221
3334
  }
3222
3335
  return parsed;
@@ -3253,7 +3366,7 @@ function planPiPackageLifecycle(input) {
3253
3366
  if (input.receiptJson !== null) {
3254
3367
  const parsedReceipt = parseReceipt(input.receiptJson, input.candidate, {
3255
3368
  kind: input.scope.kind,
3256
- codingAgentDir: path23.resolve(input.scope.codingAgentDir)
3369
+ codingAgentDir: path24.resolve(input.scope.codingAgentDir)
3257
3370
  }, input.engramBin);
3258
3371
  if (parsedReceipt === "upgrade-required") return blocked(input, "receipt-upgrade-required");
3259
3372
  if (parsedReceipt === null) return blocked(input, "receipt-corrupt");
@@ -3288,7 +3401,7 @@ function planPiPackageLifecycle(input) {
3288
3401
  },
3289
3402
  receipt: expectedReceipt(input.candidate, "installing", {
3290
3403
  kind: input.scope.kind,
3291
- codingAgentDir: path23.resolve(input.scope.codingAgentDir)
3404
+ codingAgentDir: path24.resolve(input.scope.codingAgentDir)
3292
3405
  }, input.engramBin),
3293
3406
  ownership: ownership(true)
3294
3407
  };
@@ -3303,7 +3416,7 @@ function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
3303
3416
  const parsed = JSON.parse(body);
3304
3417
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3305
3418
  const record = parsed;
3306
- if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path23.isAbsolute(record.package.root) || path23.resolve(packageRunner) !== path23.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
3419
+ if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path24.isAbsolute(record.package.root) || path24.resolve(packageRunner) !== path24.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
3307
3420
  return null;
3308
3421
  }
3309
3422
  return record;
@@ -3389,10 +3502,10 @@ function validateOwnedOperationState(input) {
3389
3502
  }))) {
3390
3503
  return { kind: "blocked", reason: "receipt-untrusted" };
3391
3504
  }
3392
- if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path23.resolve(receipt.scope.codingAgentDir) !== path23.resolve(input.paths.codingAgentDir)) {
3505
+ if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path24.resolve(receipt.scope.codingAgentDir) !== path24.resolve(input.paths.codingAgentDir)) {
3393
3506
  return { kind: "blocked", reason: "source-divergent" };
3394
3507
  }
3395
- if (input.engramBin !== null && path23.resolve(receipt.engram.binary) !== path23.resolve(input.engramBin)) {
3508
+ if (input.engramBin !== null && path24.resolve(receipt.engram.binary) !== path24.resolve(input.engramBin)) {
3396
3509
  return { kind: "blocked", reason: "receipt-corrupt" };
3397
3510
  }
3398
3511
  const source = receipt.candidate.package.source;
@@ -3499,7 +3612,7 @@ var RETIRED_PLAYWRIGHT_FILES = [
3499
3612
  "references/video-recording.md"
3500
3613
  ];
3501
3614
  function retiredPlaywrightFiles(scope) {
3502
- return RETIRED_PLAYWRIGHT_FILES.map((file) => path24.resolve(scope.home, ".agents", "skills", "playwright-cli", file));
3615
+ return RETIRED_PLAYWRIGHT_FILES.map((file) => path25.resolve(scope.home, ".agents", "skills", "playwright-cli", file));
3503
3616
  }
3504
3617
  function handoffEnabled(input, kind) {
3505
3618
  return (kind === "devtools" ? input.devtoolsMcpEnabled : input.playwrightHandoffEnabled) === true;
@@ -3508,18 +3621,18 @@ var preparedPiProjectionUninstalls = /* @__PURE__ */ new WeakMap();
3508
3621
  function projectionScope(scope) {
3509
3622
  return {
3510
3623
  ...scope,
3511
- settingsFile: path24.join(scope.codingAgentDir, "settings.json")
3624
+ settingsFile: path25.join(scope.codingAgentDir, "settings.json")
3512
3625
  };
3513
3626
  }
3514
3627
  function isInside(root, file) {
3515
- const relative = path24.relative(path24.resolve(root), path24.resolve(file));
3516
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path24.sep}`) && !path24.isAbsolute(relative);
3628
+ const relative = path25.relative(path25.resolve(root), path25.resolve(file));
3629
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path25.sep}`) && !path25.isAbsolute(relative);
3517
3630
  }
3518
3631
  function isManagedPath(scope, file) {
3519
3632
  return isInside(scope.home, file) || isInside(scope.codingAgentDir, file);
3520
3633
  }
3521
3634
  function isAllowedPath(scope, file) {
3522
- return isManagedPath(scope, file) || path24.resolve(file) === path24.resolve(scope.receiptFile);
3635
+ return isManagedPath(scope, file) || path25.resolve(file) === path25.resolve(scope.receiptFile);
3523
3636
  }
3524
3637
  function projectedPiAdapter(scope) {
3525
3638
  const paths = piAdapter.paths(scope.codingAgentDir);
@@ -3527,11 +3640,11 @@ function projectedPiAdapter(scope) {
3527
3640
  ...piAdapter,
3528
3641
  paths: () => ({
3529
3642
  ...paths,
3530
- skillsDir: path24.join(scope.home, ".agents", "skills")
3643
+ skillsDir: path25.join(scope.home, ".agents", "skills")
3531
3644
  })
3532
3645
  };
3533
3646
  }
3534
- function projectionPlan(input, scope) {
3647
+ function projectionPlan(input, scope, includePrompt = true) {
3535
3648
  const ctx = {
3536
3649
  writingStyle: input.writingStyle,
3537
3650
  stackDir: input.stackDir,
@@ -3544,7 +3657,7 @@ function projectionPlan(input, scope) {
3544
3657
  };
3545
3658
  const adapter = projectedPiAdapter(scope);
3546
3659
  const actions = [
3547
- ...planSystemPrompt(adapter, ctx),
3660
+ ...includePrompt ? planSystemPrompt(adapter, ctx) : [],
3548
3661
  ...planSkills(adapter, ctx),
3549
3662
  ...planCommands(adapter, ctx)
3550
3663
  ];
@@ -3571,20 +3684,20 @@ function projectionPlan(input, scope) {
3571
3684
  return actions;
3572
3685
  }
3573
3686
  function handoffPath(scope, kind) {
3574
- return path24.resolve(scope.codingAgentDir, "jorgex-pi", `${kind}.v1.json`);
3687
+ return path25.resolve(scope.codingAgentDir, "jorgex-pi", `${kind}.v1.json`);
3575
3688
  }
3576
3689
  function contentHash(content) {
3577
3690
  return createHash("sha256").update(content).digest("hex");
3578
3691
  }
3579
3692
  function canonicalActionContent(action) {
3580
- return action.kind === "write" ? action.content : fs17.readFileSync(action.source, "utf8");
3693
+ return action.kind === "write" ? action.content : fs18.readFileSync(action.source, "utf8");
3581
3694
  }
3582
3695
  function hasActionDrift(action, deps) {
3583
3696
  return deps.readText(action.target) !== canonicalActionContent(action);
3584
3697
  }
3585
3698
  function applyActions(actions, deps, handoffs = []) {
3586
3699
  for (const action of actions) {
3587
- const handoff = handoffs.find((entry) => path24.resolve(action.target) === entry.file);
3700
+ const handoff = handoffs.find((entry) => path25.resolve(action.target) === entry.file);
3588
3701
  if (handoff) {
3589
3702
  try {
3590
3703
  if (deps.readText(handoff.file) !== handoff.previous) return handoffConflict(handoff.kind, handoff.file);
@@ -3604,9 +3717,9 @@ function assertPlanContained(plan, scope) {
3604
3717
  }
3605
3718
  }
3606
3719
  function receiptFor(plan, scope) {
3607
- const systemPrompt = path24.join(scope.codingAgentDir, "AGENTS.md");
3720
+ const systemPrompt = path25.join(scope.codingAgentDir, "AGENTS.md");
3608
3721
  const owned = [...new Set(
3609
- plan.map((action) => path24.resolve(action.target)).filter((target) => target !== systemPrompt)
3722
+ plan.map((action) => path25.resolve(action.target)).filter((target) => target !== systemPrompt)
3610
3723
  )];
3611
3724
  const receipt = {
3612
3725
  schemaVersion: 1,
@@ -3619,7 +3732,7 @@ function receiptFor(plan, scope) {
3619
3732
  owned
3620
3733
  };
3621
3734
  for (const kind of HANDOFF_KINDS) {
3622
- const action = plan.find((entry) => path24.resolve(entry.target) === handoffPath(scope, kind));
3735
+ const action = plan.find((entry) => path25.resolve(entry.target) === handoffPath(scope, kind));
3623
3736
  if (action) receipt[kind] = { sha256: contentHash(canonicalActionContent(action)) };
3624
3737
  }
3625
3738
  return receipt;
@@ -3660,7 +3773,7 @@ function parseReceipt2(raw, expected) {
3660
3773
  if (matches(expectedOwned)) return { schemaVersion: 1, scope: expected.scope, owned: expectedOwned, ...digests };
3661
3774
  const retired = retiredPlaywrightFiles(expected.scope);
3662
3775
  if (retired.some((file) => expectedOwned.includes(file))) return null;
3663
- const skillsRoot = `${path24.resolve(expected.scope.home, ".agents", "skills")}${path24.sep}`;
3776
+ const skillsRoot = `${path25.resolve(expected.scope.home, ".agents", "skills")}${path25.sep}`;
3664
3777
  const insertion = expectedOwned.findIndex((file) => !file.startsWith(skillsRoot) || file > retired[0]);
3665
3778
  const legacyOwned = [...expectedOwned];
3666
3779
  legacyOwned.splice(insertion === -1 ? legacyOwned.length : insertion, 0, ...retired);
@@ -3671,7 +3784,7 @@ function parseReceipt2(raw, expected) {
3671
3784
  }
3672
3785
  }
3673
3786
  function expectedProjectionReceipt(input, scope) {
3674
- const plan = projectionPlan(input, scope);
3787
+ const plan = projectionPlan(input, scope, false);
3675
3788
  assertPlanContained(plan, scope);
3676
3789
  return receiptFor(plan, scope);
3677
3790
  }
@@ -3679,13 +3792,13 @@ function realPiProjectionScope() {
3679
3792
  return {
3680
3793
  kind: "real",
3681
3794
  home: HOME,
3682
- codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path24.join(HOME, ".pi", "agent"),
3683
- receiptFile: path24.join(dataDir(), "pi-projection-receipt.json")
3795
+ codingAgentDir: path25.dirname(piSystemPromptFile()),
3796
+ receiptFile: path25.join(dataDir(), "pi-projection-receipt.json")
3684
3797
  };
3685
3798
  }
3686
3799
  function readTextOnlyIfMissing(file) {
3687
3800
  try {
3688
- return fs17.readFileSync(file, "utf8");
3801
+ return fs18.readFileSync(file, "utf8");
3689
3802
  } catch (error) {
3690
3803
  if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT") return null;
3691
3804
  throw error;
@@ -3718,13 +3831,13 @@ function manifestOwned(manifest) {
3718
3831
  return new Set([
3719
3832
  ...manifest.runtimes.codex?.owned ?? [],
3720
3833
  ...manifest.runtimes.opencode?.owned ?? []
3721
- ].map((file) => path24.resolve(file)));
3834
+ ].map((file) => path25.resolve(file)));
3722
3835
  }
3723
3836
  function withoutManagedPromptSections(content) {
3724
- return ["system-prompt", "engram-protocol", "browser", "writing-style"].reduce((current, section) => removeMarkdownSection(current, section), content);
3837
+ return removeSystemPromptSections(content);
3725
3838
  }
3726
3839
  function uniquePaths(paths) {
3727
- return [...new Set(paths.map((file) => path24.resolve(file)))];
3840
+ return [...new Set(paths.map((file) => path25.resolve(file)))];
3728
3841
  }
3729
3842
  function blocked2(reason, paths, remedy) {
3730
3843
  return { kind: "blocked", reason, paths: uniquePaths(paths), remedy };
@@ -3831,15 +3944,20 @@ function preparePiProjectionUninstall(input, deps) {
3831
3944
  }
3832
3945
  const scope = projectionScope(input.scope);
3833
3946
  const expectedReceipt2 = expectedProjectionReceipt(input, scope);
3834
- const prompt = path24.join(scope.codingAgentDir, "AGENTS.md");
3947
+ const prompt = path25.join(scope.codingAgentDir, "AGENTS.md");
3835
3948
  let existingPrompt;
3836
3949
  try {
3837
3950
  existingPrompt = deps.readText(prompt);
3838
3951
  } catch {
3839
3952
  return cleanupFailure([prompt]);
3840
3953
  }
3954
+ try {
3955
+ assertSystemPromptMarkers(existingPrompt, prompt);
3956
+ } catch (error) {
3957
+ return blocked2("projection-prompt-markers", [prompt], error instanceof Error ? error.message : String(error));
3958
+ }
3841
3959
  const promptContent = existingPrompt === null ? null : withoutManagedPromptSections(existingPrompt);
3842
- const promptUpdate = promptContent === null || promptContent === existingPrompt ? null : { file: path24.resolve(prompt), content: promptContent };
3960
+ const promptUpdate = promptContent === null || promptContent === existingPrompt ? null : { file: path25.resolve(prompt), content: promptContent };
3843
3961
  const receiptRead = readProjectionReceipt(scope.receiptFile, deps);
3844
3962
  if (receiptRead.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3845
3963
  let ownedToRemove = [];
@@ -3857,7 +3975,7 @@ function preparePiProjectionUninstall(input, deps) {
3857
3975
  return cleanupFailure([scope.receiptFile]);
3858
3976
  }
3859
3977
  ownedToRemove = receipt.owned.filter((owned) => !retained.has(owned));
3860
- receiptFile = path24.resolve(scope.receiptFile);
3978
+ receiptFile = path25.resolve(scope.receiptFile);
3861
3979
  backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
3862
3980
  }
3863
3981
  const handoffs = [];
@@ -3935,10 +4053,16 @@ function runPiProjectionLifecycle(input, deps) {
3935
4053
  const prepared = preparePiProjectionUninstall(input, deps);
3936
4054
  return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
3937
4055
  }
3938
- if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path24.isAbsolute(input.pnpmBin))) {
4056
+ const prompt = path25.join(scope.codingAgentDir, "AGENTS.md");
4057
+ try {
4058
+ assertSystemPromptMarkers(deps.readText(prompt), prompt);
4059
+ } catch (error) {
4060
+ return blocked2("projection-prompt-markers", [prompt], error instanceof Error ? error.message : String(error));
4061
+ }
4062
+ if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path25.isAbsolute(input.pnpmBin))) {
3939
4063
  return blocked2("projection-devtools-command", [handoffPath(scope, "devtools")], "DevTools requiere un ejecutable pnpm absoluto disponible en PATH.");
3940
4064
  }
3941
- if (input.playwrightHandoffEnabled && (!input.playwrightCliCommand || !path24.isAbsolute(input.playwrightCliCommand) || /[\u0000-\u001f\u007f]/.test(input.playwrightCliCommand))) {
4065
+ if (input.playwrightHandoffEnabled && (!input.playwrightCliCommand || !path25.isAbsolute(input.playwrightCliCommand) || /[\u0000-\u001f\u007f]/.test(input.playwrightCliCommand))) {
3942
4066
  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.");
3943
4067
  }
3944
4068
  const plan = projectionPlan(input, scope);
@@ -3951,12 +4075,12 @@ function runPiProjectionLifecycle(input, deps) {
3951
4075
  const expectedReceipt2 = receiptContent(receipt);
3952
4076
  const drifted = plan.filter((action) => hasActionDrift(action, deps));
3953
4077
  for (const { kind, file } of checked.handoffs) {
3954
- if (handoffEnabled(input, kind) && checked.previous?.[kind]?.sha256 !== receipt[kind]?.sha256 && !drifted.some((action) => path24.resolve(action.target) === file)) {
4078
+ if (handoffEnabled(input, kind) && checked.previous?.[kind]?.sha256 !== receipt[kind]?.sha256 && !drifted.some((action) => path25.resolve(action.target) === file)) {
3955
4079
  return input.operation === "doctor" ? { kind: "drift", paths: [file] } : handoffConflict(kind, file);
3956
4080
  }
3957
4081
  }
3958
4082
  if (input.operation === "doctor") {
3959
- const paths = drifted.map((action) => path24.resolve(action.target));
4083
+ const paths = drifted.map((action) => path25.resolve(action.target));
3960
4084
  paths.push(...removedHandoffs.map(({ file }) => file), ...retiredSkills);
3961
4085
  const currentSettings2 = deps.readText(scope.settingsFile);
3962
4086
  if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
@@ -4015,12 +4139,12 @@ function runPiProjectionLifecycle(input, deps) {
4015
4139
  return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0 };
4016
4140
  }
4017
4141
  function systemProjectionLifecycle(input) {
4018
- const targetRoot = input.targetDir === void 0 ? null : path24.resolve(input.targetDir);
4142
+ const targetRoot = input.targetDir === void 0 ? null : path25.resolve(input.targetDir);
4019
4143
  const scope = targetRoot === null ? realPiProjectionScope() : {
4020
4144
  kind: "target-dir",
4021
- home: path24.join(targetRoot, "home"),
4022
- codingAgentDir: path24.join(targetRoot, "pi-agent"),
4023
- receiptFile: path24.join(targetRoot, "state", "pi-projection-receipt.json")
4145
+ home: path25.join(targetRoot, "home"),
4146
+ codingAgentDir: path25.dirname(piSystemPromptFile(targetRoot)),
4147
+ receiptFile: path25.join(targetRoot, "state", "pi-projection-receipt.json")
4024
4148
  };
4025
4149
  return {
4026
4150
  input: {
@@ -4041,14 +4165,14 @@ function systemProjectionLifecycle(input) {
4041
4165
  backup: (paths) => createBackup(
4042
4166
  paths,
4043
4167
  `pi-projection-${input.operation}`,
4044
- targetRoot === null ? void 0 : path24.join(targetRoot, "backups")
4168
+ targetRoot === null ? void 0 : path25.join(targetRoot, "backups")
4045
4169
  ),
4046
4170
  writeText,
4047
4171
  copyFile,
4048
4172
  removeFile: (file) => {
4049
- fs17.rmSync(file, { force: true });
4050
- if (retiredPlaywrightFiles(scope).includes(path24.resolve(file))) {
4051
- pruneEmptyDirs(file, path24.join(scope.home, ".agents", "skills"));
4173
+ fs18.rmSync(file, { force: true });
4174
+ if (retiredPlaywrightFiles(scope).includes(path25.resolve(file))) {
4175
+ pruneEmptyDirs(file, path25.join(scope.home, ".agents", "skills"));
4052
4176
  }
4053
4177
  },
4054
4178
  readManifest: targetRoot === null ? readManifest : () => ({ runtimes: {} })
@@ -4056,6 +4180,8 @@ function systemProjectionLifecycle(input) {
4056
4180
  };
4057
4181
  }
4058
4182
  function preparePiProjectionUninstallSystem(input) {
4183
+ const error = systemPromptPreflight(input.targetDir);
4184
+ if (error) return error;
4059
4185
  const lifecycle = systemProjectionLifecycle(input);
4060
4186
  return preparePiProjectionUninstall(lifecycle.input, lifecycle.deps);
4061
4187
  }
@@ -4064,9 +4190,19 @@ function completePiProjectionUninstallSystem(plan, input) {
4064
4190
  return completePiProjectionUninstall(plan, lifecycle.deps);
4065
4191
  }
4066
4192
  function runPiProjectionLifecycleSystem(input) {
4193
+ const error = systemPromptPreflight(input.targetDir);
4194
+ if (error) return error;
4067
4195
  const lifecycle = systemProjectionLifecycle(input);
4068
4196
  return runPiProjectionLifecycle(lifecycle.input, lifecycle.deps);
4069
4197
  }
4198
+ function systemPromptPreflight(targetDir) {
4199
+ const target = piSystemPromptFile(targetDir);
4200
+ try {
4201
+ assertSystemPromptFile(target, targetDir);
4202
+ } catch (error) {
4203
+ return blocked2("projection-prompt-markers", [target], error instanceof Error ? error.message : String(error));
4204
+ }
4205
+ }
4070
4206
 
4071
4207
  // src/uninstall.ts
4072
4208
  function resolvePlaywrightUninstallPlan(input) {
@@ -4077,6 +4213,18 @@ function resolvePlaywrightUninstallPlan(input) {
4077
4213
  }
4078
4214
  async function runUninstall(opts) {
4079
4215
  p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
4216
+ try {
4217
+ for (const id of opts.runtimes) {
4218
+ const adapter = ADAPTERS[id];
4219
+ if (!adapter) continue;
4220
+ const detection = adapter.detect();
4221
+ if (opts.targetDir === void 0 && !detection.installed) continue;
4222
+ assertSystemPromptFile(adapter.paths(opts.targetDir ?? detection.configDir).systemPromptFile, opts.targetDir);
4223
+ }
4224
+ } catch (error) {
4225
+ p2.log.error(error instanceof Error ? error.message : String(error));
4226
+ return 1;
4227
+ }
4080
4228
  const useBrowserPreferences = opts.targetDir === void 0;
4081
4229
  const preferenceErrors = useBrowserPreferences ? [...browserPreferenceErrors(), primaryModelOwnershipError()].filter((error) => error !== null) : [];
4082
4230
  if (preferenceErrors.length > 0) {
@@ -4127,7 +4275,7 @@ async function runUninstall(opts) {
4127
4275
  if (!detection.installed) continue;
4128
4276
  const keepCtx = makeContext(keep, detection.configDir);
4129
4277
  if (!keepCtx) continue;
4130
- for (const action of buildPlan(keep, keepCtx)) retained.add(path25.resolve(action.target));
4278
+ for (const action of buildPlan(keep, keepCtx)) retained.add(path26.resolve(action.target));
4131
4279
  }
4132
4280
  }
4133
4281
  for (const id of opts.runtimes) {
@@ -4146,15 +4294,15 @@ async function runUninstall(opts) {
4146
4294
  if (!ctx) continue;
4147
4295
  ctx.preserveEngram = !removeEngram;
4148
4296
  const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
4149
- const mergedTargets = new Set(unmerge.map((a) => path25.resolve(a.target)));
4297
+ const mergedTargets = new Set(unmerge.map((a) => path26.resolve(a.target)));
4150
4298
  const usingRealConfig = opts.targetDir === void 0;
4151
4299
  const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
4152
- const pruneRoot = usingRealConfig ? HOME : path25.dirname(configDir);
4300
+ const pruneRoot = usingRealConfig ? HOME : path26.dirname(configDir);
4153
4301
  const planTargets = [
4154
- .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path25.resolve(a.target)), ...prevOwned.map((t) => path25.resolve(t))])
4155
- ].filter((t) => !mergedTargets.has(t) && fs18.existsSync(t));
4302
+ .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path26.resolve(a.target)), ...prevOwned.map((t) => path26.resolve(t))])
4303
+ ].filter((t) => !mergedTargets.has(t) && fs19.existsSync(t));
4156
4304
  const deleteTargets = planTargets.filter(
4157
- (t) => !retained.has(t) && !(ctx.preserveEngram && path25.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
4305
+ (t) => !retained.has(t) && !(ctx.preserveEngram && path26.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
4158
4306
  );
4159
4307
  const sharedKept = planTargets.length - deleteTargets.length;
4160
4308
  p2.log.step(`${adapter.name} \u2192 ${configDir}`);
@@ -4164,7 +4312,7 @@ async function runUninstall(opts) {
4164
4312
  let backup;
4165
4313
  try {
4166
4314
  backup = createBackup(
4167
- [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs18.existsSync(t))],
4315
+ [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs19.existsSync(t))],
4168
4316
  `uninstall-${id}`
4169
4317
  );
4170
4318
  } catch (error) {
@@ -4174,13 +4322,13 @@ async function runUninstall(opts) {
4174
4322
  }
4175
4323
  if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
4176
4324
  for (const target of deleteTargets) {
4177
- fs18.rmSync(target, { force: true });
4325
+ fs19.rmSync(target, { force: true });
4178
4326
  pruneEmptyDirs(target, pruneRoot);
4179
4327
  }
4180
4328
  for (const action of unmerge) {
4181
4329
  if (action.kind !== "write") continue;
4182
4330
  if (action.content.trim() === "") {
4183
- fs18.rmSync(action.target, { force: true });
4331
+ fs19.rmSync(action.target, { force: true });
4184
4332
  } else {
4185
4333
  writeText(action.target, action.content);
4186
4334
  }
@@ -4234,12 +4382,12 @@ async function runUninstall(opts) {
4234
4382
  }
4235
4383
 
4236
4384
  // src/doctor.ts
4237
- import path26 from "path";
4238
- import fs19 from "fs";
4385
+ import path27 from "path";
4386
+ import fs20 from "fs";
4239
4387
  import * as p3 from "@clack/prompts";
4240
4388
  function readDoctorTextIfExists(file) {
4241
4389
  try {
4242
- return fs19.readFileSync(file, "utf8");
4390
+ return fs20.readFileSync(file, "utf8");
4243
4391
  } catch (error) {
4244
4392
  if (error.code === "ENOENT") return null;
4245
4393
  throw error;
@@ -4259,11 +4407,12 @@ function resolvePlaywrightDoctorState(input) {
4259
4407
  if (input.browserCache?.status === "unreadable") {
4260
4408
  return { status: "unreadable", path: input.browserCache.path, errorCode: input.browserCache.errorCode };
4261
4409
  }
4410
+ if (input.browserVerified === false && input.browserCache?.status === "ready") return { status: "broken" };
4262
4411
  if (!input.browserReady) return { status: "missing", missing: "browser" };
4263
4412
  return { status: "healthy" };
4264
4413
  }
4265
4414
  function context7KeyConfigured(id, configDir) {
4266
- const file = id === "codex" ? path26.join(configDir, "config.toml") : id === "claude-code" ? path26.join(path26.dirname(configDir), `${path26.basename(configDir)}.json`) : path26.join(configDir, "opencode.json");
4415
+ const file = id === "codex" ? path27.join(configDir, "config.toml") : id === "claude-code" ? path27.join(path27.dirname(configDir), `${path27.basename(configDir)}.json`) : path27.join(configDir, "opencode.json");
4267
4416
  const content = readTextIfExists(file);
4268
4417
  if (content === null) return null;
4269
4418
  const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
@@ -4283,7 +4432,7 @@ function reportWritingStyle(options, style, mode) {
4283
4432
  for (const id of runtimes) {
4284
4433
  const adapter = id === "pi" ? piAdapter : ADAPTERS[id];
4285
4434
  if (adapter === void 0) continue;
4286
- const configDir = id === "pi" ? options.targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path26.join(HOME, ".pi", "agent") : path26.join(options.targetDir, "pi-agent") : options.targetDir ?? ADAPTERS[id].detect().configDir;
4435
+ const configDir = id === "pi" ? options.targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path27.join(HOME, ".pi", "agent") : path27.join(options.targetDir, "pi-agent") : options.targetDir ?? ADAPTERS[id].detect().configDir;
4287
4436
  const file = adapter.paths(configDir).systemPromptFile;
4288
4437
  try {
4289
4438
  const content = readDoctorTextIfExists(file) ?? "";
@@ -4298,7 +4447,7 @@ function reportWritingStyle(options, style, mode) {
4298
4447
  problems++;
4299
4448
  }
4300
4449
  if (id === "codex") {
4301
- const override = path26.join(configDir, "AGENTS.override.md");
4450
+ const override = path27.join(configDir, "AGENTS.override.md");
4302
4451
  if ((readDoctorTextIfExists(override) ?? "").trim() !== "") {
4303
4452
  p3.log.warn(`Codex: ${override} no vac\xEDo puede ocultar el AGENTS.md gestionado; rev\xEDsalo sin modificarlo autom\xE1ticamente.`);
4304
4453
  problems++;
@@ -4341,34 +4490,41 @@ async function runDoctor(options = {}) {
4341
4490
  p3.log.success(`Engram: ${version} (${engramBin})`);
4342
4491
  }
4343
4492
  }
4344
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path26.join(HOME, ".engram");
4345
- const engramDb = path26.join(engramDataDir, "engram.db");
4346
- if (fs19.existsSync(engramDb)) {
4347
- const sizeMb = (fs19.statSync(engramDb).size / 1024 / 1024).toFixed(1);
4493
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path27.join(HOME, ".engram");
4494
+ const engramDb = path27.join(engramDataDir, "engram.db");
4495
+ if (fs20.existsSync(engramDb)) {
4496
+ const sizeMb = (fs20.statSync(engramDb).size / 1024 / 1024).toFixed(1);
4348
4497
  p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
4349
4498
  }
4350
- if (!fs19.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
4499
+ if (!fs20.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
4351
4500
  const preferenceErrors = browserPreferenceErrors();
4352
4501
  const primaryOwnershipError = primaryModelOwnershipError();
4353
4502
  if (primaryOwnershipError !== null) {
4354
4503
  p3.log.error(primaryOwnershipError);
4355
4504
  problems++;
4356
4505
  }
4506
+ let effectivePlaywright;
4357
4507
  if (preferenceErrors.length > 0) {
4358
4508
  for (const error of preferenceErrors) p3.log.error(error);
4359
4509
  problems += preferenceErrors.length;
4510
+ } else if (options.dryRun) {
4511
+ p3.log.info("Playwright CLI: comprobaci\xF3n omitida en dry-run; no se eval\xFAa el estado del paquete ni del navegador.");
4360
4512
  } else {
4361
- const browserCache = isPlaywrightBrowserReady();
4513
+ const enabled = loadPlaywrightCliPreference();
4514
+ const capability = options.playwrightCapability ?? (enabled === true ? inspectPlaywrightCapability() : void 0);
4515
+ effectivePlaywright = capability?.effective;
4516
+ const cli = capability?.cli ?? { status: "absent" };
4362
4517
  const playwright = resolvePlaywrightDoctorState({
4363
- enabled: loadPlaywrightCliPreference(),
4364
- cli: detectPlaywrightCli(),
4365
- browserReady: browserCache.status === "ready",
4366
- browserCache
4518
+ enabled,
4519
+ cli,
4520
+ browserReady: capability?.effective ?? false,
4521
+ browserVerified: capability?.browserVerified,
4522
+ browserCache: capability?.browserCache
4367
4523
  });
4368
4524
  if (playwright.status === "disabled") {
4369
4525
  p3.log.info("Playwright CLI: deshabilitado (opcional). Usa 'install --playwright' para instalarlo de forma expl\xEDcita.");
4370
4526
  } else if (playwright.status === "healthy") {
4371
- p3.log.success("Playwright CLI: paquete y cach\xE9 de Chromium detectados (doctor no prueba el arranque).");
4527
+ p3.log.success("Playwright CLI: paquete y arranque de Chromium verificados.");
4372
4528
  } else if (playwright.status === "not-in-path") {
4373
4529
  p3.log.warn("Playwright CLI: instalado en el directorio de pnpm, pero fuera del PATH de esta terminal. Abre una terminal nueva tras pnpm setup; no hace falta reinstalarlo.");
4374
4530
  problems++;
@@ -4377,7 +4533,8 @@ async function runDoctor(options = {}) {
4377
4533
  p3.log.warn(`Playwright CLI: habilitado, pero falta ${target} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
4378
4534
  problems++;
4379
4535
  } else if (playwright.status === "broken") {
4380
- p3.log.error("Playwright CLI: el binario detectado no responde correctamente \u2192 ejecuta 'jorgex-stack install --playwright'.");
4536
+ const launchFailed = capability?.cli.status === "current" && capability.browserCache.status === "ready" && capability.browserVerified === false;
4537
+ p3.log.error(`Playwright CLI: ${launchFailed ? "Chromium no arranca" : "el binario detectado no responde correctamente"} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
4381
4538
  problems++;
4382
4539
  } else if (playwright.status === "unreadable") {
4383
4540
  p3.log.error(`Playwright CLI: no se puede leer la cach\xE9 de navegadores en ${playwright.path} (${playwright.errorCode}) \u2192 revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
@@ -4388,7 +4545,7 @@ async function runDoctor(options = {}) {
4388
4545
  }
4389
4546
  }
4390
4547
  const manifest = readManifest();
4391
- const current = collectAllCurrentTargets(modePreference);
4548
+ const current = collectAllCurrentTargets(modePreference, effectivePlaywright);
4392
4549
  if (!current.complete || current.warnings.length > 0) {
4393
4550
  p3.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
4394
4551
  for (const warning of current.warnings) p3.log.warn(warning);
@@ -4404,7 +4561,7 @@ async function runDoctor(options = {}) {
4404
4561
  const capabilityReport = adapter.reportCapabilities(detection.configDir);
4405
4562
  const capabilitySummary = capabilityReport.capabilities.map((capability) => `${capability.id}=${capability.state}`).join(", ");
4406
4563
  p3.log.info(`${adapter.name}: capabilities diagnostic (${capabilitySummary}); no certifica enforcement local.`);
4407
- const ctx = makeContext(adapter, detection.configDir, modePreference);
4564
+ const ctx = makeContext(adapter, detection.configDir, modePreference, true, effectivePlaywright);
4408
4565
  if (!ctx) continue;
4409
4566
  ctx.writingStyle = writingStyle;
4410
4567
  let pending;
@@ -4429,7 +4586,7 @@ async function runDoctor(options = {}) {
4429
4586
  p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
4430
4587
  problems++;
4431
4588
  }
4432
- if (adapter.id === "codex" && fs19.existsSync(path26.join(detection.configDir, "hooks.json"))) {
4589
+ if (adapter.id === "codex" && fs20.existsSync(path27.join(detection.configDir, "hooks.json"))) {
4433
4590
  p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
4434
4591
  }
4435
4592
  const key = context7KeyConfigured(adapter.id, detection.configDir);
@@ -4440,15 +4597,15 @@ async function runDoctor(options = {}) {
4440
4597
  }
4441
4598
 
4442
4599
  // src/update.ts
4443
- import fs22 from "fs";
4444
- import path29 from "path";
4600
+ import fs23 from "fs";
4601
+ import path30 from "path";
4445
4602
  import os3 from "os";
4446
4603
  import { execFileSync as execFileSync5 } from "child_process";
4447
4604
  import * as p4 from "@clack/prompts";
4448
4605
 
4449
4606
  // src/lib/github.ts
4450
- import fs20 from "fs";
4451
- import path27 from "path";
4607
+ import fs21 from "fs";
4608
+ import path28 from "path";
4452
4609
  import { execFileSync as execFileSync3 } from "child_process";
4453
4610
  import os2 from "os";
4454
4611
  import { Readable } from "stream";
@@ -4518,19 +4675,19 @@ async function latestGithubCommit(repo) {
4518
4675
  }
4519
4676
  }
4520
4677
  function validateExtractedTree(destDir) {
4521
- const resolved = path27.resolve(destDir);
4678
+ const resolved = path28.resolve(destDir);
4522
4679
  const walk = (dir) => {
4523
4680
  let entries;
4524
4681
  try {
4525
- entries = fs20.readdirSync(dir, { withFileTypes: true });
4682
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
4526
4683
  } catch {
4527
4684
  return false;
4528
4685
  }
4529
4686
  for (const entry of entries) {
4530
- const full = path27.join(dir, entry.name);
4687
+ const full = path28.join(dir, entry.name);
4531
4688
  let stat;
4532
4689
  try {
4533
- stat = fs20.lstatSync(full);
4690
+ stat = fs21.lstatSync(full);
4534
4691
  } catch {
4535
4692
  return false;
4536
4693
  }
@@ -4546,15 +4703,15 @@ function validateExtractedTree(destDir) {
4546
4703
  }
4547
4704
  function resolveTarBin() {
4548
4705
  if (process.platform !== "win32") return "tar";
4549
- const winTar = path27.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
4550
- return fs20.existsSync(winTar) ? winTar : "tar";
4706
+ const winTar = path28.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
4707
+ return fs21.existsSync(winTar) ? winTar : "tar";
4551
4708
  }
4552
4709
  async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
4553
4710
  const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
4554
- const tmp = path27.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
4711
+ const tmp = path28.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
4555
4712
  const fail = (reason) => {
4556
4713
  try {
4557
- fs20.rmSync(destDir, { recursive: true, force: true });
4714
+ fs21.rmSync(destDir, { recursive: true, force: true });
4558
4715
  } catch {
4559
4716
  }
4560
4717
  return { ok: false, reason };
@@ -4573,10 +4730,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
4573
4730
  if (!res.body) return fail("respuesta HTTP sin cuerpo");
4574
4731
  await pipeline(
4575
4732
  Readable.fromWeb(res.body),
4576
- fs20.createWriteStream(tmp)
4733
+ fs21.createWriteStream(tmp)
4577
4734
  );
4578
- fs20.rmSync(destDir, { recursive: true, force: true });
4579
- fs20.mkdirSync(destDir, { recursive: true });
4735
+ fs21.rmSync(destDir, { recursive: true, force: true });
4736
+ fs21.mkdirSync(destDir, { recursive: true });
4580
4737
  try {
4581
4738
  execFileSync3(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
4582
4739
  } catch (err) {
@@ -4584,30 +4741,30 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
4584
4741
  const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
4585
4742
  return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
4586
4743
  }
4587
- const resolvedDest = path27.resolve(destDir);
4588
- const validateRoot = validateSubdir ? path27.resolve(resolvedDest, validateSubdir) : resolvedDest;
4744
+ const resolvedDest = path28.resolve(destDir);
4745
+ const validateRoot = validateSubdir ? path28.resolve(resolvedDest, validateSubdir) : resolvedDest;
4589
4746
  if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
4590
4747
  return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
4591
4748
  }
4592
- if (fs20.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
4749
+ if (fs21.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
4593
4750
  return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
4594
4751
  }
4595
- const validated = fs20.existsSync(validateRoot);
4752
+ const validated = fs21.existsSync(validateRoot);
4596
4753
  return { ok: true, validated };
4597
4754
  } catch (err) {
4598
4755
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
4599
4756
  return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
4600
4757
  } finally {
4601
4758
  try {
4602
- fs20.rmSync(tmp, { force: true });
4759
+ fs21.rmSync(tmp, { force: true });
4603
4760
  } catch {
4604
4761
  }
4605
4762
  }
4606
4763
  }
4607
4764
 
4608
4765
  // src/lib/skill-update.ts
4609
- import fs21 from "fs";
4610
- import path28 from "path";
4766
+ import fs22 from "fs";
4767
+ import path29 from "path";
4611
4768
  import { execFileSync as execFileSync4 } from "child_process";
4612
4769
  var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
4613
4770
  "agent-delegation",
@@ -4618,8 +4775,8 @@ var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
4618
4775
  "xreview"
4619
4776
  ]);
4620
4777
  function sameTextContentNormalized(a, b) {
4621
- const ba = fs21.readFileSync(a);
4622
- const bb = fs21.readFileSync(b);
4778
+ const ba = fs22.readFileSync(a);
4779
+ const bb = fs22.readFileSync(b);
4623
4780
  if (ba.equals(bb)) return true;
4624
4781
  const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
4625
4782
  const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
@@ -4627,10 +4784,10 @@ function sameTextContentNormalized(a, b) {
4627
4784
  }
4628
4785
  function diffSkillDirs(upstreamDir, localDir) {
4629
4786
  const upstreamFiles = new Set(
4630
- listFilesRecursive(upstreamDir).map((f) => path28.relative(upstreamDir, f))
4787
+ listFilesRecursive(upstreamDir).map((f) => path29.relative(upstreamDir, f))
4631
4788
  );
4632
4789
  const localFiles = new Set(
4633
- listFilesRecursive(localDir).map((f) => path28.relative(localDir, f))
4790
+ listFilesRecursive(localDir).map((f) => path29.relative(localDir, f))
4634
4791
  );
4635
4792
  const added = [];
4636
4793
  const modified = [];
@@ -4638,7 +4795,7 @@ function diffSkillDirs(upstreamDir, localDir) {
4638
4795
  for (const rel of upstreamFiles) {
4639
4796
  if (!localFiles.has(rel)) {
4640
4797
  added.push(rel);
4641
- } else if (!sameTextContentNormalized(path28.join(upstreamDir, rel), path28.join(localDir, rel))) {
4798
+ } else if (!sameTextContentNormalized(path29.join(upstreamDir, rel), path29.join(localDir, rel))) {
4642
4799
  modified.push(rel);
4643
4800
  }
4644
4801
  }
@@ -4700,8 +4857,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
4700
4857
  if (PROTECTED_SKILLS.has(name)) {
4701
4858
  throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
4702
4859
  }
4703
- const upstreamsFile = upstreamsFilePath ?? path28.join(path28.dirname(stackRoot()), "upstreams.json");
4704
- const raw = fs21.readFileSync(upstreamsFile, "utf8");
4860
+ const upstreamsFile = upstreamsFilePath ?? path29.join(path29.dirname(stackRoot()), "upstreams.json");
4861
+ const raw = fs22.readFileSync(upstreamsFile, "utf8");
4705
4862
  const data = JSON.parse(raw);
4706
4863
  const skillEntry = data?.skills?.[name];
4707
4864
  if (!skillEntry) {
@@ -4710,8 +4867,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
4710
4867
  if (skillEntry.kind === "release") {
4711
4868
  throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
4712
4869
  }
4713
- const skillsRoot = localSkillsRoot ?? path28.join(stackRoot(), "skills");
4714
- const localSkillDir = path28.join(skillsRoot, name);
4870
+ const skillsRoot = localSkillsRoot ?? path29.join(stackRoot(), "skills");
4871
+ const localSkillDir = path29.join(skillsRoot, name);
4715
4872
  const localFiles = listFilesRecursive(localSkillDir);
4716
4873
  if (localFiles.length > 0) {
4717
4874
  createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
@@ -4720,24 +4877,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
4720
4877
  try {
4721
4878
  const upstreamFiles = listFilesRecursive(upstreamSkillDir);
4722
4879
  for (const src of upstreamFiles) {
4723
- const st = fs21.lstatSync(src);
4880
+ const st = fs22.lstatSync(src);
4724
4881
  if (st.isSymbolicLink()) {
4725
4882
  throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
4726
4883
  }
4727
- const rel = path28.relative(upstreamSkillDir, src);
4728
- const dest = path28.join(stagingDir, rel);
4729
- ensureDir(path28.dirname(dest));
4884
+ const rel = path29.relative(upstreamSkillDir, src);
4885
+ const dest = path29.join(stagingDir, rel);
4886
+ ensureDir(path29.dirname(dest));
4730
4887
  copyFile(src, dest);
4731
4888
  }
4732
4889
  const oldDir = `${localSkillDir}.old-${process.pid}`;
4733
- if (fs21.existsSync(localSkillDir)) {
4734
- fs21.renameSync(localSkillDir, oldDir);
4890
+ if (fs22.existsSync(localSkillDir)) {
4891
+ fs22.renameSync(localSkillDir, oldDir);
4735
4892
  }
4736
- fs21.renameSync(stagingDir, localSkillDir);
4737
- fs21.rmSync(oldDir, { recursive: true, force: true });
4893
+ fs22.renameSync(stagingDir, localSkillDir);
4894
+ fs22.rmSync(oldDir, { recursive: true, force: true });
4738
4895
  } catch (err) {
4739
4896
  try {
4740
- fs21.rmSync(stagingDir, { recursive: true, force: true });
4897
+ fs22.rmSync(stagingDir, { recursive: true, force: true });
4741
4898
  } catch {
4742
4899
  }
4743
4900
  throw err;
@@ -4751,8 +4908,8 @@ function rateLimitHint(prefix) {
4751
4908
  return ghPresentButTokenFailed() ? `${prefix} Tienes gh instalado pero \`gh auth token\` no devolvi\xF3 credencial (\xBFsesi\xF3n caducada?) \u2014 prueba \`gh auth login\` o define GH_TOKEN.` : `${prefix} Define GH_TOKEN o inicia sesi\xF3n en gh CLI.`;
4752
4909
  }
4753
4910
  function loadUpstreams() {
4754
- const file = path29.join(path29.dirname(stackRoot()), "upstreams.json");
4755
- return JSON.parse(fs22.readFileSync(file, "utf8"));
4911
+ const file = path30.join(path30.dirname(stackRoot()), "upstreams.json");
4912
+ return JSON.parse(fs23.readFileSync(file, "utf8"));
4756
4913
  }
4757
4914
  function skillsToScan(maintainer, upstreams) {
4758
4915
  return maintainer ? Object.keys(upstreams.skills) : [];
@@ -4901,8 +5058,8 @@ function isEngramRunning() {
4901
5058
  return null;
4902
5059
  }
4903
5060
  }
4904
- function isGitClone(projectRoot = path29.dirname(stackRoot())) {
4905
- return fs22.existsSync(path29.join(projectRoot, ".git"));
5061
+ function isGitClone(projectRoot = path30.dirname(stackRoot())) {
5062
+ return fs23.existsSync(path30.join(projectRoot, ".git"));
4906
5063
  }
4907
5064
  var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
4908
5065
  function resolvePnpm() {
@@ -4912,12 +5069,12 @@ function resolvePnpm() {
4912
5069
  }
4913
5070
  function cleanupTmp(dir) {
4914
5071
  try {
4915
- fs22.rmSync(dir, { recursive: true, force: true });
5072
+ fs23.rmSync(dir, { recursive: true, force: true });
4916
5073
  } catch {
4917
5074
  }
4918
5075
  }
4919
5076
  function updateStackGitClone() {
4920
- const projectRoot = path29.dirname(stackRoot());
5077
+ const projectRoot = path30.dirname(stackRoot());
4921
5078
  const git = lookPath("git");
4922
5079
  if (!git) throw new Error("git no encontrado en PATH.");
4923
5080
  const pnpm = resolvePnpm();
@@ -4934,7 +5091,7 @@ function updateStackGlobal() {
4934
5091
  execFileSync5(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
4935
5092
  }
4936
5093
  async function downloadSkillToTemp(repo, sha, skillPath) {
4937
- const root = fs22.mkdtempSync(path29.join(os3.tmpdir(), "jorgex-skill-"));
5094
+ const root = fs23.mkdtempSync(path30.join(os3.tmpdir(), "jorgex-skill-"));
4938
5095
  try {
4939
5096
  const result = await downloadRepoTarball(repo, sha, root, skillPath);
4940
5097
  if (!result.ok) {
@@ -4942,15 +5099,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
4942
5099
  return { error: result.reason };
4943
5100
  }
4944
5101
  if (skillPath) {
4945
- const sub = path29.resolve(path29.join(root, skillPath));
5102
+ const sub = path30.resolve(path30.join(root, skillPath));
4946
5103
  if (!isContainedIn(sub, root)) {
4947
5104
  cleanupTmp(root);
4948
5105
  return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
4949
5106
  }
4950
- if (fs22.existsSync(sub)) return { dir: sub, root };
5107
+ if (fs23.existsSync(sub)) return { dir: sub, root };
4951
5108
  const lastSeg = skillPath.split("/").pop();
4952
- const sub2 = path29.resolve(path29.join(root, lastSeg));
4953
- if (isContainedIn(sub2, root) && fs22.existsSync(sub2)) {
5109
+ const sub2 = path30.resolve(path30.join(root, lastSeg));
5110
+ if (isContainedIn(sub2, root) && fs23.existsSync(sub2)) {
4954
5111
  if (!validateExtractedTree(sub2)) {
4955
5112
  cleanupTmp(root);
4956
5113
  return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
@@ -4969,11 +5126,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
4969
5126
  function pruneEngramDbBackups() {
4970
5127
  try {
4971
5128
  const dir = dataDir();
4972
- if (!fs22.existsSync(dir)) return;
4973
- const backups = fs22.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs22.statSync(path29.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
5129
+ if (!fs23.existsSync(dir)) return;
5130
+ const backups = fs23.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs23.statSync(path30.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
4974
5131
  for (const old of backups.slice(3)) {
4975
5132
  try {
4976
- fs22.rmSync(path29.join(dir, old.name));
5133
+ fs23.rmSync(path30.join(dir, old.name));
4977
5134
  } catch {
4978
5135
  }
4979
5136
  }
@@ -4981,18 +5138,18 @@ function pruneEngramDbBackups() {
4981
5138
  }
4982
5139
  }
4983
5140
  function rotateLockedBinary(binPath, sweepRoot = HOME) {
4984
- if (!fs22.existsSync(binPath)) return null;
4985
- const dir = path29.dirname(binPath);
4986
- const base = path29.basename(binPath);
5141
+ if (!fs23.existsSync(binPath)) return null;
5142
+ const dir = path30.dirname(binPath);
5143
+ const base = path30.basename(binPath);
4987
5144
  const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
4988
5145
  const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
4989
- const resolvedDir = path29.resolve(dir);
4990
- if (resolvedDir === path29.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
5146
+ const resolvedDir = path30.resolve(dir);
5147
+ if (resolvedDir === path30.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
4991
5148
  try {
4992
- for (const entry of fs22.readdirSync(dir)) {
5149
+ for (const entry of fs23.readdirSync(dir)) {
4993
5150
  if (oldPattern.test(entry)) {
4994
5151
  try {
4995
- fs22.rmSync(path29.join(dir, entry), { force: true });
5152
+ fs23.rmSync(path30.join(dir, entry), { force: true });
4996
5153
  } catch {
4997
5154
  }
4998
5155
  }
@@ -5000,8 +5157,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
5000
5157
  } catch {
5001
5158
  }
5002
5159
  }
5003
- const rotated = path29.join(dir, `${base}.old-${Date.now()}`);
5004
- fs22.renameSync(binPath, rotated);
5160
+ const rotated = path30.join(dir, `${base}.old-${Date.now()}`);
5161
+ fs23.renameSync(binPath, rotated);
5005
5162
  return rotated;
5006
5163
  }
5007
5164
  async function updateEngram(engramRepo, latestVersion) {
@@ -5010,19 +5167,19 @@ async function updateEngram(engramRepo, latestVersion) {
5010
5167
  "Engram est\xE1 en ejecuci\xF3n: los procesos vivos seguir\xE1n usando la versi\xF3n antigua hasta que reinicies los clientes (Claude Code/OpenCode/Codex)."
5011
5168
  );
5012
5169
  }
5013
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path29.join(HOME, ".engram");
5014
- const engramDb = path29.join(engramDataDir, "engram.db");
5015
- if (fs22.existsSync(engramDb)) {
5170
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path30.join(HOME, ".engram");
5171
+ const engramDb = path30.join(engramDataDir, "engram.db");
5172
+ if (fs23.existsSync(engramDb)) {
5016
5173
  const doBackup = await p4.confirm({
5017
5174
  message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
5018
5175
  initialValue: true
5019
5176
  });
5020
5177
  if (!p4.isCancel(doBackup) && doBackup) {
5021
5178
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5022
- const dest = path29.join(dataDir(), `engram-db-backup-${ts}.db`);
5179
+ const dest = path30.join(dataDir(), `engram-db-backup-${ts}.db`);
5023
5180
  try {
5024
- if (!fs22.existsSync(dataDir())) fs22.mkdirSync(dataDir(), { recursive: true });
5025
- fs22.copyFileSync(engramDb, dest);
5181
+ if (!fs23.existsSync(dataDir())) fs23.mkdirSync(dataDir(), { recursive: true });
5182
+ fs23.copyFileSync(engramDb, dest);
5026
5183
  p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
5027
5184
  pruneEngramDbBackups();
5028
5185
  } catch (err) {
@@ -5072,10 +5229,10 @@ async function updateEngram(engramRepo, latestVersion) {
5072
5229
  ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
5073
5230
  { stdio: "inherit" }
5074
5231
  );
5075
- const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs22.existsSync(bin ?? "") });
5232
+ const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs23.existsSync(bin ?? "") });
5076
5233
  if (rollbackOk.action === "restore") {
5077
5234
  try {
5078
- fs22.renameSync(rotated, bin);
5235
+ fs23.renameSync(rotated, bin);
5079
5236
  p4.log.warn(rollbackOk.messages.onRestore);
5080
5237
  } catch {
5081
5238
  p4.log.warn(rollbackOk.messages.onRenameFail);
@@ -5083,10 +5240,10 @@ async function updateEngram(engramRepo, latestVersion) {
5083
5240
  }
5084
5241
  return true;
5085
5242
  } catch (err) {
5086
- const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs22.existsSync(bin ?? "") });
5243
+ const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs23.existsSync(bin ?? "") });
5087
5244
  if (rollbackFail.action === "restore") {
5088
5245
  try {
5089
- fs22.renameSync(rotated, bin);
5246
+ fs23.renameSync(rotated, bin);
5090
5247
  p4.log.info(rollbackFail.messages.onRestore);
5091
5248
  } catch {
5092
5249
  p4.log.error(rollbackFail.messages.onRenameFail);
@@ -5183,6 +5340,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBr
5183
5340
  let exitCode = 0;
5184
5341
  let appliedUpdates = false;
5185
5342
  const updated = [];
5343
+ let playwrightCapability;
5186
5344
  const maintainer = isGitClone();
5187
5345
  const spin = p4.spinner();
5188
5346
  spin.start("Consultando versiones upstream\u2026");
@@ -5356,7 +5514,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBr
5356
5514
  continue;
5357
5515
  }
5358
5516
  const { dir: tmpDir, root: tmpRoot } = tmpResult;
5359
- const localSkillDir = path29.join(stackRoot(), "skills", skillInfo.name);
5517
+ const localSkillDir = path30.join(stackRoot(), "skills", skillInfo.name);
5360
5518
  const diff = renderSkillDiff(tmpDir, localSkillDir);
5361
5519
  if (diff) {
5362
5520
  p4.log.info(`Diff de ${skillInfo.name}:
@@ -5402,6 +5560,7 @@ ${diff}`);
5402
5560
  p4.log.success("Playwright CLI actualizado al pin aprobado.");
5403
5561
  appliedUpdates = true;
5404
5562
  updated.push("playwright-cli");
5563
+ playwrightCapability = inspectPlaywrightCapability({ browserVerified: true });
5405
5564
  } else {
5406
5565
  const failedResult = packageResult.ok ? browserResult : packageResult;
5407
5566
  const pnpmRemedy = failedResult && !failedResult.ok ? resolvePnpmFailureRemedy(failedResult.reason) : null;
@@ -5438,11 +5597,16 @@ ${diff}`);
5438
5597
  }
5439
5598
  }
5440
5599
  p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
5441
- return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
5600
+ return {
5601
+ exitCode,
5602
+ appliedUpdates,
5603
+ syncRequired: resolveUpdateSyncRequired(updated),
5604
+ ...playwrightCapability === void 0 ? {} : { playwrightCapability }
5605
+ };
5442
5606
  }
5443
5607
 
5444
5608
  // src/models-picker.ts
5445
- import path30 from "path";
5609
+ import path31 from "path";
5446
5610
  import * as p5 from "@clack/prompts";
5447
5611
  var TIERS = ["strong", "standard", "cheap"];
5448
5612
  var EFFORTS = ["low", "medium", "high", "xhigh"];
@@ -5468,7 +5632,7 @@ function opencodeLiveModels(binPath) {
5468
5632
  }
5469
5633
  function agentsByTier() {
5470
5634
  const grouped = { strong: [], standard: [], cheap: [] };
5471
- for (const agent of loadCanonicalAgents(path30.join(stackRoot(), "agents"))) {
5635
+ for (const agent of loadCanonicalAgents(path31.join(stackRoot(), "agents"))) {
5472
5636
  if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
5473
5637
  }
5474
5638
  return grouped;
@@ -5653,16 +5817,16 @@ function cancelled() {
5653
5817
  }
5654
5818
 
5655
5819
  // src/lib/release.ts
5656
- import fs23 from "fs";
5657
- import path31 from "path";
5820
+ import fs24 from "fs";
5821
+ import path32 from "path";
5658
5822
  import { execFileSync as execFileSync6 } from "child_process";
5659
5823
  import { fileURLToPath } from "url";
5660
5824
  function findPackageJson() {
5661
- let dir = path31.dirname(fileURLToPath(import.meta.url));
5825
+ let dir = path32.dirname(fileURLToPath(import.meta.url));
5662
5826
  for (let i = 0; i < 6; i++) {
5663
- const candidate = path31.join(dir, "package.json");
5664
- if (fs23.existsSync(candidate)) return candidate;
5665
- dir = path31.dirname(dir);
5827
+ const candidate = path32.join(dir, "package.json");
5828
+ if (fs24.existsSync(candidate)) return candidate;
5829
+ dir = path32.dirname(dir);
5666
5830
  }
5667
5831
  throw new Error("No se encontr\xF3 package.json cerca del CLI.");
5668
5832
  }
@@ -5671,7 +5835,7 @@ function readPackageVersion() {
5671
5835
  }
5672
5836
  function readPackageMetadata() {
5673
5837
  const packageJson = findPackageJson();
5674
- const raw = fs23.readFileSync(packageJson, "utf8");
5838
+ const raw = fs24.readFileSync(packageJson, "utf8");
5675
5839
  const parsed = JSON.parse(raw);
5676
5840
  const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
5677
5841
  const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
@@ -5683,8 +5847,8 @@ function readPackageMetadata() {
5683
5847
 
5684
5848
  // src/lib/pi-runtime.ts
5685
5849
  import os4 from "os";
5686
- import path32 from "path";
5687
- import fs24 from "fs";
5850
+ import path33 from "path";
5851
+ import fs25 from "fs";
5688
5852
  import { spawnSync } from "child_process";
5689
5853
  import { createHash as createHash2 } from "crypto";
5690
5854
 
@@ -5851,14 +6015,14 @@ function healthyDoctor(stdout, stderr, packageRunner, candidate) {
5851
6015
  if (record === null || typeof record !== "object" || Array.isArray(record)) return false;
5852
6016
  const packageValue = Reflect.get(record, "package");
5853
6017
  const result = Reflect.get(record, "result");
5854
- return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path32.resolve(packageRunner) === path32.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
6018
+ return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path33.resolve(packageRunner) === path33.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
5855
6019
  } catch {
5856
6020
  return false;
5857
6021
  }
5858
6022
  }
5859
6023
  function installPiFromVerifiedTarball(input, deps) {
5860
6024
  const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.piExecutable) : targetPaths(input.targetDir, input.engramBin, input.piExecutable);
5861
- const destination = input.targetDir === void 0 ? path32.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path32.join(path32.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
6025
+ const destination = input.targetDir === void 0 ? path33.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path33.join(path33.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
5862
6026
  const artifact = deps.download(destination);
5863
6027
  if (artifact.bytes !== input.candidate.bytes || artifact.sha256 !== input.candidate.sha256 || artifact.sha512 !== input.candidate.sha512) {
5864
6028
  return { kind: "blocked", reason: "tarball-integrity" };
@@ -5866,7 +6030,7 @@ function installPiFromVerifiedTarball(input, deps) {
5866
6030
  deps.backupSettings();
5867
6031
  const scope = {
5868
6032
  kind: input.targetDir === void 0 ? "real" : "target-dir",
5869
- codingAgentDir: path32.resolve(paths.codingAgentDir)
6033
+ codingAgentDir: path33.resolve(paths.codingAgentDir)
5870
6034
  };
5871
6035
  const installing = flatCandidateReceipt(input.candidate, scope, "installing", input.engramBin);
5872
6036
  deps.writeReceiptAtomic(`${JSON.stringify(installing)}
@@ -5895,30 +6059,30 @@ function installPiFromVerifiedTarball(input, deps) {
5895
6059
  return { kind: "installed", receipt };
5896
6060
  }
5897
6061
  function runtimePath(piExecutable) {
5898
- const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path32.dirname(piExecutable), path32.dirname(process.execPath), process.env.SystemRoot ? path32.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path32.dirname(piExecutable), path32.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
5899
- return [...new Set(entries.filter((entry) => entry !== null))].join(path32.delimiter);
6062
+ const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path33.dirname(piExecutable), path33.dirname(process.execPath), process.env.SystemRoot ? path33.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path33.dirname(piExecutable), path33.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
6063
+ return [...new Set(entries.filter((entry) => entry !== null))].join(path33.delimiter);
5900
6064
  }
5901
6065
  function targetPaths(targetDir, engramBin, piExecutable) {
5902
- const root = path32.resolve(targetDir);
5903
- const codingAgentDir = path32.join(root, "pi-agent");
5904
- const home = path32.join(root, "home");
5905
- const temporary = path32.join(root, "tmp");
6066
+ const root = path33.resolve(targetDir);
6067
+ const codingAgentDir = path33.join(root, "pi-agent");
6068
+ const home = path33.join(root, "home");
6069
+ const temporary = path33.join(root, "tmp");
5906
6070
  return {
5907
6071
  codingAgentDir,
5908
- receiptPath: path32.join(root, "state", "pi-receipt.json"),
5909
- packageRunner: path32.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
6072
+ receiptPath: path33.join(root, "state", "pi-receipt.json"),
6073
+ packageRunner: path33.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
5910
6074
  environment: {
5911
6075
  HOME: home,
5912
6076
  USERPROFILE: home,
5913
- APPDATA: path32.join(root, "appdata"),
5914
- LOCALAPPDATA: path32.join(root, "localappdata"),
5915
- XDG_CONFIG_HOME: path32.join(root, "xdg-config"),
5916
- XDG_DATA_HOME: path32.join(root, "xdg-data"),
5917
- XDG_CACHE_HOME: path32.join(root, "xdg-cache"),
6077
+ APPDATA: path33.join(root, "appdata"),
6078
+ LOCALAPPDATA: path33.join(root, "localappdata"),
6079
+ XDG_CONFIG_HOME: path33.join(root, "xdg-config"),
6080
+ XDG_DATA_HOME: path33.join(root, "xdg-data"),
6081
+ XDG_CACHE_HOME: path33.join(root, "xdg-cache"),
5918
6082
  TEMP: temporary,
5919
6083
  TMP: temporary,
5920
6084
  TMPDIR: temporary,
5921
- npm_config_cache: path32.join(root, "npm-cache"),
6085
+ npm_config_cache: path33.join(root, "npm-cache"),
5922
6086
  NPM_CONFIG_IGNORE_SCRIPTS: "true",
5923
6087
  NPM_CONFIG_UPDATE_NOTIFIER: "false",
5924
6088
  PI_CODING_AGENT_DIR: codingAgentDir,
@@ -5929,15 +6093,15 @@ function targetPaths(targetDir, engramBin, piExecutable) {
5929
6093
  }
5930
6094
  function userPaths(engramBin, piExecutable) {
5931
6095
  const home = os4.homedir();
5932
- const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path32.join(home, ".pi", "agent");
6096
+ const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path33.join(home, ".pi", "agent");
5933
6097
  return {
5934
6098
  codingAgentDir,
5935
- receiptPath: path32.join(dataDir(), "pi-receipt.json"),
5936
- packageRunner: path32.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
6099
+ receiptPath: path33.join(dataDir(), "pi-receipt.json"),
6100
+ packageRunner: path33.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
5937
6101
  environment: {
5938
6102
  HOME: home,
5939
- XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path32.join(home, ".config"),
5940
- XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path32.join(home, ".cache"),
6103
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path33.join(home, ".config"),
6104
+ XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path33.join(home, ".cache"),
5941
6105
  TMPDIR: os4.tmpdir(),
5942
6106
  NPM_CONFIG_IGNORE_SCRIPTS: "true",
5943
6107
  NPM_CONFIG_UPDATE_NOTIFIER: "false",
@@ -5965,7 +6129,7 @@ function runPiRuntime(input, deps) {
5965
6129
  return { kind: "blocked", reason: "tarball-integrity" };
5966
6130
  }
5967
6131
  const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
5968
- const settingsJson = deps.readSettings(path32.join(paths.codingAgentDir, "settings.json"));
6132
+ const settingsJson = deps.readSettings(path33.join(paths.codingAgentDir, "settings.json"));
5969
6133
  const receiptJson = deps.readReceipt(paths.receiptPath);
5970
6134
  const lifecycleInput = {
5971
6135
  candidate: PI_RUNTIME_CANDIDATE,
@@ -6023,19 +6187,19 @@ function runPiRuntime(input, deps) {
6023
6187
  return result;
6024
6188
  }
6025
6189
  function readJsonFile(file) {
6026
- return JSON.parse(fs24.readFileSync(file, "utf8"));
6190
+ return JSON.parse(fs25.readFileSync(file, "utf8"));
6027
6191
  }
6028
6192
  function packageVersionFromExecutable(executable) {
6029
6193
  let current;
6030
6194
  try {
6031
- current = path32.dirname(fs24.realpathSync(executable));
6195
+ current = path33.dirname(fs25.realpathSync(executable));
6032
6196
  } catch {
6033
6197
  return null;
6034
6198
  }
6035
6199
  for (let depth = 0; depth < 8; depth++) {
6036
6200
  const manifests = [
6037
- path32.join(current, "package.json"),
6038
- path32.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
6201
+ path33.join(current, "package.json"),
6202
+ path33.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
6039
6203
  ];
6040
6204
  for (const manifest of manifests) {
6041
6205
  try {
@@ -6046,7 +6210,7 @@ function packageVersionFromExecutable(executable) {
6046
6210
  } catch {
6047
6211
  }
6048
6212
  }
6049
- const parent = path32.dirname(current);
6213
+ const parent = path33.dirname(current);
6050
6214
  if (parent === current) break;
6051
6215
  current = parent;
6052
6216
  }
@@ -6061,35 +6225,35 @@ function detectPiRuntime() {
6061
6225
  installed: executable !== null,
6062
6226
  executable,
6063
6227
  version: executable === null ? null : packageVersionFromExecutable(executable),
6064
- codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path32.join(home, ".pi", "agent")
6228
+ codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path33.join(home, ".pi", "agent")
6065
6229
  };
6066
6230
  }
6067
6231
  function hasManagedPiRuntime(targetDir) {
6068
- const receipt = targetDir === void 0 ? path32.join(dataDir(), "pi-receipt.json") : path32.join(path32.resolve(targetDir), "state", "pi-receipt.json");
6069
- return fs24.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
6232
+ const receipt = targetDir === void 0 ? path33.join(dataDir(), "pi-receipt.json") : path33.join(path33.resolve(targetDir), "state", "pi-receipt.json");
6233
+ return fs25.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
6070
6234
  }
6071
6235
  function resolvePiEngramBin(targetDir) {
6072
6236
  if (targetDir === void 0) return detectEngram();
6073
- const candidate = path32.join(path32.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
6074
- return fs24.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
6237
+ const candidate = path33.join(path33.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
6238
+ return fs25.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
6075
6239
  }
6076
6240
  function readOptional(file, fallback) {
6077
6241
  try {
6078
- return fs24.readFileSync(file, "utf8");
6242
+ return fs25.readFileSync(file, "utf8");
6079
6243
  } catch (error) {
6080
6244
  if (error.code === "ENOENT") return fallback;
6081
6245
  throw error;
6082
6246
  }
6083
6247
  }
6084
6248
  function hashPiTarball(file) {
6085
- const descriptor = fs24.openSync(file, "r");
6249
+ const descriptor = fs25.openSync(file, "r");
6086
6250
  const sha2562 = createHash2("sha256");
6087
6251
  const sha512 = createHash2("sha512");
6088
6252
  const buffer = Buffer.allocUnsafe(1024 * 1024);
6089
6253
  let bytes = 0;
6090
6254
  try {
6091
6255
  while (true) {
6092
- const read = fs24.readSync(descriptor, buffer, 0, buffer.length, null);
6256
+ const read = fs25.readSync(descriptor, buffer, 0, buffer.length, null);
6093
6257
  if (read === 0) break;
6094
6258
  bytes += read;
6095
6259
  const chunk = buffer.subarray(0, read);
@@ -6097,27 +6261,27 @@ function hashPiTarball(file) {
6097
6261
  sha512.update(chunk);
6098
6262
  }
6099
6263
  } finally {
6100
- fs24.closeSync(descriptor);
6264
+ fs25.closeSync(descriptor);
6101
6265
  }
6102
6266
  return { path: file, bytes, sha256: sha2562.digest("hex"), sha512: sha512.digest("hex") };
6103
6267
  }
6104
6268
  async function acquirePiTarball(destination) {
6105
- const existing = fs24.statSync(destination, { throwIfNoEntry: false });
6269
+ const existing = fs25.statSync(destination, { throwIfNoEntry: false });
6106
6270
  if (existing?.isFile()) {
6107
6271
  const observed = hashPiTarball(destination);
6108
6272
  if (observed.bytes === PI_RUNTIME_CANDIDATE.tarball.bytes && observed.sha256 === PI_RUNTIME_CANDIDATE.tarball.sha256 && observed.sha512 === PI_RUNTIME_CANDIDATE.tarball.sha512) {
6109
6273
  return observed;
6110
6274
  }
6111
- fs24.rmSync(destination, { force: true });
6275
+ fs25.rmSync(destination, { force: true });
6112
6276
  }
6113
- fs24.mkdirSync(path32.dirname(destination), { recursive: true });
6277
+ fs25.mkdirSync(path33.dirname(destination), { recursive: true });
6114
6278
  const partial = `${destination}.partial-${process.pid}`;
6115
6279
  const response = await fetch(`https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`, {
6116
6280
  redirect: "error",
6117
6281
  headers: { accept: "application/octet-stream" }
6118
6282
  });
6119
6283
  if (!response.ok || response.body === null) throw new Error(`No se pudo descargar jorgex-pi@${PI_RUNTIME_CANDIDATE.package.version} (${response.status}).`);
6120
- const descriptor = fs24.openSync(partial, "wx", 384);
6284
+ const descriptor = fs25.openSync(partial, "wx", 384);
6121
6285
  let bytes = 0;
6122
6286
  try {
6123
6287
  for await (const chunk of response.body) {
@@ -6125,15 +6289,15 @@ async function acquirePiTarball(destination) {
6125
6289
  bytes += buffer.length;
6126
6290
  if (bytes > PI_RUNTIME_CANDIDATE.tarball.bytes) throw new Error("El tarball de jorgex-pi excede el tama\xF1o fijado.");
6127
6291
  let offset = 0;
6128
- while (offset < buffer.length) offset += fs24.writeSync(descriptor, buffer, offset);
6292
+ while (offset < buffer.length) offset += fs25.writeSync(descriptor, buffer, offset);
6129
6293
  }
6130
6294
  } catch (error) {
6131
- fs24.closeSync(descriptor);
6132
- fs24.rmSync(partial, { force: true });
6295
+ fs25.closeSync(descriptor);
6296
+ fs25.rmSync(partial, { force: true });
6133
6297
  throw error;
6134
6298
  }
6135
- fs24.closeSync(descriptor);
6136
- fs24.renameSync(partial, destination);
6299
+ fs25.closeSync(descriptor);
6300
+ fs25.renameSync(partial, destination);
6137
6301
  return hashPiTarball(destination);
6138
6302
  }
6139
6303
  function runProcess(invocation) {
@@ -6183,7 +6347,7 @@ async function runPiRuntimeSystem(input) {
6183
6347
  remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
6184
6348
  };
6185
6349
  }
6186
- const destination = input.targetDir === void 0 ? path32.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path32.join(path32.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
6350
+ const destination = input.targetDir === void 0 ? path33.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path33.join(path33.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
6187
6351
  let artifact;
6188
6352
  try {
6189
6353
  artifact = await acquirePiTarball(destination);
@@ -6203,18 +6367,18 @@ async function runPiRuntimeSystem(input) {
6203
6367
  }, {
6204
6368
  download: () => artifact,
6205
6369
  backupSettings: () => createBackup(
6206
- [path32.join(paths.codingAgentDir, "settings.json")],
6370
+ [path33.join(paths.codingAgentDir, "settings.json")],
6207
6371
  "pi-package-install",
6208
- input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
6372
+ input.targetDir === void 0 ? void 0 : path33.join(path33.resolve(input.targetDir), "backups")
6209
6373
  ),
6210
6374
  run: runProcess,
6211
- readSettings: () => readOptional(path32.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
6212
- rewriteSettings: (content) => writeText(path32.join(paths.codingAgentDir, "settings.json"), `${content}
6375
+ readSettings: () => readOptional(path33.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
6376
+ rewriteSettings: (content) => writeText(path33.join(paths.codingAgentDir, "settings.json"), `${content}
6213
6377
  `),
6214
6378
  writeReceiptAtomic: (content) => writeText(paths.receiptPath, content)
6215
6379
  });
6216
6380
  }
6217
- const packageRoot = path32.dirname(path32.dirname(paths.packageRunner));
6381
+ const packageRoot = path33.dirname(path33.dirname(paths.packageRunner));
6218
6382
  const writeReceipt = (receipt) => {
6219
6383
  writeText(paths.receiptPath, `${JSON.stringify(receipt, null, 2)}
6220
6384
  `);
@@ -6232,13 +6396,13 @@ async function runPiRuntimeSystem(input) {
6232
6396
  value,
6233
6397
  {
6234
6398
  backupSettings: () => createBackup(
6235
- [path32.join(paths.codingAgentDir, "settings.json")],
6399
+ [path33.join(paths.codingAgentDir, "settings.json")],
6236
6400
  "pi-package-uninstall",
6237
- input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
6401
+ input.targetDir === void 0 ? void 0 : path33.join(path33.resolve(input.targetDir), "backups")
6238
6402
  ),
6239
6403
  run: runProcess,
6240
- isPackageAbsent: () => !fs24.existsSync(packageRoot),
6241
- deleteReceipt: () => fs24.rmSync(paths.receiptPath, { force: true })
6404
+ isPackageAbsent: () => !fs25.existsSync(packageRoot),
6405
+ deleteReceipt: () => fs25.rmSync(paths.receiptPath, { force: true })
6242
6406
  }
6243
6407
  )
6244
6408
  };
@@ -6313,19 +6477,35 @@ async function runManagedPiSystem(input) {
6313
6477
  const {
6314
6478
  devtoolsMcpEnabled: explicitDevtools,
6315
6479
  playwrightCliEnabled: explicitPlaywright,
6480
+ playwrightCapability,
6481
+ packageOnly,
6316
6482
  writingStyle: suppliedStyle,
6317
6483
  writingStyleMode,
6318
6484
  ...runtimeInput
6319
6485
  } = input;
6486
+ if (input.operation === "doctor" && packageOnly) {
6487
+ const result2 = managedPackageResult(await runPiRuntimeSystem(runtimeInput));
6488
+ return result2.kind === "manual-existing" ? manualExistingResult(result2) : result2;
6489
+ }
6320
6490
  const readsStyle = input.operation !== "uninstall" && input.operation !== "models";
6491
+ if (input.operation !== "models") {
6492
+ try {
6493
+ assertSystemPromptFile(piSystemPromptFile(input.targetDir), input.targetDir);
6494
+ } catch (error) {
6495
+ return { kind: "blocked", reason: "projection-prompt-markers", remedy: error instanceof Error ? error.message : String(error) };
6496
+ }
6497
+ }
6321
6498
  const preparedStyle = readsStyle && suppliedStyle === void 0 ? prepareWritingStyle(resolveWritingStyleFile({ targetDir: input.targetDir }), { rootDir: input.targetDir }) : void 0;
6322
6499
  const style = readsStyle ? suppliedStyle ?? preparedStyle : void 0;
6323
6500
  const mode = readsStyle ? writingStyleMode ?? (input.targetDir === void 0 ? loadInstallModePreference().mode : "human") : "human";
6324
6501
  const writingStyle = style && mode === "programmatic" ? { ...style, content: null } : style;
6325
6502
  const devtoolsMcpEnabled = explicitDevtools ?? (input.targetDir === void 0 && loadDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), "pi"));
6326
6503
  const supportsPlaywright = PI_RUNTIME_CANDIDATE.contract.capabilities.includes("playwright-handoff-v1");
6327
- const playwrightCliEnabled = input.targetDir === void 0 && supportsPlaywright && (explicitPlaywright ?? loadPlaywrightCliPreference(void 0, "pi") === true);
6328
- const playwrightCliCommand = playwrightCliEnabled && input.operation !== "uninstall" && input.operation !== "models" ? detectPlaywrightCli().binPath : null;
6504
+ const persistedPlaywright = input.targetDir === void 0 && loadPlaywrightCliPreference(void 0, "pi") === true;
6505
+ const selectedPlaywright = explicitPlaywright ?? persistedPlaywright;
6506
+ const playwrightCapabilityEffective = playwrightCapability?.effective === true;
6507
+ const playwrightCliEnabled = input.targetDir === void 0 && supportsPlaywright && selectedPlaywright && playwrightCapabilityEffective;
6508
+ const playwrightCliCommand = playwrightCliEnabled && input.operation !== "uninstall" && input.operation !== "models" ? playwrightCapability?.cli.binPath ?? null : null;
6329
6509
  const projectionInput = {
6330
6510
  writingStyle,
6331
6511
  targetDir: input.targetDir,
@@ -6371,7 +6551,7 @@ async function runManagedPiSystem(input) {
6371
6551
  }
6372
6552
 
6373
6553
  // src/lib/quality-runner.ts
6374
- import path33 from "path";
6554
+ import path34 from "path";
6375
6555
  import { spawn, spawnSync as spawnSync2 } from "child_process";
6376
6556
  function normalizeLimit(value, name) {
6377
6557
  if (value === void 0) return void 0;
@@ -6503,7 +6683,7 @@ function killProcessTree(child) {
6503
6683
  if (pid === void 0) return;
6504
6684
  if (process.platform === "win32") {
6505
6685
  const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
6506
- const taskkill = path33.join(systemRoot, "System32", "taskkill.exe");
6686
+ const taskkill = path34.join(systemRoot, "System32", "taskkill.exe");
6507
6687
  const result = spawnSync2(taskkill, ["/pid", String(pid), "/t", "/f"], {
6508
6688
  shell: false,
6509
6689
  stdio: "ignore",
@@ -6749,9 +6929,9 @@ async function runQualityPlan(input) {
6749
6929
  }
6750
6930
 
6751
6931
  // src/lib/engram-install.ts
6752
- import fs25 from "fs";
6932
+ import fs26 from "fs";
6753
6933
  import os5 from "os";
6754
- import path34 from "path";
6934
+ import path35 from "path";
6755
6935
  import { execFileSync as execFileSync7 } from "child_process";
6756
6936
  import { createHash as createHash3 } from "crypto";
6757
6937
 
@@ -6796,15 +6976,15 @@ var engram_release_default = {
6796
6976
  async function installMissingEngram(options = {}) {
6797
6977
  const platform = options.platform ?? process.platform;
6798
6978
  const homeDir = options.homeDir ?? os5.homedir();
6799
- if (!path34.isAbsolute(homeDir)) return { ok: false, reason: "El directorio personal debe ser absoluto." };
6979
+ if (!path35.isAbsolute(homeDir)) return { ok: false, reason: "El directorio personal debe ser absoluto." };
6800
6980
  const binaryName = platform === "win32" ? "engram.exe" : "engram";
6801
- const bin = path34.join(homeDir, ".local", "bin", binaryName);
6981
+ const bin = path35.join(homeDir, ".local", "bin", binaryName);
6802
6982
  let staging;
6803
6983
  try {
6804
- const existing = fs25.lstatSync(bin, { throwIfNoEntry: false });
6984
+ const existing = fs26.lstatSync(bin, { throwIfNoEntry: false });
6805
6985
  if (existing) {
6806
- if (!fs25.statSync(bin).isFile()) throw new Error(`El destino Engram no es un archivo: ${bin}`);
6807
- fs25.accessSync(bin, platform === "win32" ? fs25.constants.F_OK : fs25.constants.X_OK);
6986
+ if (!fs26.statSync(bin).isFile()) throw new Error(`El destino Engram no es un archivo: ${bin}`);
6987
+ fs26.accessSync(bin, platform === "win32" ? fs26.constants.F_OK : fs26.constants.X_OK);
6808
6988
  return { ok: true, bin };
6809
6989
  }
6810
6990
  const key = `${platform === "win32" ? "windows" : platform}_${options.arch ?? process.arch}`;
@@ -6833,24 +7013,24 @@ async function installMissingEngram(options = {}) {
6833
7013
  if (size !== asset.size || createHash3("sha256").update(archive).digest("hex") !== asset.sha256) {
6834
7014
  throw new Error("Engram: tama\xF1o o hash SHA-256 no coincide con el artefacto aprobado.");
6835
7015
  }
6836
- fs25.mkdirSync(path34.dirname(bin), { recursive: true });
6837
- staging = fs25.mkdtempSync(path34.join(path34.dirname(bin), ".engram-install-"));
6838
- const archivePath = path34.join(staging, asset.name);
6839
- fs25.writeFileSync(archivePath, archive, { flag: "wx", mode: 384 });
7016
+ fs26.mkdirSync(path35.dirname(bin), { recursive: true });
7017
+ staging = fs26.mkdtempSync(path35.join(path35.dirname(bin), ".engram-install-"));
7018
+ const archivePath = path35.join(staging, asset.name);
7019
+ fs26.writeFileSync(archivePath, archive, { flag: "wx", mode: 384 });
6840
7020
  const binary = execFileSync7(resolveTarBin(), ["-xOf", archivePath, binaryName], {
6841
7021
  maxBuffer: 64 * 1024 * 1024,
6842
7022
  timeout: 3e4,
6843
7023
  stdio: ["ignore", "pipe", "pipe"]
6844
7024
  });
6845
7025
  if (binary.length === 0) throw new Error("El artefacto Engram no contiene un binario v\xE1lido.");
6846
- const stagedBin = path34.join(staging, binaryName);
6847
- fs25.writeFileSync(stagedBin, binary, { flag: "wx", mode: 493 });
6848
- fs25.linkSync(stagedBin, bin);
7026
+ const stagedBin = path35.join(staging, binaryName);
7027
+ fs26.writeFileSync(stagedBin, binary, { flag: "wx", mode: 493 });
7028
+ fs26.linkSync(stagedBin, bin);
6849
7029
  return { ok: true, bin };
6850
7030
  } catch (error) {
6851
7031
  return { ok: false, reason: error instanceof Error ? error.message : String(error) };
6852
7032
  } finally {
6853
- if (staging) fs25.rmSync(staging, { recursive: true, force: true });
7033
+ if (staging) fs26.rmSync(staging, { recursive: true, force: true });
6854
7034
  }
6855
7035
  }
6856
7036
 
@@ -6877,6 +7057,9 @@ async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
6877
7057
  );
6878
7058
  return false;
6879
7059
  }
7060
+ function shouldInspectPlaywrightCapability(targetDir, dryRun) {
7061
+ return targetDir === void 0 && !dryRun && browserPreferenceErrors().length === 0 && loadPlaywrightCliPreference() === true;
7062
+ }
6880
7063
  function parseFlags(args, allowReceipt = false) {
6881
7064
  const flags = {
6882
7065
  agents: [],
@@ -7109,6 +7292,18 @@ async function resolveDevtoolsMcpSelection(command, flags, runtimes) {
7109
7292
  const enabled = new Set(selected);
7110
7293
  return Object.fromEntries(runtimes.map((runtime) => [runtime, enabled.has(runtime)]));
7111
7294
  }
7295
+ function assertSelectedPromptFiles(runtimes, targetDir) {
7296
+ for (const runtime of runtimes) {
7297
+ if (runtime === "pi") assertSystemPromptFile(piSystemPromptFile(targetDir), targetDir);
7298
+ else {
7299
+ const adapter = ADAPTERS[runtime];
7300
+ if (!adapter) continue;
7301
+ const detection = adapter.detect();
7302
+ if (targetDir === void 0 && !detection.installed) continue;
7303
+ assertSystemPromptFile(adapter.paths(targetDir ?? detection.configDir).systemPromptFile, targetDir);
7304
+ }
7305
+ }
7306
+ }
7112
7307
  function parseCliArgs(argv) {
7113
7308
  const [first, ...rest] = argv;
7114
7309
  const isCommand = COMMANDS.includes(first ?? "install");
@@ -7174,7 +7369,19 @@ async function resolveHostEngramForInstall(flags) {
7174
7369
  return { ok: false, message: `Engram: ${error instanceof Error ? error.message : String(error)}` };
7175
7370
  }
7176
7371
  }
7177
- async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBin, devtoolsMcpEnabled, writingStyle, modePreference, playwrightCliEnabled) {
7372
+ async function runSelectedPi(options) {
7373
+ const {
7374
+ operation,
7375
+ targetDir,
7376
+ yes = false,
7377
+ resolvedEngramBin,
7378
+ devtoolsMcpEnabled,
7379
+ writingStyle: suppliedWritingStyle,
7380
+ modePreference,
7381
+ playwrightCliEnabled,
7382
+ playwrightCapability
7383
+ } = options;
7384
+ let writingStyle = suppliedWritingStyle;
7178
7385
  if (targetDir === void 0 && operation !== "models") {
7179
7386
  const preferenceErrors = browserPreferenceErrors();
7180
7387
  if (preferenceErrors.length > 0) {
@@ -7220,6 +7427,7 @@ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBi
7220
7427
  engramBin = requirement.bin;
7221
7428
  }
7222
7429
  const result = await runManagedPiSystem({
7430
+ ...options.packageOnly ? { packageOnly: true } : {},
7223
7431
  writingStyle,
7224
7432
  writingStyleMode: modePreference?.mode,
7225
7433
  operation,
@@ -7227,7 +7435,8 @@ async function runSelectedPi(operation, targetDir, yes = false, resolvedEngramBi
7227
7435
  detected: { executable: detected.executable, version: detected.version },
7228
7436
  engramBin,
7229
7437
  ...devtoolsMcpEnabled === void 0 ? {} : { devtoolsMcpEnabled },
7230
- ...playwrightCliEnabled === void 0 ? {} : { playwrightCliEnabled }
7438
+ ...playwrightCliEnabled === void 0 ? {} : { playwrightCliEnabled },
7439
+ ...playwrightCapability === void 0 ? {} : { playwrightCapability }
7231
7440
  });
7232
7441
  if (result.kind === "blocked") {
7233
7442
  const paths = "paths" in result ? `: ${result.paths.join(", ")}` : "";
@@ -7335,7 +7544,7 @@ Flags disponibles: jorgex-stack --help`
7335
7544
  return;
7336
7545
  }
7337
7546
  try {
7338
- const plan = JSON.parse(fs26.readFileSync(flags.positional[0], "utf8"));
7547
+ const plan = JSON.parse(fs27.readFileSync(flags.positional[0], "utf8"));
7339
7548
  const result = await runQualityPlan(plan);
7340
7549
  const serialized = serializeQualityReceipt(result.receipt);
7341
7550
  if (flags.receipt === void 0) process.stdout.write(`${serialized}
@@ -7363,6 +7572,7 @@ Flags disponibles: jorgex-stack --help`
7363
7572
  let completed = false;
7364
7573
  p6.intro(`jorgex-stack ${command}${flags.dryRun ? " (dry-run)" : ""}`);
7365
7574
  try {
7575
+ assertSelectedPromptFiles(runtimes, flags.targetDir);
7366
7576
  const writingStyle = prepareWritingStyle(resolveWritingStyleFile({ targetDir: flags.targetDir }), { rootDir: flags.targetDir });
7367
7577
  if (flags.targetDir === void 0) {
7368
7578
  const errors = browserPreferenceErrors();
@@ -7384,6 +7594,11 @@ Flags disponibles: jorgex-stack --help`
7384
7594
  exitCode = process.exitCode === 1 ? 1 : 0;
7385
7595
  return;
7386
7596
  }
7597
+ const playwrightToolPlan = resolvePlaywrightToolPlan(playwrightToolConsent);
7598
+ let playwrightCapability = shouldInspectPlaywrightCapability(flags.targetDir, flags.dryRun) && playwrightToolPlan.actions.length === 0 ? inspectPlaywrightCapability() : void 0;
7599
+ const capturePlaywrightCapability = (snapshot) => {
7600
+ playwrightCapability = snapshot;
7601
+ };
7387
7602
  p6.log.info(`Estilo de escritura: ${writingStyle.sourcePath}${flags.dryRun ? " (instalaci\xF3n prevista; sin escrituras)" : ""}.`);
7388
7603
  applyWritingStyle(writingStyle, flags.dryRun);
7389
7604
  if (!await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes)) {
@@ -7411,11 +7626,13 @@ Flags disponibles: jorgex-stack --help`
7411
7626
  playwrightToolConsent,
7412
7627
  devtoolsMcpSelection,
7413
7628
  engramBin,
7629
+ ...playwrightCapability === void 0 ? {} : { playwrightCapability },
7630
+ ...playwrightToolPlan.actions.length === 0 ? {} : { onPlaywrightCapability: capturePlaywrightCapability },
7414
7631
  showSummary: false
7415
7632
  });
7416
7633
  }
7417
7634
  let piCanRun = true;
7418
- if (command === "install" && fileRuntimes.length === 0 && runtimes.includes("pi") && resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0) {
7635
+ if (command === "install" && fileRuntimes.length === 0 && runtimes.includes("pi") && playwrightToolPlan.actions.length > 0) {
7419
7636
  if (flags.dryRun) {
7420
7637
  p6.log.info("Playwright CLI: instalaci\xF3n global y navegador previstos (dry-run; no se ejecutan).");
7421
7638
  } else {
@@ -7427,6 +7644,7 @@ Flags disponibles: jorgex-stack --help`
7427
7644
  yes: flags.yes,
7428
7645
  playwrightToolConsent,
7429
7646
  engramBin,
7647
+ onPlaywrightCapability: capturePlaywrightCapability,
7430
7648
  showSummary: false
7431
7649
  });
7432
7650
  piCanRun = exitCode === 0;
@@ -7435,16 +7653,17 @@ Flags disponibles: jorgex-stack --help`
7435
7653
  if (runtimes.includes("pi") && piCanRun) {
7436
7654
  if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
7437
7655
  else {
7438
- const piExitCode = await runSelectedPi(
7439
- command,
7440
- flags.targetDir,
7441
- flags.yes,
7442
- flags.targetDir === void 0 ? engramBin : void 0,
7443
- devtoolsMcpSelection.pi,
7656
+ const piExitCode = await runSelectedPi({
7657
+ operation: command,
7658
+ targetDir: flags.targetDir,
7659
+ yes: flags.yes,
7660
+ resolvedEngramBin: flags.targetDir === void 0 ? engramBin : void 0,
7661
+ devtoolsMcpEnabled: devtoolsMcpSelection.pi,
7444
7662
  writingStyle,
7445
- mode,
7446
- flags.targetDir === void 0 && exitCode === 0 && resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0 ? playwrightToolConsent.runtimeSelection?.pi : void 0
7447
- );
7663
+ modePreference: mode,
7664
+ playwrightCliEnabled: flags.targetDir === void 0 && exitCode === 0 && playwrightToolPlan.actions.length > 0 ? playwrightToolConsent.runtimeSelection?.pi : void 0,
7665
+ playwrightCapability
7666
+ });
7448
7667
  exitCode = Math.max(exitCode, piExitCode);
7449
7668
  }
7450
7669
  }
@@ -7468,6 +7687,13 @@ Flags disponibles: jorgex-stack --help`
7468
7687
  process.exitCode = 1;
7469
7688
  return;
7470
7689
  }
7690
+ try {
7691
+ assertSelectedPromptFiles(runtimes, flags.targetDir);
7692
+ } catch (error) {
7693
+ p6.log.error(error instanceof Error ? error.message : String(error));
7694
+ process.exitCode = 1;
7695
+ return;
7696
+ }
7471
7697
  const fileRuntimes = runtimes.filter(isFileManagedRuntime);
7472
7698
  let exitCode = fileRuntimes.length > 0 || flags.removePlaywright ? await runUninstall({
7473
7699
  runtimes: fileRuntimes,
@@ -7479,7 +7705,7 @@ Flags disponibles: jorgex-stack --help`
7479
7705
  }) : 0;
7480
7706
  if (runtimes.includes("pi")) {
7481
7707
  if (flags.dryRun) p6.log.info("Pi: uninstall previsto; dry-run conserva paquete y receipt.");
7482
- else exitCode = Math.max(exitCode, await runSelectedPi("uninstall", flags.targetDir));
7708
+ else exitCode = Math.max(exitCode, await runSelectedPi({ operation: "uninstall", targetDir: flags.targetDir }));
7483
7709
  }
7484
7710
  process.exitCode = exitCode;
7485
7711
  return;
@@ -7492,8 +7718,25 @@ Flags disponibles: jorgex-stack --help`
7492
7718
  ...Object.values(ADAPTERS).filter((adapter) => adapter.detect().installed).map((adapter) => adapter.id),
7493
7719
  ...piSelected ? ["pi"] : []
7494
7720
  ] : [...Object.keys(ADAPTERS), ...piSelected ? ["pi"] : []];
7495
- let exitCode = await runDoctor({ targetDir: flags.targetDir, runtimes: doctorRuntimes, mode });
7496
- if (piSelected) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir, false, void 0, void 0, void 0, mode));
7721
+ const doctorCapability = shouldInspectPlaywrightCapability(flags.targetDir, flags.dryRun) ? inspectPlaywrightCapability() : void 0;
7722
+ let exitCode = await runDoctor({
7723
+ targetDir: flags.targetDir,
7724
+ runtimes: doctorRuntimes,
7725
+ mode,
7726
+ ...flags.dryRun ? { dryRun: true } : {},
7727
+ ...doctorCapability === void 0 ? {} : { playwrightCapability: doctorCapability }
7728
+ });
7729
+ if (piSelected) {
7730
+ if (flags.dryRun) p6.log.info("Pi: doctor previsto; dry-run no ejecuta subprocess ni smoke de Playwright.");
7731
+ else {
7732
+ exitCode = Math.max(exitCode, await runSelectedPi({
7733
+ operation: "doctor",
7734
+ targetDir: flags.targetDir,
7735
+ modePreference: mode,
7736
+ playwrightCapability: doctorCapability
7737
+ }));
7738
+ }
7739
+ }
7497
7740
  process.exitCode = exitCode;
7498
7741
  return;
7499
7742
  }
@@ -7502,12 +7745,28 @@ Flags disponibles: jorgex-stack --help`
7502
7745
  const piExplicit = flags.agents.includes("pi");
7503
7746
  const fileRuntimeExplicit = flags.agents.some(isFileManagedRuntime);
7504
7747
  let exitCode = flags.agents.length === 0 || fileRuntimeExplicit ? await runUpdateCheck(VERSION, flags.targetDir === void 0) : 0;
7505
- if (piExplicit) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir));
7748
+ if (piExplicit) {
7749
+ if (flags.dryRun) p6.log.info("Pi: doctor previsto; dry-run no ejecuta subprocess ni smoke de Playwright.");
7750
+ else {
7751
+ exitCode = Math.max(exitCode, await runSelectedPi({
7752
+ operation: "doctor",
7753
+ targetDir: flags.targetDir,
7754
+ packageOnly: true
7755
+ }));
7756
+ }
7757
+ }
7506
7758
  process.exitCode = exitCode;
7507
7759
  return;
7508
7760
  }
7509
7761
  const runtimes = await resolveRuntimes(flags);
7510
7762
  if (runtimes === null) return;
7763
+ try {
7764
+ assertSelectedPromptFiles(runtimes, flags.targetDir);
7765
+ } catch (error) {
7766
+ p6.log.error(error instanceof Error ? error.message : String(error));
7767
+ process.exitCode = 1;
7768
+ return;
7769
+ }
7511
7770
  const fileRuntimes = runtimes.filter(isFileManagedRuntime);
7512
7771
  const preferenceFile = installModePreferenceFile();
7513
7772
  const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
@@ -7520,8 +7779,15 @@ Flags disponibles: jorgex-stack --help`
7520
7779
  { rootDir: flags.targetDir }
7521
7780
  );
7522
7781
  applyWritingStyle(writingStyle, flags.dryRun);
7782
+ let updateCapability = shouldInspectPlaywrightCapability(flags.targetDir, flags.dryRun) ? inspectPlaywrightCapability() : void 0;
7523
7783
  if (fileRuntimes.length === 0 && runtimes.includes("pi")) {
7524
- const piExitCode = await runSelectedPi("update", flags.targetDir, false, void 0, void 0, writingStyle, mode);
7784
+ const piExitCode = await runSelectedPi({
7785
+ operation: "update",
7786
+ targetDir: flags.targetDir,
7787
+ writingStyle,
7788
+ modePreference: mode,
7789
+ playwrightCapability: updateCapability
7790
+ });
7525
7791
  process.exitCode = piExitCode;
7526
7792
  persistSuccessfulGlobalMode(mode, flags.targetDir, flags.dryRun, piExitCode);
7527
7793
  return;
@@ -7534,7 +7800,8 @@ Flags disponibles: jorgex-stack --help`
7534
7800
  targetDir: flags.targetDir,
7535
7801
  dryRun: flags.dryRun,
7536
7802
  yes: true,
7537
- mode
7803
+ mode,
7804
+ ...updateCapability === void 0 ? {} : { playwrightCapability: updateCapability }
7538
7805
  });
7539
7806
  if (code !== 0) {
7540
7807
  process.exitCode = code;
@@ -7550,12 +7817,35 @@ Flags disponibles: jorgex-stack --help`
7550
7817
  flags.targetDir === void 0
7551
7818
  );
7552
7819
  process.exitCode = result.exitCode;
7820
+ let playwrightReconciled = false;
7821
+ if (result.exitCode === 0 && result.playwrightCapability !== void 0) {
7822
+ updateCapability = result.playwrightCapability;
7823
+ if (fileRuntimes.length > 0 && canSync) {
7824
+ const code = await runInstall({
7825
+ runtimes: fileRuntimes,
7826
+ writingStyle,
7827
+ targetDir: flags.targetDir,
7828
+ dryRun: false,
7829
+ yes: true,
7830
+ mode,
7831
+ playwrightCapability: updateCapability
7832
+ });
7833
+ process.exitCode = Math.max(process.exitCode ?? 0, code);
7834
+ playwrightReconciled = true;
7835
+ }
7836
+ }
7553
7837
  if (result.exitCode === 0 && runtimes.includes("pi")) {
7554
- process.exitCode = Math.max(process.exitCode, await runSelectedPi("update", flags.targetDir, false, void 0, void 0, writingStyle, mode));
7838
+ process.exitCode = Math.max(process.exitCode ?? 0, await runSelectedPi({
7839
+ operation: "update",
7840
+ targetDir: flags.targetDir,
7841
+ writingStyle,
7842
+ modePreference: mode,
7843
+ playwrightCapability: updateCapability
7844
+ }));
7555
7845
  }
7556
7846
  if (result.syncRequired && fileRuntimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
7557
7847
  p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
7558
- } else if (result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
7848
+ } else if (!playwrightReconciled && result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
7559
7849
  const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
7560
7850
  if (!p6.isCancel(apply) && apply) {
7561
7851
  process.exitCode = await runInstall({
@@ -7564,7 +7854,8 @@ Flags disponibles: jorgex-stack --help`
7564
7854
  targetDir: flags.targetDir,
7565
7855
  dryRun: false,
7566
7856
  yes: false,
7567
- mode
7857
+ mode,
7858
+ ...updateCapability === void 0 ? {} : { playwrightCapability: updateCapability }
7568
7859
  });
7569
7860
  } else {
7570
7861
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
@@ -7587,7 +7878,7 @@ Flags disponibles: jorgex-stack --help`
7587
7878
  }
7588
7879
  const fileRuntimes = runtimes.filter(isFileManagedRuntime);
7589
7880
  let code = fileRuntimes.length > 0 ? await runModelsPicker({ yes: flags.yes, runtimes: fileRuntimes }) : 0;
7590
- if (runtimes.includes("pi")) code = Math.max(code, await runSelectedPi("models", flags.targetDir));
7881
+ if (runtimes.includes("pi")) code = Math.max(code, await runSelectedPi({ operation: "models", targetDir: flags.targetDir }));
7591
7882
  process.exitCode = code;
7592
7883
  if (code === 0 && fileRuntimes.length > 0 && !flags.yes && process.stdout.isTTY) {
7593
7884
  const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
@@ -7602,7 +7893,15 @@ Flags disponibles: jorgex-stack --help`
7602
7893
  }
7603
7894
  const mode = await resolveInstallMode(flags, false);
7604
7895
  if (mode === null) return;
7605
- process.exitCode = await runInstall({ runtimes: fileRuntimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
7896
+ const playwrightCapability = shouldInspectPlaywrightCapability(flags.targetDir, flags.dryRun) ? inspectPlaywrightCapability() : void 0;
7897
+ process.exitCode = await runInstall({
7898
+ runtimes: fileRuntimes,
7899
+ targetDir: flags.targetDir,
7900
+ dryRun: flags.dryRun,
7901
+ yes: false,
7902
+ mode,
7903
+ ...playwrightCapability === void 0 ? {} : { playwrightCapability }
7904
+ });
7606
7905
  } else {
7607
7906
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
7608
7907
  }