jorgex-stack 1.0.14 → 1.0.16

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
@@ -5,8 +5,8 @@ import * as p6 from "@clack/prompts";
5
5
  import { pathToFileURL as pathToFileURL2 } from "url";
6
6
 
7
7
  // src/install.ts
8
- import fs11 from "fs";
9
- import path17 from "path";
8
+ import fs14 from "fs";
9
+ import path19 from "path";
10
10
  import * as p from "@clack/prompts";
11
11
 
12
12
  // src/adapters/opencode.ts
@@ -1104,53 +1104,136 @@ args = [${args}]`);
1104
1104
  }
1105
1105
  };
1106
1106
 
1107
- // src/lib/backup.ts
1107
+ // src/lib/install-mode.ts
1108
1108
  import fs6 from "fs";
1109
1109
  import path10 from "path";
1110
+ var INSTALL_MODES = /* @__PURE__ */ new Set(["human", "programmatic"]);
1111
+ var SUBAGENT_CONCURRENCIES = /* @__PURE__ */ new Set(["serial", "parallel"]);
1112
+ var DEFAULT_INSTALL_MODE_PREFERENCE = {
1113
+ mode: "human",
1114
+ subagentConcurrency: "serial"
1115
+ };
1116
+ function installModePreferenceFile() {
1117
+ return path10.join(dataDir(), "install-mode.json");
1118
+ }
1119
+ function hasInstallModePreference(file = installModePreferenceFile()) {
1120
+ return fs6.existsSync(file);
1121
+ }
1122
+ function isInstallMode(value) {
1123
+ return INSTALL_MODES.has(value);
1124
+ }
1125
+ function isSubagentConcurrency(value) {
1126
+ return SUBAGENT_CONCURRENCIES.has(value);
1127
+ }
1128
+ function normalizeInstallModePreference(value) {
1129
+ if (!value) {
1130
+ throw new Error("Preferencia de instalaci\xF3n vac\xEDa o corrupta.");
1131
+ }
1132
+ if (value.mode === "human") {
1133
+ if (value.subagentConcurrency !== "serial") {
1134
+ throw new Error("Preferencia de instalaci\xF3n inconsistente: human solo puede usar serial.");
1135
+ }
1136
+ return DEFAULT_INSTALL_MODE_PREFERENCE;
1137
+ }
1138
+ if (value.mode === "programmatic") {
1139
+ const subagentConcurrency = value.subagentConcurrency;
1140
+ if (subagentConcurrency === void 0 || !isSubagentConcurrency(subagentConcurrency)) {
1141
+ throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
1142
+ }
1143
+ return {
1144
+ mode: "programmatic",
1145
+ subagentConcurrency
1146
+ };
1147
+ }
1148
+ throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
1149
+ }
1150
+ function loadInstallModePreference(file = installModePreferenceFile()) {
1151
+ if (!fs6.existsSync(file)) return DEFAULT_INSTALL_MODE_PREFERENCE;
1152
+ try {
1153
+ const raw = fs6.readFileSync(file, "utf8");
1154
+ return normalizeInstallModePreference(JSON.parse(raw));
1155
+ } catch (error) {
1156
+ const message = error instanceof Error ? error.message : String(error);
1157
+ throw new Error(`No se pudo leer la preferencia de instalaci\xF3n en ${file}: ${message}`);
1158
+ }
1159
+ }
1160
+ function saveInstallModePreference(file, value) {
1161
+ writeText(file, JSON.stringify(value, null, 2) + "\n");
1162
+ }
1163
+ function parseInstallModePreferenceFlags(mode, subagentConcurrency) {
1164
+ if (mode !== void 0 && !isInstallMode(mode)) {
1165
+ return { error: `Modo inv\xE1lido: ${mode}` };
1166
+ }
1167
+ if (subagentConcurrency !== void 0 && !isSubagentConcurrency(subagentConcurrency)) {
1168
+ return { error: `Concurrencia de subagentes inv\xE1lida: ${subagentConcurrency}` };
1169
+ }
1170
+ if (subagentConcurrency !== void 0 && mode !== "programmatic") {
1171
+ return { error: "--subagent-concurrency requiere --mode programmatic." };
1172
+ }
1173
+ if (mode === "human") {
1174
+ if (subagentConcurrency !== void 0) {
1175
+ return { error: "--subagent-concurrency no se puede usar con --mode human." };
1176
+ }
1177
+ return { preference: DEFAULT_INSTALL_MODE_PREFERENCE };
1178
+ }
1179
+ if (mode === "programmatic") {
1180
+ return {
1181
+ preference: {
1182
+ mode,
1183
+ subagentConcurrency: subagentConcurrency ?? "serial"
1184
+ }
1185
+ };
1186
+ }
1187
+ return {};
1188
+ }
1189
+
1190
+ // src/lib/backup.ts
1191
+ import fs7 from "fs";
1192
+ import path11 from "path";
1110
1193
  import crypto from "crypto";
1111
1194
  var KEEP_BACKUPS = 10;
1112
1195
  function backupsRoot() {
1113
- return path10.join(dataDir(), "backups");
1196
+ return path11.join(dataDir(), "backups");
1114
1197
  }
1115
1198
  function compositeChecksum(files) {
1116
1199
  const hash = crypto.createHash("sha256");
1117
1200
  for (const file of [...files].sort()) {
1118
- const content = crypto.createHash("sha256").update(fs6.readFileSync(file)).digest("hex");
1201
+ const content = crypto.createHash("sha256").update(fs7.readFileSync(file)).digest("hex");
1119
1202
  hash.update(`${file}:${content}
