nexakit 0.1.0 → 0.2.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.
Files changed (113) hide show
  1. package/.claude-plugin/marketplace.json +18 -1
  2. package/README.md +40 -30
  3. package/assets/brand/README.md +0 -2
  4. package/assets/runtime/package-lock.json +2 -2
  5. package/bin/agent-task.js +12 -10
  6. package/bin/agent-toolkit-gemini.js +11 -9
  7. package/bin/agent-toolkit-inbox.js +12 -10
  8. package/bin/agent-toolkit-mcp.js +14 -8
  9. package/bin/agent-toolkit.js +12 -9
  10. package/bin/nexakit.js +12 -9
  11. package/bin/project-memory-mcp.js +12 -9
  12. package/bin/project-memory.js +5 -3
  13. package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
  14. package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
  15. package/catalogs/skills.json +70 -35
  16. package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
  17. package/package.json +5 -3
  18. package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
  19. package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
  20. package/plugins/nexakit-modelo/hooks/register.js +48 -0
  21. package/plugins/nexakit-modelo/hooks/status.js +8 -0
  22. package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
  23. package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
  24. package/plugins/nexakit-panel/README.md +20 -0
  25. package/plugins/nexakit-panel/hooks/hooks.json +1 -0
  26. package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
  27. package/plugins/nexakit-panel/hooks/panel.js +279 -0
  28. package/plugins/nexakit-panel/hooks/register.tsx +248 -0
  29. package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
  30. package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
  31. package/plugins/nexakit-panel/tests/register.test.ts +167 -0
  32. package/plugins/nexakit-panel/types/index.d.ts +41 -0
  33. package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
  34. package/plugins/nexakit-sesion/README.md +12 -0
  35. package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
  36. package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
  37. package/plugins/nexakit-sesion/hooks/register.js +28 -0
  38. package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
  39. package/schemas/skill-catalog.schema.json +9 -2
  40. package/skills/cerrar-memoria/SKILL.md +6 -2
  41. package/skills/cerrar-memoria/references/cierre.md +5 -1
  42. package/skills/elegir-modelo/SKILL.md +68 -0
  43. package/skills/elegir-modelo/modelos.mjs +108 -0
  44. package/skills/gemini/SKILL.md +1 -1
  45. package/skills/project-handoff/SKILL.md +39 -12
  46. package/skills/project-handoff/references/handoff-template.md +24 -17
  47. package/skills/quality-gate/SKILL.md +6 -12
  48. package/skills/revisar/SKILL.md +6 -16
  49. package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
  50. package/skills/revisar-skill/SKILL.md +94 -0
  51. package/skills/revision-cruzada/SKILL.md +53 -475
  52. package/skills/web-design-guidelines/AGENTS.md +156 -0
  53. package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
  54. package/skills/web-design-guidelines/SKILL.md +20 -30
  55. package/src/agent-models.js +53 -13
  56. package/src/brand.js +117 -0
  57. package/src/cli-executable.js +5 -3
  58. package/src/cli.js +16 -9
  59. package/src/connect.js +56 -4
  60. package/src/init.js +9 -1
  61. package/src/install-lock.js +127 -0
  62. package/src/install-registry.js +61 -0
  63. package/src/legacy-repair.js +71 -0
  64. package/src/link-states.js +172 -0
  65. package/src/memory-locator.js +118 -0
  66. package/src/missing-dependencies.js +21 -0
  67. package/src/model-command.js +147 -0
  68. package/src/panel-cli.js +182 -0
  69. package/src/project-root.js +41 -0
  70. package/src/project-signals.js +339 -0
  71. package/src/recommend-report.js +77 -0
  72. package/src/review-instructions.js +211 -0
  73. package/src/review-policy.js +35 -0
  74. package/src/revision-adapters.js +157 -0
  75. package/src/revision-context.js +86 -0
  76. package/src/revision-isolation.js +222 -0
  77. package/src/revision-process.js +103 -0
  78. package/src/revision-provenance.js +89 -0
  79. package/src/revision-snapshot.js +120 -0
  80. package/src/revision.js +242 -0
  81. package/src/setup-apply.js +231 -0
  82. package/src/setup-plan.js +231 -0
  83. package/src/setup-state.js +277 -0
  84. package/src/setup-tui.js +555 -0
  85. package/src/skill-catalog-add.js +91 -0
  86. package/src/skill-catalog-audit.js +26 -27
  87. package/src/skill-files.js +39 -0
  88. package/src/skill-recommendations.js +321 -208
  89. package/src/skill-scan-report.js +14 -0
  90. package/src/skill-scan.js +178 -0
  91. package/src/stable-command.js +53 -0
  92. package/src/stable-install.js +222 -0
  93. package/src/superpowers.js +133 -0
  94. package/src/sync-apply.js +8 -5
  95. package/src/sync-files.js +2 -1
  96. package/src/terminal-text.js +68 -0
  97. package/src/toolkit-cli.js +397 -70
  98. package/src/toolkit-commands-guide.js +53 -39
  99. package/src/toolkit-setup.js +507 -110
  100. package/assets/brand/agent-toolkit-mark-v1.png +0 -0
  101. package/skills/depurar/SKILL.md +0 -69
  102. package/skills/depurar/agents/openai.yaml +0 -7
  103. package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
  104. package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
  105. package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
  106. package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
  107. package/skills/estado/SKILL.md +0 -41
  108. package/skills/programar/SKILL.md +0 -19
  109. package/skills/spec-driven-development/SKILL.md +0 -48
  110. package/skills/spec-driven-development/agents/openai.yaml +0 -7
  111. package/skills/spec-driven-development/references/spec-template.md +0 -55
  112. package/skills/spec-driven-development/references/workflow.md +0 -70
  113. package/src/toolkit-tui.js +0 -961
