jorgex-stack 1.0.15 → 1.0.17

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,23 +1466,23 @@ 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
  }
@@ -1335,12 +1498,14 @@ var ADAPTERS = {
1335
1498
  "claude-code": claudeCodeAdapter,
1336
1499
  codex: codexAdapter
1337
1500
  };
1338
- function makeContext(adapter, configDir) {
1501
+ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
1339
1502
  const models = loadModelMap()[adapter.id];
1340
1503
  if (!models) return null;
1341
1504
  return {
1342
1505
  stackDir: stackRoot(),
1343
1506
  configDir,
1507
+ mode: mode.mode,
1508
+ subagentConcurrency: mode.subagentConcurrency,
1344
1509
  engramBin: detectEngram(),
1345
1510
  models,
1346
1511
  warnings: []
@@ -1364,7 +1529,7 @@ function diffPlan(plan) {
1364
1529
  if (current === null) return { action, status: "create" };
1365
1530
  return { action, status: current === action.content ? "unchanged" : "update" };
1366
1531
  }
1367
- if (!fs11.existsSync(action.target)) return { action, status: "create" };
1532
+ if (!fs14.existsSync(action.target)) return { action, status: "create" };
1368
1533
  return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
1369
1534
  });
1370
1535
  }
@@ -1374,35 +1539,49 @@ function applyChanges(changes) {
1374
1539
  else copyFile(action.source, action.target);
1375
1540
  }
1376
1541
  }
1377
- function collectAllCurrentTargets() {
1542
+ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
1378
1543
  const targets = /* @__PURE__ */ new Set();
1379
1544
  let complete = true;
1545
+ const warnings = [];
1380
1546
  for (const adapter of Object.values(ADAPTERS)) {
1381
1547
  const detection = adapter.detect();
1382
1548
  if (!detection.installed) continue;
1383
- const ctx = makeContext(adapter, detection.configDir);
1384
- 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
+ }
1385
1555
  try {
1386
- for (const action of buildPlan(adapter, ctx)) targets.add(path17.resolve(action.target));
1387
- } catch {
1556
+ for (const action of buildPlan(adapter, ctx)) targets.add(path19.resolve(action.target));
1557
+ } catch (error) {
1388
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
+ );
1389
1562
  }
1390
1563
  }
1391
- return { targets, complete };
1564
+ return { targets, complete, warnings };
1392
1565
  }
1393
1566
  async function runInstall(opts) {
1394
1567
  p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
1395
1568
  const stackDir = stackRoot();
1396
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;
1397
1572
  const modelMap = loadModelMap();
1398
- ensureModelMapFile();
1573
+ if (useManifest) ensureModelMapFile();
1399
1574
  p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
1400
- const useManifest = opts.targetDir === void 0;
1401
- const current = useManifest ? collectAllCurrentTargets() : { targets: /* @__PURE__ */ new Set(), complete: false };
1575
+ const current = useManifest ? collectAllCurrentTargets(modePreference) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
1402
1576
  const canOrphan = useManifest && current.complete;
1403
1577
  const canonicalMcp = loadCanonicalMcp(stackDir);
1404
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
+ }
1405
1583
  let exitCode = 0;
1584
+ let successfulRuns = 0;
1406
1585
  for (const id of opts.runtimes) {
1407
1586
  const adapter = ADAPTERS[id];
1408
1587
  if (!adapter) {
@@ -1423,6 +1602,8 @@ async function runInstall(opts) {
1423
1602
  const ctx = {
1424
1603
  stackDir,
1425
1604
  configDir,
1605
+ mode: modePreference.mode,
1606
+ subagentConcurrency: modePreference.subagentConcurrency,
1426
1607
  engramBin,
1427
1608
  models,
1428
1609
  warnings: []
@@ -1432,8 +1613,8 @@ async function runInstall(opts) {
1432
1613
  let creates = diff.filter((d) => d.status === "create");
1433
1614
  let updates = diff.filter((d) => d.status === "update");
1434
1615
  let changes = [...creates, ...updates];
1435
- const prevManifest = canOrphan ? readManifest().runtimes[id] : void 0;
1436
- 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) : [];
1437
1618
  p.log.step(`${adapter.name} \u2192 ${configDir}`);
1438
1619
  p.log.info(
1439
1620
  `${diff.length} archivos gestionados: ${creates.length} nuevos, ${updates.length} modificados, ${diff.length - changes.length} sin cambios`
@@ -1448,13 +1629,17 @@ async function runInstall(opts) {
1448
1629
  }
1449
1630
  const writeManifest = () => {
1450
1631
  if (!useManifest) return;
1451
- const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path17.resolve(a.target)));
1452
- 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])];
1453
1637
  writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1454
1638
  };