1120
1203
  `);
1121
1204
  }
1122
1205
  return hash.digest("hex");
1123
1206
  }
1124
1207
  function createBackup(files, label, root = backupsRoot()) {
1125
- const existing = [...new Set(files)].filter((f) => fs6.existsSync(f));
1208
+ const existing = [...new Set(files)].filter((f) => fs7.existsSync(f));
1126
1209
  if (existing.length === 0) return null;
1127
1210
  const checksum = compositeChecksum(existing);
1128
1211
  const latest = listBackups(root)[0];
1129
1212
  if (latest?.checksum === checksum) return latest;
1130
1213
  const base = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
1131
1214
  let id = base;
1132
- let dir = path10.join(root, id);
1133
- for (let n = 1; fs6.existsSync(dir); n++) {
1215
+ let dir = path11.join(root, id);
1216
+ for (let n = 1; fs7.existsSync(dir); n++) {
1134
1217
  id = `${base}-${n}`;
1135
- dir = path10.join(root, id);
1218
+ dir = path11.join(root, id);
1136
1219
  }
1137
- ensureDir(path10.join(dir, "files"));
1220
+ ensureDir(path11.join(dir, "files"));
1138
1221
  const entries = existing.map((original, i) => {
1139
- const stored = path10.join(dir, "files", `${String(i).padStart(4, "0")}-${path10.basename(original)}`);
1140
- fs6.copyFileSync(original, stored);
1222
+ const stored = path11.join(dir, "files", `${String(i).padStart(4, "0")}-${path11.basename(original)}`);
1223
+ fs7.copyFileSync(original, stored);
1141
1224
  return { original, stored };
1142
1225
  });
1143
1226
  const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
1144
- writeText(path10.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
1227
+ writeText(path11.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
1145
1228
  pruneBackups(root);
1146
1229
  return info;
1147
1230
  }
1148
1231
  function listBackups(root = backupsRoot()) {
1149
- if (!fs6.existsSync(root)) return [];
1232
+ if (!fs7.existsSync(root)) return [];
1150
1233
  const infos = [];
1151
- for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
1234
+ for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
1152
1235
  if (!entry.isDirectory()) continue;
1153
- const manifest = readTextIfExists(path10.join(root, entry.name, "manifest.json"));
1236
+ const manifest = readTextIfExists(path11.join(root, entry.name, "manifest.json"));
1154
1237
  if (manifest === null) continue;
1155
1238
  try {
1156
1239
  infos.push(JSON.parse(manifest));
@@ -1165,10 +1248,10 @@ function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
1165
1248
  if (!info) throw new Error(`Backup no encontrado: ${id}`);
1166
1249
  let restored = 0;
1167
1250
  for (const { original, stored } of info.files) {
1168
- if (!fs6.existsSync(stored)) continue;
1251
+ if (!fs7.existsSync(stored)) continue;
1169
1252
  if (!isContainedIn(original, boundary)) continue;
1170
- ensureDir(path10.dirname(original));
1171
- fs6.copyFileSync(stored, original);
1253
+ ensureDir(path11.dirname(original));
1254
+ fs7.copyFileSync(stored, original);
1172
1255
  restored++;
1173
1256
  }
1174
1257
  return restored;
@@ -1176,15 +1259,15 @@ function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
1176
1259
  function pruneBackups(root) {
1177
1260
  const all = listBackups(root);
1178
1261
  for (const old of all.slice(KEEP_BACKUPS)) {
1179
- fs6.rmSync(path10.join(root, old.id), { recursive: true, force: true });
1262
+ fs7.rmSync(path11.join(root, old.id), { recursive: true, force: true });
1180
1263
  }
1181
1264
  }
1182
1265
 
1183
1266
  // src/lib/manifest.ts
1184
- import fs7 from "fs";
1185
- import path11 from "path";
1267
+ import fs8 from "fs";
1268
+ import path12 from "path";
1186
1269
  function manifestFile() {
1187
- return path11.join(dataDir(), "manifest.json");
1270
+ return path12.join(dataDir(), "manifest.json");
1188
1271
  }
1189
1272
  function readManifest(file = manifestFile()) {
1190
1273
  const raw = readTextIfExists(file);
@@ -1208,23 +1291,92 @@ function removeRuntimeManifest(id, file = manifestFile()) {
1208
1291
  writeText(file, JSON.stringify(manifest, null, 2) + "\n");
1209
1292
  }
1210
1293
  function findOrphans(prevOwned, currentTargets, root = HOME) {
1211
- return prevOwned.map((f) => path11.resolve(f)).filter(
1212
- (f) => !currentTargets.has(f) && path11.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs7.existsSync(f)
1294
+ return prevOwned.map((f) => path12.resolve(f)).filter(
1295
+ (f) => !currentTargets.has(f) && path12.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs8.existsSync(f)
1213
1296
  );
1214
1297
  }
1215
1298
 
1216
1299
  // src/components/system-prompt.ts
1217
- import path12 from "path";
1218
- import fs8 from "fs";
1219
- var normalize = (s) => s.replace(/\r\n/g, "\n");
1300
+ import path14 from "path";
1301
+ import fs10 from "fs";
1302
+
1303
+ // src/lib/mode-composition.ts
1304
+ import fs9 from "fs";
1305
+ import path13 from "path";
1306
+ var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
1307
+ var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
1308
+ var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
1309
+ var LEGACY_SKILL_DELEGATION_SECTION = /\n?##\s+Formato obligatorio[\s\S]*$/;
1310
+ var LEGACY_DELEGATION_LINE = /- For each `→ \[agent\]: \.\.\.` line, launch the corresponding specialist\./;
1311
+ var LEGACY_PROGRAMMATIC_PHRASES = [
1312
+ [/\bResult contract\b/g, "strict JSON handoff"],
1313
+ [/Status \/ Delegations \/ Risks/g, "status, delegations, and risks"],
1314
+ [LEGACY_DELEGATION_LINE, "- Process the JSON `delegations[]` array and launch the corresponding specialist."]
1315
+ ];
1316
+ var normalize = (value) => value.replace(/\r\n/g, "\n");
1317
+ function loadProgrammaticAddendum(stackDir, fileName) {
1318
+ return normalize(fs9.readFileSync(path13.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
1319
+ }
1320
+ function appendAddendum(base, addendum) {
1321
+ const normalizedBase = normalize(base);
1322
+ if (normalizedBase.includes(PROGRAMMATIC_MARKER)) return normalizedBase;
1323
+ return `${normalizedBase.trimEnd()}
1324
+
1325
+ ${addendum}
1326
+ `;
1327
+ }
1328
+ function stripLegacyResultContract(body) {
1329
+ return LEGACY_PROGRAMMATIC_PHRASES.reduce(
1330
+ (text2, [pattern, replacement]) => text2.replace(pattern, replacement),
1331
+ normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "")
1332
+ ).trimEnd();
1333
+ }
1334
+ function concurrencyRule(concurrency) {
1335
+ if (concurrency === "parallel") {
1336
+ return [
1337
+ "- Parallel delegation is allowed when safe.",
1338
+ "- Set max_parallel_subagents explicitly when the runtime supports it."
1339
+ ].join("\n");
1340
+ }
1341
+ return [
1342
+ "- Launch one subagent at a time.",
1343
+ "- No parallel delegation."
1344
+ ].join("\n");
1345
+ }
1346
+ function composeProgrammaticSystemPrompt(stackDir, content, mode) {
1347
+ if (mode !== "programmatic") return normalize(content);
1348
+ return appendAddendum(normalize(content), loadProgrammaticAddendum(stackDir, "AGENTS.addendum.md"));
1349
+ }
1350
+ function composeProgrammaticAgentBody(stackDir, agent, mode, concurrency) {
1351
+ if (mode !== "programmatic") return normalize(agent.body);
1352
+ const fileName = agent.mode === "primary" ? "orchestrator.addendum.md" : "subagent.addendum.md";
1353
+ let addendum = loadProgrammaticAddendum(stackDir, fileName);
1354
+ if (agent.mode === "primary") {
1355
+ addendum = addendum.replace("{{CONCURRENCY_RULE}}", concurrencyRule(concurrency ?? "serial"));
1356
+ }
1357
+ return appendAddendum(stripLegacyResultContract(agent.body), addendum);
1358
+ }
1359
+ function composeProgrammaticSkillBody(stackDir, skillPath, content, mode) {
1360
+ if (mode !== "programmatic") return normalize(content);
1361
+ if (skillPath !== path13.join("agent-delegation", "SKILL.md")) return normalize(content);
1362
+ const base = normalize(content).replace(
1363
+ "Las delegaciones van **en tu output final**, en el formato de abajo. El orquestador las lee y decide a qui\xE9n invocar.",
1364
+ "Las delegaciones van como strings en el JSON final `delegations[]`. El orquestador las lee y decide a qui\xE9n invocar."
1365
+ ).replace(LEGACY_SKILL_DELEGATION_SECTION, "").trimEnd();
1366
+ return appendAddendum(base, loadProgrammaticAddendum(stackDir, "agent-delegation.addendum.md"));
1367
+ }
1368
+
1369
+ // src/components/system-prompt.ts
1370
+ var normalize2 = (s) => s.replace(/\r\n/g, "\n");
1220
1371
  function planSystemPrompt(adapter, ctx) {
1221
1372
  const target = adapter.paths(ctx.configDir).systemPromptFile;
1222
- const agentsMd = normalize(fs8.readFileSync(path12.join(ctx.stackDir, "system-prompt", "AGENTS.md"), "utf8"));
1373
+ const agentsMd = normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "AGENTS.md"), "utf8"));
1223
1374
  const protocol = stripLeadingHtmlComments(
1224
- normalize(fs8.readFileSync(path12.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
1375
+ normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
1225
1376
  );
1377
+ const composedAgentsMd = composeProgrammaticSystemPrompt(ctx.stackDir, agentsMd, ctx.mode);
1226
1378
  let content = readTextIfExists(target);
1227
- content = upsertMarkdownSection(content, "system-prompt", agentsMd);
1379
+ content = upsertMarkdownSection(content, "system-prompt", composedAgentsMd);
1228
1380
  if (adapter.injectEngramProtocol(ctx)) {
1229
1381
  content = upsertMarkdownSection(content, "engram-protocol", protocol);
1230
1382
  } else {
@@ -1234,7 +1386,7 @@ function planSystemPrompt(adapter, ctx) {
1234
1386
  }
1235
1387
 
1236
1388
  // src/components/agents.ts
1237
- import path13 from "path";
1389
+ import path15 from "path";
1238
1390
  function planAgents(adapter, ctx) {
1239
1391
  const { agentsDir, commandsDir, outputStylesDir, skillsDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
1240
1392
  const dirFor = {
@@ -1245,51 +1397,62 @@ function planAgents(adapter, ctx) {
1245
1397
  profile: profilesDir
1246
1398
  };
1247
1399
  const scriptsBase = scriptsDir.replace(/\\/g, "/");
1248
- return loadCanonicalAgents(path13.join(ctx.stackDir, "agents")).flatMap(
1249
- (agent) => adapter.renderAgent(agent, ctx.models).flatMap((rendered) => {
1400
+ return loadCanonicalAgents(path15.join(ctx.stackDir, "agents")).flatMap((agent) => {
1401
+ const composedAgent = {
1402
+ ...agent,
1403
+ body: composeProgrammaticAgentBody(ctx.stackDir, agent, ctx.mode, ctx.subagentConcurrency)
1404
+ };
1405
+ return adapter.renderAgent(composedAgent, ctx.models).flatMap((rendered) => {
1250
1406
  const dir = dirFor[rendered.kind];
1251
1407
  if (dir === null) {
1252
1408
  ctx.warnings.push(`${adapter.name}: sin destino para '${rendered.kind}' (${rendered.file}) \u2014 omitido.`);
1253
1409
  return [];
1254
1410
  }
1255
1411
  const content = rendered.content.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsBase);
1256
- return [{ kind: "write", target: path13.join(dir, rendered.file), content }];
1257
- })
1258
- );
1412
+ return [{ kind: "write", target: path15.join(dir, rendered.file), content }];
1413
+ });
1414
+ });
1259
1415
  }
1260
1416
 
1261
1417
  // src/components/skills.ts
1262
- import path14 from "path";
1418
+ import path16 from "path";
1419
+ import fs11 from "fs";
1263
1420
  function planSkills(adapter, ctx) {
1264
1421
  const { skillsDir } = adapter.paths(ctx.configDir);
1265
- const source = path14.join(ctx.stackDir, "skills");
1266
- return listFilesRecursive(source).map((file) => ({
1267
- kind: "copy",
1268
- source: file,
1269
- target: path14.join(skillsDir, path14.relative(source, file))
1270
- }));
1422
+ const source = path16.join(ctx.stackDir, "skills");
1423
+ return listFilesRecursive(source).map((file) => {
1424
+ const relative = path16.relative(source, file);
1425
+ if (relative === path16.join("agent-delegation", "SKILL.md") && ctx.mode === "programmatic") {
1426
+ return {
1427
+ kind: "write",
1428
+ target: path16.join(skillsDir, relative),
1429
+ content: composeProgrammaticSkillBody(ctx.stackDir, relative, fs11.readFileSync(file, "utf8"), ctx.mode)
1430
+ };
1431
+ }
1432
+ return { kind: "copy", source: file, target: path16.join(skillsDir, relative) };
1433
+ });
1271
1434
  }
1272
1435
 
1273
1436
  // src/components/commands.ts
1274
- import path15 from "path";
1275
- import fs9 from "fs";
1437
+ import path17 from "path";
1438
+ import fs12 from "fs";
1276
1439
  function planCommands(adapter, ctx) {
1277
1440
  const { commandsDir } = adapter.paths(ctx.configDir);
1278
- const source = path15.join(ctx.stackDir, "commands");
1279
- if (!fs9.existsSync(source)) return [];
1441
+ const source = path17.join(ctx.stackDir, "commands");
1442
+ if (!fs12.existsSync(source)) return [];
1280
1443
  const commandFiles = [
1281
1444
  ...listMarkdownFiles(source),
1282
- ...listMarkdownFiles(path15.join(source, adapter.id))
1445
+ ...listMarkdownFiles(path17.join(source, adapter.id))
1283
1446
  ];
1284
1447
  return commandFiles.map(({ file, fullPath }) => {
1285
- const raw = fs9.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
1448
+ const raw = fs12.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
1286
1449
  const rendered = adapter.renderCommand(file, raw);
1287
- return { kind: "write", target: path15.join(commandsDir, rendered.file), content: rendered.content };
1450
+ return { kind: "write", target: path17.join(commandsDir, rendered.file), content: rendered.content };
1288
1451
  });
1289
1452
  }
1290
1453
  function listMarkdownFiles(dir) {
1291
- if (!fs9.existsSync(dir)) return [];
1292
- return fs9.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path15.join(dir, entry.name) }));
1454
+ if (!fs12.existsSync(dir)) return [];
1455
+ 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) }));
1293
1456
  }
1294
1457
 
1295
1458
  // src/components/hooks.ts
@@ -1303,26 +1466,27 @@ function planMcp(adapter, ctx) {
1303
1466
  }
1304
1467
 
1305
1468
  // src/components/plugins.ts
1306
- import path16 from "path";
1307
- import fs10 from "fs";
1469
+ import path18 from "path";
1470
+ import fs13 from "fs";
1308
1471
  function planPlugins(adapter, ctx) {
1309
1472
  const { pluginsDir } = adapter.paths(ctx.configDir);
1310
1473
  if (pluginsDir === null) return [];
1311
- const source = path16.join(ctx.stackDir, "plugins", adapter.id);
1312
- if (!fs10.existsSync(source)) return [];
1474
+ const source = path18.join(ctx.stackDir, "plugins", adapter.id);
1475
+ if (!fs13.existsSync(source)) return [];
1313
1476
  return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).map((sourceFile) => {
1314
- const target = path16.join(pluginsDir, path16.relative(source, sourceFile));
1315
- const raw = fs10.readFileSync(sourceFile, "utf8");
1477
+ const target = path18.join(pluginsDir, path18.relative(source, sourceFile));
1478
+ const raw = fs13.readFileSync(sourceFile, "utf8");
1316
1479
  let content = raw;
1317
1480
  if (content.includes('"{{ENGRAM_BIN}}"')) {
1318
1481
  content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
1319
1482
  }
1320
1483
  if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
1321
1484
  const protocol = stripLeadingHtmlComments(
1322
- fs10.readFileSync(path16.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
1485
+ fs13.readFileSync(path18.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
1323
1486
  );
1324
1487
  content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
1325
1488
  }
1489
+ content = content.replace(/(from\s+["'])(\.{1,2}\/[^"']+?)\.js(["'])/g, "$1$2.ts$3");
1326
1490
  if (content === raw) return { kind: "copy", source: sourceFile, target };
1327
1491
  return { kind: "write", target, content };
1328
1492
  });
@@ -1334,12 +1498,14 @@ var ADAPTERS = {
1334
1498
  "claude-code": claudeCodeAdapter,
1335
1499
  codex: codexAdapter
1336
1500
  };
1337
- function makeContext(adapter, configDir) {
1501
+ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
1338
1502
  const models = loadModelMap()[adapter.id];
1339
1503
  if (!models) return null;
1340
1504
  return {
1341
1505
  stackDir: stackRoot(),
1342
1506
  configDir,
1507
+ mode: mode.mode,
1508
+ subagentConcurrency: mode.subagentConcurrency,
1343
1509
  engramBin: detectEngram(),
1344
1510
  models,
1345
1511
  warnings: []
@@ -1363,7 +1529,7 @@ function diffPlan(plan) {
1363
1529
  if (current === null) return { action, status: "create" };
1364
1530
  return { action, status: current === action.content ? "unchanged" : "update" };
1365
1531
  }
1366
- if (!fs11.existsSync(action.target)) return { action, status: "create" };
1532
+ if (!fs14.existsSync(action.target)) return { action, status: "create" };
1367
1533
  return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
1368
1534
  });
1369
1535
  }
@@ -1373,35 +1539,49 @@ function applyChanges(changes) {
1373
1539
  else copyFile(action.source, action.target);
1374
1540
  }
1375
1541
  }
1376
- function collectAllCurrentTargets() {
1542
+ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
1377
1543
  const targets = /* @__PURE__ */ new Set();
1378
1544
  let complete = true;
1545
+ const warnings = [];
1379
1546
  for (const adapter of Object.values(ADAPTERS)) {
1380
1547
  const detection = adapter.detect();
1381
1548
  if (!detection.installed) continue;
1382
- const ctx = makeContext(adapter, detection.configDir);
1383
- if (!ctx) continue;
1549
+ const ctx = makeContext(adapter, detection.configDir, mode);
1550
+ if (!ctx) {
1551
+ complete = false;
1552
+ warnings.push(`${adapter.name}: limpieza de hu\xE9rfanos deshabilitada \u2014 falta contexto/model-map instalable para este runtime.`);
1553
+ continue;
1554
+ }
1384
1555
  try {
1385
- for (const action of buildPlan(adapter, ctx)) targets.add(path17.resolve(action.target));
1386
- } catch {
1556
+ for (const action of buildPlan(adapter, ctx)) targets.add(path19.resolve(action.target));
1557
+ } catch (error) {
1387
1558
  complete = false;
1559
+ warnings.push(
1560
+ `${adapter.name}: limpieza de hu\xE9rfanos deshabilitada \u2014 no se pudo construir el plan completo (${error instanceof Error ? error.message : String(error)}).`
1561
+ );
1388
1562
  }
1389
1563
  }
1390
- return { targets, complete };
1564
+ return { targets, complete, warnings };
1391
1565
  }
1392
1566
  async function runInstall(opts) {
1393
1567
  p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
1394
1568
  const stackDir = stackRoot();
1395
1569
  const engramBin = detectEngram();
1570
+ const modePreference = opts.mode === void 0 ? opts.targetDir === void 0 ? loadInstallModePreference() : DEFAULT_INSTALL_MODE_PREFERENCE : normalizeInstallModePreference(opts.mode);
1571
+ const useManifest = opts.targetDir === void 0;
1396
1572
  const modelMap = loadModelMap();
1397
- ensureModelMapFile();
1573
+ if (useManifest) ensureModelMapFile();
1398
1574
  p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
1399
- const useManifest = opts.targetDir === void 0;
1400
- const current = useManifest ? collectAllCurrentTargets() : { targets: /* @__PURE__ */ new Set(), complete: false };
1575
+ const current = useManifest ? collectAllCurrentTargets(modePreference) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
1401
1576
  const canOrphan = useManifest && current.complete;
1402
1577
  const canonicalMcp = loadCanonicalMcp(stackDir);
1403
1578
  const canonicalHooks = loadCanonicalHooks(stackDir);
1579
+ if (useManifest && (!current.complete || current.warnings.length > 0)) {
1580
+ p.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
1581
+ for (const warning of current.warnings) p.log.warn(warning);
1582
+ }
1404
1583
  let exitCode = 0;
1584
+ let successfulRuns = 0;
1405
1585
  for (const id of opts.runtimes) {
1406
1586
  const adapter = ADAPTERS[id];
1407
1587
  if (!adapter) {
@@ -1422,6 +1602,8 @@ async function runInstall(opts) {
1422
1602
  const ctx = {
1423
1603
  stackDir,
1424
1604
  configDir,
1605
+ mode: modePreference.mode,
1606
+ subagentConcurrency: modePreference.subagentConcurrency,
1425
1607
  engramBin,
1426
1608
  models,
1427
1609
  warnings: []
@@ -1431,8 +1613,8 @@ async function runInstall(opts) {
1431
1613
  let creates = diff.filter((d) => d.status === "create");
1432
1614
  let updates = diff.filter((d) => d.status === "update");
1433
1615
  let changes = [...creates, ...updates];
1434
- const prevManifest = canOrphan ? readManifest().runtimes[id] : void 0;
1435
- const orphans = prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
1616
+ const prevManifest = useManifest ? readManifest().runtimes[id] : void 0;
1617
+ const orphans = canOrphan && prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
1436
1618
  p.log.step(`${adapter.name} \u2192 ${configDir}`);
1437
1619
  p.log.info(
1438
1620
  `${diff.length} archivos gestionados: ${creates.length} nuevos, ${updates.length} modificados, ${diff.length - changes.length} sin cambios`
@@ -1447,13 +1629,17 @@ async function runInstall(opts) {
1447
1629
  }
1448
1630
  const writeManifest = () => {
1449
1631
  if (!useManifest) return;
1450
- const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path17.resolve(a.target)));
1451
- const owned = plan.map((a) => path17.resolve(a.target)).filter((t) => !unmergeTargets.has(t));
1632
+ const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path19.resolve(a.target)));
1633
+ const keepTarget = (target) => !unmergeTargets.has(target);
1634
+ const liveOwned = plan.map((a) => path19.resolve(a.target)).filter(keepTarget);
1635
+ const previousOwned = (prevManifest?.owned ?? []).map((target) => path19.resolve(target)).filter(keepTarget);
1636
+ const owned = canOrphan ? liveOwned : [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
1452
1637
  writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1453
1638
  };
1454
1639
  if (changes.length === 0 && orphans.length === 0) {
1455
1640
  writeManifest();
1456
1641
  p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
1642
+ successfulRuns++;
1457
1643
  continue;
1458
1644
  }
1459
1645
  if (!opts.yes && process.stdout.isTTY) {
@@ -1469,12 +1655,12 @@ async function runInstall(opts) {
1469
1655
  updates = diff.filter((d) => d.status === "update");
1470
1656
  changes = [...creates, ...updates];
1471
1657
  }
1472
- const backup = createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`);
1658
+ const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
1473
1659
  if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1474
