ostacky 0.8.8 → 0.9.1

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
@@ -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.8",
624
+ version: "0.9.1",
625
625
  repo: "JaimeHoracio/Ostacky",
626
- tag: "v0.8.8",
626
+ tag: "v0.9.1",
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.8: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
632
- version: "0.8.8",
633
- sha256: "bac9b7bc91d9d958bee8c6b2e094b1e1124bcc7f3e23cd13769ebbc5766f4747"
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.1: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
632
+ version: "0.9.1",
633
+ sha256: "8186994cc9d92585310d8f4c67255e5169f1f3b3d7fc5b3563c2fa79f1203d24"
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.8: controller plugin como alma hard-gate, Context7 removido del stack.",
641
- version: "0.8.8",
642
- sha256: "28ddf7ac8a2472faa1a5dbd14e22bc11e0b8715971907b3fab739b9933aa7dad"
640
+ description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.9.1: controller plugin como alma hard-gate, Context7 removido del stack.",
641
+ version: "0.9.1",
642
+ sha256: "af62710619308920f7be8743ed43c726d3591ba2af6535c3cc3ca6db95ad9715"
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.8",
648
+ version: "0.9.1",
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.8: 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.8",
658
- sha256: "a1d9744ffe70e976c7c96343bf101c1dedf615b27058559a376c0e559a341328"
656
+ description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.9.1: 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.1",
658
+ sha256: "d5d4712e5d823186c267f63cea776feddec4700f1b15286796586efb60c65241"
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.8",
664
+ version: "0.9.1",
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.8",
673
+ version: "0.9.1",
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.8",
680
+ version: "0.9.1",
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.8",
687
+ version: "0.9.1",
688
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.8",
694
+ version: "0.9.1",
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.8",
701
+ version: "0.9.1",
702
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.8",
708
+ version: "0.9.1",
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.8",
715
+ version: "0.9.1",
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.8",
722
+ version: "0.9.1",
723
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.8",
729
+ version: "0.9.1",
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.8",
736
+ version: "0.9.1",
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.8",
743
+ version: "0.9.1",
744
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.8",
750
+ version: "0.9.1",
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.8",
757
+ version: "0.9.1",
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.8",
764
+ version: "0.9.1",
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.8",
771
+ version: "0.9.1",
772
772
  sha256: "363449bd9e6f800a0a3168667cf78f767fb105d86e4a4adadcc649d418139303"
773
773
  }
774
774
  ]
@@ -820,6 +820,10 @@ function getBundledMcpPath(name) {
820
820
  validateFilePath(name);
821
821
  return join3(BUNDLED_MCP_DIR, name);
822
822
  }
823
+ function getBundledExpectedHash(category, name) {
824
+ const items = manifest_default[category] ?? [];
825
+ return items.find((item) => item.name === name)?.sha256 ?? null;
826
+ }
823
827
  function getRawUrl(repo, tag, path) {
824
828
  return `${GITHUB_RAW}/${repo}/${tag}/${path}`;
825
829
  }
@@ -1058,9 +1062,14 @@ function setMcpEntryAtProjectRoot(projectRoot, name, entry) {
1058
1062
  const config = readOpenCodeConfig(configPath);
1059
1063
  if (!config)
1060
1064
  throw new Error(`Error parseando ${configPath}`);
1061
- if (!config.mcp)
1065
+ if (!config.mcp || typeof config.mcp !== "object")
1062
1066
  config.mcp = {};
1063
- config.mcp[name] = entry;
1067
+ const mcp = config.mcp;
1068
+ if (!mcp.servers || typeof mcp.servers !== "object")
1069
+ mcp.servers = {};
1070
+ mcp.servers[name] = entry;
1071
+ if (name in mcp && name !== "servers")
1072
+ delete mcp[name];
1064
1073
  writeOpenCodeConfig(configPath, config);
1065
1074
  }
1066
1075
  function patchOpenCodeConfig(projectRoot = findProjectRoot()) {
@@ -1079,16 +1088,30 @@ function patchOpenCodeConfig(projectRoot = findProjectRoot()) {
1079
1088
  };
1080
1089
  }
1081
1090
  let changed = false;