1455
1639
  if (changes.length === 0 && orphans.length === 0) {
1456
1640
  writeManifest();
1457
1641
  p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
1642
+ successfulRuns++;
1458
1643
  continue;
1459
1644
  }
1460
1645
  if (!opts.yes && process.stdout.isTTY) {
@@ -1470,12 +1655,12 @@ async function runInstall(opts) {
1470
1655
  updates = diff.filter((d) => d.status === "update");
1471
1656
  changes = [...creates, ...updates];
1472
1657
  }
1473
- 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;
1474
1659
  if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1475
1660
  applyChanges(changes);
1476
- const pruneRoot = useManifest ? HOME : path17.dirname(configDir);
1661
+ const pruneRoot = useManifest ? HOME : path19.dirname(configDir);
1477
1662
  for (const orphan of orphans) {
1478
- fs11.rmSync(orphan, { force: true });
1663
+ fs14.rmSync(orphan, { force: true });
1479
1664
  pruneEmptyDirs(orphan, pruneRoot);
1480
1665
  }
1481
1666
  const verifyCtx = { ...ctx, warnings: [] };
@@ -1487,15 +1672,19 @@ async function runInstall(opts) {
1487
1672
  } else {
1488
1673
  writeManifest();
1489
1674
  p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
1675
+ successfulRuns++;
1490
1676
  }
1491
1677
  }
1678
+ if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
1679
+ saveInstallModePreference(installModePreferenceFile(), modePreference);
1680
+ }
1492
1681
  p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
1493
1682
  return exitCode;
1494
1683
  }
1495
1684
 
1496
1685
  // src/uninstall.ts
1497
- import fs12 from "fs";
1498
- import path18 from "path";
1686
+ import fs15 from "fs";
1687
+ import path20 from "path";
1499
1688
  import * as p2 from "@clack/prompts";
1500
1689
  async function runUninstall(opts) {
1501
1690
  p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
@@ -1525,7 +1714,7 @@ async function runUninstall(opts) {
1525
1714
  if (!detection.installed) continue;
1526
1715
  const keepCtx = makeContext(keep, detection.configDir);
1527
1716
  if (!keepCtx) continue;
1528
- 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));
1529
1718
  }
1530
1719
  for (const id of opts.runtimes) {
1531
1720
  const adapter = ADAPTERS[id];
@@ -1543,15 +1732,15 @@ async function runUninstall(opts) {
1543
1732
  if (!ctx) continue;
1544
1733
  ctx.preserveEngram = !removeEngram;
1545
1734
  const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
1546
- const mergedTargets = new Set(unmerge.map((a) => path18.resolve(a.target)));
1735
+ const mergedTargets = new Set(unmerge.map((a) => path20.resolve(a.target)));
1547
1736
  const usingRealConfig = opts.targetDir === void 0;
1548
1737
  const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
1549
- const pruneRoot = usingRealConfig ? HOME : path18.dirname(configDir);
1738
+ const pruneRoot = usingRealConfig ? HOME : path20.dirname(configDir);
1550
1739
  const planTargets = [
1551
- .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path18.resolve(a.target)), ...prevOwned.map((t) => path18.resolve(t))])
1552
- ].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));
1553
1742
  const deleteTargets = planTargets.filter(
1554
- (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)
1555
1744
  );
1556
1745
  const sharedKept = planTargets.length - deleteTargets.length;
1557
1746
  p2.log.step(`${adapter.name} \u2192 ${configDir}`);
@@ -1559,18 +1748,18 @@ async function runUninstall(opts) {
1559
1748
  if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
1560
1749
  if (opts.dryRun) continue;
1561
1750
  const backup = createBackup(
1562
- [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs12.existsSync(t))],
1751
+ [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs15.existsSync(t))],
1563
1752
  `uninstall-${id}`
1564
1753
  );
1565
1754
  if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1566
1755
  for (const target of deleteTargets) {
1567
- fs12.rmSync(target, { force: true });
1756
+ fs15.rmSync(target, { force: true });
1568
1757
  pruneEmptyDirs(target, pruneRoot);
1569
1758
  }
