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.
- package/.claude-plugin/marketplace.json +18 -1
- package/README.md +40 -30
- package/assets/brand/README.md +0 -2
- package/assets/runtime/package-lock.json +2 -2
- package/bin/agent-task.js +12 -10
- package/bin/agent-toolkit-gemini.js +11 -9
- package/bin/agent-toolkit-inbox.js +12 -10
- package/bin/agent-toolkit-mcp.js +14 -8
- package/bin/agent-toolkit.js +12 -9
- package/bin/nexakit.js +12 -9
- package/bin/project-memory-mcp.js +12 -9
- package/bin/project-memory.js +5 -3
- package/{skills/depurar → catalogs/skills/vercel-composition-patterns}/LICENSE.txt +6 -6
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills.json +70 -35
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +1 -1
- package/package.json +5 -3
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/schemas/skill-catalog.schema.json +9 -2
- package/skills/cerrar-memoria/SKILL.md +6 -2
- package/skills/cerrar-memoria/references/cierre.md +5 -1
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/gemini/SKILL.md +1 -1
- package/skills/project-handoff/SKILL.md +39 -12
- package/skills/project-handoff/references/handoff-template.md +24 -17
- package/skills/quality-gate/SKILL.md +6 -12
- package/skills/revisar/SKILL.md +6 -16
- package/skills/{desarrollo-con-pruebas/LICENSE.txt → revisar-skill/LICENSE-find-skills.txt} +5 -5
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +53 -475
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/{spec-driven-development → web-design-guidelines}/LICENSE.txt +1 -6
- package/skills/web-design-guidelines/SKILL.md +20 -30
- package/src/agent-models.js +53 -13
- package/src/brand.js +117 -0
- package/src/cli-executable.js +5 -3
- package/src/cli.js +16 -9
- package/src/connect.js +56 -4
- package/src/init.js +9 -1
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/link-states.js +172 -0
- package/src/memory-locator.js +118 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/panel-cli.js +182 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +26 -27
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +321 -208
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +8 -5
- package/src/sync-files.js +2 -1
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +397 -70
- package/src/toolkit-commands-guide.js +53 -39
- package/src/toolkit-setup.js +507 -110
- package/assets/brand/agent-toolkit-mark-v1.png +0 -0
- package/skills/depurar/SKILL.md +0 -69
- package/skills/depurar/agents/openai.yaml +0 -7
- package/skills/depurar/references/tecnicas-de-diagnostico.md +0 -37
- package/skills/desarrollo-con-pruebas/SKILL.md +0 -55
- package/skills/desarrollo-con-pruebas/agents/openai.yaml +0 -7
- package/skills/desarrollo-con-pruebas/references/buenas-pruebas.md +0 -33
- package/skills/estado/SKILL.md +0 -41
- package/skills/programar/SKILL.md +0 -19
- package/skills/spec-driven-development/SKILL.md +0 -48
- package/skills/spec-driven-development/agents/openai.yaml +0 -7
- package/skills/spec-driven-development/references/spec-template.md +0 -55
- package/skills/spec-driven-development/references/workflow.md +0 -70
- package/src/toolkit-tui.js +0 -961
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Lógica pura del panel: reduce los eventos de la sesión a un estado que el panel dibuja. Sin datos inventados:
|
|
2
|
+
// todo sale de lo que el motor informa (pasos, subagentes, llamadas del servidor, herramientas).
|
|
3
|
+
export const MAX_REGISTRO = 60;
|
|
4
|
+
export const MAX_AGENTES = 8;
|
|
5
|
+
|
|
6
|
+
export const crear = () => ({
|
|
7
|
+
lider: { modelo: null, esfuerzo: null, leidos: 0, escritos: 0, herramientas: 0 },
|
|
8
|
+
agentes: [],
|
|
9
|
+
asesor: { llamadas: 0, ultimo: null, tiempo: 0 },
|
|
10
|
+
registro: [],
|
|
11
|
+
pasos: 0,
|
|
12
|
+
recomendacion: null,
|
|
13
|
+
revisiones: [],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function familia(modelo) {
|
|
17
|
+
const m = String(modelo ?? '').toLowerCase();
|
|
18
|
+
for (const nombre of ['haiku', 'sonnet', 'opus', 'fable']) if (m.includes(nombre)) return nombre;
|
|
19
|
+
return 'otro';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// «claude-sonnet-5-5» → «Sonnet 5.5»; ignora la fecha final de los identificadores largos.
|
|
23
|
+
export function etiquetaModelo(modelo) {
|
|
24
|
+
if (!modelo) return 'modelo ?';
|
|
25
|
+
const m = String(modelo).toLowerCase().replace(/^claude-/u, '');
|
|
26
|
+
const partes = m.match(/^(haiku|sonnet|opus|fable)(?:-(\d{1,2})(?:-(\d{1,2}))?)?(?:-|$)/u);
|
|
27
|
+
if (!partes) return String(modelo);
|
|
28
|
+
const nombre = partes[1][0].toUpperCase() + partes[1].slice(1);
|
|
29
|
+
if (!partes[2]) return nombre;
|
|
30
|
+
return `${nombre} ${partes[2]}${partes[3] ? `.${partes[3]}` : ''}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const NIVELES = { low: 1, medium: 2, high: 3, xhigh: 4, max: 4 };
|
|
34
|
+
export function barraEsfuerzo(esfuerzo) {
|
|
35
|
+
const llenos = NIVELES[esfuerzo] ?? 0;
|
|
36
|
+
return '▮'.repeat(llenos) + '▯'.repeat(4 - llenos);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function duracion(ms) {
|
|
40
|
+
if (!Number.isFinite(ms) || ms < 1000) return '0 s';
|
|
41
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)} s`;
|
|
42
|
+
return `${Math.floor(ms / 60_000)} min ${Math.round((ms % 60_000) / 1000)} s`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hora(ms) {
|
|
46
|
+
const d = new Date(ms);
|
|
47
|
+
return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, '0')).join(':');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const conRegistro = (estado, entrada) => ({ ...estado, registro: [...estado.registro, entrada].slice(-MAX_REGISTRO) });
|
|
51
|
+
const nombreDe = (estado, id) => (id ? (estado.agentes.find((a) => a.id === id)?.nombre ?? 'agente') : 'líder');
|
|
52
|
+
|
|
53
|
+
export function registrarPaso(estado, { modelo, esfuerzo, agentId }) {
|
|
54
|
+
if (agentId) {
|
|
55
|
+
return { ...estado, agentes: estado.agentes.map((a) => (a.id === agentId && modelo ? { ...a, modelo } : a)) };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...estado,
|
|
59
|
+
pasos: estado.pasos + 1,
|
|
60
|
+
lider: { ...estado.lider, modelo: modelo ?? estado.lider.modelo, esfuerzo: esfuerzo ?? estado.lider.esfuerzo },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function iniciarAgente(estado, { id, nombre, modelo, at }) {
|
|
65
|
+
const agente = { id, nombre, modelo, estado: 'corriendo', inicio: at, fin: null, herramientas: 0, leidos: 0, escritos: 0, ultimo: '' };
|
|
66
|
+
return conRegistro(
|
|
67
|
+
{ ...estado, agentes: [...estado.agentes, agente].slice(-MAX_AGENTES) },
|
|
68
|
+
{ t: at, quien: nombre, texto: `empieza · ${etiquetaModelo(modelo)}` },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function terminarAgente(estado, { id, ok, at }) {
|
|
73
|
+
const existe = estado.agentes.some((a) => a.id === id);
|
|
74
|
+
if (!existe) return estado;
|
|
75
|
+
const agentes = estado.agentes.map((a) => (a.id === id ? { ...a, estado: ok ? 'listo' : 'error', fin: at } : a));
|
|
76
|
+
return conRegistro({ ...estado, agentes }, { t: at, quien: nombreDe(estado, id), texto: ok ? 'terminó' : 'terminó con error' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function registrarHerramienta(estado, { tool, detalle, agentId, at }) {
|
|
80
|
+
const texto = detalle ? `${tool} · ${detalle}` : tool;
|
|
81
|
+
const agentes = agentId
|
|
82
|
+
? estado.agentes.map((a) => (a.id === agentId ? { ...a, herramientas: a.herramientas + 1, ultimo: texto } : a))
|
|
83
|
+
: estado.agentes;
|
|
84
|
+
const lider = agentId ? estado.lider : { ...estado.lider, herramientas: (estado.lider.herramientas ?? 0) + 1 };
|
|
85
|
+
return conRegistro({ ...estado, agentes, lider }, { t: at, quien: nombreDe(estado, agentId), texto });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function registrarAsesor(estado, { llamadas, at, tiempo = 0 }) {
|
|
89
|
+
if (!llamadas) return estado;
|
|
90
|
+
return conRegistro(
|
|
91
|
+
{ ...estado, asesor: { llamadas: estado.asesor.llamadas + llamadas, ultimo: at, tiempo: (estado.asesor.tiempo ?? 0) + tiempo } },
|
|
92
|
+
{ t: at, quien: 'asesor', texto: `consultado ${llamadas} ${llamadas === 1 ? 'vez' : 'veces'}`, ms: tiempo },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const esNumero = (valor) => typeof valor === 'number' && Number.isFinite(valor);
|
|
97
|
+
// Línea de uso: solo lo que se conoce; undefined si no hay nada que mostrar.
|
|
98
|
+
export function textoDeUso(uso) {
|
|
99
|
+
const partes = [];
|
|
100
|
+
if (esNumero(uso?.context?.percent)) partes.push(`contexto ${Math.round(uso.context.percent)} %`);
|
|
101
|
+
for (const limite of uso?.rateLimits ?? []) {
|
|
102
|
+
if (!esNumero(limite?.percentUsed)) continue;
|
|
103
|
+
if (limite.kind === 'five_hour') partes.push(`5 h ${Math.round(limite.percentUsed)} %`);
|
|
104
|
+
if (limite.kind === 'seven_day') partes.push(`semana ${Math.round(limite.percentUsed)} %`);
|
|
105
|
+
}
|
|
106
|
+
return partes.length > 0 ? partes.join(' · ') : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Diagrama ---------------------------------------------------------------------------------------------------
|
|
110
|
+
export function barra(fraccion, ancho) {
|
|
111
|
+
const valor = Number.isFinite(fraccion) ? Math.min(1, Math.max(0, fraccion)) : 0;
|
|
112
|
+
const llenos = Math.round(valor * ancho);
|
|
113
|
+
return '█'.repeat(llenos) + '░'.repeat(ancho - llenos);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resumenDelegacion(agentes) {
|
|
117
|
+
return {
|
|
118
|
+
total: agentes.length,
|
|
119
|
+
corriendo: agentes.filter((a) => a.estado === 'corriendo').length,
|
|
120
|
+
listos: agentes.filter((a) => a.estado === 'listo').length,
|
|
121
|
+
errores: agentes.filter((a) => a.estado === 'error').length,
|
|
122
|
+
herramientas: agentes.reduce((suma, a) => suma + a.herramientas, 0),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Lo que se escribe en --model: la familia si se reconoce, y el texto tal cual si no.
|
|
127
|
+
export function alias(modelo) {
|
|
128
|
+
if (!modelo) return '?';
|
|
129
|
+
const f = familia(modelo);
|
|
130
|
+
return f === 'otro' ? String(modelo) : f;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// La familia de subagentes más usada, en el orden en que aparecieron si hay empate.
|
|
134
|
+
function familiaDominante(agentes) {
|
|
135
|
+
const cuentas = new Map();
|
|
136
|
+
for (const a of agentes) cuentas.set(familia(a.modelo), (cuentas.get(familia(a.modelo)) ?? 0) + 1);
|
|
137
|
+
let mejor = null;
|
|
138
|
+
for (const [nombre, n] of cuentas) if (mejor === null || n > cuentas.get(mejor)) mejor = nombre;
|
|
139
|
+
return mejor;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// «Haiku 5.5» si es la familia de subagentes más usada; null si no hay subagentes.
|
|
143
|
+
export function etiquetaEnjambre(agentes) {
|
|
144
|
+
if (agentes.length === 0) return null;
|
|
145
|
+
const dominante = familiaDominante(agentes);
|
|
146
|
+
const representante = [...agentes].reverse().find((a) => familia(a.modelo) === dominante);
|
|
147
|
+
return etiquetaModelo(representante.modelo);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function tituloDeOrquesta(estado) {
|
|
151
|
+
let titulo = 'NEXAKIT · ORQUESTA';
|
|
152
|
+
if (estado.lider.modelo) titulo += ` · ${etiquetaModelo(estado.lider.modelo).toUpperCase()} DIRIGE`;
|
|
153
|
+
const enjambre = etiquetaEnjambre(estado.agentes);
|
|
154
|
+
if (enjambre) titulo += ` · ${enjambre.toUpperCase()} EN ENJAMBRE`;
|
|
155
|
+
return titulo;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Una etiqueta por familia de subagentes, el avance de la delegación y el asesor; solo lo que ocurre.
|
|
159
|
+
export function estadoPorFamilia(estado) {
|
|
160
|
+
const partes = [];
|
|
161
|
+
const familias = [];
|
|
162
|
+
for (const a of estado.agentes) if (!familias.includes(familia(a.modelo))) familias.push(familia(a.modelo));
|
|
163
|
+
for (const nombre of familias) {
|
|
164
|
+
const suyos = estado.agentes.filter((a) => familia(a.modelo) === nombre);
|
|
165
|
+
const etiqueta = suyos.some((a) => a.estado === 'corriendo') ? 'enjambre' : suyos.some((a) => a.estado === 'error') ? 'con error' : 'listo';
|
|
166
|
+
partes.push(`${nombre === 'otro' ? 'agente' : nombre} [${etiqueta}]`);
|
|
167
|
+
}
|
|
168
|
+
if (estado.agentes.length > 0) {
|
|
169
|
+
const r = resumenDelegacion(estado.agentes);
|
|
170
|
+
partes.push(`subagentes [${r.total - r.corriendo}/${r.total}]`);
|
|
171
|
+
}
|
|
172
|
+
if (estado.asesor.llamadas > 0) partes.push(`asesor [consultado ${estado.asesor.llamadas}]`);
|
|
173
|
+
const ultimaPorIa = new Map((estado.revisiones ?? []).map((r) => [r.ia, r]));
|
|
174
|
+
for (const [ia, r] of ultimaPorIa) partes.push(`${ia} [${r.estado === 'revisando' ? 'revisando' : r.estado === 'lista' ? 'revisó' : 'falló'}]`);
|
|
175
|
+
return partes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Datos ficticios para ver el diseño sin esperar a que haya actividad real. Siempre se muestran marcados como ejemplo.
|
|
179
|
+
export function ejemplo(ahora) {
|
|
180
|
+
const t = (segundos) => ahora - segundos * 1000;
|
|
181
|
+
let s = { ...registrarPaso(crear(), { modelo: 'claude-sonnet-5-5', esfuerzo: 'high' }), pasos: 24 };
|
|
182
|
+
s = registrarUso(s, { usage: { input_tokens: 8_400, output_tokens: 12_300, cache_read_input_tokens: 660_000, cache_creation_input_tokens: 0 } });
|
|
183
|
+
s = registrarAsesor(s, { llamadas: 1, at: t(95), tiempo: 4_200 });
|
|
184
|
+
s = iniciarAgente(s, { id: 'ej1', nombre: 'search', modelo: 'claude-haiku-5-5', at: t(40) });
|
|
185
|
+
s = iniciarAgente(s, { id: 'ej2', nombre: 'grep', modelo: 'claude-haiku-5-5', at: t(38) });
|
|
186
|
+
s = iniciarAgente(s, { id: 'ej3', nombre: 'audit', modelo: 'claude-haiku-5-5', at: t(36) });
|
|
187
|
+
s = registrarHerramienta(s, { tool: 'Glob', detalle: 'src/**/*.ts', agentId: 'ej1', at: t(31) });
|
|
188
|
+
s = registrarHerramienta(s, { tool: 'Grep', detalle: 'verifyToken', agentId: 'ej2', at: t(28) });
|
|
189
|
+
s = registrarHerramienta(s, { tool: 'Read', detalle: 'src/auth.ts', agentId: 'ej3', at: t(24) });
|
|
190
|
+
s = registrarHerramienta(s, { tool: 'Read', detalle: 'src/session.ts', agentId: 'ej3', at: t(20) });
|
|
191
|
+
s = registrarHerramienta(s, { tool: 'Bash', detalle: 'npm test', agentId: 'ej3', at: t(14) });
|
|
192
|
+
s = registrarUso(s, { agentId: 'ej1', usage: { input_tokens: 41_000, output_tokens: 900, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } });
|
|
193
|
+
s = registrarUso(s, { agentId: 'ej2', usage: { input_tokens: 72_000, output_tokens: 1_400, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } });
|
|
194
|
+
s = registrarUso(s, { agentId: 'ej3', usage: { input_tokens: 230_000, output_tokens: 3_100, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } });
|
|
195
|
+
s = terminarAgente(s, { id: 'ej3', ok: true, at: t(10) });
|
|
196
|
+
s = registrarHerramienta(s, { tool: 'Edit', detalle: 'src/auth.ts', at: t(8) });
|
|
197
|
+
s = registrarAsesor(s, { llamadas: 1, at: t(5), tiempo: 2_800 });
|
|
198
|
+
return s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Uso real de cada paso: leídos = entrada + caché leída + caché escrita; escritos = salida.
|
|
202
|
+
export function registrarUso(estado, { agentId, usage }) {
|
|
203
|
+
if (!usage) return estado;
|
|
204
|
+
const leidos = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
205
|
+
const escritos = usage.output_tokens ?? 0;
|
|
206
|
+
if (agentId) {
|
|
207
|
+
return { ...estado, agentes: estado.agentes.map((a) => (a.id === agentId ? { ...a, leidos: a.leidos + leidos, escritos: a.escritos + escritos } : a)) };
|
|
208
|
+
}
|
|
209
|
+
return { ...estado, lider: { ...estado.lider, leidos: estado.lider.leidos + leidos, escritos: estado.lider.escritos + escritos } };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function tokens(n) {
|
|
213
|
+
if (!Number.isFinite(n) || n < 1000) return String(Math.max(0, Math.round(n || 0)));
|
|
214
|
+
if (n < 1_000_000) {
|
|
215
|
+
const k = n / 1000;
|
|
216
|
+
return `${k < 10 ? Math.round(k * 10) / 10 : Math.round(k)}k`;
|
|
217
|
+
}
|
|
218
|
+
return `${Math.round(n / 100_000) / 10}M`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// La recomendación que registra el mod nexakit-modelo, observada en su llamada.
|
|
222
|
+
export function registrarRecomendacion(estado, { modelo, esfuerzo, motivo }) {
|
|
223
|
+
return { ...estado, recomendacion: { modelo, esfuerzo, motivo } };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Tacha los secretos evidentes de un detalle antes de mostrarlo o guardarlo en disco.
|
|
227
|
+
export function sinSecretos(texto) {
|
|
228
|
+
return String(texto)
|
|
229
|
+
.replace(/\b([A-Za-z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD)[A-Za-z0-9_]*)=([^\s"']+)/giu, '$1=***')
|
|
230
|
+
.replace(/(Bearer\s+)[^\s"']+/giu, '$1***')
|
|
231
|
+
.replace(/(--?(?:password|passwd|token|secret|api-key)[= ])[^\s"']+/giu, '$1***');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- Revisiones que la sesión manda a otra IA con `nexakit revision --con <ia>` -----------------------------------
|
|
235
|
+
export function esRevision(comando) {
|
|
236
|
+
const c = String(comando ?? '');
|
|
237
|
+
if (!/nexakit(?:\.cmd)?['"]?\s+revision\b/u.test(c) || /--disponibles\b/u.test(c)) return null;
|
|
238
|
+
const valor = (bandera) => {
|
|
239
|
+
const m = c.match(new RegExp(`${bandera}\\s+(?:"([^"]+)"|'([^']+)'|(\\S+))`, 'u'));
|
|
240
|
+
return m ? (m[1] ?? m[2] ?? m[3]) : null;
|
|
241
|
+
};
|
|
242
|
+
const ia = valor('--con');
|
|
243
|
+
return ia ? { ia, modelo: valor('--modelo'), esfuerzo: valor('--esfuerzo') } : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Las dos líneas fijas que imprime `nexakit revision` al terminar: modelo que respondió y tokens.
|
|
247
|
+
export function leerSalidaRevision(texto) {
|
|
248
|
+
const t = String(texto ?? '');
|
|
249
|
+
const numero = (v) => (v && /^\d+$/u.test(v.trim()) ? Number(v.trim()) : null);
|
|
250
|
+
const tokensLinea = t.match(/Tokens: entrada (.+?) \(caché (.+?)\) · salida (.+?)\s*$/mu);
|
|
251
|
+
const respondio = t.match(/Modelo que respondió: (.+?) · /u)?.[1]?.trim();
|
|
252
|
+
return {
|
|
253
|
+
respondio: respondio && respondio !== 'no informado' ? respondio : null,
|
|
254
|
+
entrada: numero(tokensLinea?.[1]),
|
|
255
|
+
cache: numero(tokensLinea?.[2]),
|
|
256
|
+
salida: numero(tokensLinea?.[3]),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function iniciarRevision(estado, { id, ia, modelo, esfuerzo, at }) {
|
|
261
|
+
const revision = { id, ia, modelo, esfuerzo, estado: 'revisando', inicio: at, fin: null, entrada: null, cache: null, salida: null, respondio: null };
|
|
262
|
+
return conRegistro(
|
|
263
|
+
{ ...estado, revisiones: [...(estado.revisiones ?? []), revision].slice(-4) },
|
|
264
|
+
{ t: at, quien: ia, texto: `revisión · ${modelo ?? 'modelo ?'}${esfuerzo ? ` · ${esfuerzo}` : ''}` },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function terminarRevision(estado, { id, ok, at, cifras }) {
|
|
269
|
+
if (!(estado.revisiones ?? []).some((r) => r.id === id)) return estado;
|
|
270
|
+
let ia = '';
|
|
271
|
+
const revisiones = estado.revisiones.map((r) => {
|
|
272
|
+
if (r.id !== id) return r;
|
|
273
|
+
ia = r.ia;
|
|
274
|
+
const datos = Object.fromEntries(Object.entries(cifras ?? {}).filter(([, v]) => v != null));
|
|
275
|
+
return { ...r, ...datos, estado: ok ? 'lista' : 'error', fin: at };
|
|
276
|
+
});
|
|
277
|
+
const total = (cifras?.entrada ?? 0) + (cifras?.salida ?? 0);
|
|
278
|
+
return conRegistro({ ...estado, revisiones }, { t: at, quien: ia, texto: ok ? `revisión lista${total ? ` · tokens ${tokens(total)}` : ''}` : 'revisión falló' });
|
|
279
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { atom, read, update } from 'claude-code'
|
|
2
|
+
import type { Register } from 'claude-code'
|
|
3
|
+
|
|
4
|
+
import type { PanelEstado } from '../types'
|
|
5
|
+
import { ANCHO, dibujar, medidoresDeUso, modeloFrenteARecomendado, tramosParaPanel } from './lienzo.js'
|
|
6
|
+
import {
|
|
7
|
+
alias, barra, barraEsfuerzo, crear, duracion, ejemplo, estadoPorFamilia, etiquetaEnjambre, etiquetaModelo, familia, hora, iniciarAgente,
|
|
8
|
+
registrarAsesor, registrarHerramienta, registrarPaso, registrarRecomendacion, registrarUso, resumenDelegacion, sinSecretos,
|
|
9
|
+
esRevision, iniciarRevision, leerSalidaRevision, terminarRevision, terminarAgente, textoDeUso, tituloDeOrquesta,
|
|
10
|
+
} from './panel.js'
|
|
11
|
+
|
|
12
|
+
const PANE = 'panel'
|
|
13
|
+
const TITULO = 'Orquesta'
|
|
14
|
+
// Desde este ancho cabe el diagrama de varias columnas; por debajo se dibuja una lista vertical.
|
|
15
|
+
const ANCHO_DIAGRAMA = ANCHO
|
|
16
|
+
const RECOMENDACION = 'mcp__nexakit-modelo__nexakit_recomendacion'
|
|
17
|
+
// Cambia la etiqueta cuando cambien los campos del estado: descarta lo que guardó una versión anterior.
|
|
18
|
+
const panel = atom({ plugin: 'nexakit-panel', key: 'panel' } as const, crear() as PanelEstado, { shape: 'panel-4' })
|
|
19
|
+
// Con `/panel-demo` el panel enseña datos de ejemplo (marcados como tales) sin tocar los reales.
|
|
20
|
+
const demo = atom({ plugin: 'nexakit-panel', key: 'demo' } as const, false, { shape: 'demo-1' })
|
|
21
|
+
|
|
22
|
+
const COLOR = {
|
|
23
|
+
sonnet: '#fb923c', haiku: '#60a5fa', opus: '#a78bfa', fable: '#f472b6', otro: '#9ca3af', ok: '#4ade80', error: '#f87171', marca: '#a78bfa',
|
|
24
|
+
} as const
|
|
25
|
+
const colorDe = (modelo: string | null) => COLOR[familia(modelo) as keyof typeof COLOR] ?? COLOR.otro
|
|
26
|
+
|
|
27
|
+
// Un detalle corto de lo que hizo la herramienta, sin volcar contenido.
|
|
28
|
+
const breve = (entrada: Record<string, unknown>) => {
|
|
29
|
+
const valor = entrada.file_path ?? entrada.command ?? entrada.pattern ?? entrada.path ?? entrada.description ?? entrada.url ?? ''
|
|
30
|
+
return sinSecretos(String(valor).replace(/\s+/gu, ' ')).slice(0, 48)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Foto del estado real para `nexakit panel`, que la dibuja a pantalla completa en otra división de la terminal.
|
|
34
|
+
async function guardarFoto($: Parameters<Parameters<Register>[0]>[1] extends never ? never : any) {
|
|
35
|
+
try {
|
|
36
|
+
const home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE'))
|
|
37
|
+
if (!home) return
|
|
38
|
+
const sesion = await $.session.id()
|
|
39
|
+
const estado = await read($, panel)
|
|
40
|
+
let uso = null
|
|
41
|
+
try { uso = await $.session.usage() } catch { /* sin uso */ }
|
|
42
|
+
let carpeta = ''
|
|
43
|
+
try { carpeta = String(await $.session.cwd()).split(/[\\/]/u).filter(Boolean).pop() ?? '' } catch { /* sin carpeta */ }
|
|
44
|
+
let asesorModelo: string | null = null
|
|
45
|
+
try { const valor = await $.settings.get('advisorModel'); asesorModelo = typeof valor === 'string' && valor ? valor : null } catch { /* sin ajuste */ }
|
|
46
|
+
const foto = JSON.stringify({ version: 1, sesion, actualizado: await $.clock.now(), carpeta, asesorModelo, uso, estado })
|
|
47
|
+
const carpetaFotos = `${home}/.nexakit/panel`
|
|
48
|
+
await $.fs.write(`${carpetaFotos}/${sesion}.json`, foto)
|
|
49
|
+
await $.fs.write(`${carpetaFotos}/actual.json`, foto)
|
|
50
|
+
} catch { /* la foto es un extra: nunca rompe la sesión */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const register: Register = on => {
|
|
54
|
+
on('session.start', async ($, e, next) => {
|
|
55
|
+
await $.command.register({ name: 'panel', description: 'Abre el panel de la orquestación de esta sesión' })
|
|
56
|
+
await $.command.register({ name: 'panel-demo', description: 'Activa o desactiva datos de ejemplo en el panel, para ver el diseño' })
|
|
57
|
+
void $.ui.open({ id: PANE, title: TITULO, columns: ANCHO_DIAGRAMA })
|
|
58
|
+
return next(e)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
on('command.run', { command: 'panel' }, async $ => {
|
|
62
|
+
await $.ui.open({ id: PANE, title: TITULO, columns: ANCHO_DIAGRAMA })
|
|
63
|
+
return { text: 'Panel de la orquestación abierto.' }
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
on('command.run', { command: 'panel-demo' }, async $ => {
|
|
67
|
+
const activo = !(await read($, demo))
|
|
68
|
+
await update($, demo, () => activo)
|
|
69
|
+
await $.ui.open({ id: PANE, title: TITULO, columns: ANCHO_DIAGRAMA })
|
|
70
|
+
return { text: activo ? 'Panel con datos de ejemplo (ficticios). Repite /panel-demo para volver a los reales.' : 'Panel con los datos reales de la sesión.' }
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
on('turn.step', async function* ($, e, next) {
|
|
74
|
+
try {
|
|
75
|
+
await update($, panel, s => registrarPaso(s, { modelo: e.model, esfuerzo: e.effort == null ? null : String(e.effort), agentId: e.agentId }))
|
|
76
|
+
} catch { /* observar nunca debe romper la sesión */ }
|
|
77
|
+
const result = yield* next(e)
|
|
78
|
+
try {
|
|
79
|
+
const asesor = (result?.serverToolUses ?? []).filter(uso => uso.name === 'advisor')
|
|
80
|
+
const at = await $.clock.now()
|
|
81
|
+
const usage = result?.usage ?? null
|
|
82
|
+
const agentId = e.agentId
|
|
83
|
+
await update($, panel, s => {
|
|
84
|
+
let siguiente = registrarUso(s, { agentId, usage })
|
|
85
|
+
if (asesor.length > 0) {
|
|
86
|
+
const tiempo = asesor.reduce((n, u) => n + Math.max(0, (u.endedAt ?? u.startedAt) - u.startedAt), 0)
|
|
87
|
+
siguiente = registrarAsesor(siguiente, { llamadas: asesor.length, at, tiempo })
|
|
88
|
+
}
|
|
89
|
+
return siguiente
|
|
90
|
+
})
|
|
91
|
+
} catch { /* idem */ }
|
|
92
|
+
await guardarFoto($)
|
|
93
|
+
return result
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
on('agent.spawn', async ($, e, next) => {
|
|
97
|
+
const salida = await next(e)
|
|
98
|
+
try {
|
|
99
|
+
if ('agentId' in salida && salida.agentId) {
|
|
100
|
+
const at = await $.clock.now()
|
|
101
|
+
const id = salida.agentId
|
|
102
|
+
const nombre = e.name ?? e.subagentType ?? e.description ?? 'agente'
|
|
103
|
+
await update($, panel, s => iniciarAgente(s, { id, nombre: String(nombre).slice(0, 24), modelo: salida.model ?? '', at }))
|
|
104
|
+
await guardarFoto($)
|
|
105
|
+
}
|
|
106
|
+
} catch { /* idem */ }
|
|
107
|
+
return salida
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
on('turn.complete', async ($, e, next) => {
|
|
111
|
+
try {
|
|
112
|
+
if (e.agentId) {
|
|
113
|
+
const at = await $.clock.now()
|
|
114
|
+
const id = e.agentId
|
|
115
|
+
await update($, panel, s => terminarAgente(s, { id, ok: e.reason === 'answer', at }))
|
|
116
|
+
}
|
|
117
|
+
await guardarFoto($)
|
|
118
|
+
} catch { /* idem */ }
|
|
119
|
+
return next(e)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
on('tool.call', async ($, e, next) => {
|
|
123
|
+
const entrada = e as unknown as Record<string, unknown>
|
|
124
|
+
let revision: { id: string } | null = null
|
|
125
|
+
try {
|
|
126
|
+
const at = await $.clock.now()
|
|
127
|
+
const detalle = breve(entrada)
|
|
128
|
+
const pedida = e.tool === 'Bash' ? esRevision(entrada.command) : null
|
|
129
|
+
if (pedida) revision = { id: String(entrada.tool_use_id ?? at) }
|
|
130
|
+
await update($, panel, s => {
|
|
131
|
+
let siguiente = registrarHerramienta(s, { tool: e.tool, detalle, agentId: e.agentId, at })
|
|
132
|
+
if (e.tool === RECOMENDACION) {
|
|
133
|
+
siguiente = registrarRecomendacion(siguiente, { modelo: String(entrada.model ?? ''), esfuerzo: String(entrada.effort ?? ''), motivo: String(entrada.reason ?? '') })
|
|
134
|
+
}
|
|
135
|
+
if (pedida && revision) siguiente = iniciarRevision(siguiente, { id: revision.id, ...pedida, at })
|
|
136
|
+
return siguiente
|
|
137
|
+
})
|
|
138
|
+
await guardarFoto($)
|
|
139
|
+
} catch { /* idem */ }
|
|
140
|
+
const resultado = await next(e)
|
|
141
|
+
if (revision) {
|
|
142
|
+
try {
|
|
143
|
+
const at = await $.clock.now()
|
|
144
|
+
const r = resultado as { result?: { stdout?: string }; text?: string; isError?: boolean; deny?: string }
|
|
145
|
+
const cifras = leerSalidaRevision(r?.result?.stdout ?? r?.text ?? '')
|
|
146
|
+
const id = revision.id
|
|
147
|
+
await update($, panel, s => terminarRevision(s, { id, ok: !r?.isError && !r?.deny, at, cifras }))
|
|
148
|
+
await guardarFoto($)
|
|
149
|
+
} catch { /* idem */ }
|
|
150
|
+
}
|
|
151
|
+
return resultado
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
on('session.end', async ($, e, next) => {
|
|
155
|
+
try { await update($, panel, () => crear() as PanelEstado) } catch { /* idem */ }
|
|
156
|
+
return next(e)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
160
|
+
const { Box, Text } = $.ui.resolve(e)
|
|
161
|
+
const ahora = await $.clock.now()
|
|
162
|
+
const esEjemplo = await read($, demo)
|
|
163
|
+
const s = esEjemplo ? ejemplo(ahora) : await read($, panel)
|
|
164
|
+
let uso = null
|
|
165
|
+
try { uso = await $.session.usage() } catch { /* sin datos de uso: el panel sigue */ }
|
|
166
|
+
let carpeta = ''
|
|
167
|
+
try { carpeta = String(await $.session.cwd()).split('/').filter(Boolean).pop() ?? '' } catch { /* sin directorio */ }
|
|
168
|
+
const linea = textoDeUso(uso)
|
|
169
|
+
const ancho = (e.props as { bodyColumns?: number } | undefined)?.bodyColumns ?? e.viewport?.columns ?? 80
|
|
170
|
+
const filas = Math.max(6, (e.viewport?.rows ?? 30) - 4)
|
|
171
|
+
const vacio = !esEjemplo && s.pasos === 0 && s.agentes.length === 0 && s.registro.length === 0
|
|
172
|
+
const quienColor = (quien: string) => {
|
|
173
|
+
if (quien === 'líder') return colorDe(s.lider.modelo)
|
|
174
|
+
if (quien === 'asesor') return COLOR.opus
|
|
175
|
+
const agente = s.agentes.find(a => a.nombre === quien)
|
|
176
|
+
return agente ? colorDe(agente.modelo) : COLOR.otro
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let asesorModelo: string | null = null
|
|
180
|
+
try { const valor = await $.settings.get('advisorModel'); asesorModelo = typeof valor === 'string' && valor ? valor : null } catch { /* sin ajuste */ }
|
|
181
|
+
|
|
182
|
+
// ---- Diagrama exacto, carácter a carácter, cuando cabe -------------------------------------------------
|
|
183
|
+
if (ancho >= ANCHO) {
|
|
184
|
+
const { filas: lineas } = dibujar(s, { ahora, uso, carpeta, filasMax: e.viewport?.rows, asesorModelo, esEjemplo })
|
|
185
|
+
return (
|
|
186
|
+
<Box flexDirection="column">
|
|
187
|
+
{tramosParaPanel(lineas).map(tramos => (
|
|
188
|
+
<Box flexDirection="row">
|
|
189
|
+
{tramos.map(t => (
|
|
190
|
+
<Text mono wrap="truncate" color={t.c} bold={t.b} dimColor={t.d} backgroundColor={t.bg}>{t.t}</Text>
|
|
191
|
+
))}
|
|
192
|
+
</Box>
|
|
193
|
+
))}
|
|
194
|
+
</Box>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- Versión estrecha: lista vertical, con aviso de cómo ver el diagrama ------------------------------
|
|
199
|
+
{
|
|
200
|
+
return (
|
|
201
|
+
<Box flexDirection="column" gap={1}>
|
|
202
|
+
<Text bold color={COLOR.marca}>NEXAKIT · ORQUESTA</Text>
|
|
203
|
+
{esEjemplo && <Text color="#facc15">EJEMPLO · datos ficticios para ver el diseño</Text>}
|
|
204
|
+
<Text dimColor>{`El diagrama completo necesita ${ANCHO} columnas (ahora ${ancho}). Amplía la ventana o el panel.`}</Text>
|
|
205
|
+
{vacio && <Text dimColor>Aún no hay actividad en esta sesión.</Text>}
|
|
206
|
+
|
|
207
|
+
{s.lider.modelo && (
|
|
208
|
+
<Box flexDirection="column" borderStyle="round" borderColor={colorDe(s.lider.modelo)} paddingX={1}>
|
|
209
|
+
<Text bold color={colorDe(s.lider.modelo)}>{`${etiquetaModelo(s.lider.modelo)} · sesión principal`}</Text>
|
|
210
|
+
<Text>{`esfuerzo ${barraEsfuerzo(s.lider.esfuerzo)} ${s.lider.esfuerzo ?? ''}`.trimEnd()}</Text>
|
|
211
|
+
</Box>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{s.asesor.llamadas > 0 && (
|
|
215
|
+
<Box flexDirection="column" borderStyle="round" borderColor={COLOR.opus} paddingX={1}>
|
|
216
|
+
<Text bold color={COLOR.opus}>{`Asesor · consultas ${s.asesor.llamadas}`}</Text>
|
|
217
|
+
{s.asesor.ultimo != null && <Text dimColor>{`última a las ${hora(s.asesor.ultimo)}`}</Text>}
|
|
218
|
+
</Box>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{s.agentes.length > 0 && <Text bold>Subagentes</Text>}
|
|
222
|
+
{s.agentes.map(agente => (
|
|
223
|
+
<Box key={agente.id} flexDirection="column" borderStyle="round" borderColor={colorDe(agente.modelo)} paddingX={1}>
|
|
224
|
+
<Text bold color={colorDe(agente.modelo)}>{agente.nombre}</Text>
|
|
225
|
+
<Text dimColor>{etiquetaModelo(agente.modelo)}</Text>
|
|
226
|
+
<Text color={agente.estado === 'listo' ? COLOR.ok : agente.estado === 'error' ? COLOR.error : undefined}>
|
|
227
|
+
{agente.estado === 'listo' ? '✓ listo' : agente.estado === 'error' ? '✗ con error' : '● corriendo'}
|
|
228
|
+
</Text>
|
|
229
|
+
<Text dimColor>{`${agente.herramientas} herramientas · ${duracion((agente.fin ?? ahora) - agente.inicio)}`}</Text>
|
|
230
|
+
</Box>
|
|
231
|
+
))}
|
|
232
|
+
|
|
233
|
+
{s.registro.length > 0 && (
|
|
234
|
+
<Box flexDirection="column" borderStyle="single" borderColor={COLOR.otro} paddingX={1}>
|
|
235
|
+
<Text bold dimColor>registro</Text>
|
|
236
|
+
{s.registro.slice(-filas).map(r => (
|
|
237
|
+
<Text>{`${hora(r.t)} ${r.quien} ${r.texto}`}</Text>
|
|
238
|
+
))}
|
|
239
|
+
</Box>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{medidoresDeUso(uso).length > 0 && <Text dimColor>{medidoresDeUso(uso).map(([n, p]) => `${n} ${barra(p / 100, 8)} ${Math.round(p)} %`).join(' ')}</Text>}
|
|
243
|
+
{s.lider.modelo && (() => { const m = modeloFrenteARecomendado(s); return <Text color={m.coincide ? COLOR.ok : '#facc15'}>{`${m.actual}${m.cola}`}</Text> })()}
|
|
244
|
+
</Box>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, test } from 'claude-code/testing';
|
|
2
|
+
import { ANCHO, aAnsi, dibujar, texto } from '../hooks/lienzo.js';
|
|
3
|
+
import { crear, ejemplo, iniciarRevision, registrarPaso, registrarRecomendacion } from '../hooks/panel.js';
|
|
4
|
+
|
|
5
|
+
const AHORA = 1_800_000_000_000;
|
|
6
|
+
const USO = { context: { window: 1000000, percent: 31 }, rateLimits: [{ kind: 'five_hour', percentUsed: 43 }, { kind: 'seven_day', percentUsed: 60 }] };
|
|
7
|
+
const demo = () => texto(dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'mi-app' }).filas);
|
|
8
|
+
const cuenta = (lineas: string[], c: string) => lineas.join('').split(c).length - 1;
|
|
9
|
+
|
|
10
|
+
describe('nexakit-panel · lienzo', () => {
|
|
11
|
+
test('cada línea mide exactamente el ancho del diagrama', () => {
|
|
12
|
+
const lineas = demo();
|
|
13
|
+
expect(ANCHO).toBe(104);
|
|
14
|
+
for (const l of lineas) expect([...l].length).toBe(ANCHO);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('cabecera: título centrado, regla doble y leyenda', () => {
|
|
18
|
+
const l = demo();
|
|
19
|
+
expect(l[0]).toContain('NEXAKIT · ORQUESTA · SONNET 5.5 DIRIGE · HAIKU 5.5 EN ENJAMBRE');
|
|
20
|
+
expect(l[1]).toMatch(/═{90,}/);
|
|
21
|
+
expect(l[2]).toContain('■ sonnet 5.5 · líder');
|
|
22
|
+
expect(l[2]).toContain('■ haiku 5.5 · enjambre');
|
|
23
|
+
expect(l[2]).toContain('■ delegación');
|
|
24
|
+
expect(l[2]).toContain('■ asesor');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('todas las cajas y conectores quedan cerrados', () => {
|
|
28
|
+
const l = demo();
|
|
29
|
+
expect(cuenta(l, '┌')).toBe(cuenta(l, '┐'));
|
|
30
|
+
expect(cuenta(l, '└')).toBe(cuenta(l, '┘'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('líder, delegación, subagentes en fila y vuelta al líder, con uso real', () => {
|
|
34
|
+
const l = demo().join('\n');
|
|
35
|
+
expect(l).toContain('Sonnet 5.5 · sesión principal');
|
|
36
|
+
expect(l).toMatch(/esfuerzo ▮▮▮▯ high/);
|
|
37
|
+
expect(l).toMatch(/leídos \S+ · escritos \S+/);
|
|
38
|
+
expect(l).toMatch(/DELEGACIÓN · subagentes +3/);
|
|
39
|
+
expect(l).toMatch(/listos +█*░* +1\/3/);
|
|
40
|
+
expect(l).toMatch(/corriendo +█*░* +2\/3/);
|
|
41
|
+
expect(l).toContain('delegar a subagentes · Haiku 5.5');
|
|
42
|
+
expect(l).toContain('de vuelta en Sonnet 5.5 · líder');
|
|
43
|
+
const fila = demo().find((x) => x.includes('search') && x.includes('grep') && x.includes('audit'));
|
|
44
|
+
expect(fila).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('el porcentaje de cada subagente es su parte real del uso y suma 100', () => {
|
|
48
|
+
const fila = demo().find((x) => (x.match(/\d+%/g) ?? []).length === 3)!;
|
|
49
|
+
expect(fila).toBeTruthy();
|
|
50
|
+
const suma = (fila.match(/(\d+)%/g) ?? []).map((p) => Number(p.slice(0, -1))).reduce((a, b) => a + b, 0);
|
|
51
|
+
expect(Math.abs(suma - 100)).toBeLessThanOrEqual(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('asesor a la izquierda con sus consultas, flechas y cifras', () => {
|
|
55
|
+
const l = demo();
|
|
56
|
+
expect(l.some((x) => x.includes('ASESOR'))).toBe(true);
|
|
57
|
+
expect(l.some((x) => /◆ \d\d:\d\d:\d\d/.test(x))).toBe(true);
|
|
58
|
+
expect(l.some((x) => x.includes('┈▶'))).toBe(true);
|
|
59
|
+
expect(l.some((x) => /consultas +2/.test(x))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('registro con título en el borde, y abajo la orden y el estado', () => {
|
|
63
|
+
const l = demo();
|
|
64
|
+
expect(l.some((x) => x.includes('┤ registro de sesión ├'))).toBe(true);
|
|
65
|
+
expect(l.some((x) => x.includes('→ src/auth.ts'))).toBe(true);
|
|
66
|
+
expect(l.some((x) => x.includes('mi-app $ claude --model sonnet'))).toBe(true);
|
|
67
|
+
expect(l.some((x) => /haiku \[enjambre\] +subagentes \[1\/3\] +asesor \[consultado 2\]/.test(x))).toBe(true);
|
|
68
|
+
expect(l.some((x) => /contexto +█+░* +31 %/.test(x))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('sin actividad no dibuja cajas inventadas', () => {
|
|
72
|
+
const l = texto(dibujar(crear(), { ahora: AHORA, uso: null, carpeta: 'mi-app' }).filas);
|
|
73
|
+
expect(l.join('\n')).toContain('Aún no hay actividad');
|
|
74
|
+
expect(cuenta(l, '┌')).toBe(0);
|
|
75
|
+
for (const x of l) expect([...x].length).toBe(ANCHO);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('se adapta al alto: recorta el registro y no pasa del máximo', () => {
|
|
79
|
+
const alto = dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'mi-app', filasMax: 48 }).alto;
|
|
80
|
+
expect(alto).toBeLessThanOrEqual(48);
|
|
81
|
+
const completo = dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'mi-app' }).alto;
|
|
82
|
+
expect(completo).toBeGreaterThan(alto);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('a ANSI: sin color no hay escapes; con color de 24 bits sí', () => {
|
|
86
|
+
const { filas } = dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'mi-app' });
|
|
87
|
+
expect(aAnsi(filas, 'none')).not.toContain('\x1b');
|
|
88
|
+
expect(aAnsi(filas, 'truecolor')).toContain('38;2;');
|
|
89
|
+
expect(aAnsi(filas, '256')).toContain('38;5;');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('la caja del líder no corta sus cifras', () => {
|
|
93
|
+
const l = demo().join('\n');
|
|
94
|
+
expect(l).toMatch(/leídos 668k · escritos 12k/);
|
|
95
|
+
expect(l).not.toMatch(/escritos 1…/);
|
|
96
|
+
expect(l).toMatch(/pasos 24 · herramientas 1/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('cada consulta del asesor dice cuánto duró', () => {
|
|
100
|
+
expect(demo().some((x) => /duró 4 s/.test(x))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('si el asesor está configurado, lo nombra y lo pone en la orden', () => {
|
|
104
|
+
const l = texto(dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'app', asesorModelo: 'opus' }).filas);
|
|
105
|
+
expect(l.some((x) => x.includes('OPUS'))).toBe(true);
|
|
106
|
+
expect(l.some((x) => x.includes('claude --model sonnet --advisor opus'))).toBe(true);
|
|
107
|
+
expect(demo().some((x) => x.includes('--advisor'))).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('abajo, el uso con barras y el modelo frente al recomendado', () => {
|
|
111
|
+
const conRec = registrarRecomendacion(registrarPaso(ejemplo(AHORA), { modelo: 'claude-opus-5-5', esfuerzo: 'high' }), { modelo: 'sonnet', esfuerzo: 'medium', motivo: 'x' });
|
|
112
|
+
const l = texto(dibujar(conRec, { ahora: AHORA, uso: USO, carpeta: 'app' }).filas);
|
|
113
|
+
expect(l.some((x) => /contexto +█+░* +31 %/.test(x) && /5 h +█*░+ +43 %/.test(x) && /semana +█+░* +60 %/.test(x))).toBe(true);
|
|
114
|
+
expect(l.some((x) => x.includes('modelo claude-opus-5-5 · high → recomendado: sonnet · medium'))).toBe(true);
|
|
115
|
+
const igual = registrarRecomendacion(registrarPaso(ejemplo(AHORA), { modelo: 'claude-sonnet-5-5', esfuerzo: 'medium' }), { modelo: 'sonnet', esfuerzo: 'medium', motivo: 'x' });
|
|
116
|
+
expect(texto(dibujar(igual, { ahora: AHORA, uso: USO, carpeta: 'app' }).filas).some((x) => x.includes('modelo claude-sonnet-5-5 · medium ✓'))).toBe(true);
|
|
117
|
+
for (const x of l) expect([...x].length).toBe(ANCHO);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('una revisión externa sale en su caja a la derecha del líder, con su IA y modelo', () => {
|
|
121
|
+
const conRev = iniciarRevision(ejemplo(AHORA), { id: 'r1', ia: 'codex', modelo: 'gpt-6.1-sol', esfuerzo: 'high', at: AHORA - 45_000 });
|
|
122
|
+
const l = texto(dibujar(conRev, { ahora: AHORA, uso: USO, carpeta: 'app' }).filas);
|
|
123
|
+
expect(l.some((x) => x.includes('Sonnet 5.5 · sesión principal') && x.includes('REVISIÓN'))).toBe(true);
|
|
124
|
+
for (const t of ['Codex', 'gpt-6.1-sol', '◐ revisando', '■ codex · revisa']) expect(l.some((x) => x.includes(t))).toBe(true);
|
|
125
|
+
for (const x of l) expect([...x].length).toBe(ANCHO);
|
|
126
|
+
expect(texto(dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'app' }).filas).some((x) => x.includes('REVISIÓN'))).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('si se le pasa el uso de Codex, lo enseña abajo con sus límites y cuándo fue', () => {
|
|
130
|
+
const codex = { modelo: 'gpt-6.1-sol', esfuerzo: 'low', leidos: 372_693, cache: 323_584, escritos: 3_899, limite5h: 1, limiteSemana: 14, actualizado: AHORA - 180_000 };
|
|
131
|
+
const l = texto(dibujar(ejemplo(AHORA), { ahora: AHORA, uso: USO, carpeta: 'app', codex }).filas);
|
|
132
|
+
const fila = l.find((x) => x.includes('codex gpt-6.1-sol'));
|
|
133
|
+
expect(fila).toBeTruthy();
|
|
134
|
+
expect(fila).toMatch(/leídos 373k · escritos 3.9k/);
|
|
135
|
+
expect(fila).toMatch(/5 h .* 1 %/);
|
|
136
|
+
expect(fila).toMatch(/semana .* 14 %/);
|
|
137
|
+
expect(fila).toMatch(/hace 3 min/);
|
|
138
|
+
const vacio = texto(dibujar(crear(), { ahora: AHORA, uso: null, carpeta: 'app', codex }).filas);
|
|
139
|
+
expect(vacio.some((x) => x.includes('codex gpt-6.1-sol'))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|