ostacky 0.8.8 → 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
@@ -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.0",
625
625
  repo: "JaimeHoracio/Ostacky",
626
- tag: "v0.8.8",
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.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.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.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.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.8",
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.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.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.8",
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.8",
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.8",
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.8",
687
+ version: "0.9.0",
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.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.8",
701
+ version: "0.9.0",
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.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.8",
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.8",
722
+ version: "0.9.0",
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.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.8",
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.8",
743
+ version: "0.9.0",
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.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.8",
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.8",
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.8",
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) {
@@ -1495,10 +1516,112 @@ var init_stack = __esm(() => {
1495
1516
  init_config();
1496
1517
  init_github();
1497
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
+ });
1498
1621
  // package.json
1499
1622
  var package_default = {
1500
1623
  name: "ostacky",
1501
- version: "0.8.8",
1624
+ version: "0.9.0",
1502
1625
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1503
1626
  type: "module",
1504
1627
  bin: {
@@ -2525,7 +2648,9 @@ function createMcpConfigEntry(name, nodeExecutable, serverPath, statePath) {
2525
2648
  const entry = {
2526
2649
  type: "local",
2527
2650
  command: [nodeExecutable, serverPath],
2528
- enabled: true
2651
+ disabled: false,
2652
+ timeout: { catalog: 30000, execution: 300000 },
2653
+ protocol: "legacy"
2529
2654
  };
2530
2655
  if (name === "ostacky-controller" && statePath) {
2531
2656
  entry.environment = { OSTACKY_STATE_PATH: statePath };
@@ -2675,7 +2800,7 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2675
2800
  params: {
2676
2801
  protocolVersion: "2025-03-26",
2677
2802
  capabilities: {},
2678
- clientInfo: { name: "ostacky-installer", version: "0.8.8" }
2803
+ clientInfo: { name: "ostacky-installer", version: "0.9.0" }
2679
2804
  }
2680
2805
  });
2681
2806
  });
@@ -2956,8 +3081,17 @@ function uninstallMcpServer(name, paths) {
2956
3081
  const config = readOpenCodeConfig(configPath);
2957
3082
  if (config) {
2958
3083
  const mcp = config.mcp;
2959
- if (mcp && mcp[name]) {
2960
- 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
+ }
2961
3095
  if (Object.keys(mcp).length === 0) {
2962
3096
  delete config.mcp;
2963
3097
  }
@@ -3799,6 +3933,23 @@ function isOpencodeInstalled() {
3799
3933
  return false;
3800
3934
  }
3801
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
+ }
3802
3953
  function getOpencodeInstallCommand(platform = process.platform, _arch = process.arch) {
3803
3954
  if (platform === "win32") {
3804
3955
  return {
@@ -3824,8 +3975,17 @@ function getOpencodeInstallCommand(platform = process.platform, _arch = process.
3824
3975
  };
3825
3976
  }
3826
3977
  async function ensureOpencodeInstalled() {
3827
- 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
+ }
3828
3987
  return;
3988
+ }
3829
3989
  const platform = process.platform;
3830
3990
  const info = getOpencodeInstallCommand(platform);
3831
3991
  v2.warn("OpenCode no detectado en este sistema.");
@@ -4242,8 +4402,8 @@ async function runUninstallMcpCommand(name, scope) {
4242
4402
 
4243
4403
  // src/cli.ts
4244
4404
  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";
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";
4247
4407
  var HELP = `
4248
4408
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4249
4409
 
@@ -4314,8 +4474,8 @@ var argvNoScope = withoutScopeArgs(process.argv);
4314
4474
  var [, , cmd, subcmd] = argvNoScope;
4315
4475
  async function runDoctorCommand() {
4316
4476
  const cwd = process.cwd();
4317
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4318
- const statePath = join10(opencodeDir, "ostacky-state.json");
4477
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4478
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4319
4479
  let hasError = false;
4320
4480
  let hasWarn = false;
4321
4481
  const check = (label, ok, warn = false) => {
@@ -4330,23 +4490,23 @@ async function runDoctorCommand() {
4330
4490
  }
4331
4491
  };
4332
4492
  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")
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")
4339
4499
  ];
4340
- const pluginActive = pluginPaths.some((p) => existsSync8(p));
4500
+ const pluginActive = pluginPaths.some((p) => existsSync9(p));
4341
4501
  try {
4342
- if (!existsSync8(statePath)) {
4502
+ if (!existsSync9(statePath)) {
4343
4503
  if (pluginActive)
4344
4504
  console.log(`✅ controller: plugin active (no state yet)`);
4345
4505
  else
4346
4506
  check("controller: state file missing", false, true);
4347
4507
  } else {
4348
4508
  const stat = statSync3(statePath);
4349
- const raw = readFileSync6(statePath, "utf-8");
4509
+ const raw = readFileSync7(statePath, "utf-8");
4350
4510
  const parsed = JSON.parse(raw);
4351
4511
  if (pluginActive) {
4352
4512
  console.log(`✅ controller: plugin active (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`);
@@ -4410,15 +4570,15 @@ async function runDoctorCommand() {
4410
4570
  check(`controller: ${e.message}`, false);
4411
4571
  }
4412
4572
  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)) {
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)) {
4416
4576
  let ageStr = "";
4417
4577
  try {
4418
- const ts = parseInt(readFileSync6(lockTs, "utf-8"), 10);
4578
+ const ts = parseInt(readFileSync7(lockTs, "utf-8"), 10);
4419
4579
  const age = Date.now() - ts;
4420
4580
  ageStr = `${Math.floor(age / 1000)}s`;
4421
- const pid = readFileSync6(lockPid, "utf-8").trim();
4581
+ const pid = readFileSync7(lockPid, "utf-8").trim();
4422
4582
  let alive = false;
4423
4583
  try {
4424
4584
  process.kill(parseInt(pid, 10), 0);
@@ -4436,15 +4596,15 @@ async function runDoctorCommand() {
4436
4596
  }
4437
4597
  const tools = ["codegraph", "engram"];
4438
4598
  for (const t of tools) {
4439
- const p = join10(opencodeDir, "tools", t, "bin", t);
4599
+ const p = join11(opencodeDir, "tools", t, "bin", t);
4440
4600
  const pExe = p + ".exe";
4441
- 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);
4442
4602
  }
4443
4603
  try {
4444
- const manifest = JSON.parse(readFileSync6(join10(cwd, "manifest.json"), "utf-8"));
4604
+ const manifest = JSON.parse(readFileSync7(join11(cwd, "manifest.json"), "utf-8"));
4445
4605
  const expected = manifest.mcpServers?.find((x) => x.name === "ostacky-controller")?.sha256;
4446
4606
  if (expected) {
4447
- const actual = computeTreeHash(join10(cwd, "assets", "mcp", "ostacky-controller"));
4607
+ const actual = computeTreeHash(join11(cwd, "assets", "mcp", "ostacky-controller"));
4448
4608
  check(`manifest hash: ${expected.slice(0, 8)} vs actual ${actual.slice(0, 8)}`, expected === actual);
4449
4609
  if (expected !== actual)
4450
4610
  console.log(" Run: bun run hash:update");
@@ -4452,22 +4612,32 @@ async function runDoctorCommand() {
4452
4612
  } catch {
4453
4613
  check("manifest: not found", false, true);
4454
4614
  }
4455
- 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)) {
4456
4626
  try {
4457
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4627
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4458
4628
  if (s.allowedFiles || s.deniedFiles) {}
4459
4629
  } catch {}
4460
4630
  }
4461
4631
  try {
4462
- const cacheDir = join10(opencodeDir, "cache", "codegraph");
4463
- if (!existsSync8(cacheDir)) {
4632
+ const cacheDir = join11(opencodeDir, "cache", "codegraph");
4633
+ if (!existsSync9(cacheDir)) {
4464
4634
  console.log("ℹ️ cache: no cache dir yet (ok)");
4465
4635
  } else {
4466
4636
  const files = readdirSync3(cacheDir);
4467
4637
  let total = 0;
4468
4638
  for (const f of files) {
4469
4639
  try {
4470
- total += statSync3(join10(cacheDir, f)).size;
4640
+ total += statSync3(join11(cacheDir, f)).size;
4471
4641
  } catch {}
4472
4642
  }
4473
4643
  const totalMB = (total / 1048576).toFixed(2);
@@ -4478,7 +4648,7 @@ async function runDoctorCommand() {
4478
4648
  console.log(`✅ cache: OK (${files.length} files, ${totalMB}MB)`);
4479
4649
  }
4480
4650
  try {
4481
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4651
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4482
4652
  if (s.cacheHitCount !== undefined) {
4483
4653
  console.log(`ℹ️ cacheHitCount=${s.cacheHitCount} cacheMissCount=${s.cacheMissCount || 0} tokenSavingEstimate=${s.tokenSavingEstimate || 0}`);
4484
4654
  }
@@ -4488,28 +4658,28 @@ async function runDoctorCommand() {
4488
4658
  console.log(`⚠️ cache: check failed ${e.message}`);
4489
4659
  }
4490
4660
  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);
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);
4495
4665
  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");
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");
4499
4669
  check(`controller-core: synced (${ok ? "STATES present" : "mismatch"})`, ok);
4500
4670
  if (!ok)
4501
4671
  console.log(" Run: bun run scripts/sync-controller-core.ts (if exists) or copy src/controller-core.ts");
4502
4672
  } else {
4503
4673
  check("controller-core: synced", false, true);
4504
4674
  }
4505
- const auditPath = join10(opencodeDir, "ostacky-audit.jsonl");
4506
- if (existsSync8(auditPath)) {
4675
+ const auditPath = join11(opencodeDir, "ostacky-audit.jsonl");
4676
+ if (existsSync9(auditPath)) {
4507
4677
  const sz = statSync3(auditPath).size;
4508
- const lines = readFileSync6(auditPath, "utf-8").split(`
4678
+ const lines = readFileSync7(auditPath, "utf-8").split(`
4509
4679
  `).filter(Boolean).length;
4510
4680
  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"));
4681
+ if (existsSync9(statePath)) {
4682
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4513
4683
  const stateSize = statSync3(statePath).size;
4514
4684
  check(`state size <50KB (${(stateSize / 1024).toFixed(1)}KB)`, stateSize < 51200);
4515
4685
  if (s.audit && s.audit.length > 20)
@@ -4520,16 +4690,16 @@ async function runDoctorCommand() {
4520
4690
  } else {
4521
4691
  console.log("ℹ️ audit: jsonl not yet created (will be created on next audit)");
4522
4692
  }
4523
- const ostackyMd = readFileSync6(join10(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4693
+ const ostackyMd = readFileSync7(join11(cwd, "assets", "agents", "ostacky.md"), "utf-8");
4524
4694
  const hasLevels = ostackyMd.includes("LEVEL_THRESHOLDS") || ostackyMd.includes("classifyLevel");
4525
4695
  check("levels: unified via tiered.ts", hasLevels, true);
4526
4696
  const hasHonesty = ostackyMd.includes("Principios de honestidad");
4527
4697
  check("honesty: 7 SHALL", hasHonesty);
4528
4698
  if (!hasHonesty)
4529
4699
  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");
4700
+ const pluginPath = join11(cwd, "assets", "plugins", "ostacky-plugin.ts");
4701
+ if (existsSync9(pluginPath)) {
4702
+ const plugin = readFileSync7(pluginPath, "utf-8");
4533
4703
  const hasSpecGuard = plugin.includes("getDiscoverySnapshot") && plugin.includes("specSnapshot");
4534
4704
  const hasNoOverwrite = plugin.includes("No edites sin Read fresco") || plugin.includes("specSnapshot");
4535
4705
  check("spec: no-overwrite guard", hasNoOverwrite, true);
@@ -4540,12 +4710,12 @@ async function runDoctorCommand() {
4540
4710
  console.log(`⚠️ honesty/spec checks failed: ${e.message}`);
4541
4711
  }
4542
4712
  try {
4543
- const giPath = join10(cwd, ".gitignore");
4544
- if (!existsSync8(giPath)) {
4713
+ const giPath = join11(cwd, ".gitignore");
4714
+ if (!existsSync9(giPath)) {
4545
4715
  console.log("⚠️ .gitignore: missing (run npx ostacky install --scope local to create)");
4546
4716
  hasWarn = true;
4547
4717
  } else {
4548
- const gi = readFileSync6(giPath, "utf-8");
4718
+ const gi = readFileSync7(giPath, "utf-8");
4549
4719
  const needed = [
4550
4720
  ".opencode/tools/",
4551
4721
  ".opencode/cache/",
@@ -4565,12 +4735,12 @@ async function runDoctorCommand() {
4565
4735
  }
4566
4736
  } catch {}
4567
4737
  try {
4568
- const secPath = join10(cwd, "src", "security.ts");
4569
- if (!existsSync8(secPath)) {
4738
+ const secPath = join11(cwd, "src", "security.ts");
4739
+ if (!existsSync9(secPath)) {
4570
4740
  console.log("⚠️ src/security.ts: missing (source-of-truth)");
4571
4741
  hasWarn = true;
4572
4742
  } else {
4573
- const sec = readFileSync6(secPath, "utf-8");
4743
+ const sec = readFileSync7(secPath, "utf-8");
4574
4744
  const hasSensitiveDefault = sec.includes("SENSITIVE_DEFAULT");
4575
4745
  const hasBashRe = sec.includes("BASH_SENSITIVE_RE");
4576
4746
  const hasIsSensitive = sec.includes("function isSensitive");
@@ -4584,7 +4754,7 @@ async function runDoctorCommand() {
4584
4754
  }
4585
4755
  } catch {}
4586
4756
  try {
4587
- const s = JSON.parse(readFileSync6(statePath, "utf-8"));
4757
+ const s = JSON.parse(readFileSync7(statePath, "utf-8"));
4588
4758
  if (s.sensitiveAccess?.blockedAttempts > 0) {
4589
4759
  console.log(`ℹ️ sensitiveAccess: blockedAttempts includes bash (${s.sensitiveAccess.blockedAttempts})`);
4590
4760
  }
@@ -4597,14 +4767,14 @@ async function runDoctorCommand() {
4597
4767
  async function runStatusCommand(args) {
4598
4768
  const isJson = args.includes("--json");
4599
4769
  const cwd = process.cwd();
4600
- const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4601
- const statePath = join10(opencodeDir, "ostacky-state.json");
4602
- if (!existsSync8(statePath)) {
4770
+ const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4771
+ const statePath = join11(opencodeDir, "ostacky-state.json");
4772
+ if (!existsSync9(statePath)) {
4603
4773
  console.log(isJson ? JSON.stringify({ error: "no state" }) : "No state file");
4604
4774
  return;
4605
4775
  }
4606
4776
  try {
4607
- const parsed = JSON.parse(readFileSync6(statePath, "utf-8"));
4777
+ const parsed = JSON.parse(readFileSync7(statePath, "utf-8"));
4608
4778
  const completed = Object.values(parsed.tasks || {}).filter((t) => t.status === "COMPLETED").length;
4609
4779
  const expected = parsed.expectedTaskCount ?? parsed.expectedTasks?.length ?? Object.keys(parsed.tasks || {}).length;
4610
4780
  const degraded = parsed.degraded ? " degraded" : "";