1570
1759
  for (const action of unmerge) {
1571
1760
  if (action.kind !== "write") continue;
1572
1761
  if (action.content.trim() === "") {
1573
- fs12.rmSync(action.target, { force: true });
1762
+ fs15.rmSync(action.target, { force: true });
1574
1763
  } else {
1575
1764
  writeText(action.target, action.content);
1576
1765
  }
@@ -1583,8 +1772,8 @@ async function runUninstall(opts) {
1583
1772
  }
1584
1773
 
1585
1774
  // src/doctor.ts
1586
- import path19 from "path";
1587
- import fs13 from "fs";
1775
+ import path21 from "path";
1776
+ import fs16 from "fs";
1588
1777
  import * as p3 from "@clack/prompts";
1589
1778
  function engramVersion(bin) {
1590
1779
  const out = runDetectedBin(bin, ["--version"], 5e3);
@@ -1592,7 +1781,7 @@ function engramVersion(bin) {
1592
1781
  return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
1593
1782
  }
1594
1783
  function context7KeyConfigured(id, configDir) {
1595
- 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");
1596
1785
  const content = readTextIfExists(file);
1597
1786
  if (content === null) return null;
1598
1787
  const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
@@ -1615,22 +1804,28 @@ async function runDoctor() {
1615
1804
  p3.log.success(`Engram: ${version} (${engramBin})`);
1616
1805
  }
1617
1806
  }
1618
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path19.join(HOME, ".engram");
1619
- const engramDb = path19.join(engramDataDir, "engram.db");
1620
- if (fs13.existsSync(engramDb)) {
1621
- 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);
1622
1811
  p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
1623
1812
  }
1624
- 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').");
1625
1814
  const manifest = readManifest();
1626
- 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
+ }
1627
1822
  for (const adapter of Object.values(ADAPTERS)) {
1628
1823
  const detection = adapter.detect();
1629
1824
  if (!detection.installed) {
1630
1825
  p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
1631
1826
  continue;
1632
1827
  }
1633
- const ctx = makeContext(adapter, detection.configDir);
1828
+ const ctx = makeContext(adapter, detection.configDir, modePreference);
1634
1829
  if (!ctx) continue;
1635
1830
  let pending;
1636
1831
  try {
@@ -1654,10 +1849,10 @@ async function runDoctor() {
1654
1849
  p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
1655
1850
  problems++;
1656
1851
  }
1657
- 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"))) {
1658
1853
  p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
1659
1854
  }
1660
- 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"))) {
1661
1856
  p3.log.warn(
1662
1857
  "Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
1663
1858
  );
@@ -1671,15 +1866,15 @@ async function runDoctor() {
1671
1866
  }
1672
1867
 
1673
1868
  // src/update.ts
1674
- import fs16 from "fs";
1675
- import path22 from "path";
1869
+ import fs19 from "fs";
1870
+ import path24 from "path";
1676
1871
  import os3 from "os";
1677
1872
  import { execFileSync as execFileSync4 } from "child_process";
1678
1873
  import * as p4 from "@clack/prompts";
1679
1874
 
1680
1875
  // src/lib/github.ts
1681
- import fs14 from "fs";
1682
- import path20 from "path";
1876
+ import fs17 from "fs";
1877
+ import path22 from "path";
1683
1878
  import { execFileSync as execFileSync2 } from "child_process";
1684
1879
  import os2 from "os";
1685
1880
  import { Readable } from "stream";
@@ -1749,19 +1944,19 @@ async function latestGithubCommit(repo) {
1749
1944
  }
1750
1945
  }