@@ -20,7 +20,7 @@ El usuario aprobó en el chat la planificación en lectura y preparar el worktre
20
20
  run_codex_plan devolvió «La ejecución fue cancelada; no se llamó al modelo».
21
21
  No se reintentó ni se avanzaron manualmente las etapas del almacén de tareas.
22
22
 
23
- El [plan preparado por la sesión actual](../../planes/2026-10-28-recomendador-skills-plan.md)
23
+ El plan preparado por la sesión actual (en la memoria de Obsidian de NexaKit (`03-IMPLEMENTACION/Planes/2026-10-28-recomendador-skills-plan.md`)
24
24
  permite revisar el encargo. Ejecutar el runner nativo requiere una vista previa
25
25
  vigente y completar la confirmación interactiva del MCP.
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexakit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Asistente por pasos de skills verificadas por SHA-256, coordinación entre agentes y memoria por proyecto para Claude Code y Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,10 +14,12 @@
14
14
  "agent-toolkit-gemini": "./bin/agent-toolkit-gemini.js"
15
15
  },
16
16
  "scripts": {
17
- "test": "node --test",
17
+ "test": "node --test --import ./test-support/git-quiet.js \"test/*.test.js\"",
18
18
  "catalog:validate": "node scripts/validate-skill-catalog.js",
19
19
  "catalog:files": "node scripts/validate-skill-catalog.js --files",
20
- "prepack": "node scripts/prepare-package.js"
20
+ "catalog:add": "node scripts/add-catalog-skill.js",
21
+ "prepack": "node scripts/prepare-package.js",
22
+ "test:mod": "claude plugin test plugins/nexakit-modelo"
21
23
  },