1082
- if ("plugin" in config) {
1083
- delete config.plugin;
1084
- changed = true;
1091
+ if ("plugin" in config && Array.isArray(config.plugin)) {
1092
+ const entries = config.plugin;
1093
+ const kept = entries.filter((e) => !isSuperpowersPluginEntry(e));
1094
+ if (kept.length !== entries.length) {
1095
+ if (kept.length === 0)
1096
+ delete config.plugin;
1097
+ else
1098
+ config.plugin = kept;
1099
+ changed = true;
1100
+ }
1085
1101
  }
1086
1102
  if (changed) {
1087
1103
  writeOpenCodeConfig(configPath, config);
1088
- return { success: true, message: "Config actualizada (plugin legacy eliminado)" };
1104
+ return { success: true, message: "Config actualizada (entradas superpowers eliminadas)" };
1089
1105
  }
1090
1106
  return { success: true, message: "Config de OpenCode ya está limpia" };
1091
1107
  }
1108
+ function isSuperpowersPluginEntry(entry) {
1109
+ try {
1110
+ return JSON.stringify(entry).toLowerCase().includes("superpowers");
1111
+ } catch {
1112
+ return false;
1113
+ }
1114
+ }
1092
1115
  var init_config = __esm(() => {
1093
1116
  init_fs();
1094
1117
  });
@@ -1136,7 +1159,9 @@ function configureLocalTool(projectRoot, name, command) {
1136
1159
  setMcpEntryAtProjectRoot(projectRoot, name, {
1137
1160
  type: "local",
1138
1161
  command,
1139
- enabled: true
1162
+ disabled: false,
1163
+ timeout: { catalog: 30000, execution: 300000 },
1164
+ protocol: "legacy"
1140
1165
  });
1141
1166
  }
