ostacky 0.8.7 → 0.9.0

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
@@ -186,7 +186,7 @@ var require_picocolors = __commonJS(function(exports, module) {
186
186
  });
187
187
 
188
188
  // src/security.ts
189
- import { createHash } from "crypto";
189
+ import { createHash } from "node:crypto";
190
190
  function validateFilePath(filePath) {
191
191
  if (filePath.includes("..") || filePath.startsWith("/") || filePath.startsWith("\\") || /^[a-zA-Z]:/.test(filePath)) {
192
192
  throw new Error(`Ruta de archivo inválida: "${filePath}"`);
@@ -621,31 +621,31 @@ var init_fs = __esm(() => {
621
621
  var manifest_default;
622
622
  var init_manifest = __esm(() => {
623
623
  manifest_default = {
624
- version: "0.8.7",
624
+ version: "0.9.0",
625
625
  repo: "JaimeHoracio/Ostacky",
626
- tag: "v0.8.7",
626
+ tag: "v0.9.0",
627
627
  agents: [
628
628
  {
629
629
  name: "ostacky",
630
630
  file: "assets/agents/ostacky.md",
631
- description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.8.7: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
632
- version: "0.8.7",
633
- sha256: "191a317be68bde517f6d3877ec3a016720617d7382944fff0d08d31b985a6644"
631
+ description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.9.0: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
632
+ version: "0.9.0",
633
+ sha256: "ecd046b5eb1d604f5e88bc7e5c7c2bda7d6fe63786fad085a038253202c59eeb"
634
634
  }
635
635
  ],
636
636
  commands: [
637
637
  {
638
638
  name: "install-stack",
639
639
  file: "assets/commands/install-stack.md",
640
- description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.8.7: controller plugin como alma hard-gate, Context7 removido del stack.",
641
- version: "0.8.7",
642
- sha256: "315d8fcdf2a945cef863bb4576838f95d7eb4a0acca38c12575dc80ecc4724dc"
640
+ description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.9.0: controller plugin como alma hard-gate, Context7 removido del stack.",
641
+ version: "0.9.0",
642
+ sha256: "5cdceea5d8c4ef8eb2d1dcc441ad05f87c8c666dcf9810eb0766c9446840b57c"
643
643
  },
644
644
  {
645
645
  name: "opsx-sync",
646
646
  file: "assets/commands/opsx-sync.md",
647
647
  description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
648
- version: "0.8.7",
648
+ version: "0.9.0",
649
649
  sha256: "fe0158478f2ca63b315037a85fc1632b77868532e319af6c7384285441767d64"
650
650
  }
651
651
  ],
@@ -653,15 +653,15 @@ var init_manifest = __esm(() => {
653
653
  {
654
654
  name: "ostacky-controller",
655
655
  file: "assets/mcp/ostacky-controller/",
656
- description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.8.7: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
657
- version: "0.8.7",
658
- sha256: "9de463763ea6538c03b9307e7e36dcd2ae854dcc460bd25981e75f4f70df0e66"
656
+ description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.9.0: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
657
+ version: "0.9.0",
658
+ sha256: "29857bfbe1b67a0cf1e7ee6215b991fe86ab3f2765feae8f099af5fac610afc1"
659
659
  },
660
660
  {
661
661
  name: "openspec",
662
662
  file: "assets/mcp/openspec/",
663
663
  description: "MCP server local para OpenSpec - proposal, apply, archive, sync de cambios",
664
- version: "0.8.7",
664
+ version: "0.9.0",
665
665
  sha256: "fa4be28bfc1e75db8f1a037e2580f04a60066c80e8e5934e90e1041020d04609"
666
666
  }
667
667
  ],
@@ -670,105 +670,105 @@ var init_manifest = __esm(() => {
670
670
  name: "brainstorming",
671
671
  file: "assets/skills/brainstorming/SKILL.md",
672
672
  description: "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a implementación directa o openspec-propose) y open-exploration (exploración libre)",
673
- version: "0.8.7",
673
+ version: "0.9.0",
674
674
  sha256: "f1013a86a8f27914d85a716e23c7fd32db1786d93615e8b0834b13d184308549"
675
675
  },
676
676
  {
677
677
  name: "execution-mode-evaluation",
678
678
  file: "assets/skills/execution-mode-evaluation/SKILL.md",
679
679
  description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
680
- version: "0.8.7",
680
+ version: "0.9.0",
681
681
  sha256: "2158a6864c0912e868a9d720db7fd79441e6cc298e3320833d2527c9727c852e"
682
682
  },
683
683
  {
684
684
  name: "tdd",
685
685
  file: "assets/skills/tdd/SKILL.md",
686
686
  description: "Skill de test-driven development (Superpowers)",
687
- version: "0.8.7",
688
- sha256: "aa412298980b7826165c211145c1b8e9135f68f36247bb3cb96d0a0eae274486"
687
+ version: "0.9.0",
688
+ sha256: "f0ebfbbc43a8868d31a3fd2df37ae7f60fe8add09c6475862395a68269e5ff7f"
689
689
  },
690
690
  {
691
691
  name: "subagent-driven-development",
692
692
  file: "assets/skills/subagent-driven-development/SKILL.md",
693
693
  description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
694
- version: "0.8.7",
694
+ version: "0.9.0",
695
695
  sha256: "1a42d714a9a13faf05f0bf7b580e8d837c2e77a5633839542ccce603023419e8"
696
696
  },
697
697
  {
698
698
  name: "dispatching-parallel-agents",
699
699
  file: "assets/skills/dispatching-parallel-agents/SKILL.md",
700
700
  description: "Skill de dispatch paralelo de agentes (Superpowers)",
701
- version: "0.8.7",
702
- sha256: "3c8a66d51ae2e719e877d02c3dac32bd804722c3bb9227c258ecae78252a20ab"
701
+ version: "0.9.0",
702
+ sha256: "7e7b712ef742d5631e302e6a71583e2bbbd33094da9281abfb84b43505fc3ef1"
703
703
  },
704
704
  {
705
705
  name: "review",
706
706
  file: "assets/skills/review/SKILL.md",
707
707
  description: "Skill de revisión de código (Superpowers)",
708
- version: "0.8.7",
708
+ version: "0.9.0",
709
709
  sha256: "b19650ed4d1d4d9857a4dd5b7e08e91328a1a1fc9c45bcc2fb600fa9d0213279"
710
710
  },
711
711
  {
712
712
  name: "receiving-code-review",
713
713
  file: "assets/skills/receiving-code-review/SKILL.md",
714
714
  description: "Skill de recibir y procesar feedback de code review",
715
- version: "0.8.7",
715
+ version: "0.9.0",
716
716
  sha256: "d761e884e71d8d3476ac734d287cae403a4024506a7d12e7361a448217c0a831"
717
717
  },
718
718
  {
719
719
  name: "openspec-propose",
720
720
  file: "assets/skills/openspec-propose/SKILL.md",
721
721
  description: "Skill de generación de proposal (OpenSpec)",
722
- version: "0.8.7",
723
- sha256: "bb59100b9fd3c9f9a1ec6509fa975ec55bf7fc737df4fe73bb3e9bbc217b83f7"
722
+ version: "0.9.0",
723
+ sha256: "6a517f750f24c008208156a2d653e97ed30b1e53878aeac5ce394e573db0678f"
724
724
  },
725
725
  {
726
726
  name: "openspec-apply-change",
727
727
  file: "assets/skills/openspec-apply-change/SKILL.md",
728
728
  description: "Skill de aplicación de change (OpenSpec)",
729
- version: "0.8.7",
729
+ version: "0.9.0",
730
730
  sha256: "dfc823bf89fc7505e91ab6dee9c1f004be38a410bd3d88fb67b211b1b6cbb1d0"
731
731
  },
732
732
  {
733
733
  name: "openspec-archive-change",
734
734
  file: "assets/skills/openspec-archive-change/SKILL.md",
735
735
  description: "Skill de archivo de change (OpenSpec)",
736
- version: "0.8.7",
736
+ version: "0.9.0",
737
737
  sha256: "16e4b561de7747283663fed602e506abf502452c49a8d4b86d7a5539c40f0195"
738
738
  },
739
739
  {
740
740
  name: "openspec-explore",
741
741
  file: "assets/skills/openspec-explore/SKILL.md",
742
742
  description: "Modo explore para OpenSpec — thinking partner para explorar ideas, investigar problemas y clarificar requisitos antes/durante un cambio",
743
- version: "0.8.7",
744
- sha256: "37ae4aaf17ea71a395bab6dc4d9d61b9a49d7910f6692535ae8553244c46880b"
743
+ version: "0.9.0",
744
+ sha256: "9eafc3a8c693b8d9916867ef764d49e5222da717fde501c957bb3f6c312d2415"
745
745
  },
746
746
  {
747
747
  name: "using-git-worktrees",
748
748
  file: "assets/skills/using-git-worktrees/SKILL.md",
749
749
  description: "Skill de uso de git worktrees para aislamiento de trabajo",
750
- version: "0.8.7",
750
+ version: "0.9.0",
751
751
  sha256: "93341bc1b7c053618a8b6dc07e3615b77990d7b549a0201f5835d95fef67ce13"
752
752
  },
753
753
  {
754
754
  name: "using-superpowers",
755
755
  file: "assets/skills/using-superpowers/SKILL.md",
756
756
  description: "Skill de orquestación de Superpowers skills",
757
- version: "0.8.7",
757
+ version: "0.9.0",
758
758
  sha256: "7e54536f96d2a379185a10bfc1e970850caa2561b6d8aca7080f0defea0381a4"
759
759
  },
760
760
  {
761
761
  name: "writing-skills",
762
762
  file: "assets/skills/writing-skills/SKILL.md",
763
763
  description: "Skill de creación y edición de skills",
764
- version: "0.8.7",
764
+ version: "0.9.0",
765
765
  sha256: "3d76b906cee518a2b809febb70db95697a35b9f368986bb504b4120c3bfb437a"
766
766
  },
767
767
  {
768
768
  name: "graceful-degradation",
769
769
  file: "assets/skills/graceful-degradation/SKILL.md",
770
770
  description: "Skill de degradación graceful cuando múltiples tools están indisponibles",
771
- version: "0.8.7",
771
+ version: "0.9.0",
772
772
  sha256: "363449bd9e6f800a0a3168667cf78f767fb105d86e4a4adadcc649d418139303"
773
773
  }
774
774
  ]
@@ -1058,9 +1058,14 @@ function setMcpEntryAtProjectRoot(projectRoot, name, entry) {
1058
1058
  const config = readOpenCodeConfig(configPath);
1059
1059
  if (!config)
1060
1060
  throw new Error(`Error parseando ${configPath}`);
1061
- if (!config.mcp)
1061
+ if (!config.mcp || typeof config.mcp !== "object")
1062
1062
  config.mcp = {};
1063
- config.mcp[name] = entry;
1063
+ const mcp = config.mcp;
1064
+ if (!mcp.servers || typeof mcp.servers !== "object")
1065
+ mcp.servers = {};
1066
+ mcp.servers[name] = entry;
1067
+ if (name in mcp && name !== "servers")
1068
+ delete mcp[name];
1064
1069
  writeOpenCodeConfig(configPath, config);
1065
1070
  }
1066
1071
  function patchOpenCodeConfig(projectRoot = findProjectRoot()) {
@@ -1079,16 +1084,30 @@ function patchOpenCodeConfig(projectRoot = findProjectRoot()) {
1079
1084
  };
1080
1085
  }
1081
1086
  let changed = false;
1082
- if ("plugin" in config) {
1083
- delete config.plugin;
1084
- changed = true;
1087
+ if ("plugin" in config && Array.isArray(config.plugin)) {
1088
+ const entries = config.plugin;
1089
+ const kept = entries.filter((e) => !isSuperpowersPluginEntry(e));
1090
+ if (kept.length !== entries.length) {
1091
+ if (kept.length === 0)
1092
+ delete config.plugin;
1093
+ else
1094
+ config.plugin = kept;
1095
+ changed = true;
1096
+ }
1085
1097
  }
1086
1098
  if (changed) {
1087
1099
  writeOpenCodeConfig(configPath, config);
1088
- return { success: true, message: "Config actualizada (plugin legacy eliminado)" };
1100
+ return { success: true, message: "Config actualizada (entradas superpowers eliminadas)" };
1089
1101
  }
1090
1102
  return { success: true, message: "Config de OpenCode ya está limpia" };
1091
1103
  }
1104
+ function isSuperpowersPluginEntry(entry) {
1105
+ try {
1106
+ return JSON.stringify(entry).toLowerCase().includes("superpowers");
1107
+ } catch {
1108
+ return false;
1109
+ }
1110
+ }
1092
1111
  var init_config = __esm(() => {
1093
1112
  init_fs();
1094
1113
  });
@@ -1136,7 +1155,9 @@ function configureLocalTool(projectRoot, name, command) {
1136
1155
  setMcpEntryAtProjectRoot(projectRoot, name, {
1137
1156
  type: "local",
1138
1157
  command,
1139
- enabled: true
1158
+ disabled: false,
1159
+ timeout: { catalog: 30000, execution: 300000 },
1160
+ protocol: "legacy"
1140
1161
  });
1141
1162
  }
1142
1163
  function buildLocalMcpCommand(executable, args, platform = process.platform) {
@@ -1155,6 +1176,8 @@ function copyEngramPlugin(projectRoot) {
1155
1176
  function copyOstackyControllerPlugin(projectRoot) {
1156
1177
  const pluginSource = join7(PACKAGE_ROOT, "assets", "plugins", "ostacky-plugin.ts");
1157
1178
  const coreSource = join7(PACKAGE_ROOT, "assets", "plugins", "controller-core.ts");
1179
+ const securitySource = join7(PACKAGE_ROOT, "assets", "plugins", "security.ts");
1180
+ const tieredSource = join7(PACKAGE_ROOT, "assets", "plugins", "tiered.ts");
1158
1181
  const pluginsDir = join7(projectRoot, ".opencode", "plugins");
1159
1182
  if (!existsSync6(pluginSource)) {
1160
1183
  throw new Error(`Plugin bundleado de OstackyController no encontrado: ${pluginSource}`);
@@ -1163,6 +1186,10 @@ function copyOstackyControllerPlugin(projectRoot) {
1163
1186
  copyFileSync3(pluginSource, join7(pluginsDir, "ostacky-plugin.ts"));
1164
1187
  if (existsSync6(coreSource))
1165
1188
  copyFileSync3(coreSource, join7(pluginsDir, "controller-core.ts"));
1189
+ if (existsSync6(securitySource))
1190
+ copyFileSync3(securitySource, join7(pluginsDir, "security.ts"));
1191
+ if (existsSync6(tieredSource))
1192
+ copyFileSync3(tieredSource, join7(pluginsDir, "tiered.ts"));
1166
1193
  }
1167
1194
  function buildEngramDownloadUrl(tag, platform = process.platform, arch = process.arch) {
1168
1195
  const target = getEngramReleaseTarget(platform, arch);
@@ -1489,17 +1516,119 @@ var init_stack = __esm(() => {
1489
1516
  init_config();
1490
1517
  init_github();
1491
1518
  });
1519
+
1520
+ // src/doctor-v2.ts
1521
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1522
+ import { join as join10, delimiter, isAbsolute } from "path";
1523
+ function checkDoctorV2(projectRoot) {
1524
+ const lines = [];
1525
+ const cwd = projectRoot;
1526
+ const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
1527
+ const pluginPaths = [
1528
+ join10(opencodeDir, "plugins", "ostacky-plugin.ts"),
1529
+ join10(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
1530
+ join10(cwd, "assets", "plugins", "ostacky-plugin.ts")
1531
+ ];
1532
+ const found = pluginPaths.find((p) => existsSync8(p));
1533
+ if (found) {
1534
+ try {
1535
+ const src = readFileSync6(found, "utf-8");
1536
+ const isV2 = src.includes("Plugin.define") && src.includes("@opencode/plugin") && !src.includes("@opencode-ai/plugin");
1537
+ if (isV2)
1538
+ lines.push(`✅ controller: plugin V2 (${found})`);
1539
+ else
1540
+ lines.push(`⚠️ controller: plugin legacy V1 en ${found} — no corre en V2, reinstalá con ostacky`);
1541
+ } catch {
1542
+ lines.push(`⚠️ controller: no se pudo leer ${found}`);
1543
+ }
1544
+ }
1545
+ const configPath = findOpenCodeConfig(cwd);
1546
+ if (!configPath) {
1547
+ lines.push("ℹ️ config: no hay opencode.json ni opencode.jsonc");
1548
+ return lines;
1549
+ }
1550
+ let config = null;
1551
+ try {
1552
+ config = readOpenCodeConfig(configPath);
1553
+ } catch {
1554
+ lines.push(`⚠️ config: no se pudo parsear ${configPath}`);
1555
+ return lines;
1556
+ }
1557
+ if (!config) {
1558
+ lines.push(`⚠️ config: no se pudo parsear ${configPath}`);
1559
+ return lines;
1560
+ }
1561
+ const mcp = config.mcp ?? {};
1562
+ const servers = mcp.servers ?? {};
1563
+ const serverNames = Object.keys(servers);
1564
+ if (serverNames.length > 0)
1565
+ lines.push(`✅ mcp.servers: OK (${serverNames.length}: ${serverNames.join(", ")})`);
1566
+ for (const [key, value] of Object.entries(mcp)) {
1567
+ if (key === "servers" || key === "timeout")
1568
+ continue;
1569
+ if (value && typeof value === "object" && (("command" in value) || ("enabled" in value) || ("url" in value))) {
1570
+ lines.push(`⚠️ mcp.${key} legacy (forma V1) — mové a mcp.servers.${key} con disabled (install lo migra con restore ante fallo)`);
1571
+ }
1572
+ }
1573
+ const renames = {
1574
+ permission: "permissions[]",
1575
+ agent: "agents",
1576
+ command: "commands",
1577
+ reference: "references",
1578
+ provider: "providers",
1579
+ snapshot: "snapshots",
1580
+ attachment: "media"
1581
+ };
1582
+ for (const [oldKey, newKey] of Object.entries(renames)) {
1583
+ if (oldKey in config) {
1584
+ lines.push(`⚠️ ${oldKey} legacy ajeno — V2 usa ${newKey}; se normaliza en memoria, migrá cuando quieras (doctor no lo modifica)`);
1585
+ }
1586
+ }
1587
+ for (const [name, entry] of Object.entries(servers)) {
1588
+ if (!entry || typeof entry !== "object")
1589
+ continue;
1590
+ const cmd = entry.command;
1591
+ if (entry.type === "local" && Array.isArray(cmd) && typeof cmd[0] === "string") {
1592
+ const bin = cmd[0];
1593
+ const ok = isAbsolute(bin) ? existsSync8(bin) : lookupOnPath(bin) !== null;
1594
+ if (!ok)
1595
+ lines.push(`⚠️ mcp.servers.${name} command no existe (${bin}) — ¿proyecto movido? re-corré install`);
1596
+ }
1597
+ }
1598
+ return lines;
1599
+ }
1600
+ function lookupOnPath(name) {
1601
+ try {
1602
+ const pathEnv = process.env.PATH ?? "";
1603
+ const suffix = process.platform === "win32" ? ".exe" : "";
1604
+ for (const dir of pathEnv.split(delimiter)) {
1605
+ if (!dir)
1606
+ continue;
1607
+ try {
1608
+ if (existsSync8(join10(dir, name + suffix)))
1609
+ return join10(dir, name + suffix);
1610
+ if (suffix && existsSync8(join10(dir, name)))
1611
+ return join10(dir, name);
1612
+ } catch {}
1613
+ }
1614
+ } catch {}
1615
+ return null;
1616
+ }
1617
+ var init_doctor_v2 = __esm(() => {
1618
+ init_config();
1619
+ init_fs();
1620
+ });
1492
1621
  // package.json
1493
1622
  var package_default = {
1494
1623
  name: "ostacky",
1495
- version: "0.8.7",
1624
+ version: "0.9.0",
1496
1625
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1497
1626
  type: "module",
1498
1627
  bin: {
1499
1628
  ostacky: "./dist/cli.js"
1500
1629
  },
1501
1630
  scripts: {
1502
- prebuild: "bun run scripts/sync-version.ts",
1631
+ prebuild: "bun run scripts/sync-version.ts && bun run scripts/sync-controller-core.ts",
1503
1632
  dev: "bun run src/cli.ts",
1504
1633
  build: "bun build src/cli.ts --target=node --format=esm --outfile dist/cli.js && bun scripts/add-shebang.ts",
1505
1634
  start: "node dist/cli.js",
@@ -1510,7 +1639,7 @@ var package_default = {
1510
1639
  "hash:update": "bun run scripts/update-manifest-hashes.ts",
1511
1640
  "manifest:prepare": "bun run scripts/update-manifest-hashes.ts",
1512
1641
  "manifest:check": "bun run scripts/update-manifest-hashes.ts --check",
1513
- prepublishOnly: "bun run manifest:prepare && bun run build"
1642
+ prepublishOnly: "bun run scripts/sync-controller-core.ts && bun run manifest:prepare && bun run build"
1514
1643
  },
1515
1644
  files: [
1516
1645
  "dist/",
@@ -2519,7 +2648,9 @@ function createMcpConfigEntry(name, nodeExecutable, serverPath, statePath) {
2519
2648
  const entry = {
2520
2649
  type: "local",
2521
2650
  command: [nodeExecutable, serverPath],
2522
- enabled: true
2651
+ disabled: false,
2652
+ timeout: { catalog: 30000, execution: 300000 },
2653
+ protocol: "legacy"
2523
2654
  };
2524
2655
  if (name === "ostacky-controller" && statePath) {
2525
2656
  entry.environment = { OSTACKY_STATE_PATH: statePath };
@@ -2669,7 +2800,7 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2669
2800
  params: {
2670
2801
  protocolVersion: "2025-03-26",
2671
2802
  capabilities: {},
2672
- clientInfo: { name: "ostacky-installer", version: "0.8.7" }
2803
+ clientInfo: { name: "ostacky-installer", version: "0.9.0" }
2673
2804
  }
2674
2805
  });
2675
2806
  });
@@ -2950,8 +3081,17 @@ function uninstallMcpServer(name, paths) {
2950
3081
  const config = readOpenCodeConfig(configPath);
2951
3082
  if (config) {
2952
3083
  const mcp = config.mcp;
2953
- if (mcp && mcp[name]) {
2954
- delete mcp[name];
3084
+ if (mcp) {
3085
+ const servers = mcp.servers;
3086
+ if (servers && name in servers) {
3087
+ delete servers[name];
3088
+ if (Object.keys(servers).length === 0) {
3089
+ delete mcp.servers;
3090
+ }
3091
+ }
3092
+ if (name in mcp && name !== "servers") {
3093
+ delete mcp[name];
3094
+ }
2955
3095
  if (Object.keys(mcp).length === 0) {
2956
3096
  delete config.mcp;
2957
3097
  }
@@ -2978,7 +3118,15 @@ function uninstallAll(paths) {
2978
3118
  for (const name of Object.keys(lockfile.mcpServers ?? {})) {
2979
3119
  uninstallMcpServer(name, paths);
2980
3120
  }
2981
- const ostackyPlugins = ["ostacky-plugin.ts", "engram.ts", "ostacky-guard.ts", "ostacky-controller.ts"];
3121
+ const ostackyPlugins = [
3122
+ "ostacky-plugin.ts",
3123
+ "engram.ts",
3124
+ "ostacky-guard.ts",
3125
+ "ostacky-controller.ts",
3126
+ "controller-core.ts",
3127
+ "security.ts",
3128
+ "tiered.ts"
3129
+ ];
2982
3130
  for (const file of ostackyPlugins) {
2983
3131
  const fp = join6(paths.plugins, file);
2984
3132
  if (existsSync5(fp)) {
@@ -3785,6 +3933,23 @@ function isOpencodeInstalled() {
3785
3933
  return false;
3786
3934
  }
3787
3935
  }
3936
+ function parseOpencodeVersion(output) {
3937
+ const m = /v?(\d+)\.\d+\.\d+/.exec(output ?? "");
3938
+ if (!m)
3939
+ return null;
3940
+ const major = parseInt(m[1], 10);
3941
+ return Number.isNaN(major) ? null : major;
3942
+ }
3943
+ function getOpencodeVersion() {
3944
+ try {
3945
+ return execFileSync4("opencode", ["--version"], {
3946
+ encoding: "utf-8",
3947
+ stdio: ["pipe", "pipe", "pipe"]
3948
+ }).trim();
3949
+ } catch {
3950
+ return null;
3951
+ }
3952
+ }
3788
3953
  function getOpencodeInstallCommand(platform = process.platform, _arch = process.arch) {
3789
3954
  if (platform === "win32") {
3790
3955
  return {
@@ -3810,8 +3975,17 @@ function getOpencodeInstallCommand(platform = process.platform, _arch = process.
3810
3975
  };
3811
3976
  }
3812
3977
  async function ensureOpencodeInstalled() {
3813
- if (isOpencodeInstalled())
3978
+ if (isOpencodeInstalled()) {
3979
+ const raw = getOpencodeVersion();
3980
+ const major = raw ? parseOpencodeVersion(raw) : null;
3981
+ if (major !== null && major < 2) {
3982
+ v2.warn(`OpenCode V1 detectado (${raw}). Ostacky requiere OpenCode V2.`);
3983
+ ye(`El instalador V2 reemplaza el binario V1. Desinstalá V1 (package manager) e instalá V2, luego re-ejecutá.
3984
+ Docs: https://opencode.ai/v2/docs/migrate-v1`, "Actualización requerida");
3985
+ process.exit(1);
3986
+ }
3814
3987
  return;
3988
+ }
3815
3989
  const platform = process.platform;
3816
3990
  const info = getOpencodeInstallCommand(platform);
3817
3991
  v2.warn("OpenCode no detectado en este sistema.");
@@ -4228,8 +4402,8 @@ async function runUninstallMcpCommand(name, scope) {
4228
4402
 
4229
4403
  // src/cli.ts
4230
4404
  init_fs();
4231
- import { existsSync as existsSync8, statSync as statSync3, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "node:fs";
4232
- import { join as join10, dirname as dirname8 } from "node:path";
4405
+ import { existsSync as existsSync9, statSync as statSync3, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "node:fs";
4406
+ import { join as join11, dirname as dirname8 } from "node:path";
4233
4407
  var HELP = `
4234
4408
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4235
4409
 
@@ -4300,8 +4474,8 @@ var argvNoScope = withoutScopeArgs(process.argv);
4300
4474
  var [, , cmd, subcmd] = argvNoScope;
4301
4475
  async function runDoctorCommand() {
4302
4476
  const cwd = process.cwd();
4303
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4304
- const statePath = join10(opencodeDir, "ostacky-state.json");
4477
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4478
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4305
4479
  let hasError = false;
4306
4480
  let hasWarn = false;
4307
4481
  const check = (label, ok, warn = false) => {
@@ -4316,23 +4490,23 @@ async function runDoctorCommand() {
4316
4490
  }
4317
4491
  };
4318
4492
  const pluginPaths = [
4319
- join10(cwd, "assets", "plugins", "ostacky-plugin.ts"),
4320
- join10(opencodeDir, "plugins", "ostacky-plugin.ts"),
4321
- join10(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
4322
- join10(cwd, "assets", "plugins", "ostacky-controller.ts"),
4323
- join10(opencodeDir, "plugins", "ostacky-controller.ts"),
4324
- join10(cwd, ".opencode", "plugins", "ostacky-controller.ts")
4493
+ join11(cwd, "assets", "plugins", "ostacky-plugin.ts"),
4494
+ join11(opencodeDir, "plugins", "ostacky-plugin.ts"),
4495
+ join11(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
4496
+ join11(cwd, "assets", "plugins", "ostacky-controller.ts"),
4497
+ join11(opencodeDir, "plugins", "ostacky-controller.ts"),
4498
+ join11(cwd, ".opencode", "plugins", "ostacky-controller.ts")
4325
4499
  ];
4326
- const pluginActive = pluginPaths.some((p) => existsSync8(p));
4500
+ const pluginActive = pluginPaths.some((p) => existsSync9(p));
4327
4501
  try {
4328
- if (!existsSync8(statePath)) {
4502
+ if (!existsSync9(statePath)) {
4329
4503
  if (pluginActive)
4330
4504
  console.log(`✅ controller: plugin active (no state yet)`);
4331
4505
  else
4332
4506
  check("controller: state file missing", false, true);
4333
4507
  } else {
4334
4508
  const stat = statSync3(statePath);
4335
- const raw = readFileSync6(statePath, "utf-8");
4509
+ const raw = readFileSync7(statePath, "utf-8");
4336
4510
  const parsed = JSON.parse(raw);
4337
4511
  if (pluginActive) {
4338
4512
  console.log(`✅ controller: plugin active (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`);
@@ -4347,6 +4521,8 @@ async function runDoctorCommand() {
4347
4521
  console.log(`⚠️ degraded: confirmation not audited in controller (degradedEditsCount=${parsed.degradedEditsCount})`);
4348
4522
  if (parsed.codegraphBypassCount > 0)
4349
4523
  console.log(`⚠️ codegraphBypassCount=${parsed.codegraphBypassCount} (inefficient: codegraph bypass)`);
4524
+ if (parsed.engramBypassCount > 0)
4525
+ console.log(`⚠️ engramBypassCount=${parsed.engramBypassCount} (contradiction check bypassed — sin mem_search)`);
4350
4526
  if (parsed.stateOversizedCount > 0)
4351
4527
  console.log(`⚠️ stateOversizedCount=${parsed.stateOversizedCount} snapshots perdidos`);
4352
4528
  if (parsed.sensitiveAccess)
@@ -4394,15 +4570,15 @@ async function runDoctorCommand() {
4394
4570
  check(`controller: ${e.message}`, false);
4395
4571
  }
4396
4572
  try {
4397
- const lockPid = join10(opencodeDir, "ostacky-state.json.lock.pid");
4398
- const lockTs = join10(opencodeDir, "ostacky-state.json.lock.timestamp");
4399
- if (existsSync8(lockPid) || existsSync8(lockTs)) {
4573
+ const lockPid = join11(opencodeDir, "ostacky-state.json.lock.pid");
4574
+ const lockTs = join11(opencodeDir, "ostacky-state.json.lock.timestamp");
4575
+ if (existsSync9(lockPid) || existsSync9(lockTs)) {
4400
4576
  let ageStr = "";
4401
4577
  try {
4402
- const ts = parseInt(readFileSync6(lockTs, "utf-8"), 10);
4578
+ const ts = parseInt(readFileSync7(lockTs, "utf-8"), 10);
4403
4579
  const age = Date.now() - ts;
4404
4580
  ageStr = `${Math.floor(age / 1000)}s`;
4405
- const pid = readFileSync6(lockPid, "utf-8").trim();
4581
+ const pid = readFileSync7(lockPid, "utf-8").trim();
4406
4582
  let alive = false;
4407
4583
  try {
4408
4584
  process.kill(parseInt(pid, 10), 0);
@@ -4420,15 +4596,15 @@ async function runDoctorCommand() {
4420
4596
  }
4421
4597
  const tools = ["codegraph", "engram"];
4422
4598
  for (const t of tools) {
4423
- const p = join10(opencodeDir, "tools", t, "bin", t);
4599
+ const p = join11(opencodeDir, "tools", t, "bin", t);
4424
4600
  const pExe = p + ".exe";
4425
- check(`tool ${t}: ${existsSync8(p) || existsSync8(pExe) ? "found" : "missing"}`, existsSync8(p) || existsSync8(pExe), true);
4601
+ check(`tool ${t}: ${existsSync9(p) || existsSync9(pExe) ? "found" : "missing"}`, existsSync9(p) || existsSync9(pExe), true);
4426
4602
  }
4427
4603
  try {
4428
- const manifest = JSON.parse(readFileSync6(join10(cwd, "manifest.json"), "utf-8"));
4604
+ const manifest = JSON.parse(readFileSync7(join11(cwd, "manifest.json"), "utf-8"));
4429
4605
  const expected = manifest.mcpServers?.find((x) => x.name === "ostacky-controller")?.sha256;
4430
4606
  if (expected) {
4431
- const actual = computeTreeHash(join10(cwd, "assets", "mcp", "ostacky-controller"));
4607
+ const actual = computeTreeHash(join11(cwd, "assets", "mcp", "ostacky-controller"));
4432
4608
  check(`manifest hash: ${expected.slice(0, 8)} vs actual ${actual.slice(0, 8)}`, expected === actual);
4433
4609
  if (expected !== actual)
4434
4610
  console.log(" Run: bun run hash:update");
@@ -4436,22 +4612,32 @@ async function runDoctorCommand() {
4436
4612
  } catch {
4437
4613
  check("manifest: not found", false, true);
4438
4614
  }
4439
- if (existsSync8(statePath)) {
4615
+ try {
4616
+ await Promise.resolve().then(() => init_doctor_v2());
4617
+ for (const line of checkDoctorV2(cwd)) {
4618
+ console.log(line);
4619
+ if (line.startsWith("⚠️") || line.startsWith("❌"))
4620
+ hasWarn = true;
4621
+ }
4622
+ } catch {
4623
+ check("doctor-v2: check failed", false, true);
4624
+ }
4625
+ if (existsSync9(statePath)) {
4440
4626
  try {
4441
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4627
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4442
4628
  if (s.allowedFiles || s.deniedFiles) {}
4443
4629
  } catch {}
4444
4630
  }
4445
4631
  try {
4446
- const cacheDir = join10(opencodeDir, "cache", "codegraph");
4447
- if (!existsSync8(cacheDir)) {
4632
+ const cacheDir = join11(opencodeDir, "cache", "codegraph");
4633
+ if (!existsSync9(cacheDir)) {
4448
4634
  console.log("ℹ️ cache: no cache dir yet (ok)");
4449
4635
  } else {
4450
4636
  const files = readdirSync3(cacheDir);
4451
4637
  let total = 0;
4452
4638
  for (const f of files) {
4453
4639
  try {
4454
- total += statSync3(join10(cacheDir, f)).size;
4640
+ total += statSync3(join11(cacheDir, f)).size;
4455
4641
  } catch {}
4456
4642
  }
4457
4643
  const totalMB = (total / 1048576).toFixed(2);
@@ -4462,7 +4648,7 @@ async function runDoctorCommand() {
4462
4648
  console.log(`✅ cache: OK (${files.length} files, ${totalMB}MB)`);
4463
4649
  }
4464
4650
  try {
4465
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4651
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4466
4652
  if (s.cacheHitCount !== undefined) {
4467
4653
  console.log(`ℹ️ cacheHitCount=${s.cacheHitCount} cacheMissCount=${s.cacheMissCount || 0} tokenSavingEstimate=${s.tokenSavingEstimate || 0}`);
4468
4654
  }
@@ -4472,28 +4658,28 @@ async function runDoctorCommand() {
4472
4658
  console.log(`⚠️ cache: check failed ${e.message}`);
4473
4659
  }
4474
4660
  try {
4475
- const coreSrc = join10(cwd, "src", "controller-core.ts");
4476
- const coreMcp = join10(cwd, "assets", "mcp", "ostacky-controller", "controller-core.js");
4477
- const corePlugin = join10(cwd, "assets", "plugins", "controller-core.ts");
4478
- const coreSync = existsSync8(coreSrc) && existsSync8(coreMcp) && existsSync8(corePlugin);
4661
+ const coreSrc = join11(cwd, "src", "controller-core.ts");
4662
+ const coreMcp = join11(cwd, "assets", "mcp", "ostacky-controller", "controller-core.js");
4663
+ const corePlugin = join11(cwd, "assets", "plugins", "controller-core.ts");
4664
+ const coreSync = existsSync9(coreSrc) && existsSync9(coreMcp) && existsSync9(corePlugin);
4479
4665
  if (coreSync) {
4480
- const srcHash = readFileSync6(coreSrc, "utf-8").slice(0, 100);
4481
- const mcpHash = readFileSync6(coreMcp, "utf-8").slice(0, 100);
4482
- const ok = readFileSync6(coreMcp, "utf-8").includes("STATES") && readFileSync6(corePlugin, "utf-8").includes("STATES");
4666
+ const srcHash = readFileSync7(coreSrc, "utf-8").slice(0, 100);
4667
+ const mcpHash = readFileSync7(coreMcp, "utf-8").slice(0, 100);
4668
+ const ok = readFileSync7(coreMcp, "utf-8").includes("STATES") && readFileSync7(corePlugin, "utf-8").includes("STATES");
4483
4669
  check(`controller-core: synced (${ok ? "STATES present" : "mismatch"})`, ok);
4484
4670
  if (!ok)
4485
4671
  console.log(" Run: bun run scripts/sync-controller-core.ts (if exists) or copy src/controller-core.ts");
4486
4672
  } else {
4487
4673
  check("controller-core: synced", false, true);
4488
4674
  }
4489
- const auditPath = join10(opencodeDir, "ostacky-audit.jsonl");
4490
- if (existsSync8(auditPath)) {
4675
+ const auditPath = join11(opencodeDir, "ostacky-audit.jsonl");
4676
+ if (existsSync9(auditPath)) {
4491
4677
  const sz = statSync3(auditPath).size;
4492
- const lines = readFileSync6(auditPath, "utf-8").split(`
4678
+ const lines = readFileSync7(auditPath, "utf-8").split(`
4493
4679
  `).filter(Boolean).length;
4494
4680
  check(`audit: jsonl ${lines} entries, ${(sz / 1024).toFixed(1)}KB`, sz < 512000);
4495
- if (existsSync8(statePath)) {
4496
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4681
+ if (existsSync9(statePath)) {
4682
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4497
4683
  const stateSize = statSync3(statePath).size;
4498
4684
  check(`state size <50KB (${(stateSize / 1024).toFixed(1)}KB)`, stateSize < 51200);
4499
4685
  if (s.audit && s.audit.length > 20)
@@ -4504,16 +4690,16 @@ async function runDoctorCommand() {
4504
4690
  } else {
4505
4691
  console.log("ℹ️ audit: jsonl not yet created (will be created on next audit)");
4506
4692
  }
4507
- const ostackyMd = readFileSync6(join10(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4693
+ const ostackyMd = readFileSync7(join11(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4508
4694
  const hasLevels = ostackyMd.includes("LEVEL_THRESHOLDS") || ostackyMd.includes("classifyLevel");
4509
4695
  check("levels: unified via tiered.ts", hasLevels, true);
4510
4696
  const hasHonesty = ostackyMd.includes("Principios de honestidad");
4511
4697
  check("honesty: 7 SHALL", hasHonesty);
4512
4698
  if (!hasHonesty)
4513
4699
  console.log(" Expected: ## Principios de honestidad (SHALL) in ostacky.md");
4514
- const pluginPath = join10(cwd, "assets", "plugins", "ostacky-plugin.ts");
4515
- if (existsSync8(pluginPath)) {
4516
- const plugin = readFileSync6(pluginPath, "utf-8");
4700
+ const pluginPath = join11(cwd, "assets", "plugins", "ostacky-plugin.ts");
4701
+ if (existsSync9(pluginPath)) {
4702
+ const plugin = readFileSync7(pluginPath, "utf-8");
4517
4703
  const hasSpecGuard = plugin.includes("getDiscoverySnapshot") && plugin.includes("specSnapshot");
4518
4704
  const hasNoOverwrite = plugin.includes("No edites sin Read fresco") || plugin.includes("specSnapshot");
4519
4705
  check("spec: no-overwrite guard", hasNoOverwrite, true);
@@ -4524,12 +4710,12 @@ async function runDoctorCommand() {
4524
4710
  console.log(`⚠️ honesty/spec checks failed: ${e.message}`);
4525
4711
  }
4526
4712
  try {
4527
- const giPath = join10(cwd, ".gitignore");
4528
- if (!existsSync8(giPath)) {
4713
+ const giPath = join11(cwd, ".gitignore");
4714
+ if (!existsSync9(giPath)) {
4529
4715
  console.log("⚠️ .gitignore: missing (run npx ostacky install --scope local to create)");
4530
4716
  hasWarn = true;
4531
4717
  } else {
4532
- const gi = readFileSync6(giPath, "utf-8");
4718
+ const gi = readFileSync7(giPath, "utf-8");
4533
4719
  const needed = [
4534
4720
  ".opencode/tools/",
4535
4721
  ".opencode/cache/",
@@ -4549,12 +4735,12 @@ async function runDoctorCommand() {
4549
4735
  }
4550
4736
  } catch {}
4551
4737
  try {
4552
- const secPath = join10(cwd, "src", "security.ts");
4553
- if (!existsSync8(secPath)) {
4738
+ const secPath = join11(cwd, "src", "security.ts");
4739
+ if (!existsSync9(secPath)) {
4554
4740
  console.log("⚠️ src/security.ts: missing (source-of-truth)");
4555
4741
  hasWarn = true;
4556
4742
  } else {
4557
- const sec = readFileSync6(secPath, "utf-8");
4743
+ const sec = readFileSync7(secPath, "utf-8");
4558
4744
  const hasSensitiveDefault = sec.includes("SENSITIVE_DEFAULT");
4559
4745
  const hasBashRe = sec.includes("BASH_SENSITIVE_RE");
4560
4746
  const hasIsSensitive = sec.includes("function isSensitive");
@@ -4568,7 +4754,7 @@ async function runDoctorCommand() {
4568
4754
  }
4569
4755
  } catch {}
4570
4756
  try {
4571
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4757
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4572
4758
  if (s.sensitiveAccess?.blockedAttempts > 0) {
4573
4759
  console.log(`ℹ️ sensitiveAccess: blockedAttempts includes bash (${s.sensitiveAccess.blockedAttempts})`);
4574
4760
  }
@@ -4581,14 +4767,14 @@ async function runDoctorCommand() {
4581
4767
  async function runStatusCommand(args) {
4582
4768
  const isJson = args.includes("--json");
4583
4769
  const cwd = process.cwd();
4584
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4585
- const statePath = join10(opencodeDir, "ostacky-state.json");
4586
- if (!existsSync8(statePath)) {
4770
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4771
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4772
+ if (!existsSync9(statePath)) {
4587
4773
  console.log(isJson ? JSON.stringify({ error: "no state" }) : "No state file");
4588
4774
  return;
4589
4775
  }
4590
4776
  try {
4591
- const parsed = JSON.parse(readFileSync6(statePath, "utf-8"));
4777
+ const parsed = JSON.parse(readFileSync7(statePath, "utf-8"));
4592
4778
  const completed = Object.values(parsed.tasks || {}).filter((t) => t.status === "COMPLETED").length;
4593
4779
  const expected = parsed.expectedTaskCount ?? parsed.expectedTasks?.length ?? Object.keys(parsed.tasks || {}).length;
4594
4780
  const degraded = parsed.degraded ? " degraded" : "";