22
24
  "engines": {
23
25
  "node": ">=24"
@@ -0,0 +1 @@
1
+ {"name":"nexakit-modelo","version":"0.1.0","description":"Muestra modelo y esfuerzo observados frente a una recomendación, sin cambiarlos."}
@@ -0,0 +1 @@
1
+ {"modules":["./register.js"]}
@@ -0,0 +1,48 @@
1
+ import { modelStatus } from './status.js';
2
+ async function redraw($, observed, recommendation) {
3
+ const model = await $.session.model();
4
+ $.ui.status(modelStatus(model, observed?.model === model ? observed.effort : null, recommendation));
5
+ }
6
+
7
+ export function register(on) {
8
+ let recommendation = null;
9
+ let observed = null;
10
+ on('session.start', async ($, e, next) => {
11
+ await $.tool.register({
12
+ name: 'nexakit_recomendacion',
13
+ description: 'Registra una recomendación de modelo y esfuerzo para mostrarla, sin cambiar la sesión.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: { model: { type: 'string' }, effort: { type: 'string' }, reason: { type: 'string' } },
17
+ required: ['model', 'effort', 'reason'],
18
+ additionalProperties: false,
19
+ },
20
+ });
21
+ await redraw($, observed, recommendation);
22
+ return next(e);
23
+ });
24
+ on('tool.call', { tool: 'mcp__nexakit-modelo__nexakit_recomendacion' }, async ($, e) => {
25
+ recommendation = { model: e.model, effort: e.effort, reason: e.reason };
26
+ await redraw($, observed, recommendation);
27
+ return { result: 'Recomendación mostrada; no se cambió el modelo ni el esfuerzo.' };
28
+ });
29
+ on('turn.step', async function* ($, e, next) {
30
+ if (!e.agentId) {
31
+ observed = { model: e.model, effort: e.effort ?? null };
32
+ await redraw($, observed, recommendation);
33
+ }
34
+ yield* next(e);
35
+ });
36
+ on('turn.complete', async ($, e, next) => {
37
+ // Effort can change between turns; never present the previous request as current.
38
+ observed = null;
39
+ await redraw($, observed, recommendation);
40
+ return next(e);
41
+ });
42
+ on('session.end', async ($, e, next) => {
43
+ recommendation = null;
44
+ observed = null;
45
+ $.ui.status(undefined);
46
+ return next(e);
47
+ });
48
+ }
@@ -0,0 +1,8 @@
1
+ const family = (model) => model?.match(/(?:^|claude-)(haiku|sonnet|opus|fable)(?:-|$)/u)?.[1] ?? model;
2
+ export function modelStatus(model, effort, recommendation) {
3
+ const actual = `${model ?? 'modelo no informado'} · ${effort ?? 'esfuerzo no informado'}`;
4
+ if (!recommendation) return actual;
5
+ const matches =
6
+ effort && family(model) === family(recommendation.model) && effort === recommendation.effort;
7
+ return `${actual} ${matches ? '✓' : `→ recomendado: ${recommendation.model} · ${recommendation.effort}`}`;
8
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from 'claude-code/testing';
2
+
3
+ describe('nexakit-modelo API', () => {
4
+ test('registers the recommendation tool and reads the session model without a model call', async ($, on) => {
5
+ const tools: string[] = [];
6
+ const statuses: string[] = [];
7
+ on('session.start', ($, e) => ({ cwd: e.cwd }));
8
+ on('session.model', () => ({ value: 'haiku' }));
9
+ on('tool.register', ($, e) => {
10
+ tools.push(e.name);
11
+ return { value: { tool: e.name } };
12
+ });
13
+ on('ui.status', ($, e) => {
14
+ statuses.push(e.text ?? '');
15
+ return { value: undefined };
16
+ });
17
+ await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/test' });
18
+ expect(tools).toContain('nexakit_recomendacion');
19
+ expect(statuses.some((text) => text.includes('esfuerzo no informado'))).toBe(true);
20
+ });
21
+ });
@@ -0,0 +1 @@
1
+ {"name": "nexakit-panel", "version": "0.1.0", "description": "Panel en vivo de la orquestación: modelo líder, subagentes, consultas al asesor, registro de la sesión, contexto y límites.", "types": "./types/index.d.ts", "license": "MIT"}
@@ -0,0 +1,20 @@
1
+ # nexakit-panel
2
+
3
+ Panel de la orquestación de una sesión de Claude Code: el modelo líder y su esfuerzo, las consultas al asesor,
4
+ los subagentes con su modelo, estado y parte del uso, un registro de la sesión, el contexto, los límites de
5
+ 5 horas y semanal, y el modelo frente al recomendado por `nexakit-modelo`. Todo sale de lo que informa Claude
6
+ Code; no muestra datos inventados.
7
+
8
+ ## Uso
9
+
10
+ - `/panel` abre el panel junto al chat. Con al menos 104 columnas dibuja el diagrama completo; si no, una lista.
11
+ - `/panel-demo` alterna datos de ejemplo, marcados como ficticios, para ver el diseño.
12
+ - Para verlo siempre completo, abre `nexakit panel` en otra división de la terminal: lee la foto que este mod
13
+ guarda en `~/.nexakit/panel/` y la dibuja a pantalla completa.
14
+
15
+ La foto se guarda en tu disco con los secretos evidentes tachados (variables `*TOKEN*`, `*KEY*`, `*SECRET*`,
16
+ `*PASSWORD*`, cabeceras `Bearer` y opciones `--password`).
17
+
18
+ ## Pruebas
19
+
20
+ `claude plugin test plugins/nexakit-panel`
@@ -0,0 +1 @@
1
+ {"modules":["./register.tsx"]}
@@ -0,0 +1,382 @@
1
+ // Dibuja la orquesta carácter a carácter, en posiciones fijas, como una pantalla de terminal de 104 columnas.
2
+ // Devuelve filas de tramos con estilo; la terminal (nexakit panel) y el panel de Claude los pintan igual.
3
+ import {
4
+ alias, barra, barraEsfuerzo, duracion, estadoPorFamilia, etiquetaEnjambre, etiquetaModelo, familia, hora,
5
+ resumenDelegacion, tituloDeOrquesta, tokens,
6
+ } from './panel.js';
7
+
8
+ export const ANCHO = 104;
9
+ export const COLOR = {
10
+ sonnet: '#fb923c', haiku: '#60a5fa', opus: '#a78bfa', fable: '#f472b6', otro: '#9ca3af',
11
+ ok: '#4ade80', error: '#f87171', texto: '#e5e7eb', tenue: '#6b7280', resalte: '#2e2a4a',
12
+ codex: '#2dd4bf', antigravity: '#fbbf24', claude: '#fb923c',
13
+ };
14
+ const NOMBRE_IA = { codex: 'Codex', antigravity: 'Antigravity', claude: 'Claude' };
15
+ const colorIA = (ia) => COLOR[ia] ?? COLOR.otro;
16
+ const REVISION = { x: 84, w: 20, h: 8 };
17
+ const colorDe = (modelo) => COLOR[familia(modelo)] ?? COLOR.otro;
18
+
19
+ // Geometría, tomada de la imagen de referencia.
20
+ const CX = 66; // eje del flujo central
21
+ const ASESOR = { x: 2, w: 26 };
22
+ const LIDER = { w: 34, y: 4, h: 7 };
23
+ const DELEGACION = { w: 52, y: 12, h: 7 };
24
+ const SUB = { xs: [29, 54, 79], w: 24, y: 23, h: 8 };
25
+ const VUELTA = { w: 40, y: 34, h: 4 };
26
+ const REGISTRO_Y = 39;
27
+
28
+ function lienzo(alto) {
29
+ const celdas = Array.from({ length: alto }, () => Array.from({ length: ANCHO }, () => ({ ch: ' ' })));
30
+ const poner = (x, y, txt, estilo = {}) => {
31
+ [...String(txt)].forEach((ch, i) => {
32
+ const xx = x + i;
33
+ if (y >= 0 && y < alto && xx >= 0 && xx < ANCHO) celdas[y][xx] = { ch, ...estilo };
34
+ });
35
+ };
36
+ const corto = (txt, ancho) => ([...String(txt)].length <= ancho ? String(txt) : `${[...String(txt)].slice(0, Math.max(0, ancho - 1)).join('')}…`);
37
+ const centro = (x, w, y, txt, estilo) => {
38
+ const t = corto(txt, w);
39
+ poner(x + Math.floor((w - [...t].length) / 2), y, t, estilo);
40
+ };
41
+ const caja = (x, y, w, h, color, titulo) => {
42
+ const borde = { c: color };
43
+ poner(x, y, `┌${'─'.repeat(w - 2)}┐`, borde);
44
+ for (let i = 1; i < h - 1; i++) { poner(x, y + i, '│', borde); poner(x + w - 1, y + i, '│', borde); }
45
+ poner(x, y + h - 1, `└${'─'.repeat(w - 2)}┘`, borde);
46
+ if (titulo) poner(x + 4, y, `┤ ${titulo} ├`, { c: COLOR.tenue });
47
+ };
48
+ return { celdas, poner, centro, caja, corto };
49
+ }
50
+
51
+ // Junta las celdas contiguas con el mismo estilo en tramos.
52
+ function aTramos(celdas) {
53
+ return celdas.map((fila) => {
54
+ const tramos = [];
55
+ for (const { ch, ...estilo } of fila) {
56
+ const ultimo = tramos.at(-1);
57
+ const clave = JSON.stringify(estilo);
58
+ if (ultimo && ultimo.clave === clave) ultimo.t += ch;
59
+ else tramos.push({ t: ch, ...estilo, clave });
60
+ }
61
+ return tramos.map(({ clave, ...resto }) => resto);
62
+ });
63
+ }
64
+
65
+ export const texto = (filas) => filas.map((tramos) => tramos.map((x) => x.t).join(''));
66
+
67
+ export function dibujar(estado, { ahora, uso, carpeta = '', filasMax, asesorModelo = null, esEjemplo = false, codex = null } = {}) {
68
+ const s = estado;
69
+ const vacio = s.pasos === 0 && s.agentes.length === 0 && s.registro.length === 0;
70
+ const medidores = medidoresDeUso(uso);
71
+
72
+ if (vacio) {
73
+ const L = lienzo(codex ? 8 : 7);
74
+ L.centro(0, ANCHO, 0, 'NEXAKIT · ORQUESTA', { c: COLOR.opus, b: true });
75
+ L.poner(2, 1, '═'.repeat(ANCHO - 4), { c: COLOR.tenue });
76
+ L.centro(0, ANCHO, 3, 'Aún no hay actividad en esta sesión.', { c: COLOR.texto });
77
+ L.centro(0, ANCHO, 4, 'Trabaja con normalidad: este panel se llena solo.', { c: COLOR.tenue });
78
+ if (medidores.length) pintarMedidores(L, 6, medidores);
79
+ if (codex) pintarCodex(L, medidores.length ? 7 : 6, codex, ahora);
80
+ return { filas: aTramos(L.celdas), ancho: ANCHO, alto: codex ? 8 : 7 };
81
+ }
82
+
83
+ // El registro ocupa lo que sobre: entre 1 y 8 líneas (4 por defecto, como la referencia).
84
+ // Filas fijas: hasta el registro, sus dos bordes, una en blanco, la orden, el estado y el uso si lo hay.
85
+ const lineaModelo = s.lider.modelo ? modeloFrenteARecomendado(s) : null;
86
+ const fijas = REGISTRO_Y + 2 + 3 + (medidores.length ? 1 : 0) + (lineaModelo ? 1 : 0) + (codex ? 1 : 0);
87
+ const lineasRegistro = Math.max(1, Math.min(8, filasMax ? filasMax - fijas : 4, s.registro.length || 1));
88
+ const alto = fijas + lineasRegistro;
89
+ const L = lienzo(alto);
90
+ const r = resumenDelegacion(s.agentes);
91
+ const lider = etiquetaModelo(s.lider.modelo);
92
+ const cLider = colorDe(s.lider.modelo);
93
+ const enjambre = etiquetaEnjambre(s.agentes);
94
+ const cEnjambre = s.agentes.length ? colorDe([...s.agentes].reverse().find((a) => etiquetaModelo(a.modelo) === enjambre)?.modelo) : COLOR.haiku;
95
+
96
+ // ---- Cabecera
97
+ const titulo = tituloDeOrquesta(s);
98
+ const x0 = Math.floor((ANCHO - [...titulo].length) / 2);
99
+ let x = x0;
100
+ for (const [i, parte] of titulo.split(' · ').entries()) {
101
+ const sep = i === 0 ? '' : ' · ';
102
+ L.poner(x, 0, sep, { c: COLOR.tenue }); x += sep.length;
103
+ const color = /DIRIGE/.test(parte) ? cLider : /ENJAMBRE/.test(parte) ? cEnjambre : COLOR.texto;
104
+ L.poner(x, 0, parte, { c: color, b: true }); x += [...parte].length;
105
+ }
106
+ L.poner(2, 1, '═'.repeat(ANCHO - 4), { c: COLOR.tenue });
107
+ if (esEjemplo) L.centro(0, ANCHO, 3, 'EJEMPLO · datos ficticios para ver el diseño', { c: '#facc15', b: true });
108
+ const leyenda = [];
109
+ if (s.lider.modelo) leyenda.push([`${lider.toLowerCase()} · líder`, cLider]);
110
+ if (enjambre) leyenda.push([`${enjambre.toLowerCase()} · enjambre`, cEnjambre]);
111
+ leyenda.push(['delegación', COLOR.ok], ['asesor', COLOR.opus]);
112
+ const revision = (s.revisiones ?? []).at(-1);
113
+ if (revision) leyenda.push([`${revision.ia} · revisa`, colorIA(revision.ia)]);
114
+ x = 4;
115
+ for (const [txt, color] of leyenda) { L.poner(x, 2, '■', { c: color }); L.poner(x + 2, 2, txt, { c: COLOR.texto }); x += [...txt].length + 7; }
116
+
117
+ // ---- Líder
118
+ const lx = CX - LIDER.w / 2;
119
+ L.caja(lx, LIDER.y, LIDER.w, LIDER.h, cLider);
120
+ L.centro(lx + 1, LIDER.w - 2, LIDER.y + 1, `${lider} · sesión principal`, { c: cLider, b: true });
121
+ L.centro(lx + 1, LIDER.w - 2, LIDER.y + 2, `pasos ${s.pasos} · herramientas ${s.lider.herramientas ?? 0}`, { c: COLOR.texto });
122
+ L.centro(lx + 1, LIDER.w - 2, LIDER.y + 3, `leídos ${tokens(s.lider.leidos)} · escritos ${tokens(s.lider.escritos)}`, { c: COLOR.texto });
123
+ const esf = `esfuerzo ${barraEsfuerzo(s.lider.esfuerzo)} ${s.lider.esfuerzo ?? ''}`.trimEnd();
124
+ L.centro(lx + 1, LIDER.w - 2, LIDER.y + 4, esf, { c: COLOR.texto });
125
+ L.centro(lx + 1, LIDER.w - 2, LIDER.y + 5, `--model ${alias(s.lider.modelo)}`, { c: COLOR.tenue });
126
+
127
+ // ---- Revisión externa (a la derecha del líder)
128
+ if (revision) {
129
+ const c = colorIA(revision.ia);
130
+ L.poner(lx + LIDER.w, LIDER.y + 3, '▶', { c });
131
+ L.caja(REVISION.x, LIDER.y, REVISION.w, REVISION.h, c);
132
+ const dentro = (y, txt, estilo) => L.centro(REVISION.x + 1, REVISION.w - 2, y, txt, estilo);
133
+ dentro(LIDER.y + 1, 'REVISIÓN', { c, b: true });
134
+ dentro(LIDER.y + 2, NOMBRE_IA[revision.ia] ?? revision.ia, { c });
135
+ dentro(LIDER.y + 3, revision.respondio ?? revision.modelo ?? 'modelo ?', { c: COLOR.texto });
136
+ dentro(LIDER.y + 4, revision.esfuerzo ? `esfuerzo ${revision.esfuerzo}` : '', { c: COLOR.tenue });
137
+ const t = duracion((revision.fin ?? ahora) - revision.inicio);
138
+ const est = revision.estado === 'lista' ? [`✓ lista · ${t}`, COLOR.ok] : revision.estado === 'error' ? ['✗ falló', COLOR.error] : [`◐ revisando ${t}`, c];
139
+ dentro(LIDER.y + 5, est[0], { c: est[1] });
140
+ const total = (revision.entrada ?? 0) + (revision.salida ?? 0);
141
+ dentro(LIDER.y + 6, total ? `tokens ${tokens(total)}` : 'tokens —', { c: COLOR.tenue });
142
+ }
143
+
144
+ // ---- Delegación
145
+ const dx = CX - DELEGACION.w / 2;
146
+ L.poner(CX, LIDER.y + LIDER.h, '┊', { c: COLOR.tenue });
147
+ L.caja(dx, DELEGACION.y, DELEGACION.w, DELEGACION.h, COLOR.ok);
148
+ L.poner(dx + 2, DELEGACION.y + 1, 'DELEGACIÓN · subagentes', { c: COLOR.ok, b: true });
149
+ L.poner(dx + DELEGACION.w - 3 - String(r.total).length, DELEGACION.y + 1, String(r.total), { c: COLOR.texto, b: true });
150
+ const filaD = (y, nombre, n, color) => {
151
+ L.poner(dx + 2, y, nombre, { c: COLOR.texto });
152
+ L.poner(dx + 15, y, barra(r.total ? n / r.total : 0, 22), { c: color });
153
+ L.poner(dx + 39, y, `${n}/${r.total}`, { c: COLOR.texto });
154
+ };
155
+ filaD(DELEGACION.y + 2, 'listos', r.listos, COLOR.ok);
156
+ filaD(DELEGACION.y + 3, 'corriendo', r.corriendo, cEnjambre);
157
+ filaD(DELEGACION.y + 4, 'con error', r.errores, COLOR.error);
158
+ const tokSub = s.agentes.reduce((n, a) => n + a.leidos, 0);
159
+ L.poner(dx + 2, DELEGACION.y + 5, `herramientas ${r.herramientas} · tokens leídos ${tokens(tokSub)}`, { c: COLOR.ok });
160
+
161
+ // ---- Delegar y subagentes
162
+ const dy = DELEGACION.y + DELEGACION.h;
163
+ L.poner(CX, dy, '┊', { c: COLOR.tenue });
164
+ const frase = `delegar a subagentes · ${enjambre ?? '—'}`;
165
+ const fx = CX - Math.floor([...frase].length / 2);
166
+ L.poner(fx, dy + 1, 'delegar a subagentes · ', { c: COLOR.texto });
167
+ L.poner(fx + 'delegar a subagentes · '.length, dy + 1, enjambre ?? '—', { c: cEnjambre, b: true });
168
+ const visibles = s.agentes.slice(-3);
169
+ const centros = SUB.xs.slice(0, Math.max(1, visibles.length)).map((x1) => x1 + SUB.w / 2);
170
+ const by = SUB.y - 2;
171
+ if (visibles.length > 1) {
172
+ L.poner(centros[0], by, `┌${'─'.repeat(centros.at(-1) - centros[0] - 1)}┐`, { c: COLOR.tenue });
173
+ L.poner(CX, by, centros.includes(CX) ? '┴' : '┴', { c: COLOR.tenue });
174
+ } else {
175
+ L.poner(centros[0], by, '┊', { c: COLOR.tenue });
176
+ }
177
+ centros.forEach((cx, i) => L.poner(cx, by + 1, i === 1 ? '●' : '▼', { c: cEnjambre }));
178
+ const totalSub = visibles.reduce((n, a) => n + a.leidos, 0);
179
+ visibles.forEach((a, i) => {
180
+ const x1 = SUB.xs[i];
181
+ const c = colorDe(a.modelo);
182
+ L.caja(x1, SUB.y, SUB.w, SUB.h, c);
183
+ const dentro = (y, txt, estilo) => L.centro(x1 + 1, SUB.w - 2, y, txt, estilo);
184
+ dentro(SUB.y + 1, a.nombre, { c: COLOR.texto, b: true });
185
+ dentro(SUB.y + 2, etiquetaModelo(a.modelo), { c });
186
+ const parte = totalSub ? a.leidos / totalSub : 0;
187
+ L.poner(x1 + 2, SUB.y + 3, barra(parte, 13), { c, d: a.estado === 'corriendo' && parte === 0 });
188
+ const pct = `${Math.round(parte * 100)}%`;
189
+ L.poner(x1 + SUB.w - 2 - pct.length, SUB.y + 3, pct, { c: COLOR.texto });
190
+ dentro(SUB.y + 4, a.ultimo || '—', { c: COLOR.texto });
191
+ const est = a.estado === 'listo' ? ['✓ listo', COLOR.ok] : a.estado === 'error' ? ['✗ con error', COLOR.error] : ['◐ corriendo', c];
192
+ dentro(SUB.y + 5, est[0], { c: est[1] });
193
+ dentro(SUB.y + 6, `leídos ${tokens(a.leidos)} · ${duracion((a.fin ?? ahora) - a.inicio)}`, { c: COLOR.tenue });
194
+ });
195
+ if (s.agentes.length > visibles.length) L.poner(SUB.xs[0], SUB.y + SUB.h, `+${s.agentes.length - visibles.length} más`, { c: COLOR.tenue });
196
+
197
+ // ---- Vuelta al líder
198
+ const my = SUB.y + SUB.h;
199
+ if (visibles.length > 1) {
200
+ centros.forEach((cx) => L.poner(cx, my, '│', { c: COLOR.tenue }));
201
+ L.poner(centros[0], my + 1, `└${'─'.repeat(centros.at(-1) - centros[0] - 1)}┘`, { c: COLOR.tenue });
202
+ L.poner(CX, my + 1, '●', { c: COLOR.texto });
203
+ } else {
204
+ L.poner(centros[0], my, '┊', { c: COLOR.tenue });
205
+ }
206
+ L.poner(CX, my + 2, '┊', { c: COLOR.tenue });
207
+ const vx = CX - VUELTA.w / 2;
208
+ L.caja(vx, VUELTA.y, VUELTA.w, VUELTA.h, cLider);
209
+ L.centro(vx + 1, VUELTA.w - 2, VUELTA.y + 1, `de vuelta en ${lider} · líder`, { c: cLider, b: true });
210
+ L.centro(vx + 1, VUELTA.w - 2, VUELTA.y + 2, r.corriendo > 0 ? `esperando ${r.corriendo} ${r.corriendo === 1 ? 'subagente' : 'subagentes'}` : 'subagentes terminados', { c: COLOR.texto });
211
+
212
+ // ---- Asesor (columna izquierda, de arriba a la vuelta al líder)
213
+ const ay = LIDER.y;
214
+ const ah = VUELTA.y + VUELTA.h - ay;
215
+ L.caja(ASESOR.x, ay, ASESOR.w, ah, COLOR.opus);
216
+ L.centro(ASESOR.x + 1, ASESOR.w - 2, ay + 1, asesorModelo ? etiquetaModelo(asesorModelo).toUpperCase() : 'ASESOR', { c: COLOR.opus, b: true });
217
+ L.centro(ASESOR.x + 1, ASESOR.w - 2, ay + 2, s.asesor.llamadas > 0 ? 'asesor · consultado' : 'asesor · en espera', { c: COLOR.opus });
218
+ const consultas = s.registro.filter((e) => e.quien === 'asesor').slice(-6);
219
+ const primera = LIDER.y + 3;
220
+ const ultima = VUELTA.y + 1;
221
+ const filasC = consultas.length <= 1 ? [primera]
222
+ : consultas.map((_, i) => (i === 0 ? primera : i === consultas.length - 1 ? ultima : Math.round(primera + 5 + ((ultima - primera - 10) * i) / (consultas.length - 1))));
223
+ const ocupadas = new Set(filasC.flatMap((y) => [y, y + 1]));
224
+ if (consultas.length > 1) for (let y = filasC[0] + 1; y < filasC.at(-1); y++) if (!ocupadas.has(y)) L.poner(ASESOR.x + 2, y, '┊', { c: COLOR.tenue });
225
+ consultas.forEach((c, i) => {
226
+ const y = filasC[i];
227
+ const final = i === consultas.length - 1;
228
+ const estilo = final ? { c: COLOR.opus, b: true, bg: COLOR.resalte } : { c: COLOR.texto };
229
+ if (final) L.poner(ASESOR.x + 1, y, ' '.repeat(ASESOR.w - 2), { bg: COLOR.resalte });
230
+ L.poner(ASESOR.x + 2, y, `${final ? '◆' : '◇'} ${hora(c.t)} consulta`, estilo);
231
+ if (c.ms) L.poner(ASESOR.x + 4, y + 1, `duró ${duracion(c.ms)}`, { c: COLOR.tenue });
232
+ });
233
+ if (consultas.length > 0) {
234
+ L.poner(ASESOR.x + ASESOR.w + 1, filasC[0], `${'┈'.repeat(lx - (ASESOR.x + ASESOR.w + 1) - 2)}▶`, { c: COLOR.tenue });
235
+ if (consultas.length > 1) L.poner(ASESOR.x + ASESOR.w + 1, filasC.at(-1), `${'┈'.repeat(vx - (ASESOR.x + ASESOR.w + 1) - 2)}▶`, { c: COLOR.tenue });
236
+ } else {
237
+ L.centro(ASESOR.x + 1, ASESOR.w - 2, primera, 'sin consultas', { c: COLOR.tenue });
238
+ }
239
+ const sy = ay + ah - 5;
240
+ L.poner(ASESOR.x + 2, sy, 'consultas', { c: COLOR.texto });
241
+ L.poner(ASESOR.x + ASESOR.w - 3 - String(s.asesor.llamadas).length, sy, String(s.asesor.llamadas), { c: COLOR.opus, b: true });
242
+ L.poner(ASESOR.x + 2, sy + 1, 'tiempo', { c: COLOR.texto });
243
+ const t = duracion(s.asesor.tiempo ?? 0);
244
+ L.poner(ASESOR.x + ASESOR.w - 2 - [...t].length, sy + 1, t, { c: COLOR.opus });
245
+
246
+ // ---- Registro de sesión
247
+ const ry = REGISTRO_Y;
248
+ L.caja(2, ry, ANCHO - 4, lineasRegistro + 2, COLOR.tenue, 'registro de sesión');
249
+ const quienColor = (q) => (q === 'líder' ? cLider : q === 'asesor' ? COLOR.opus : colorDe(s.agentes.find((a) => a.nombre === q)?.modelo));
250
+ s.registro.slice(-lineasRegistro).forEach((e, i) => {
251
+ const y = ry + 1 + i;
252
+ L.poner(4, y, hora(e.t), { c: COLOR.tenue });
253
+ L.poner(14, y, L.corto(e.quien, 14), { c: quienColor(e.quien), b: true });
254
+ const [herramienta, ...resto] = e.texto.split(' · ');
255
+ if (resto.length && /^[A-Z]/.test(herramienta) && !/^(empieza|terminó|consultado)/.test(e.texto)) {
256
+ L.poner(30, y, L.corto(herramienta, 14), { c: COLOR.texto });
257
+ L.poner(46, y, `→ ${L.corto(resto.join(' · '), 52)}`, { c: COLOR.texto });
258
+ } else {
259
+ L.poner(30, y, L.corto(e.texto, 68), { c: COLOR.texto });
260
+ }
261
+ });
262
+
263
+ // ---- Orden y estado
264
+ const oy = ry + lineasRegistro + 3;
265
+ const prefijo = `~/${carpeta || ''} $ `.replace('~/ $', '~ $');
266
+ L.poner(2, oy, prefijo, { c: COLOR.tenue });
267
+ L.poner(2 + [...prefijo].length, oy, `claude --model ${alias(s.lider.modelo)}${asesorModelo ? ` --advisor ${alias(asesorModelo)}` : ''}`, { c: COLOR.texto, b: true });
268
+ x = 2;
269
+ for (const etiqueta of estadoPorFamilia(s)) {
270
+ const m = etiqueta.match(/^(.*?) \[(.*)\]$/u);
271
+ L.poner(x, oy + 1, `${m[1]} `, { c: COLOR.tenue }); x += m[1].length + 1;
272
+ const c = /enjambre/.test(m[2]) ? cEnjambre : /consultado/.test(m[2]) ? COLOR.opus : /error/.test(m[2]) ? COLOR.error : COLOR.ok;
273
+ L.poner(x, oy + 1, `[${m[2]}]`, { c, b: true }); x += m[2].length + 2 + 3;
274
+ }
275
+ let ly = oy + 2;
276
+ if (medidores.length) pintarMedidores(L, ly++, medidores);
277
+ if (lineaModelo) {
278
+ L.poner(2, ly, lineaModelo.actual, { c: COLOR.texto });
279
+ L.poner(2 + [...lineaModelo.actual].length, ly, lineaModelo.cola, { c: lineaModelo.coincide ? COLOR.ok : '#facc15', b: !lineaModelo.coincide });
280
+ ly++;
281
+ }
282
+ if (codex) pintarCodex(L, ly, codex, ahora);
283
+
284
+ return { filas: aTramos(L.celdas), ancho: ANCHO, alto };
285
+ }
286
+
287
+ // ---- Uso y modelo, como las líneas de estado de nexakit-sesion y nexakit-modelo, pero con barras
288
+ const esNumero = (v) => typeof v === 'number' && Number.isFinite(v);
289
+ export function medidoresDeUso(uso) {
290
+ const m = [];
291
+ if (esNumero(uso?.context?.percent)) m.push(['contexto', uso.context.percent]);
292
+ for (const l of uso?.rateLimits ?? []) {
293
+ if (!esNumero(l?.percentUsed)) continue;
294
+ if (l.kind === 'five_hour') m.push(['5 h', l.percentUsed]);
295
+ if (l.kind === 'seven_day') m.push(['semana', l.percentUsed]);
296
+ }
297
+ return m;
298
+ }
299
+ const colorDeNivel = (p) => (p >= 85 ? COLOR.error : p >= 60 ? '#facc15' : COLOR.ok);
300
+ function pintarMedidores(L, y, medidores) {
301
+ let x = 2;
302
+ for (const [nombre, p] of medidores) {
303
+ L.poner(x, y, nombre, { c: COLOR.tenue }); x += [...nombre].length + 1;
304
+ L.poner(x, y, barra(p / 100, 10), { c: colorDeNivel(p) }); x += 11;
305
+ const pct = `${Math.round(p)} %`;
306
+ L.poner(x, y, pct, { c: COLOR.texto, b: true }); x += pct.length + 4;
307
+ }
308
+ }
309
+ const hace = (ms) => (ms < 60_000 ? `hace ${Math.max(0, Math.round(ms / 1000))} s` : ms < 3_600_000 ? `hace ${Math.round(ms / 60_000)} min` : `hace ${Math.round(ms / 3_600_000)} h`);
310
+ // Uso de Codex leído de su propia sesión: tokens, límites y cuándo fue la última actividad.
311
+ function pintarCodex(L, y, codex, ahora) {
312
+ let x = 2;
313
+ const cabeza = `codex ${codex.modelo ?? '?'}${codex.esfuerzo ? ` · ${codex.esfuerzo}` : ''}`;
314
+ L.poner(x, y, cabeza, { c: COLOR.codex, b: true }); x += [...cabeza].length + 2;
315
+ const tok = `leídos ${tokens(codex.leidos)} · escritos ${tokens(codex.escritos)}`;
316
+ L.poner(x, y, tok, { c: COLOR.texto }); x += [...tok].length + 2;
317
+ for (const [nombre, p] of [['5 h', codex.limite5h], ['semana', codex.limiteSemana]]) {
318
+ if (!esNumero(p)) continue;
319
+ L.poner(x, y, nombre, { c: COLOR.tenue }); x += [...nombre].length + 1;
320
+ L.poner(x, y, barra(p / 100, 5), { c: colorDeNivel(p) }); x += 6;
321
+ const pct = `${Math.round(p)} %`;
322
+ L.poner(x, y, pct, { c: COLOR.texto, b: true }); x += pct.length + 2;
323
+ }
324
+ if (esNumero(codex.actualizado)) L.poner(x, y, hace(ahora - codex.actualizado), { c: COLOR.tenue });
325
+ }
326
+
327
+ export function modeloFrenteARecomendado(s) {
328
+ const actual = `modelo ${s.lider.modelo} · ${s.lider.esfuerzo ?? 'esfuerzo ?'}`;
329
+ const rec = s.recomendacion;
330
+ if (!rec) return { actual, cola: '', coincide: true };
331
+ const coincide = familia(s.lider.modelo) === familia(rec.modelo) && s.lider.esfuerzo === rec.esfuerzo;
332
+ return { actual, cola: coincide ? ' ✓' : ` → recomendado: ${rec.modelo} · ${rec.esfuerzo}`, coincide };
333
+ }
334
+
335
+ // Para el panel de Claude: los espacios sin estilo se pegan al tramo vecino, para no dejar textos solo de espacios.
336
+ export function tramosParaPanel(filas) {
337
+ return filas.map((tramos) => {
338
+ const salida = [];
339
+ let pendiente = '';
340
+ for (const t of tramos) {
341
+ const soloEspacio = /^ +$/u.test(t.t) && !t.bg && !t.c;
342
+ if (soloEspacio) {
343
+ if (salida.length) salida.at(-1).t += t.t;
344
+ else pendiente += t.t;
345
+ continue;
346
+ }
347
+ salida.push({ ...t, t: pendiente + t.t });
348
+ pendiente = '';
349
+ }
350
+ if (pendiente) salida.push({ t: pendiente });
351
+ return salida;
352
+ });
353
+ }
354
+
355
+ // ---- A ANSI para la terminal
356
+ const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
357
+ const a256 = ([r, g, b]) => 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
358
+ const a16 = ([r, g, b]) => {
359
+ const brillo = (r + g + b) / 3 > 140 ? 60 : 0;
360
+ return 30 + brillo + (r > 127 ? 1 : 0) + (g > 127 ? 2 : 0) + (b > 127 ? 4 : 0);
361
+ };
362
+ function sgr(tramo, nivel) {
363
+ const partes = [];
364
+ if (tramo.b) partes.push('1');
365
+ if (tramo.d) partes.push('2');
366
+ if (tramo.c) {
367
+ const c = rgb(tramo.c);
368
+ partes.push(nivel === 'truecolor' ? `38;2;${c.join(';')}` : nivel === '256' ? `38;5;${a256(c)}` : String(a16(c)));
369
+ }
370
+ if (tramo.bg) {
371
+ const c = rgb(tramo.bg);
372
+ partes.push(nivel === 'truecolor' ? `48;2;${c.join(';')}` : nivel === '256' ? `48;5;${a256(c)}` : String(a16(c) + 10));
373
+ }
374
+ return partes.join(';');
375
+ }
376
+ export function aAnsi(filas, nivel) {
377
+ return filas.map((tramos) => tramos.map((t) => {
378
+ if (nivel === 'none') return t.t;
379
+ const codigo = sgr(t, nivel);
380
+ return codigo ? `\x1b[${codigo}m${t.t}\x1b[0m` : t.t;
381
+ }).join('')).join('\n');
382
+ }