1142
1167
  function buildLocalMcpCommand(executable, args, platform = process.platform) {
@@ -1495,10 +1520,112 @@ var init_stack = __esm(() => {
1495
1520
  init_config();
1496
1521
  init_github();
1497
1522
  });
1523
+
1524
+ // src/doctor-v2.ts
1525
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1526
+ import { join as join10, delimiter, isAbsolute } from "path";
1527
+ function checkDoctorV2(projectRoot) {
1528
+ const lines = [];
1529
+ const cwd = projectRoot;
1530
+ const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
1531
+ const pluginPaths = [
1532
+ join10(opencodeDir, "plugins", "ostacky-plugin.ts"),
1533
+ join10(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
1534
+ join10(cwd, "assets", "plugins", "ostacky-plugin.ts")
1535
+ ];
1536
+ const found = pluginPaths.find((p) => existsSync8(p));
1537
+ if (found) {
1538
+ try {
1539
+ const src = readFileSync6(found, "utf-8");
1540
+ const isV2 = src.includes("Plugin.define") && src.includes("@opencode/plugin") && !src.includes("@opencode-ai/plugin");
1541
+ if (isV2)
1542
+ lines.push(`✅ controller: plugin V2 (${found})`);
1543
+ else
1544
+ lines.push(`⚠️ controller: plugin legacy V1 en ${found} — no corre en V2, reinstalá con ostacky`);
1545
+ } catch {
1546
+ lines.push(`⚠️ controller: no se pudo leer ${found}`);
1547
+ }
1548
+ }
1549
+ const configPath = findOpenCodeConfig(cwd);
1550
+ if (!configPath) {
1551
+ lines.push("ℹ️ config: no hay opencode.json ni opencode.jsonc");
1552
+ return lines;
1553
+ }
1554
+ let config = null;
1555
+ try {
1556
+ config = readOpenCodeConfig(configPath);
1557
+ } catch {
1558
+ lines.push(`⚠️ config: no se pudo parsear ${configPath}`);
1559
+ return lines;
1560
+ }
1561
+ if (!config) {
1562
+ lines.push(`⚠️ config: no se pudo parsear ${configPath}`);
1563
+ return lines;
1564
+ }
1565
+ const mcp = config.mcp ?? {};
1566
+ const servers = mcp.servers ?? {};
1567
+ const serverNames = Object.keys(servers);
1568
+ if (serverNames.length > 0)
1569
+ lines.push(`✅ mcp.servers: OK (${serverNames.length}: ${serverNames.join(", ")})`);
1570
+ for (const [key, value] of Object.entries(mcp)) {
1571
+ if (key === "servers" || key === "timeout")
1572
+ continue;
1573
+ if (value && typeof value === "object" && (("command" in value) || ("enabled" in value) || ("url" in value))) {
1574
+ lines.push(`⚠️ mcp.${key} legacy (forma V1) — mové a mcp.servers.${key} con disabled (install lo migra con restore ante fallo)`);
1575
+ }
1576
+ }
1577
+ const renames = {
1578
+ permission: "permissions[]",
1579
+ agent: "agents",
1580
+ command: "commands",
1581
+ reference: "references",
1582
+ provider: "providers",
1583
+ snapshot: "snapshots",
1584
+ attachment: "media"
1585
+ };
1586
+ for (const [oldKey, newKey] of Object.entries(renames)) {
1587
+ if (oldKey in config) {
1588
+ lines.push(`⚠️ ${oldKey} legacy ajeno — V2 usa ${newKey}; se normaliza en memoria, migrá cuando quieras (doctor no lo modifica)`);
1589
+ }
1590
+ }
1591
+ for (const [name, entry] of Object.entries(servers)) {
1592
+ if (!entry || typeof entry !== "object")
1593
+ continue;
1594
+ const cmd = entry.command;
1595
+ if (entry.type === "local" && Array.isArray(cmd) && typeof cmd[0] === "string") {
1596
+ const bin = cmd[0];
1597
+ const ok = isAbsolute(bin) ? existsSync8(bin) : lookupOnPath(bin) !== null;
1598
+ if (!ok)
1599
+ lines.push(`⚠️ mcp.servers.${name} command no existe (${bin}) — ¿proyecto movido? re-corré install`);
1600
+ }
1601
+ }
1602
+ return lines;
1603
+ }
1604
+ function lookupOnPath(name) {
1605
+ try {
1606
+ const pathEnv = process.env.PATH ?? "";
1607
+ const suffix = process.platform === "win32" ? ".exe" : "";
1608
+ for (const dir of pathEnv.split(delimiter)) {
1609
+ if (!dir)
1610
+ continue;
1611
+ try {
1612
+ if (existsSync8(join10(dir, name + suffix)))
1613
+ return join10(dir, name + suffix);
1614
+ if (suffix && existsSync8(join10(dir, name)))
1615
+ return join10(dir, name);
1616
+ } catch {}
1617
+ }
1618
+ } catch {}
1619
+ return null;
1620
+ }
1621
+ var init_doctor_v2 = __esm(() => {
1622
+ init_config();
1623
+ init_fs();
1624
+ });
1498
1625
  // package.json
1499
1626
  var package_default = {
1500
1627
  name: "ostacky",
1501
- version: "0.8.8",
1628
+ version: "0.9.1",
1502
1629
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1503
1630
  type: "module",
1504
1631
  bin: {
@@ -1532,7 +1659,7 @@ var package_default = {
1532
1659
  ],
1533
1660
  license: "MIT",
1534
1661
  dependencies: {
1535
- "@clack/prompts": "^0.9.0"
1662
+ "@clack/prompts": "^0.9.1"
1536
1663
  },
1537
1664
  devDependencies: {
1538
1665
  "@types/bun": "latest",
@@ -2525,7 +2652,9 @@ function createMcpConfigEntry(name, nodeExecutable, serverPath, statePath) {
2525
2652
  const entry = {
2526
2653
  type: "local",
2527
2654
  command: [nodeExecutable, serverPath],
2528
- enabled: true
2655
+ disabled: false,
2656
+ timeout: { catalog: 30000, execution: 300000 },
2657
+ protocol: "legacy"
2529
2658
  };
2530
2659
  if (name === "ostacky-controller" && statePath) {
2531
2660
  entry.environment = { OSTACKY_STATE_PATH: statePath };
@@ -2675,7 +2804,7 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2675
2804
  params: {
2676
2805
  protocolVersion: "2025-03-26",
2677
2806
  capabilities: {},
2678
- clientInfo: { name: "ostacky-installer", version: "0.8.8" }
2807
+ clientInfo: { name: "ostacky-installer", version: "0.9.1" }
2679
2808
  }
2680
2809
  });
2681
2810
  });
@@ -2743,6 +2872,20 @@ function upsertLockfile(paths, type, item, manifest, contentHash) {
2743
2872
  existing.lockedAt = new Date().toISOString();
2744
2873
  writeLockfile(paths.root, existing);
2745
2874
  }
2875
+ function assertBundledTreeHash(category, name, actualHash) {
2876
+ const expected = getBundledExpectedHash(category, name);
2877
+ if (expected && actualHash !== expected) {
2878
+ const labels = {
2879
+ skills: "skill",
2880
+ mcpServers: "MCP server",
2881
+ agents: "agente",
2882
+ commands: "command"
2883
+ };
2884
+ throw new Error(`Tree hash inválido para ${labels[category] ?? category} "${name}"
2885
+ esperado: ${expected}
2886
+ recibido: ${actualHash}`);
2887
+ }
2888
+ }
2746
2889
  function readBundledAsset(relativePath) {
2747
2890
  const fullPath = join6(PACKAGE_ROOT, relativePath);
2748
2891
  if (!existsSync5(fullPath))
@@ -2783,11 +2926,7 @@ async function installSkill(item, manifest, paths) {
2783
2926
  throw new Error(`Skill bundleada no encontrada: ${src}. ¿Falta el directorio assets/skills/${item.name}/?`);
2784
2927
  }
2785
2928
  const treeHash = computeTreeHash(src);
2786
- if (item.sha256 && treeHash !== item.sha256) {
2787
- throw new Error(`Tree hash inválido para skill "${item.name}"
2788
- esperado: ${item.sha256}
2789
- recibido: ${treeHash}`);
2790
- }
2929
+ assertBundledTreeHash("skills", item.name, treeHash);
2791
2930
  const dest = join6(paths.skills, item.name);
2792
2931
  if (existsSync5(dest)) {
2793
2932
  rmSync3(dest, { recursive: true, force: true });
@@ -2824,11 +2963,7 @@ async function installMcpServer(item, manifest, paths) {
2824
2963
  throw new Error(`MCP server bundleado no encontrado: ${src}. ` + `¿Falta el directorio assets/mcp/${item.name}/?`);
2825
2964
  }
2826
2965
  const treeHash = computeTreeHash(src);
2827
- if (item.sha256 && treeHash !== item.sha256) {
2828
- throw new Error(`Tree hash inválido para MCP server "${item.name}"
2829
- esperado: ${item.sha256}
2830
- recibido: ${treeHash}`);
2831
- }
2966
+ assertBundledTreeHash("mcpServers", item.name, treeHash);
2832
2967
  const nodeExecutable = getVerifiedNodeExecutable();
2833
2968
  const projectRoot = dirname4(paths.root);
2834
2969
  const dest = join6(paths.mcp, item.name);
@@ -2956,8 +3091,17 @@ function uninstallMcpServer(name, paths) {
2956
3091
  const config = readOpenCodeConfig(configPath);
2957
3092
  if (config) {
2958
3093
  const mcp = config.mcp;
2959
- if (mcp && mcp[name]) {
2960
- delete mcp[name];
3094
+ if (mcp) {
3095
+ const servers = mcp.servers;
3096
+ if (servers && name in servers) {
3097
+ delete servers[name];
3098
+ if (Object.keys(servers).length === 0) {
3099
+ delete mcp.servers;
3100
+ }
3101
+ }
3102
+ if (name in mcp && name !== "servers") {
3103
+ delete mcp[name];
3104
+ }
2961
3105
  if (Object.keys(mcp).length === 0) {
2962
3106
  delete config.mcp;
2963
3107
  }
@@ -3034,7 +3178,7 @@ function printPostInstallSteps() {
3034
3178
  "Cerrá y reiniciá OpenCode si ya estaba corriendo (los MCP no recargan su config en caliente):",
3035
3179
  " TUI → opencode",
3036
3180
  " Web → opencode web --port 4096",
3037
- "Escribí @Ostacky en el chat (TUI o web) para invocar al agente",
3181
+ "Seleccioná el agente: `/agents` → `ostacky` (o Shift+Tab / Ctrl+X A)",
3038
3182
  "Skills bundleadas en .opencode/skills/ — revisá cuáles activás",
3039
3183
  "¿Errores en el stack? Ejecutá /install-stack desde el chat de OpenCode"
3040
3184
  ].join(`
@@ -3799,6 +3943,23 @@ function isOpencodeInstalled() {
3799
3943
  return false;
3800
3944
  }
3801
3945
  }
3946
+ function parseOpencodeVersion(output) {
3947
+ const m = /v?(\d+)\.\d+\.\d+/.exec(output ?? "");
3948
+ if (!m)
3949
+ return null;
3950
+ const major = parseInt(m[1], 10);
3951
+ return Number.isNaN(major) ? null : major;
3952
+ }
3953
+ function getOpencodeVersion() {
3954
+ try {
3955
+ return execFileSync4("opencode", ["--version"], {
3956
+ encoding: "utf-8",
3957
+ stdio: ["pipe", "pipe", "pipe"]
3958
+ }).trim();
3959
+ } catch {
3960
+ return null;
3961
+ }
3962
+ }
3802
3963
  function getOpencodeInstallCommand(platform = process.platform, _arch = process.arch) {
3803
3964
  if (platform === "win32") {
3804
3965
  return {
@@ -3824,8 +3985,17 @@ function getOpencodeInstallCommand(platform = process.platform, _arch = process.
3824
3985
  };
3825
3986
  }
3826
3987
  async function ensureOpencodeInstalled() {
3827
- if (isOpencodeInstalled())
3988
+ if (isOpencodeInstalled()) {
3989
+ const raw = getOpencodeVersion();
3990
+ const major = raw ? parseOpencodeVersion(raw) : null;
3991
+ if (major !== null && major < 2) {
3992
+ v2.warn(`OpenCode V1 detectado (${raw}). Ostacky requiere OpenCode V2.`);
3993
+ ye(`El instalador V2 reemplaza el binario V1. Desinstalá V1 (package manager) e instalá V2, luego re-ejecutá.
3994
+ Docs: https://opencode.ai/v2/docs/migrate-v1`, "Actualización requerida");
3995
+ process.exit(1);
3996
+ }
3828
3997
  return;
3998
+ }
3829
3999
  const platform = process.platform;
3830
4000
  const info = getOpencodeInstallCommand(platform);
3831
4001
  v2.warn("OpenCode no detectado en este sistema.");
@@ -4242,8 +4412,8 @@ async function runUninstallMcpCommand(name, scope) {
4242
4412
 
4243
4413
  // src/cli.ts
4244
4414
  init_fs();
4245
- import { existsSync as existsSync8, statSync as statSync3, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "node:fs";
4246
- import { join as join10, dirname as dirname8 } from "node:path";
4415
+ import { existsSync as existsSync9, statSync as statSync3, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "node:fs";
4416
+ import { join as join11, dirname as dirname8 } from "node:path";
4247
4417
  var HELP = `
4248
4418
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4249
4419
 
@@ -4314,8 +4484,8 @@ var argvNoScope = withoutScopeArgs(process.argv);
4314
4484
  var [, , cmd, subcmd] = argvNoScope;
4315
4485
  async function runDoctorCommand() {
4316
4486
  const cwd = process.cwd();
4317
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4318
- const statePath = join10(opencodeDir, "ostacky-state.json");
4487
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4488
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4319
4489
  let hasError = false;
4320
4490
  let hasWarn = false;
4321
4491
  const check = (label, ok, warn = false) => {
@@ -4330,23 +4500,23 @@ async function runDoctorCommand() {
4330
4500
  }
4331
4501
  };
4332
4502
  const pluginPaths = [
4333
- join10(cwd, "assets", "plugins", "ostacky-plugin.ts"),
4334
- join10(opencodeDir, "plugins", "ostacky-plugin.ts"),
4335
- join10(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
4336
- join10(cwd, "assets", "plugins", "ostacky-controller.ts"),
4337
- join10(opencodeDir, "plugins", "ostacky-controller.ts"),
4338
- join10(cwd, ".opencode", "plugins", "ostacky-controller.ts")
4503
+ join11(cwd, "assets", "plugins", "ostacky-plugin.ts"),
4504
+ join11(opencodeDir, "plugins", "ostacky-plugin.ts"),
4505
+ join11(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
4506
+ join11(cwd, "assets", "plugins", "ostacky-controller.ts"),
4507
+ join11(opencodeDir, "plugins", "ostacky-controller.ts"),
4508
+ join11(cwd, ".opencode", "plugins", "ostacky-controller.ts")
4339
4509
  ];
4340
- const pluginActive = pluginPaths.some((p) => existsSync8(p));
4510
+ const pluginActive = pluginPaths.some((p) => existsSync9(p));
4341
4511
  try {
4342
- if (!existsSync8(statePath)) {
4512
+ if (!existsSync9(statePath)) {
4343
4513
  if (pluginActive)
4344
4514
  console.log(`✅ controller: plugin active (no state yet)`);
4345
4515
  else
4346
4516
  check("controller: state file missing", false, true);
4347
4517
  } else {
4348
4518
  const stat = statSync3(statePath);
4349
- const raw = readFileSync6(statePath, "utf-8");
4519
+ const raw = readFileSync7(statePath, "utf-8");
4350
4520
  const parsed = JSON.parse(raw);
4351
4521
  if (pluginActive) {
4352
4522
  console.log(`✅ controller: plugin active (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`);
@@ -4410,15 +4580,15 @@ async function runDoctorCommand() {
4410
4580
  check(`controller: ${e.message}`, false);
4411
4581
  }
4412
4582
  try {
4413
- const lockPid = join10(opencodeDir, "ostacky-state.json.lock.pid");
4414
- const lockTs = join10(opencodeDir, "ostacky-state.json.lock.timestamp");
4415
- if (existsSync8(lockPid) || existsSync8(lockTs)) {
4583
+ const lockPid = join11(opencodeDir, "ostacky-state.json.lock.pid");
4584
+ const lockTs = join11(opencodeDir, "ostacky-state.json.lock.timestamp");
4585
+ if (existsSync9(lockPid) || existsSync9(lockTs)) {
4416
4586
  let ageStr = "";
4417
4587
  try {
4418
- const ts = parseInt(readFileSync6(lockTs, "utf-8"), 10);
4588
+ const ts = parseInt(readFileSync7(lockTs, "utf-8"), 10);
4419
4589
  const age = Date.now() - ts;
4420
4590
  ageStr = `${Math.floor(age / 1000)}s`;
4421
- const pid = readFileSync6(lockPid, "utf-8").trim();
4591
+ const pid = readFileSync7(lockPid, "utf-8").trim();
4422
4592
  let alive = false;
4423
4593
  try {
4424
4594
  process.kill(parseInt(pid, 10), 0);
@@ -4436,15 +4606,15 @@ async function runDoctorCommand() {
4436
4606
  }
4437
4607
  const tools = ["codegraph", "engram"];
4438
4608
  for (const t of tools) {
4439
- const p = join10(opencodeDir, "tools", t, "bin", t);
4609
+ const p = join11(opencodeDir, "tools", t, "bin", t);
4440
4610
  const pExe = p + ".exe";
4441
- check(`tool ${t}: ${existsSync8(p) || existsSync8(pExe) ? "found" : "missing"}`, existsSync8(p) || existsSync8(pExe), true);
4611
+ check(`tool ${t}: ${existsSync9(p) || existsSync9(pExe) ? "found" : "missing"}`, existsSync9(p) || existsSync9(pExe), true);
4442
4612
  }
4443
4613
  try {
4444
- const manifest = JSON.parse(readFileSync6(join10(cwd, "manifest.json"), "utf-8"));
4614
+ const manifest = JSON.parse(readFileSync7(join11(cwd, "manifest.json"), "utf-8"));
4445
4615
  const expected = manifest.mcpServers?.find((x) => x.name === "ostacky-controller")?.sha256;
4446
4616
  if (expected) {
4447
- const actual = computeTreeHash(join10(cwd, "assets", "mcp", "ostacky-controller"));
4617
+ const actual = computeTreeHash(join11(cwd, "assets", "mcp", "ostacky-controller"));
4448
4618
  check(`manifest hash: ${expected.slice(0, 8)} vs actual ${actual.slice(0, 8)}`, expected === actual);
4449
4619
  if (expected !== actual)
4450
4620
  console.log(" Run: bun run hash:update");
@@ -4452,22 +4622,32 @@ async function runDoctorCommand() {
4452
4622
  } catch {
4453
4623
  check("manifest: not found", false, true);
4454
4624
  }
4455
- if (existsSync8(statePath)) {
4625
+ try {
4626
+ await Promise.resolve().then(() => init_doctor_v2());
4627
+ for (const line of checkDoctorV2(cwd)) {
4628
+ console.log(line);
4629
+ if (line.startsWith("⚠️") || line.startsWith("❌"))
4630
+ hasWarn = true;
4631
+ }
4632
+ } catch {
4633
+ check("doctor-v2: check failed", false, true);
4634
+ }
4635
+ if (existsSync9(statePath)) {
4456
4636
  try {
4457
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4637
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4458
4638
  if (s.allowedFiles || s.deniedFiles) {}
4459
4639
  } catch {}
4460
4640
  }
4461
4641
  try {
4462
- const cacheDir = join10(opencodeDir, "cache", "codegraph");
4463
- if (!existsSync8(cacheDir)) {
4642
+ const cacheDir = join11(opencodeDir, "cache", "codegraph");
4643
+ if (!existsSync9(cacheDir)) {
4464
4644
  console.log("ℹ️ cache: no cache dir yet (ok)");
4465
4645
  } else {
4466
4646
  const files = readdirSync3(cacheDir);
4467
4647
  let total = 0;
4468
4648
  for (const f of files) {
4469
4649
  try {
4470
- total += statSync3(join10(cacheDir, f)).size;
4650
+ total += statSync3(join11(cacheDir, f)).size;
4471
4651
  } catch {}
4472
4652
  }
4473
4653
  const totalMB = (total / 1048576).toFixed(2);
@@ -4478,7 +4658,7 @@ async function runDoctorCommand() {
4478
4658
  console.log(`✅ cache: OK (${files.length} files, ${totalMB}MB)`);
4479
4659
  }
4480
4660
  try {
4481
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4661
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4482
4662
  if (s.cacheHitCount !== undefined) {
4483
4663
  console.log(`ℹ️ cacheHitCount=${s.cacheHitCount} cacheMissCount=${s.cacheMissCount || 0} tokenSavingEstimate=${s.tokenSavingEstimate || 0}`);
4484
4664
  }
@@ -4488,28 +4668,28 @@ async function runDoctorCommand() {
4488
4668
  console.log(`⚠️ cache: check failed ${e.message}`);
4489
4669
  }
4490
4670
  try {
4491
- const coreSrc = join10(cwd, "src", "controller-core.ts");
4492
- const coreMcp = join10(cwd, "assets", "mcp", "ostacky-controller", "controller-core.js");
4493
- const corePlugin = join10(cwd, "assets", "plugins", "controller-core.ts");
4494
- const coreSync = existsSync8(coreSrc) && existsSync8(coreMcp) && existsSync8(corePlugin);
4671
+ const coreSrc = join11(cwd, "src", "controller-core.ts");
4672
+ const coreMcp = join11(cwd, "assets", "mcp", "ostacky-controller", "controller-core.js");
4673
+ const corePlugin = join11(cwd, "assets", "plugins", "controller-core.ts");
4674
+ const coreSync = existsSync9(coreSrc) && existsSync9(coreMcp) && existsSync9(corePlugin);
4495
4675
  if (coreSync) {
4496
- const srcHash = readFileSync6(coreSrc, "utf-8").slice(0, 100);
4497
- const mcpHash = readFileSync6(coreMcp, "utf-8").slice(0, 100);
4498
- const ok = readFileSync6(coreMcp, "utf-8").includes("STATES") && readFileSync6(corePlugin, "utf-8").includes("STATES");
4676
+ const srcHash = readFileSync7(coreSrc, "utf-8").slice(0, 100);
4677
+ const mcpHash = readFileSync7(coreMcp, "utf-8").slice(0, 100);
4678
+ const ok = readFileSync7(coreMcp, "utf-8").includes("STATES") && readFileSync7(corePlugin, "utf-8").includes("STATES");
4499
4679
  check(`controller-core: synced (${ok ? "STATES present" : "mismatch"})`, ok);
4500
4680
  if (!ok)
4501
4681
  console.log(" Run: bun run scripts/sync-controller-core.ts (if exists) or copy src/controller-core.ts");
4502
4682
  } else {
4503
4683
  check("controller-core: synced", false, true);
4504
4684
  }
4505
- const auditPath = join10(opencodeDir, "ostacky-audit.jsonl");
4506
- if (existsSync8(auditPath)) {
4685
+ const auditPath = join11(opencodeDir, "ostacky-audit.jsonl");
4686
+ if (existsSync9(auditPath)) {
4507
4687
  const sz = statSync3(auditPath).size;
4508
- const lines = readFileSync6(auditPath, "utf-8").split(`
4688
+ const lines = readFileSync7(auditPath, "utf-8").split(`
4509
4689
  `).filter(Boolean).length;
4510
4690
  check(`audit: jsonl ${lines} entries, ${(sz / 1024).toFixed(1)}KB`, sz < 512000);
4511
- if (existsSync8(statePath)) {
4512
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4691
+ if (existsSync9(statePath)) {
4692
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4513
4693
  const stateSize = statSync3(statePath).size;
4514
4694
  check(`state size <50KB (${(stateSize / 1024).toFixed(1)}KB)`, stateSize < 51200);
4515
4695
  if (s.audit && s.audit.length > 20)
@@ -4520,16 +4700,16 @@ async function runDoctorCommand() {
4520
4700
  } else {
4521
4701
  console.log("ℹ️ audit: jsonl not yet created (will be created on next audit)");
4522
4702
  }
4523
- const ostackyMd = readFileSync6(join10(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4703
+ const ostackyMd = readFileSync7(join11(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4524
4704
  const hasLevels = ostackyMd.includes("LEVEL_THRESHOLDS") || ostackyMd.includes("classifyLevel");
4525
4705
  check("levels: unified via tiered.ts", hasLevels, true);
4526
4706
  const hasHonesty = ostackyMd.includes("Principios de honestidad");
4527
4707
  check("honesty: 7 SHALL", hasHonesty);
4528
4708
  if (!hasHonesty)
4529
4709
  console.log(" Expected: ## Principios de honestidad (SHALL) in ostacky.md");
4530
- const pluginPath = join10(cwd, "assets", "plugins", "ostacky-plugin.ts");
4531
- if (existsSync8(pluginPath)) {
4532
- const plugin = readFileSync6(pluginPath, "utf-8");
4710
+ const pluginPath = join11(cwd, "assets", "plugins", "ostacky-plugin.ts");
4711
+ if (existsSync9(pluginPath)) {
4712
+ const plugin = readFileSync7(pluginPath, "utf-8");
4533
4713
  const hasSpecGuard = plugin.includes("getDiscoverySnapshot") && plugin.includes("specSnapshot");
4534
4714
  const hasNoOverwrite = plugin.includes("No edites sin Read fresco") || plugin.includes("specSnapshot");
4535
4715
  check("spec: no-overwrite guard", hasNoOverwrite, true);
@@ -4540,12 +4720,12 @@ async function runDoctorCommand() {
4540
4720
  console.log(`⚠️ honesty/spec checks failed: ${e.message}`);
4541
4721
  }
4542
4722
  try {
4543
- const giPath = join10(cwd, ".gitignore");
4544
- if (!existsSync8(giPath)) {
4723
+ const giPath = join11(cwd, ".gitignore");
4724
+ if (!existsSync9(giPath)) {
4545
4725
  console.log("⚠️ .gitignore: missing (run npx ostacky install --scope local to create)");
4546
4726
  hasWarn = true;
4547
4727
  } else {
4548
- const gi = readFileSync6(giPath, "utf-8");
4728
+ const gi = readFileSync7(giPath, "utf-8");
4549
4729
  const needed = [
4550
4730
  ".opencode/tools/",
4551
4731
  ".opencode/cache/",
@@ -4565,12 +4745,12 @@ async function runDoctorCommand() {
4565
4745
  }
4566
4746
  } catch {}
4567
4747
  try {
4568
- const secPath = join10(cwd, "src", "security.ts");
4569
- if (!existsSync8(secPath)) {
4748
+ const secPath = join11(cwd, "src", "security.ts");
4749
+ if (!existsSync9(secPath)) {
4570
4750
  console.log("⚠️ src/security.ts: missing (source-of-truth)");
4571
4751
  hasWarn = true;
4572
4752
  } else {
4573
- const sec = readFileSync6(secPath, "utf-8");
4753
+ const sec = readFileSync7(secPath, "utf-8");
4574
4754
  const hasSensitiveDefault = sec.includes("SENSITIVE_DEFAULT");
4575
4755
  const hasBashRe = sec.includes("BASH_SENSITIVE_RE");
4576
4756
  const hasIsSensitive = sec.includes("function isSensitive");
@@ -4584,7 +4764,7 @@ async function runDoctorCommand() {
4584
4764
  }
4585
4765
  } catch {}
4586
4766
  try {
4587
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4767
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4588
4768
  if (s.sensitiveAccess?.blockedAttempts > 0) {
4589
4769
  console.log(`ℹ️ sensitiveAccess: blockedAttempts includes bash (${s.sensitiveAccess.blockedAttempts})`);
4590
4770
  }
@@ -4597,14 +4777,14 @@ async function runDoctorCommand() {
4597
4777
  async function runStatusCommand(args) {
4598
4778
  const isJson = args.includes("--json");
4599
4779
  const cwd = process.cwd();
4600
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4601
- const statePath = join10(opencodeDir, "ostacky-state.json");
4602
- if (!existsSync8(statePath)) {
4780
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4781
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4782
+ if (!existsSync9(statePath)) {
4603
4783
  console.log(isJson ? JSON.stringify({ error: "no state" }) : "No state file");
4604
4784
  return;
4605
4785
  }
4606
4786
  try {
4607
- const parsed = JSON.parse(readFileSync6(statePath, "utf-8"));
4787
+ const parsed = JSON.parse(readFileSync7(statePath, "utf-8"));
4608
4788
  const completed = Object.values(parsed.tasks || {}).filter((t) => t.status === "COMPLETED").length;
4609
4789
  const expected = parsed.expectedTaskCount ?? parsed.expectedTasks?.length ?? Object.keys(parsed.tasks || {}).length;
4610
4790
  const degraded = parsed.degraded ? " degraded" : "";