1751
1946
  function validateExtractedTree(destDir) {
1752
- const resolved = path20.resolve(destDir);
1947
+ const resolved = path22.resolve(destDir);
1753
1948
  const walk = (dir) => {
1754
1949
  let entries;
1755
1950
  try {
1756
- entries = fs14.readdirSync(dir, { withFileTypes: true });
1951
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
1757
1952
  } catch {
1758
1953
  return false;
1759
1954
  }
1760
1955
  for (const entry of entries) {
1761
- const full = path20.join(dir, entry.name);
1956
+ const full = path22.join(dir, entry.name);
1762
1957
  let stat;
1763
1958
  try {
1764
- stat = fs14.lstatSync(full);
1959
+ stat = fs17.lstatSync(full);
1765
1960
  } catch {
1766
1961
  return false;
1767
1962
  }
@@ -1777,15 +1972,15 @@ function validateExtractedTree(destDir) {
1777
1972
  }
1778
1973
  function resolveTarBin() {
1779
1974
  if (process.platform !== "win32") return "tar";
1780
- const winTar = path20.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
1781
- 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";
1782
1977
  }
1783
1978
  async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1784
1979
  const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
1785
- 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`);
1786
1981
  const fail = (reason) => {
1787
1982
  try {
1788
- fs14.rmSync(destDir, { recursive: true, force: true });
1983
+ fs17.rmSync(destDir, { recursive: true, force: true });
1789
1984
  } catch {
1790
1985
  }
1791
1986
  return { ok: false, reason };
@@ -1804,10 +1999,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1804
1999
  if (!res.body) return fail("respuesta HTTP sin cuerpo");
1805
2000
  await pipeline(
1806
2001
  Readable.fromWeb(res.body),
1807
- fs14.createWriteStream(tmp)
2002
+ fs17.createWriteStream(tmp)
1808
2003
  );
1809
- fs14.rmSync(destDir, { recursive: true, force: true });
1810
- fs14.mkdirSync(destDir, { recursive: true });
2004
+ fs17.rmSync(destDir, { recursive: true, force: true });
2005
+ fs17.mkdirSync(destDir, { recursive: true });
1811
2006
  try {
1812
2007
  execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
1813
2008
  } catch (err) {
@@ -1815,35 +2010,35 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1815
2010
  const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
1816
2011
  return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
1817
2012
  }
1818
- const resolvedDest = path20.resolve(destDir);
1819
- const validateRoot = validateSubdir ? path20.resolve(resolvedDest, validateSubdir) : resolvedDest;
2013
+ const resolvedDest = path22.resolve(destDir);
2014
+ const validateRoot = validateSubdir ? path22.resolve(resolvedDest, validateSubdir) : resolvedDest;
1820
2015
  if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
1821
2016
  return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
1822
2017
  }
1823
- if (fs14.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
2018
+ if (fs17.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
1824
2019
  return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
1825
2020
  }
1826
- const validated = fs14.existsSync(validateRoot);
2021
+ const validated = fs17.existsSync(validateRoot);
1827
2022
  return { ok: true, validated };
1828
2023
  } catch (err) {
1829
2024
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
1830
2025
  return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
1831
2026
  } finally {
1832
2027
  try {
1833
- fs14.rmSync(tmp, { force: true });
2028
+ fs17.rmSync(tmp, { force: true });
1834
2029
  } catch {
1835
2030
  }
1836
2031
  }
1837
2032
  }
1838
2033
 
1839
2034
  // src/lib/skill-update.ts
1840
- import fs15 from "fs";
1841
- import path21 from "path";
2035
+ import fs18 from "fs";
2036
+ import path23 from "path";
1842
2037
  import { execFileSync as execFileSync3 } from "child_process";
1843
2038
  var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle"]);
1844
2039
  function sameTextContentNormalized(a, b) {
1845
- const ba = fs15.readFileSync(a);
1846
- const bb = fs15.readFileSync(b);
2040
+ const ba = fs18.readFileSync(a);
2041
+ const bb = fs18.readFileSync(b);
1847
2042
  if (ba.equals(bb)) return true;
1848
2043
  const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
1849
2044
  const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
@@ -1851,10 +2046,10 @@ function sameTextContentNormalized(a, b) {
1851
2046
  }
1852
2047
  function diffSkillDirs(upstreamDir, localDir) {
1853
2048
  const upstreamFiles = new Set(
1854
- listFilesRecursive(upstreamDir).map((f) => path21.relative(upstreamDir, f))
2049
+ listFilesRecursive(upstreamDir).map((f) => path23.relative(upstreamDir, f))
1855
2050
  );
1856
2051
  const localFiles = new Set(
1857
- listFilesRecursive(localDir).map((f) => path21.relative(localDir, f))
2052
+ listFilesRecursive(localDir).map((f) => path23.relative(localDir, f))
1858
2053
  );
1859
2054
  const added = [];
1860
2055
  const modified = [];
@@ -1862,7 +2057,7 @@ function diffSkillDirs(upstreamDir, localDir) {
1862
2057
  for (const rel of upstreamFiles) {
1863
2058
  if (!localFiles.has(rel)) {
1864
2059
  added.push(rel);
1865
- } else if (!sameTextContentNormalized(path21.join(upstreamDir, rel), path21.join(localDir, rel))) {
2060
+ } else if (!sameTextContentNormalized(path23.join(upstreamDir, rel), path23.join(localDir, rel))) {
1866
2061
  modified.push(rel);
1867
2062
  }
1868
2063
  }
@@ -1924,8 +2119,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1924
2119
  if (PROTECTED_SKILLS.has(name)) {
1925
2120
  throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
1926
2121
  }
1927
- const upstreamsFile = upstreamsFilePath ?? path21.join(path21.dirname(stackRoot()), "upstreams.json");
1928
- const raw = fs15.readFileSync(upstreamsFile, "utf8");
2122
+ const upstreamsFile = upstreamsFilePath ?? path23.join(path23.dirname(stackRoot()), "upstreams.json");
2123
+ const raw = fs18.readFileSync(upstreamsFile, "utf8");
1929
2124
  const data = JSON.parse(raw);
1930
2125
  const skillEntry = data?.skills?.[name];
1931
2126
  if (!skillEntry) {
@@ -1934,8 +2129,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1934
2129
  if (skillEntry.kind === "release") {
1935
2130
  throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
1936
2131
  }
1937
- const skillsRoot = localSkillsRoot ?? path21.join(stackRoot(), "skills");
1938
- const localSkillDir = path21.join(skillsRoot, name);
2132
+ const skillsRoot = localSkillsRoot ?? path23.join(stackRoot(), "skills");
2133
+ const localSkillDir = path23.join(skillsRoot, name);
1939
2134
  const localFiles = listFilesRecursive(localSkillDir);
1940
2135
  if (localFiles.length > 0) {
1941
2136
  createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
@@ -1944,24 +2139,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1944
2139
  try {
1945
2140
  const upstreamFiles = listFilesRecursive(upstreamSkillDir);
1946
2141
  for (const src of upstreamFiles) {
1947
- const st = fs15.lstatSync(src);
2142
+ const st = fs18.lstatSync(src);
1948
2143
  if (st.isSymbolicLink()) {
1949
2144
  throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
1950
2145
  }
1951
- const rel = path21.relative(upstreamSkillDir, src);
1952
- const dest = path21.join(stagingDir, rel);
1953
- ensureDir(path21.dirname(dest));
2146
+ const rel = path23.relative(upstreamSkillDir, src);
2147
+ const dest = path23.join(stagingDir, rel);
2148
+ ensureDir(path23.dirname(dest));
1954
2149
  copyFile(src, dest);
1955
2150
  }
1956
2151
  const oldDir = `${localSkillDir}.old-${process.pid}`;
1957
- if (fs15.existsSync(localSkillDir)) {
1958
- fs15.renameSync(localSkillDir, oldDir);
2152
+ if (fs18.existsSync(localSkillDir)) {
2153
+ fs18.renameSync(localSkillDir, oldDir);
1959
2154
  }
1960
- fs15.renameSync(stagingDir, localSkillDir);
1961
- fs15.rmSync(oldDir, { recursive: true, force: true });
2155
+ fs18.renameSync(stagingDir, localSkillDir);
2156
+ fs18.rmSync(oldDir, { recursive: true, force: true });
1962
2157
  } catch (err) {
1963
2158
  try {
1964
- fs15.rmSync(stagingDir, { recursive: true, force: true });
2159
+ fs18.rmSync(stagingDir, { recursive: true, force: true });
1965
2160
  } catch {
1966
2161
  }
1967
2162
  throw err;
@@ -1975,8 +2170,8 @@ function rateLimitHint(prefix) {
1975
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.`;
1976
2171
  }
1977
2172
  function loadUpstreams() {
1978
- const file = path22.join(path22.dirname(stackRoot()), "upstreams.json");
1979
- 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"));
1980
2175
  }
1981
2176
  function skillsToScan(maintainer, upstreams) {
1982
2177
  return maintainer ? Object.keys(upstreams.skills) : [];
@@ -2088,8 +2283,8 @@ function isEngramRunning() {
2088
2283
  return null;
2089
2284
  }
2090
2285
  }
2091
- function isGitClone(projectRoot = path22.dirname(stackRoot())) {
2092
- return fs16.existsSync(path22.join(projectRoot, ".git"));
2286
+ function isGitClone(projectRoot = path24.dirname(stackRoot())) {
2287
+ return fs19.existsSync(path24.join(projectRoot, ".git"));
2093
2288
  }
2094
2289
  var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
2095
2290
  function resolvePnpm() {
@@ -2099,12 +2294,12 @@ function resolvePnpm() {
2099
2294
  }
2100
2295
  function cleanupTmp(dir) {
2101
2296
  try {
2102
- fs16.rmSync(dir, { recursive: true, force: true });
2297
+ fs19.rmSync(dir, { recursive: true, force: true });
2103
2298
  } catch {
2104
2299
  }
2105
2300
  }
2106
2301
  function updateStackGitClone() {
2107
- const projectRoot = path22.dirname(stackRoot());
2302
+ const projectRoot = path24.dirname(stackRoot());
2108
2303
  const git = lookPath("git");
2109
2304
  if (!git) throw new Error("git no encontrado en PATH.");
2110
2305
  const pnpm = resolvePnpm();
@@ -2121,7 +2316,7 @@ function updateStackGlobal() {
2121
2316
  execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
2122
2317
  }
2123
2318
  async function downloadSkillToTemp(repo, sha, skillPath) {
2124
- const root = fs16.mkdtempSync(path22.join(os3.tmpdir(), "jorgex-skill-"));
2319
+ const root = fs19.mkdtempSync(path24.join(os3.tmpdir(), "jorgex-skill-"));
2125
2320
  try {
2126
2321
  const result = await downloadRepoTarball(repo, sha, root, skillPath);
2127
2322
  if (!result.ok) {
@@ -2129,15 +2324,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
2129
2324
  return { error: result.reason };
2130
2325
  }
2131
2326
  if (skillPath) {
2132
- const sub = path22.resolve(path22.join(root, skillPath));
2327
+ const sub = path24.resolve(path24.join(root, skillPath));
2133
2328
  if (!isContainedIn(sub, root)) {
2134
2329
  cleanupTmp(root);
2135
2330
  return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
2136
2331
  }
2137
- if (fs16.existsSync(sub)) return { dir: sub, root };
2332
+ if (fs19.existsSync(sub)) return { dir: sub, root };
2138
2333
  const lastSeg = skillPath.split("/").pop();
2139
- const sub2 = path22.resolve(path22.join(root, lastSeg));
2140
- if (isContainedIn(sub2, root) && fs16.existsSync(sub2)) {
2334
+ const sub2 = path24.resolve(path24.join(root, lastSeg));
2335
+ if (isContainedIn(sub2, root) && fs19.existsSync(sub2)) {
2141
2336
  if (!validateExtractedTree(sub2)) {
2142
2337
  cleanupTmp(root);
2143
2338
  return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
@@ -2156,11 +2351,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
2156
2351
  function pruneEngramDbBackups() {
2157
2352
  try {
2158
2353
  const dir = dataDir();
2159
- if (!fs16.existsSync(dir)) return;
2160
- 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);
2161
2356
  for (const old of backups.slice(3)) {
2162
2357
  try {
2163
- fs16.rmSync(path22.join(dir, old.name));
2358
+ fs19.rmSync(path24.join(dir, old.name));
2164
2359
  } catch {
2165
2360
  }
2166
2361
  }
@@ -2168,18 +2363,18 @@ function pruneEngramDbBackups() {
2168
2363
  }
2169
2364
  }
2170
2365
  function rotateLockedBinary(binPath, sweepRoot = HOME) {
2171
- if (!fs16.existsSync(binPath)) return null;
2172
- const dir = path22.dirname(binPath);
2173
- const base = path22.basename(binPath);
2366
+ if (!fs19.existsSync(binPath)) return null;
2367
+ const dir = path24.dirname(binPath);
2368
+ const base = path24.basename(binPath);
2174
2369
  const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
2175
2370
  const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
2176
- const resolvedDir = path22.resolve(dir);
2177
- if (resolvedDir === path22.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
2371
+ const resolvedDir = path24.resolve(dir);
2372
+ if (resolvedDir === path24.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
2178
2373
  try {
2179
- for (const entry of fs16.readdirSync(dir)) {
2374
+ for (const entry of fs19.readdirSync(dir)) {
2180
2375
  if (oldPattern.test(entry)) {
2181
2376
  try {
2182
- fs16.rmSync(path22.join(dir, entry), { force: true });
2377
+ fs19.rmSync(path24.join(dir, entry), { force: true });
2183
2378
  } catch {
2184
2379
  }
2185
2380
  }
@@ -2187,8 +2382,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
2187
2382
  } catch {
2188
2383
  }
2189
2384
  }
2190
- const rotated = path22.join(dir, `${base}.old-${Date.now()}`);
2191
- fs16.renameSync(binPath, rotated);
2385
+ const rotated = path24.join(dir, `${base}.old-${Date.now()}`);
2386
+ fs19.renameSync(binPath, rotated);
2192
2387
  return rotated;
2193
2388
  }
2194
2389
  async function updateEngram(engramRepo, latestVersion) {
@@ -2197,19 +2392,19 @@ async function updateEngram(engramRepo, latestVersion) {
2197
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)."
2198
2393
  );
2199
2394
  }
2200
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path22.join(HOME, ".engram");
2201
- const engramDb = path22.join(engramDataDir, "engram.db");
2202
- 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)) {
2203
2398
  const doBackup = await p4.confirm({
2204
2399
  message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
2205
2400
  initialValue: true
2206
2401
  });
2207
2402
  if (!p4.isCancel(doBackup) && doBackup) {
2208
2403
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2209
- const dest = path22.join(dataDir(), `engram-db-backup-${ts}.db`);
2404
+ const dest = path24.join(dataDir(), `engram-db-backup-${ts}.db`);
2210
2405
  try {
2211
- if (!fs16.existsSync(dataDir())) fs16.mkdirSync(dataDir(), { recursive: true });
2212
- fs16.copyFileSync(engramDb, dest);
2406
+ if (!fs19.existsSync(dataDir())) fs19.mkdirSync(dataDir(), { recursive: true });
2407
+ fs19.copyFileSync(engramDb, dest);
2213
2408
  p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
2214
2409
  pruneEngramDbBackups();
2215
2410
  } catch (err) {
@@ -2259,10 +2454,10 @@ async function updateEngram(engramRepo, latestVersion) {
2259
2454
  ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
2260
2455
  { stdio: "inherit" }
2261
2456
  );
2262
- const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2457
+ const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
2263
2458
  if (rollbackOk.action === "restore") {
2264
2459
  try {
2265
- fs16.renameSync(rotated, bin);
2460
+ fs19.renameSync(rotated, bin);
2266
2461
  p4.log.warn(rollbackOk.messages.onRestore);
2267
2462
  } catch {
2268
2463
  p4.log.warn(rollbackOk.messages.onRenameFail);
@@ -2270,10 +2465,10 @@ async function updateEngram(engramRepo, latestVersion) {
2270
2465
  }
2271
2466
  return true;
2272
2467
  } catch (err) {
2273
- const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2468
+ const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
2274
2469
  if (rollbackFail.action === "restore") {
2275
2470
  try {
2276
- fs16.renameSync(rotated, bin);
2471
+ fs19.renameSync(rotated, bin);
2277
2472
  p4.log.info(rollbackFail.messages.onRestore);
2278
2473
  } catch {
2279
2474
  p4.log.error(rollbackFail.messages.onRenameFail);
@@ -2531,7 +2726,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
2531
2726
  continue;
2532
2727
  }
2533
2728
  const { dir: tmpDir, root: tmpRoot } = tmpResult;
2534
- const localSkillDir = path22.join(stackRoot(), "skills", skillInfo.name);
2729
+ const localSkillDir = path24.join(stackRoot(), "skills", skillInfo.name);
2535
2730
  const diff = renderSkillDiff(tmpDir, localSkillDir);
2536
2731
  if (diff) {
2537
2732
  p4.log.info(`Diff de ${skillInfo.name}:
@@ -2590,7 +2785,7 @@ ${diff}`);
2590
2785
  }
2591
2786
 
2592
2787
  // src/models-picker.ts
2593
- import path23 from "path";
2788
+ import path25 from "path";
2594
2789
  import * as p5 from "@clack/prompts";
2595
2790
  var TIERS = ["strong", "standard", "cheap"];
2596
2791
  var EFFORTS = ["low", "medium", "high", "xhigh"];
@@ -2606,7 +2801,7 @@ function opencodeLiveModels(binPath) {
2606
2801
  }
2607
2802
  function agentsByTier() {
2608
2803
  const grouped = { strong: [], standard: [], cheap: [] };
2609
- for (const agent of loadCanonicalAgents(path23.join(stackRoot(), "agents"))) {
2804
+ for (const agent of loadCanonicalAgents(path25.join(stackRoot(), "agents"))) {
2610
2805
  if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
2611
2806
  }
2612
2807
  return grouped;
@@ -2765,16 +2960,16 @@ function cancelled() {
2765
2960
  }
2766
2961
 
2767
2962
  // src/lib/release.ts
2768
- import fs17 from "fs";
2769
- import path24 from "path";
2963
+ import fs20 from "fs";
2964
+ import path26 from "path";
2770
2965
  import { execFileSync as execFileSync5 } from "child_process";
2771
2966
  import { fileURLToPath as fileURLToPath2 } from "url";
2772
2967
  function findPackageJson() {
2773
- let dir = path24.dirname(fileURLToPath2(import.meta.url));
2968
+ let dir = path26.dirname(fileURLToPath2(import.meta.url));
2774
2969
  for (let i = 0; i < 6; i++) {
2775
- const candidate = path24.join(dir, "package.json");
2776
- if (fs17.existsSync(candidate)) return candidate;
2777
- dir = path24.dirname(dir);
2970
+ const candidate = path26.join(dir, "package.json");
2971
+ if (fs20.existsSync(candidate)) return candidate;
2972
+ dir = path26.dirname(dir);
2778
2973
  }
2779
2974
  throw new Error("No se encontr\xF3 package.json cerca del CLI.");
2780
2975
  }
@@ -2783,7 +2978,7 @@ function readPackageVersion() {
2783
2978
  }
2784
2979
  function readPackageMetadata() {
2785
2980
  const packageJson = findPackageJson();
2786
- const raw = fs17.readFileSync(packageJson, "utf8");
2981
+ const raw = fs20.readFileSync(packageJson, "utf8");
2787
2982
  const parsed = JSON.parse(raw);
2788
2983
  const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
2789
2984
  const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
@@ -2801,6 +2996,8 @@ function parseFlags(args) {
2801
2996
  agents: [],
2802
2997
  dryRun: false,
2803
2998
  yes: false,
2999
+ mode: void 0,
3000
+ subagentConcurrency: void 0,
2804
3001
  help: false,
2805
3002
  version: false,
2806
3003
  list: false,
@@ -2808,12 +3005,33 @@ function parseFlags(args) {
2808
3005
  removeEngram: false,
2809
3006
  positional: []
2810
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
+ };
2811
3013
  for (let i = 0; i < args.length; i++) {
2812
3014
  const arg = args[i];
2813
- if (arg === "--agents" || arg === "-a") flags.agents = (args[++i] ?? "").split(",").filter(Boolean);
2814
- else if (arg.startsWith("--agents=")) flags.agents = arg.slice(9).split(",").filter(Boolean);
2815
- else if (arg === "--target-dir") flags.targetDir = args[++i];
2816
- 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);
2817
3035
  else if (arg === "--dry-run") flags.dryRun = true;
2818
3036
  else if (arg === "--yes" || arg === "-y") flags.yes = true;
2819
3037
  else if (arg === "--help" || arg === "-h") flags.help = true;
@@ -2825,6 +3043,67 @@ function parseFlags(args) {
2825
3043
  }
2826
3044
  return flags;
2827
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
+ }
2828
3107
  function parseCliArgs(argv) {
2829
3108
  const [first, ...rest] = argv;
2830
3109
  const isCommand = COMMANDS.includes(first ?? "install");
@@ -2873,6 +3152,8 @@ Comandos:
2873
3152
 
2874
3153
  Opciones:
2875
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
2876
3157
  --target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
2877
3158
  --dry-run Muestra el plan sin escribir nada
2878
3159
  --yes, -y No interactivo
@@ -2900,6 +3181,8 @@ async function main() {
2900
3181
  switch (command) {
2901
3182
  case "install":
2902
3183
  case "sync": {
3184
+ const mode = await resolveInstallMode(flags);
3185
+ if (mode === null) return;
2903
3186
  const runtimes = await resolveRuntimes(flags);
2904
3187
  if (runtimes === null) return;
2905
3188
  if (runtimes.length === 0) {
@@ -2907,7 +3190,7 @@ async function main() {
2907
3190
  process.exitCode = 1;
2908
3191
  return;
2909
3192
  }
2910
- 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 });
2911
3194
  return;
2912
3195
  }
2913
3196
  case "uninstall": {
@@ -2942,23 +3225,46 @@ async function main() {
2942
3225
  }
2943
3226
  const runtimes = await resolveRuntimes(flags);
2944
3227
  if (runtimes === null) return;
2945
- if (runtimes.length > 0) {
2946
- 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
+ });
2947
3243
  if (code !== 0) {
2948
3244
  process.exitCode = code;
2949
3245
  return;
2950
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.");
2951
3249
  }
2952
3250
  const result = await runInteractiveUpdate(VERSION, flags.yes, flags.dryRun);
2953
3251
  process.exitCode = result.exitCode;
2954
- 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) {
2955
3255
  const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
2956
3256
  if (!p6.isCancel(apply) && apply) {
2957
- 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
+ });
2958
3264
  } else {
2959
3265
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2960
3266
  }
2961
- } 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)) {
2962
3268
  console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
2963
3269
  }
2964
3270
  return;
@@ -2976,7 +3282,17 @@ async function main() {
2976
3282
  if (code === 0 && !flags.yes && process.stdout.isTTY) {
2977
3283
  const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
2978
3284
  if (!p6.isCancel(apply) && apply) {
2979
- 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 });
2980
3296
  } else {
2981
3297
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2982
3298
  }