1660
  applyChanges(changes);
1475
- const pruneRoot = useManifest ? HOME : path17.dirname(configDir);
1661
+ const pruneRoot = useManifest ? HOME : path19.dirname(configDir);
1476
1662
  for (const orphan of orphans) {
1477
- fs11.rmSync(orphan, { force: true });
1663
+ fs14.rmSync(orphan, { force: true });
1478
1664
  pruneEmptyDirs(orphan, pruneRoot);
1479
1665
  }
1480
1666
  const verifyCtx = { ...ctx, warnings: [] };
@@ -1486,15 +1672,19 @@ async function runInstall(opts) {
1486
1672
  } else {
1487
1673
  writeManifest();
1488
1674
  p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
1675
+ successfulRuns++;
1489
1676
  }
1490
1677
  }
1678
+ if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
1679
+ saveInstallModePreference(installModePreferenceFile(), modePreference);
1680
+ }
1491
1681
  p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
1492
1682
  return exitCode;
1493
1683
  }
1494
1684
 
1495
1685
  // src/uninstall.ts
1496
- import fs12 from "fs";
1497
- import path18 from "path";
1686
+ import fs15 from "fs";
1687
+ import path20 from "path";
1498
1688
  import * as p2 from "@clack/prompts";
1499
1689
  async function runUninstall(opts) {
1500
1690
  p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
@@ -1524,7 +1714,7 @@ async function runUninstall(opts) {
1524
1714
  if (!detection.installed) continue;
1525
1715
  const keepCtx = makeContext(keep, detection.configDir);
1526
1716
  if (!keepCtx) continue;
1527
- for (const action of buildPlan(keep, keepCtx)) retained.add(path18.resolve(action.target));
1717
+ for (const action of buildPlan(keep, keepCtx)) retained.add(path20.resolve(action.target));
1528
1718
  }
1529
1719
  for (const id of opts.runtimes) {
1530
1720
  const adapter = ADAPTERS[id];
@@ -1542,15 +1732,15 @@ async function runUninstall(opts) {
1542
1732
  if (!ctx) continue;
1543
1733
  ctx.preserveEngram = !removeEngram;
1544
1734
  const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
1545
- const mergedTargets = new Set(unmerge.map((a) => path18.resolve(a.target)));
1735
+ const mergedTargets = new Set(unmerge.map((a) => path20.resolve(a.target)));
1546
1736
  const usingRealConfig = opts.targetDir === void 0;
1547
1737
  const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
1548
- const pruneRoot = usingRealConfig ? HOME : path18.dirname(configDir);
1738
+ const pruneRoot = usingRealConfig ? HOME : path20.dirname(configDir);
1549
1739
  const planTargets = [
1550
- .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path18.resolve(a.target)), ...prevOwned.map((t) => path18.resolve(t))])
1551
- ].filter((t) => !mergedTargets.has(t) && fs12.existsSync(t));
1740
+ .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path20.resolve(a.target)), ...prevOwned.map((t) => path20.resolve(t))])
1741
+ ].filter((t) => !mergedTargets.has(t) && fs15.existsSync(t));
1552
1742
  const deleteTargets = planTargets.filter(
1553
- (t) => !retained.has(t) && !(ctx.preserveEngram && path18.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
1743
+ (t) => !retained.has(t) && !(ctx.preserveEngram && path20.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
1554
1744
  );
1555
1745
  const sharedKept = planTargets.length - deleteTargets.length;
1556
1746
  p2.log.step(`${adapter.name} \u2192 ${configDir}`);
@@ -1558,18 +1748,18 @@ async function runUninstall(opts) {
1558
1748
  if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
1559
1749
  if (opts.dryRun) continue;
1560
1750
  const backup = createBackup(
1561
- [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs12.existsSync(t))],
1751
+ [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs15.existsSync(t))],
1562
1752
  `uninstall-${id}`
1563
1753
  );
1564
1754
  if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1565
1755
  for (const target of deleteTargets) {
1566
- fs12.rmSync(target, { force: true });
1756
+ fs15.rmSync(target, { force: true });
1567
1757
  pruneEmptyDirs(target, pruneRoot);
1568
1758
  }
1569
1759
  for (const action of unmerge) {
1570
1760
  if (action.kind !== "write") continue;
1571
1761
  if (action.content.trim() === "") {
1572
- fs12.rmSync(action.target, { force: true });
1762
+ fs15.rmSync(action.target, { force: true });
1573
1763
  } else {
1574
1764
  writeText(action.target, action.content);
1575
1765
  }
@@ -1582,8 +1772,8 @@ async function runUninstall(opts) {
1582
1772
  }
1583
1773
 
1584
1774
  // src/doctor.ts
1585
- import path19 from "path";
1586
- import fs13 from "fs";
1775
+ import path21 from "path";
1776
+ import fs16 from "fs";
1587
1777
  import * as p3 from "@clack/prompts";
1588
1778
  function engramVersion(bin) {
1589
1779
  const out = runDetectedBin(bin, ["--version"], 5e3);
@@ -1591,7 +1781,7 @@ function engramVersion(bin) {
1591
1781
  return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
1592
1782
  }
1593
1783
  function context7KeyConfigured(id, configDir) {
1594
- const file = id === "codex" ? path19.join(configDir, "config.toml") : id === "claude-code" ? path19.join(path19.dirname(configDir), `${path19.basename(configDir)}.json`) : path19.join(configDir, "opencode.json");
1784
+ const file = id === "codex" ? path21.join(configDir, "config.toml") : id === "claude-code" ? path21.join(path21.dirname(configDir), `${path21.basename(configDir)}.json`) : path21.join(configDir, "opencode.json");
1595
1785
  const content = readTextIfExists(file);
1596
1786
  if (content === null) return null;
1597
1787
  const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
@@ -1614,22 +1804,28 @@ async function runDoctor() {
1614
1804
  p3.log.success(`Engram: ${version} (${engramBin})`);
1615
1805
  }
1616
1806
  }
1617
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path19.join(HOME, ".engram");
1618
- const engramDb = path19.join(engramDataDir, "engram.db");
1619
- if (fs13.existsSync(engramDb)) {
1620
- const sizeMb = (fs13.statSync(engramDb).size / 1024 / 1024).toFixed(1);
1807
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path21.join(HOME, ".engram");
1808
+ const engramDb = path21.join(engramDataDir, "engram.db");
1809
+ if (fs16.existsSync(engramDb)) {
1810
+ const sizeMb = (fs16.statSync(engramDb).size / 1024 / 1024).toFixed(1);
1621
1811
  p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
1622
1812
  }
1623
- if (!fs13.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
1813
+ if (!fs16.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
1624
1814
  const manifest = readManifest();
1625
- const current = collectAllCurrentTargets();
1815
+ const modePreference = loadInstallModePreference();
1816
+ const current = collectAllCurrentTargets(modePreference);
1817
+ if (!current.complete || current.warnings.length > 0) {
1818
+ p3.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
1819
+ for (const warning of current.warnings) p3.log.warn(warning);
1820
+ problems++;
1821
+ }
1626
1822
  for (const adapter of Object.values(ADAPTERS)) {
1627
1823
  const detection = adapter.detect();
1628
1824
  if (!detection.installed) {
1629
1825
  p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
1630
1826
  continue;
1631
1827
  }
1632
- const ctx = makeContext(adapter, detection.configDir);
1828
+ const ctx = makeContext(adapter, detection.configDir, modePreference);
1633
1829
  if (!ctx) continue;
1634
1830
  let pending;
1635
1831
  try {
@@ -1653,10 +1849,10 @@ async function runDoctor() {
1653
1849
  p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
1654
1850
  problems++;
1655
1851
  }
1656
- if (adapter.id === "codex" && fs13.existsSync(path19.join(detection.configDir, "hooks.json"))) {
1852
+ if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "hooks.json"))) {
1657
1853
  p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
1658
1854
  }
1659
- if (adapter.id === "codex" && fs13.existsSync(path19.join(detection.configDir, "AGENTS.override.md"))) {
1855
+ if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "AGENTS.override.md"))) {
1660
1856
  p3.log.warn(
1661
1857
  "Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
1662
1858
  );
@@ -1670,15 +1866,15 @@ async function runDoctor() {
1670
1866
  }
1671
1867
 
1672
1868
  // src/update.ts
1673
- import fs16 from "fs";
1674
- import path22 from "path";
1869
+ import fs19 from "fs";
1870
+ import path24 from "path";
1675
1871
  import os3 from "os";
1676
1872
  import { execFileSync as execFileSync4 } from "child_process";
1677
1873
  import * as p4 from "@clack/prompts";
1678
1874
 
1679
1875
  // src/lib/github.ts
1680
- import fs14 from "fs";
1681
- import path20 from "path";
1876
+ import fs17 from "fs";
1877
+ import path22 from "path";
1682
1878
  import { execFileSync as execFileSync2 } from "child_process";
1683
1879
  import os2 from "os";
1684
1880
  import { Readable } from "stream";
@@ -1748,19 +1944,19 @@ async function latestGithubCommit(repo) {
1748
1944
  }
1749
1945
  }
1750
1946
  function validateExtractedTree(destDir) {
1751
- const resolved = path20.resolve(destDir);
1947
+ const resolved = path22.resolve(destDir);
1752
1948
  const walk = (dir) => {
1753
1949
  let entries;
1754
1950
  try {
1755
- entries = fs14.readdirSync(dir, { withFileTypes: true });
1951
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
1756
1952
  } catch {
1757
1953
  return false;
1758
1954
  }
1759
1955
  for (const entry of entries) {
1760
- const full = path20.join(dir, entry.name);
1956
+ const full = path22.join(dir, entry.name);
1761
1957
  let stat;
1762
1958
  try {
1763
- stat = fs14.lstatSync(full);
1959
+ stat = fs17.lstatSync(full);
1764
1960
  } catch {
1765
1961
  return false;
1766
1962
  }
@@ -1776,15 +1972,15 @@ function validateExtractedTree(destDir) {
1776
1972
  }
1777
1973
  function resolveTarBin() {
1778
1974
  if (process.platform !== "win32") return "tar";
1779
- const winTar = path20.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
1780
- return fs14.existsSync(winTar) ? winTar : "tar";
1975
+ const winTar = path22.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
1976
+ return fs17.existsSync(winTar) ? winTar : "tar";
1781
1977
  }
1782
1978
  async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1783
1979
  const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
1784
- const tmp = path20.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
1980
+ const tmp = path22.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
1785
1981
  const fail = (reason) => {
1786
1982
  try {
1787
- fs14.rmSync(destDir, { recursive: true, force: true });
1983
+ fs17.rmSync(destDir, { recursive: true, force: true });
1788
1984
  } catch {
1789
1985
  }
1790
1986
  return { ok: false, reason };
@@ -1803,10 +1999,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1803
1999
  if (!res.body) return fail("respuesta HTTP sin cuerpo");
1804
2000
  await pipeline(
1805
2001
  Readable.fromWeb(res.body),
1806
- fs14.createWriteStream(tmp)
2002
+ fs17.createWriteStream(tmp)
1807
2003
  );
1808
- fs14.rmSync(destDir, { recursive: true, force: true });
1809
- fs14.mkdirSync(destDir, { recursive: true });
2004
+ fs17.rmSync(destDir, { recursive: true, force: true });
2005
+ fs17.mkdirSync(destDir, { recursive: true });
1810
2006
  try {
1811
2007
  execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
1812
2008
  } catch (err) {
@@ -1814,35 +2010,35 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1814
2010
  const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
1815
2011
  return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
1816
2012
  }
1817
- const resolvedDest = path20.resolve(destDir);
1818
- const validateRoot = validateSubdir ? path20.resolve(resolvedDest, validateSubdir) : resolvedDest;
2013
+ const resolvedDest = path22.resolve(destDir);
2014
+ const validateRoot = validateSubdir ? path22.resolve(resolvedDest, validateSubdir) : resolvedDest;
1819
2015
  if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
1820
2016
  return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
1821
2017
  }
1822
- if (fs14.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
2018
+ if (fs17.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
1823
2019
  return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
1824
2020
  }
1825
- const validated = fs14.existsSync(validateRoot);
2021
+ const validated = fs17.existsSync(validateRoot);
1826
2022
  return { ok: true, validated };
1827
2023
  } catch (err) {
1828
2024
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
1829
2025
  return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
1830
2026
  } finally {
1831
2027
  try {
1832
- fs14.rmSync(tmp, { force: true });
2028
+ fs17.rmSync(tmp, { force: true });
1833
2029
  } catch {
1834
2030
  }
1835
2031
  }
1836
2032
  }
1837
2033
 
1838
2034
  // src/lib/skill-update.ts
1839
- import fs15 from "fs";
1840
- import path21 from "path";
2035
+ import fs18 from "fs";
2036
+ import path23 from "path";
1841
2037
  import { execFileSync as execFileSync3 } from "child_process";
1842
2038
  var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle"]);
1843
2039
  function sameTextContentNormalized(a, b) {
1844
- const ba = fs15.readFileSync(a);
1845
- const bb = fs15.readFileSync(b);
2040
+ const ba = fs18.readFileSync(a);
2041
+ const bb = fs18.readFileSync(b);
1846
2042
  if (ba.equals(bb)) return true;
1847
2043
  const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
1848
2044
  const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
@@ -1850,10 +2046,10 @@ function sameTextContentNormalized(a, b) {
1850
2046
  }
1851
2047
  function diffSkillDirs(upstreamDir, localDir) {
1852
2048
  const upstreamFiles = new Set(
1853
- listFilesRecursive(upstreamDir).map((f) => path21.relative(upstreamDir, f))
2049
+ listFilesRecursive(upstreamDir).map((f) => path23.relative(upstreamDir, f))
1854
2050
  );
1855
2051
  const localFiles = new Set(
1856
- listFilesRecursive(localDir).map((f) => path21.relative(localDir, f))
2052
+ listFilesRecursive(localDir).map((f) => path23.relative(localDir, f))
1857
2053
  );
1858
2054
  const added = [];
1859
2055
  const modified = [];
@@ -1861,7 +2057,7 @@ function diffSkillDirs(upstreamDir, localDir) {
1861
2057
  for (const rel of upstreamFiles) {
1862
2058
  if (!localFiles.has(rel)) {
1863
2059
  added.push(rel);
1864
- } else if (!sameTextContentNormalized(path21.join(upstreamDir, rel), path21.join(localDir, rel))) {
2060
+ } else if (!sameTextContentNormalized(path23.join(upstreamDir, rel), path23.join(localDir, rel))) {
1865
2061
  modified.push(rel);
1866
2062
  }
1867
2063
  }
@@ -1923,8 +2119,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1923
2119
  if (PROTECTED_SKILLS.has(name)) {
1924
2120
  throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
1925
2121
  }
1926
- const upstreamsFile = upstreamsFilePath ?? path21.join(path21.dirname(stackRoot()), "upstreams.json");
1927
- const raw = fs15.readFileSync(upstreamsFile, "utf8");
2122
+ const upstreamsFile = upstreamsFilePath ?? path23.join(path23.dirname(stackRoot()), "upstreams.json");
2123
+ const raw = fs18.readFileSync(upstreamsFile, "utf8");
1928
2124
  const data = JSON.parse(raw);
1929
2125
  const skillEntry = data?.skills?.[name];
1930
2126
  if (!skillEntry) {
@@ -1933,8 +2129,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1933
2129
  if (skillEntry.kind === "release") {
1934
2130
  throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
1935
2131
  }
1936
- const skillsRoot = localSkillsRoot ?? path21.join(stackRoot(), "skills");
1937
- const localSkillDir = path21.join(skillsRoot, name);
2132
+ const skillsRoot = localSkillsRoot ?? path23.join(stackRoot(), "skills");
2133
+ const localSkillDir = path23.join(skillsRoot, name);
1938
2134
  const localFiles = listFilesRecursive(localSkillDir);
1939
2135
  if (localFiles.length > 0) {
1940
2136
  createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
@@ -1943,24 +2139,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1943
2139
  try {
1944
2140
  const upstreamFiles = listFilesRecursive(upstreamSkillDir);
1945
2141
  for (const src of upstreamFiles) {
1946
- const st = fs15.lstatSync(src);
2142
+ const st = fs18.lstatSync(src);
1947
2143
  if (st.isSymbolicLink()) {
1948
2144
  throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
1949
2145
  }
1950
- const rel = path21.relative(upstreamSkillDir, src);
1951
- const dest = path21.join(stagingDir, rel);
1952
- ensureDir(path21.dirname(dest));
2146
+ const rel = path23.relative(upstreamSkillDir, src);
2147
+ const dest = path23.join(stagingDir, rel);
2148
+ ensureDir(path23.dirname(dest));
1953
2149
  copyFile(src, dest);
1954
2150
  }
1955
2151
  const oldDir = `${localSkillDir}.old-${process.pid}`;
1956
- if (fs15.existsSync(localSkillDir)) {
1957
- fs15.renameSync(localSkillDir, oldDir);
2152
+ if (fs18.existsSync(localSkillDir)) {
2153
+ fs18.renameSync(localSkillDir, oldDir);
1958
2154
  }
1959
- fs15.renameSync(stagingDir, localSkillDir);
1960
- fs15.rmSync(oldDir, { recursive: true, force: true });
2155
+ fs18.renameSync(stagingDir, localSkillDir);
2156
+ fs18.rmSync(oldDir, { recursive: true, force: true });
1961
2157
  } catch (err) {
1962
2158
  try {
1963
- fs15.rmSync(stagingDir, { recursive: true, force: true });
2159
+ fs18.rmSync(stagingDir, { recursive: true, force: true });
1964
2160
  } catch {
1965
2161
  }
1966
2162
  throw err;
@@ -1974,8 +2170,8 @@ function rateLimitHint(prefix) {
1974
2170
  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.`;
1975
2171
  }
1976
2172
  function loadUpstreams() {
1977
- const file = path22.join(path22.dirname(stackRoot()), "upstreams.json");
1978
- return JSON.parse(fs16.readFileSync(file, "utf8"));
2173
+ const file = path24.join(path24.dirname(stackRoot()), "upstreams.json");
2174
+ return JSON.parse(fs19.readFileSync(file, "utf8"));
1979
2175
  }
1980
2176
  function skillsToScan(maintainer, upstreams) {
1981
2177
  return maintainer ? Object.keys(upstreams.skills) : [];
@@ -2087,8 +2283,8 @@ function isEngramRunning() {
2087
2283
  return null;
2088
2284
  }
2089
2285
  }
2090
- function isGitClone(projectRoot = path22.dirname(stackRoot())) {
2091
- return fs16.existsSync(path22.join(projectRoot, ".git"));
2286
+ function isGitClone(projectRoot = path24.dirname(stackRoot())) {
2287
+ return fs19.existsSync(path24.join(projectRoot, ".git"));
2092
2288
  }
2093
2289
  var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
2094
2290
  function resolvePnpm() {
@@ -2098,12 +2294,12 @@ function resolvePnpm() {
2098
2294
  }
2099
2295
  function cleanupTmp(dir) {
2100
2296
  try {
2101
- fs16.rmSync(dir, { recursive: true, force: true });
2297
+ fs19.rmSync(dir, { recursive: true, force: true });
2102
2298
  } catch {
2103
2299
  }
2104
2300
  }
2105
2301
  function updateStackGitClone() {
2106
- const projectRoot = path22.dirname(stackRoot());
2302
+ const projectRoot = path24.dirname(stackRoot());
2107
2303
  const git = lookPath("git");
2108
2304
  if (!git) throw new Error("git no encontrado en PATH.");
2109
2305
  const pnpm = resolvePnpm();
@@ -2120,7 +2316,7 @@ function updateStackGlobal() {
2120
2316
  execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
2121
2317
  }
2122
2318
  async function downloadSkillToTemp(repo, sha, skillPath) {
2123
- const root = fs16.mkdtempSync(path22.join(os3.tmpdir(), "jorgex-skill-"));
2319
+ const root = fs19.mkdtempSync(path24.join(os3.tmpdir(), "jorgex-skill-"));
2124
2320
  try {
2125
2321
  const result = await downloadRepoTarball(repo, sha, root, skillPath);
2126
2322
  if (!result.ok) {
@@ -2128,15 +2324,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
2128
2324
  return { error: result.reason };
2129
2325
  }
2130
2326
  if (skillPath) {
2131
- const sub = path22.resolve(path22.join(root, skillPath));
2327
+ const sub = path24.resolve(path24.join(root, skillPath));
2132
2328
  if (!isContainedIn(sub, root)) {
2133
2329
  cleanupTmp(root);
2134
2330
  return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
2135
2331
  }
2136
- if (fs16.existsSync(sub)) return { dir: sub, root };
2332
+ if (fs19.existsSync(sub)) return { dir: sub, root };
2137
2333
  const lastSeg = skillPath.split("/").pop();
2138
- const sub2 = path22.resolve(path22.join(root, lastSeg));
2139
- if (isContainedIn(sub2, root) && fs16.existsSync(sub2)) {
2334
+ const sub2 = path24.resolve(path24.join(root, lastSeg));
2335
+ if (isContainedIn(sub2, root) && fs19.existsSync(sub2)) {
2140
2336
  if (!validateExtractedTree(sub2)) {
2141
2337
  cleanupTmp(root);
2142
2338
  return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
@@ -2155,11 +2351,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
2155
2351
  function pruneEngramDbBackups() {
2156
2352
  try {
2157
2353
  const dir = dataDir();
2158
- if (!fs16.existsSync(dir)) return;
2159
- const backups = fs16.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs16.statSync(path22.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2354
+ if (!fs19.existsSync(dir)) return;
2355
+ const backups = fs19.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs19.statSync(path24.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2160
2356
  for (const old of backups.slice(3)) {
2161
2357
  try {
2162
- fs16.rmSync(path22.join(dir, old.name));
2358
+ fs19.rmSync(path24.join(dir, old.name));
2163
2359
  } catch {
2164
2360
  }
2165
2361
  }
@@ -2167,18 +2363,18 @@ function pruneEngramDbBackups() {
2167
2363
  }
2168
2364
  }
2169
2365
  function rotateLockedBinary(binPath, sweepRoot = HOME) {
2170
- if (!fs16.existsSync(binPath)) return null;
2171
- const dir = path22.dirname(binPath);
2172
- const base = path22.basename(binPath);
2366
+ if (!fs19.existsSync(binPath)) return null;
2367
+ const dir = path24.dirname(binPath);
2368
+ const base = path24.basename(binPath);
2173
2369
  const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
2174
2370
  const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
2175
- const resolvedDir = path22.resolve(dir);
2176
- if (resolvedDir === path22.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
2371
+ const resolvedDir = path24.resolve(dir);
2372
+ if (resolvedDir === path24.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
2177
2373
  try {
2178
- for (const entry of fs16.readdirSync(dir)) {
2374
+ for (const entry of fs19.readdirSync(dir)) {
2179
2375
  if (oldPattern.test(entry)) {
2180
2376
  try {
2181
- fs16.rmSync(path22.join(dir, entry), { force: true });
2377
+ fs19.rmSync(path24.join(dir, entry), { force: true });
2182
2378
  } catch {
2183
2379
  }
2184
2380
  }
@@ -2186,8 +2382,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
2186
2382
  } catch {
2187
2383
  }
2188
2384
  }
2189
- const rotated = path22.join(dir, `${base}.old-${Date.now()}`);
2190
- fs16.renameSync(binPath, rotated);
2385
+ const rotated = path24.join(dir, `${base}.old-${Date.now()}`);
2386
+ fs19.renameSync(binPath, rotated);
2191
2387
  return rotated;
2192
2388
  }
2193
2389
  async function updateEngram(engramRepo, latestVersion) {
@@ -2196,19 +2392,19 @@ async function updateEngram(engramRepo, latestVersion) {
2196
2392
  "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)."
2197
2393
  );
2198
2394
  }
2199
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path22.join(HOME, ".engram");
2200
- const engramDb = path22.join(engramDataDir, "engram.db");
2201
- if (fs16.existsSync(engramDb)) {
2395
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path24.join(HOME, ".engram");
2396
+ const engramDb = path24.join(engramDataDir, "engram.db");
2397
+ if (fs19.existsSync(engramDb)) {
2202
2398
  const doBackup = await p4.confirm({
2203
2399
  message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
2204
2400
  initialValue: true
2205
2401
  });
2206
2402
  if (!p4.isCancel(doBackup) && doBackup) {
2207
2403
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2208
- const dest = path22.join(dataDir(), `engram-db-backup-${ts}.db`);
2404
+ const dest = path24.join(dataDir(), `engram-db-backup-${ts}.db`);
2209
2405
  try {
2210
- if (!fs16.existsSync(dataDir())) fs16.mkdirSync(dataDir(), { recursive: true });
2211
- fs16.copyFileSync(engramDb, dest);
2406
+ if (!fs19.existsSync(dataDir())) fs19.mkdirSync(dataDir(), { recursive: true });
2407
+ fs19.copyFileSync(engramDb, dest);
2212
2408
  p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
2213
2409
  pruneEngramDbBackups();
2214
2410
  } catch (err) {
@@ -2258,10 +2454,10 @@ async function updateEngram(engramRepo, latestVersion) {
2258
2454
  ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
2259
2455
  { stdio: "inherit" }
2260
2456
  );
2261
- const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2457
+ const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
2262
2458
  if (rollbackOk.action === "restore") {
2263
2459
  try {
2264
- fs16.renameSync(rotated, bin);
2460
+ fs19.renameSync(rotated, bin);
2265
2461
  p4.log.warn(rollbackOk.messages.onRestore);
2266
2462
  } catch {
2267
2463
  p4.log.warn(rollbackOk.messages.onRenameFail);
@@ -2269,10 +2465,10 @@ async function updateEngram(engramRepo, latestVersion) {
2269
2465
  }
2270
2466
  return true;
2271
2467
  } catch (err) {
2272
- const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2468
+ const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
2273
2469
  if (rollbackFail.action === "restore") {
2274
2470
  try {
2275
- fs16.renameSync(rotated, bin);
2471
+ fs19.renameSync(rotated, bin);
2276
2472
  p4.log.info(rollbackFail.messages.onRestore);
2277
2473
  } catch {
2278
2474
  p4.log.error(rollbackFail.messages.onRenameFail);
@@ -2530,7 +2726,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
2530
2726
  continue;
2531
2727
  }
2532
2728
  const { dir: tmpDir, root: tmpRoot } = tmpResult;
2533
- const localSkillDir = path22.join(stackRoot(), "skills", skillInfo.name);
2729
+ const localSkillDir = path24.join(stackRoot(), "skills", skillInfo.name);
2534
2730
  const diff = renderSkillDiff(tmpDir, localSkillDir);
2535
2731
  if (diff) {
2536
2732
  p4.log.info(`Diff de ${skillInfo.name}:
@@ -2589,7 +2785,7 @@ ${diff}`);
2589
2785
  }
2590
2786
 
2591
2787
  // src/models-picker.ts
2592
- import path23 from "path";
2788
+ import path25 from "path";
2593
2789
  import * as p5 from "@clack/prompts";
2594
2790
  var TIERS = ["strong", "standard", "cheap"];
2595
2791
  var EFFORTS = ["low", "medium", "high", "xhigh"];
@@ -2605,7 +2801,7 @@ function opencodeLiveModels(binPath) {
2605
2801
  }
2606
2802
  function agentsByTier() {
2607
2803
  const grouped = { strong: [], standard: [], cheap: [] };
2608
- for (const agent of loadCanonicalAgents(path23.join(stackRoot(), "agents"))) {
2804
+ for (const agent of loadCanonicalAgents(path25.join(stackRoot(), "agents"))) {
2609
2805
  if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
2610
2806
  }
2611
2807
  return grouped;
@@ -2764,16 +2960,16 @@ function cancelled() {
2764
2960
  }
2765
2961
 
2766
2962
  // src/lib/release.ts
2767
- import fs17 from "fs";
2768
- import path24 from "path";
2963
+ import fs20 from "fs";
2964
+ import path26 from "path";
2769
2965
  import { execFileSync as execFileSync5 } from "child_process";
2770
2966
  import { fileURLToPath as fileURLToPath2 } from "url";
2771
2967
  function findPackageJson() {
2772
- let dir = path24.dirname(fileURLToPath2(import.meta.url));
2968
+ let dir = path26.dirname(fileURLToPath2(import.meta.url));
2773
2969
  for (let i = 0; i < 6; i++) {
2774
- const candidate = path24.join(dir, "package.json");
2775
- if (fs17.existsSync(candidate)) return candidate;
2776
- dir = path24.dirname(dir);
2970
+ const candidate = path26.join(dir, "package.json");
2971
+ if (fs20.existsSync(candidate)) return candidate;
2972
+ dir = path26.dirname(dir);
2777
2973
  }
2778
2974
  throw new Error("No se encontr\xF3 package.json cerca del CLI.");
2779
2975
  }
@@ -2782,7 +2978,7 @@ function readPackageVersion() {
2782
2978
  }
2783
2979
  function readPackageMetadata() {
2784
2980
  const packageJson = findPackageJson();
2785
- const raw = fs17.readFileSync(packageJson, "utf8");
2981
+ const raw = fs20.readFileSync(packageJson, "utf8");
2786
2982
  const parsed = JSON.parse(raw);
2787
2983
  const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
2788
2984
  const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
@@ -2800,6 +2996,8 @@ function parseFlags(args) {
2800
2996
  agents: [],
2801
2997
  dryRun: false,
2802
2998
  yes: false,
2999
+ mode: void 0,
3000
+ subagentConcurrency: void 0,
2803
3001
  help: false,
2804
3002
  version: false,
2805
3003
  list: false,
@@ -2807,12 +3005,33 @@ function parseFlags(args) {
2807
3005
  removeEngram: false,
2808
3006
  positional: []
2809
3007
  };
3008
+ const readValue = (index) => {
3009
+ const value = args[index + 1];
3010
+ if (value === void 0 || value.startsWith("-")) return [void 0, index];
3011
+ return [value, index + 1];
3012
+ };
2810
3013
  for (let i = 0; i < args.length; i++) {
2811
3014
  const arg = args[i];
2812
- if (arg === "--agents" || arg === "-a") flags.agents = (args[++i] ?? "").split(",").filter(Boolean);
2813
- else if (arg.startsWith("--agents=")) flags.agents = arg.slice(9).split(",").filter(Boolean);
2814
- else if (arg === "--target-dir") flags.targetDir = args[++i];
2815
- else if (arg.startsWith("--target-dir=")) flags.targetDir = arg.slice(13);
3015
+ if (arg === "--agents" || arg === "-a") {
3016
+ const [value, nextIndex] = readValue(i);
3017
+ flags.agents = (value ?? "").split(",").filter(Boolean);
3018
+ i = nextIndex;
3019
+ } else if (arg.startsWith("--agents=")) flags.agents = arg.slice(9).split(",").filter(Boolean);
3020
+ else if (arg === "--target-dir") {
3021
+ const [value, nextIndex] = readValue(i);
3022
+ flags.targetDir = value;
3023
+ i = nextIndex;
3024
+ } else if (arg.startsWith("--target-dir=")) flags.targetDir = arg.slice(13);
3025
+ else if (arg === "--mode") {
3026
+ const [value, nextIndex] = readValue(i);
3027
+ flags.mode = value ?? "";
3028
+ i = nextIndex;
3029
+ } else if (arg.startsWith("--mode=")) flags.mode = arg.slice(7);
3030
+ else if (arg === "--subagent-concurrency") {
3031
+ const [value, nextIndex] = readValue(i);
3032
+ flags.subagentConcurrency = value ?? "";
3033
+ i = nextIndex;
3034
+ } else if (arg.startsWith("--subagent-concurrency=")) flags.subagentConcurrency = arg.slice(23);
2816
3035
  else if (arg === "--dry-run") flags.dryRun = true;
2817
3036
  else if (arg === "--yes" || arg === "-y") flags.yes = true;
2818
3037
  else if (arg === "--help" || arg === "-h") flags.help = true;
@@ -2824,6 +3043,67 @@ function parseFlags(args) {
2824
3043
  }
2825
3044
  return flags;
2826
3045
  }
3046
+ async function resolveInstallMode(flags, promptIfMissing = true) {
3047
+ const explicit = parseInstallModePreferenceFlags(flags.mode, flags.subagentConcurrency);
3048
+ if (explicit.error) {
3049
+ console.error(explicit.error);
3050
+ process.exitCode = 1;
3051
+ return null;
3052
+ }
3053
+ if (explicit.preference) return explicit.preference;
3054
+ if (flags.targetDir !== void 0) {
3055
+ p6.log.info("--target-dir ignora la preferencia guardada y usa modo human por defecto; usa --mode programmatic si quieres otro modo.");
3056
+ return DEFAULT_INSTALL_MODE_PREFERENCE;
3057
+ }
3058
+ const preferenceFile = installModePreferenceFile();
3059
+ if (hasInstallModePreference(preferenceFile)) {
3060
+ try {
3061
+ return loadInstallModePreference(preferenceFile);
3062
+ } catch (error) {
3063
+ const reason = error instanceof Error ? error.message : String(error);
3064
+ console.error(
3065
+ `${reason}
3066
+ Corrige o borra ${preferenceFile}, o vuelve a ejecutar con --mode human|programmatic.`
3067
+ );
3068
+ process.exitCode = 1;
3069
+ return null;
3070
+ }
3071
+ }
3072
+ if (!promptIfMissing) {
3073
+ console.error("No hay modo guardado; usa --mode expl\xEDcito para este sync.");
3074
+ process.exitCode = 1;
3075
+ return null;
3076
+ }
3077
+ if (flags.yes || !process.stdout.isTTY) return DEFAULT_INSTALL_MODE_PREFERENCE;
3078
+ const selected = await p6.select({
3079
+ message: "\xBFC\xF3mo quieres instalar el modo del stack?",
3080
+ options: [
3081
+ { value: "human", label: "Human (comportamiento actual)" },
3082
+ { value: "programmatic", label: "Programmatic (elige concurrencia)" }
3083
+ ],
3084
+ initialValue: DEFAULT_INSTALL_MODE_PREFERENCE.mode
3085
+ });
3086
+ if (p6.isCancel(selected)) return null;
3087
+ if (selected === "human") {
3088
+ return {
3089
+ mode: "human",
3090
+ subagentConcurrency: "serial"
3091
+ };
3092
+ }
3093
+ const concurrency = await p6.select({
3094
+ message: "Concurrencia de subagentes en modo programmatic",
3095
+ options: [
3096
+ { value: "serial", label: "Serial (default)" },
3097
+ { value: "parallel", label: "Parallel" }
3098
+ ],
3099
+ initialValue: DEFAULT_INSTALL_MODE_PREFERENCE.subagentConcurrency
3100
+ });
3101
+ if (p6.isCancel(concurrency)) return null;
3102
+ return {
3103
+ mode: "programmatic",
3104
+ subagentConcurrency: concurrency
3105
+ };
3106
+ }
2827
3107
  function parseCliArgs(argv) {
2828
3108
  const [first, ...rest] = argv;
2829
3109
  const isCommand = COMMANDS.includes(first ?? "install");
@@ -2872,6 +3152,8 @@ Comandos:
2872
3152
 
2873
3153
  Opciones:
2874
3154
  --agents, -a opencode,claude-code,codex Runtimes destino (default: detectados)
3155
+ --mode human|programmatic Modo de instalaci\xF3n (default: preferencia guardada o human)
3156
+ --subagent-concurrency serial|parallel Concurrencia de subagentes en modo programmatic
2875
3157
  --target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
2876
3158
  --dry-run Muestra el plan sin escribir nada
2877
3159
  --yes, -y No interactivo
@@ -2899,6 +3181,8 @@ async function main() {
2899
3181
  switch (command) {
2900
3182
  case "install":
2901
3183
  case "sync": {
3184
+ const mode = await resolveInstallMode(flags);
3185
+ if (mode === null) return;
2902
3186
  const runtimes = await resolveRuntimes(flags);
2903
3187
  if (runtimes === null) return;
2904
3188
  if (runtimes.length === 0) {
@@ -2906,7 +3190,7 @@ async function main() {
2906
3190
  process.exitCode = 1;
2907
3191
  return;
2908
3192
  }
2909
- process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes });
3193
+ process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes, mode });
2910
3194
  return;
2911
3195
  }
2912
3196
  case "uninstall": {
@@ -2941,23 +3225,46 @@ async function main() {
2941
3225
  }
2942
3226
  const runtimes = await resolveRuntimes(flags);
2943
3227
  if (runtimes === null) return;
2944
- if (runtimes.length > 0) {
2945
- const code = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: true });
3228
+ const preferenceFile = installModePreferenceFile();
3229
+ const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
3230
+ const hasSavedMode = hasInstallModePreference(preferenceFile);
3231
+ const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
3232
+ const mode = runtimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
3233
+ if (mode === null) return;
3234
+ const canSync = runtimes.length === 0 || canResolveMode;
3235
+ if (runtimes.length > 0 && canSync) {
3236
+ const code = await runInstall({
3237
+ runtimes,
3238
+ targetDir: flags.targetDir,
3239
+ dryRun: flags.dryRun,
3240
+ yes: true,
3241
+ mode
3242
+ });
2946
3243
  if (code !== 0) {
2947
3244
  process.exitCode = code;
2948
3245
  return;
2949
3246
  }
3247
+ } else if (runtimes.length > 0) {
3248
+ console.error("No hay modo guardado; se omite el sync previo y se contin\xFAa con update. Usa --mode expl\xEDcito si quieres sincronizar.");
2950
3249
  }
2951
3250
  const result = await runInteractiveUpdate(VERSION, flags.yes, flags.dryRun);
2952
3251
  process.exitCode = result.exitCode;
2953
- if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && !flags.yes && process.stdout.isTTY) {
3252
+ if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && !canSync) {
3253
+ p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
3254
+ } else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
2954
3255
  const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
2955
3256
  if (!p6.isCancel(apply) && apply) {
2956
- process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: false, yes: false });
3257
+ process.exitCode = await runInstall({
3258
+ runtimes,
3259
+ targetDir: flags.targetDir,
3260
+ dryRun: false,
3261
+ yes: false,
3262
+ mode
3263
+ });
2957
3264
  } else {
2958
3265
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2959
3266
  }
2960
- } else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && (flags.yes || !process.stdout.isTTY)) {
3267
+ } else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
2961
3268
  console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
2962
3269
  }
2963
3270
  return;
@@ -2975,7 +3282,17 @@ async function main() {
2975
3282
  if (code === 0 && !flags.yes && process.stdout.isTTY) {
2976
3283
  const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
2977
3284
  if (!p6.isCancel(apply) && apply) {
2978
- process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false });
3285
+ const preferenceFile = installModePreferenceFile();
3286
+ const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
3287
+ const hasSavedMode = hasInstallModePreference(preferenceFile);
3288
+ const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
3289
+ if (!canResolveMode) {
3290
+ p6.log.warn("Model-map guardado: se omite el sync con los runtimes porque falta un modo. Ejecuta jorgex-stack sync --mode human|programmatic.");
3291
+ return;
3292
+ }
3293
+ const mode = await resolveInstallMode(flags, false);
3294
+ if (mode === null) return;
3295
+ process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
2979
3296
  } else {
2980
3297
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2981
3298
  }