@thebattlearena/battle-arena-hooks 0.5.2 → 0.5.3-beta.36

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 (38) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/README.md +123 -37
  3. package/cli.mjs +89 -65
  4. package/execution-coach-nudge.mjs +2 -2
  5. package/lib/battle-order/battle-arena-logo.png +0 -0
  6. package/lib/battle-order/html.mjs +403 -0
  7. package/lib/battle-order/pdf.mjs +122 -0
  8. package/lib/battle-order/styles.mjs +158 -0
  9. package/lib/local-activity.mjs +217 -0
  10. package/lib/plugin-install.mjs +128 -0
  11. package/lib/transport.mjs +54 -0
  12. package/objective-nudge.mjs +1 -1
  13. package/package.json +6 -4
  14. package/plan.mjs +89 -0
  15. package/plugin/.claude-plugin/plugin.json +7 -0
  16. package/plugin/hooks/api.ts +118 -0
  17. package/plugin/hooks/format.ts +157 -0
  18. package/plugin/hooks/hooks.json +111 -0
  19. package/plugin/hooks/mock.ts +151 -0
  20. package/plugin/hooks/pane.tsx +666 -0
  21. package/plugin/hooks/register.tsx +278 -0
  22. package/plugin/skills/conquer/SKILL.md +268 -0
  23. package/plugin/tests/format.test.ts +34 -0
  24. package/plugin/tests/scorecard.test.tsx +294 -0
  25. package/plugin/types/index.d.ts +120 -0
  26. package/pr-review-worktree.mjs +1 -1
  27. /package/{skill → plugin/skills}/arena-objective/SKILL.md +0 -0
  28. /package/{skill → plugin/skills}/arena-self-review/SKILL.md +0 -0
  29. /package/{skill → plugin/skills}/arena-self-review/assets/fonts.css +0 -0
  30. /package/{skill → plugin/skills}/arena-self-review/assets/leracom-logo.png +0 -0
  31. /package/{skill → plugin/skills}/arena-self-review/assets/report.css +0 -0
  32. /package/{skill → plugin/skills}/arena-self-review/assets/style.css +0 -0
  33. /package/{skill → plugin/skills}/arena-self-review/scripts/gen-fonts.mjs +0 -0
  34. /package/{skill → plugin/skills}/arena-self-review/scripts/generate-pdf.sh +0 -0
  35. /package/{skill → plugin/skills}/arena-self-review/scripts/render-report.sh +0 -0
  36. /package/{skill → plugin/skills}/execution-coach/SKILL.md +0 -0
  37. /package/{skill → plugin/skills}/pr-review-loop/SKILL.md +0 -0
  38. /package/{skill → plugin/skills}/scope-technical-task/SKILL.md +0 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "battlearena-local",
3
+ "owner": {
4
+ "name": "Battle Arena"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "battlearena",
9
+ "source": "./plugin",
10
+ "description": "Battle Arena en Claude Code"
11
+ }
12
+ ]
13
+ }
package/README.md CHANGED
@@ -1,25 +1,112 @@
1
- # Hooks de Claude Code (Battle Arena)
1
+ # Battle Arena en Claude Code (plugin `battlearena`)
2
2
 
3
- Scripts **zero-dependency** que corren en la máquina de cada dev y emiten los eventos de las
4
- sesiones de Claude Code al colector (`apps/public-api`). NO les agregues imports de
5
- node_modules: la promesa es instalación-cero.
3
+ Este paquete instala **Battle Arena en Claude Code** como un solo plugin, `battlearena`: los hooks que
4
+ mandan tus sesiones al colector (`apps/public-api`), las skills del equipo y el **Scorecard** de tu día
5
+ junto al chat. Las skills quedan con el espacio de nombres del plugin (`/battlearena:conquer`), así no
6
+ chocan con las de otros plugins y en el autocompletado salen todas juntas al escribir `/battlearena`.
6
7
 
7
- | Script | Hook | Qué hace |
8
- | --------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | `emit.mjs` | todos | Enriquece con identidad git, sanitiza y postea a `/ingest`; buffer local si el colector no responde |
10
- | `session-start.mjs` | SessionStart | Handshake + inyecta ranking (`/v1/leaderboard/rank`) y alerta de rama protegida |
11
- | `check-connection.mjs` | UserPromptSubmit | Avisa si hay desconexión o eventos en cola |
12
- | `objective-nudge.mjs` | UserPromptSubmit | Recuerda invocar la skill `arena-objective` |
13
- | `execution-coach-nudge.mjs` | UserPromptSubmit | Detecta señales de ejecución subóptima en el transcript y, con throttle por sesión, sugiere invocar la skill `execution-coach` |
14
- | `pr-review-worktree.mjs` | PostToolUse | Al detectar `gh pr create`, inyecta contexto para OFRECER (opt-in) la skill `pr-review-loop`; no crea worktrees ni lanza nada (de eso se encarga la skill si el usuario acepta) |
8
+ ```bash
9
+ npx @thebattlearena/battle-arena-hooks@latest install \
10
+ --url https://api.battlearena.dev --api-key <tu-llave>
11
+ ```
12
+
13
+ La llave se genera en [battlearena.dev/api-keys](https://battlearena.dev/api-keys). Para quitarlo:
14
+ `npx @thebattlearena/battle-arena-hooks uninstall`.
15
+
16
+ ## Qué trae el plugin
17
+
18
+ ### Skills
19
+
20
+ Se invocan escribiendo el comando o pidiéndolo con tus palabras («arma mi plan de mañana»).
21
+
22
+ | Skill | Para qué sirve |
23
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `/battlearena:conquer` | Arma o actualiza tu **plan de batalla** del siguiente día hábil: revisa lo que hiciste hoy (git, tus sesiones de Claude, tus pendientes), te entrevista tema por tema (valor de negocio, DoD, ≤ 3 h, riesgos) y, desde las 16:00 de la víspera, crea tu ticket del día en Linear y tu orden de batalla en PDF. |
25
+ | `/battlearena:arena-objective` | Etiqueta el **objetivo del turno** (nuevo o continuación) y lo manda al colector, para que tu actividad se agrupe por objetivo. Claude la usa solo al inicio de cada turno; no hace falta llamarla. |
26
+ | `/battlearena:arena-self-review` | Tu **revisión semanal de desempeño**, self-service: lee tu actividad de la semana desde el MCP de Battle Arena y te da una autocrítica con evidencia. |
27
+ | `/battlearena:execution-coach` | **Interceptor mayéutico** durante la ejecución: te frena con preguntas cuando el pedido es vago o la sesión da vueltas sin converger. Se activa sola cuando el hook detecta esas señales. |
28
+ | `/battlearena:scope-technical-task` | **Coach de alcance antes de codear**: problema de negocio, sub-problemas, soluciones sin código, estimación en horas. |
29
+ | `/battlearena:pr-review-loop` | Atiende la **review de Copilot** de un PR en segundo plano (corrige → gates → commit → resolver hilos → re-review). Siempre opt-in: pregunta antes y cuántas iteraciones tolera. |
30
+
31
+ ### Comandos
32
+
33
+ | Comando | Para qué sirve |
34
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `/scorecard` | Abre el **Scorecard**: la pelea de tu día junto al chat. Tu plan con el avance y el tiempo de cada punto contra lo comprometido (semáforo verde · amarillo · rojo), los emergentes (clic para ver su motivo), los riesgos siempre visibles con ✋ Levantar la mano y 👍/👎, y el pie con Plan · Reuniones · Fuera · Total. Pestaña **Semana** con tus hábitos. Sin datos, te dice el **siguiente paso** (instalar, armar el plan, entrar a la daily). |
36
+ | `/scorecard-demo` | El Scorecard con datos de muestra; otra vez, vuelve a tus datos. |
37
+ | `/scorecard-empty` | Cómo ve el Scorecard alguien sin hooks; otra vez, vuelve a tus datos. |
38
+
39
+ Arriba del prompt sale además una **banda** compacta del Scorecard (`LER-640 ✓1 ◐1 ↷1 ⚠1 [Abrir]`).
40
+ Los comandos del Scorecard no llevan el prefijo del plugin (así registra Claude Code los comandos de
41
+ un módulo), por eso su nombre ya es único.
42
+
43
+ ### Hooks
44
+
45
+ Scripts **zero-dependency** que corren en la máquina de cada dev. NO les agregues imports de
46
+ node_modules: la promesa es instalación-cero. Viven junto al plugin y su `hooks/hooks.json` los
47
+ llama con `node "${CLAUDE_PLUGIN_ROOT}/../<script>"`.
48
+
49
+ | Script | Hook | Qué hace |
50
+ | --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
51
+ | `emit.mjs` | todos | Enriquece con identidad git, sanitiza y postea a `/ingest`; buffer local si el colector no responde |
52
+ | `session-start.mjs` | SessionStart | Handshake + inyecta ranking (`/v1/leaderboard/rank`) y alerta de rama protegida |
53
+ | `check-connection.mjs` | UserPromptSubmit | Avisa si hay desconexión o eventos en cola |
54
+ | `objective-nudge.mjs` | UserPromptSubmit | Recuerda invocar `battlearena:arena-objective` |
55
+ | `execution-coach-nudge.mjs` | UserPromptSubmit | Detecta señales de ejecución subóptima y, con throttle por sesión, sugiere `battlearena:execution-coach` |
56
+ | `pr-review-worktree.mjs` | PostToolUse | Al detectar `gh pr create`, ofrece (opt-in) `battlearena:pr-review-loop`; no lanza nada por sí solo |
57
+
58
+ Helpers que usan las skills: `objective.mjs` (el objetivo del turno) y `plan.mjs` (actividad local de
59
+ git y el PDF de la orden de batalla de `/battlearena:conquer`).
60
+
61
+ ### Estructura
15
62
 
16
- Skills distribuidas con el paquete (se copian a `~/.claude/skills/`): `arena-objective`,
17
- `arena-self-review`, `execution-coach` (interceptor mayéutico durante la ejecución),
18
- `scope-technical-task` (coach de alcance pre-ejecución; el interceptor le hace handoff) y
19
- `pr-review-loop` (atiende la review de Copilot de un PR: corrige → gates → commit → responder/resolver
20
- hilos → re-review, hasta converger).
63
+ ```
64
+ apps/claude-hooks/
65
+ cli.mjs install.sh *.mjs lib/ el paquete npm y los scripts de los hooks
66
+ .claude-plugin/marketplace.json marketplace local: battlearena → ./plugin
67
+ plugin/
68
+ .claude-plugin/plugin.json { name: "battlearena" }
69
+ hooks/hooks.json los hooks de comando + el módulo del Scorecard
70
+ hooks/register.tsx … el Scorecard (panel, banda, comandos)
71
+ skills/ las skills (las genera `npm run bundle:skill`)
72
+ tests/ pruebas del Scorecard (`claude plugin test plugin`)
73
+ ```
74
+
75
+ ### Plan de batalla (skill `/battlearena:conquer` + `plan.mjs`, ADR-0042)
76
+
77
+ `/conquer` (o «arma mi plan de mañana») arma tu plan del siguiente día hábil como estratega: revisa
78
+ lo que hiciste hoy, te propone los pendientes con su explicación y su ticket, y por cada tema pide
79
+ contexto en Linear, valor, DoD no ambiguo, duración (≤ 3 h o «divide y vencerás» en subtareas) y
80
+ riesgos. **Se completa desde las 16:00 (CDMX) de la víspera** (fin de semana libre); antes solo
81
+ guarda el borrador.
82
+
83
+ Cómo habla:
84
+
85
+ - **Conector MCP de Battle Arena** (claude.ai → Settings → Connectors, o `/mcp`): `plan_pending`,
86
+ `plan_my_day`, `plan_get`, `plan_save_draft`, `plan_can_complete`, `plan_complete`,
87
+ `plan_reopen`, `board_add`, `plan_summary`. Identidad = tu cuenta OAuth (sin api-key).
88
+ - **Conector de Linear**, como tú: busca tus tickets y, al completar, crea «tu ticket del día»
89
+ (`conquest:daily`) con el borrador que da `plan_can_complete`. Lo que no tiene ticket NO se crea:
90
+ va al tablero de la PM (`board_add`).
91
+ - **`plan.mjs`, solo local** (sin api-key ni red):
92
+
93
+ ```bash
94
+ node plan.mjs local-activity [--repo <ruta>]… # git de hoy SOLO en esas rutas (sin --repo, el cwd):
95
+ # rama, commits con tu email (máx. 10), archivos sin
96
+ # commit (solo nombres, máx. 20) y con `gh` el PR de
97
+ # la rama actual + los tuyos de los últimos 5 hábiles
98
+ node plan.mjs pdf --file summary.json [--reviewer <PM>] [--out <carpeta>] [--no-open]
99
+ ```
100
+
101
+ `pdf` arma la «orden de batalla» (diseño A4 · Primero lo importante) con lo de `plan_summary`: HTML
102
+ con el logo embebido y, si la máquina tiene Chrome/Chromium/Edge (`BATTLE_ORDER_CHROME` para otra
103
+ ruta), el PDF con `--headless --print-to-pdf` en `~/Documents/Battle Arena/`. Sin navegador deja el
104
+ HTML listo para «Imprimir → Guardar como PDF». Es solo para ti: no se sube.
21
105
 
22
- ### PR review loop (`pr-review-worktree` + skill `pr-review-loop`)
106
+ Su **fuente única** vive en este paquete: `skill/conquer/SKILL.md` (en el repo,
107
+ `.claude/skills/conquer` es un symlink hacia ella y `bundle:skill` no la toca).
108
+
109
+ ### PR review loop (`pr-review-worktree` + skill `battlearena:pr-review-loop`)
23
110
 
24
111
  Al crear un PR con `gh pr create`, el hook `pr-review-worktree` **sugiere** (no lanza) activar la skill
25
112
  `pr-review-loop`. Es **opt-in**: la skill primero **pregunta** al usuario si quiere activarlo,
@@ -74,39 +161,38 @@ que corren las dos suites: `yarn workspace @thebattlearena/battle-arena-hooks te
74
161
  Tras un envío exitoso con la key nueva, el flag se limpia solo. Lógica compartida en
75
162
  `lib/reauth.mjs`.
76
163
 
77
- ## Instalación rápida
78
-
79
- La forma recomendada (cualquier SO, sin clonar) es el paquete npm — ver README raíz:
164
+ ## Instalación
80
165
 
81
166
  ```bash
82
167
  npx @thebattlearena/battle-arena-hooks@latest install \
83
- --url https://api.battlearena.dev --api-key sk_xxx
168
+ --url https://api.battlearena.dev --api-key <tu-llave>
84
169
  ```
85
170
 
86
- Desde el clone, el equivalente para contribuidores:
171
+ Requiere Claude Code CLI (`claude` en el PATH) y node ≥ 18. El instalador:
87
172
 
88
- ```bash
89
- HOOK_STREAM_URL=https://api.battlearena.dev \
90
- HOOK_STREAM_API_KEY=sk_xxx \
91
- bash apps/claude-hooks/install.sh
92
- ```
173
+ 1. Copia los scripts y el plugin a `~/.claude/battle-arena/hooks/` (la misma ruta de antes).
174
+ 2. Guarda la url y la llave en `~/.claude/battle-arena/config.json`.
175
+ 3. **Limpia la versión anterior:** quita sus hooks de `~/.claude/settings.json` (si no, cada evento se
176
+ mandaría dos veces) y mueve sus skills sueltas de `~/.claude/skills` a
177
+ `~/.claude/battle-arena/legacy-skills/<fecha>/` (respaldo, no las borra).
178
+ 4. Registra la carpeta como marketplace local e instala `battlearena@battlearena-local` para tu usuario
179
+ (`claude plugin marketplace add` + `claude plugin install --scope user`). Se lee directo de esa
180
+ carpeta: para actualizar basta volver a correr el instalador y `/reload-plugins`.
181
+
182
+ Desde el clone (contribuidores): `HOOK_STREAM_URL=… HOOK_STREAM_API_KEY=… bash apps/claude-hooks/install.sh`.
183
+ Para probar el plugin sin instalarlo: `npm run bundle:skill` y `claude --plugin-dir apps/claude-hooks/plugin`.
93
184
 
94
185
  ## Configuración
95
186
 
96
- El instalador (`cli.mjs`) guarda el endpoint + api-key en
97
- `~/.claude/battle-arena/config.json` y registra los hooks en `~/.claude/settings.json` con
98
- comandos `node "<ruta>"` (sin `VAR=valor` inline, que no existe en Windows). Los hooks leen, en
99
- orden de prioridad:
187
+ Los hooks y el Scorecard leen, en orden de prioridad:
100
188
 
101
189
  1. **Variables de entorno** `HOOK_STREAM_URL` / `HOOK_STREAM_API_KEY` (si están seteadas).
102
- 2. **`config.json`** (`{ "url": "...", "apiKey": "..." }`).
190
+ 2. **`~/.claude/battle-arena/config.json`** (`{ "url": "...", "apiKey": "..." }`), que escribe el instalador.
103
191
  3. Default `http://127.0.0.1:4500`.
104
192
 
105
193
  `HOOK_STREAM_API_KEY` con `ARENA_REQUIRE_AUTH=true` necesita scopes `arena:read`
106
- (handshake/ranking) y `arena:write` (objetivos); `/ingest` acepta cualquier api-key válida.
107
-
108
- El nudge de objetivos requiere además instalar la skill `arena-objective` en cada repo (o
109
- global en `~/.claude/skills/`): ver `docs/how-to/install-arena-objective-skill.md`.
194
+ (handshake, ranking, Scorecard), `arena:write` (objetivos) y `arena:hands` (✋ desde el Scorecard);
195
+ `/ingest` acepta cualquier api-key válida.
110
196
 
111
197
  ## Cutover desde el colector legacy (speech-analytics/tooling/hook-stream)
112
198
 
package/cli.mjs CHANGED
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  // CLI del paquete @thebattlearena/battle-arena-hooks.
3
- // Instala los hooks + skill del Battle Arena en ~/.claude SIN clonar el repo.
3
+ // Instala Battle Arena en Claude Code como el plugin `battlearena` SIN clonar el repo: los hooks,
4
+ // las skills (/battlearena:conquer, …) y el Scorecard de tu día, todo en un solo plugin.
4
5
  //
5
6
  // npx @thebattlearena/battle-arena-hooks install \
6
7
  // --url https://api.battlearena.dev --api-key sk_xxx
8
+ // npx @thebattlearena/battle-arena-hooks uninstall
7
9
  //
8
10
  // (también lee HOOK_STREAM_URL / HOOK_STREAM_API_KEY del entorno)
9
11
  import {
10
12
  cpSync,
11
13
  existsSync,
12
14
  mkdirSync,
13
- readdirSync,
14
15
  readFileSync,
15
16
  writeFileSync,
16
17
  } from 'node:fs';
@@ -18,6 +19,15 @@ import { dirname, join } from 'node:path';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import { execFileSync } from 'node:child_process';
20
21
  import os from 'node:os';
22
+ import {
23
+ copyPlugin,
24
+ hasClaudeCli,
25
+ moveLegacySkills,
26
+ PLUGIN_ID,
27
+ registerPlugin,
28
+ unregisterPlugin,
29
+ withoutLegacyHooks,
30
+ } from './lib/plugin-install.mjs';
21
31
 
22
32
  const PKG_DIR = dirname(fileURLToPath(import.meta.url));
23
33
  const HOME = os.homedir();
@@ -35,6 +45,7 @@ const HOOK_FILES = [
35
45
  'execution-coach-nudge.mjs',
36
46
  'pr-review-worktree.mjs',
37
47
  'objective.mjs',
48
+ 'plan.mjs',
38
49
  ];
39
50
 
40
51
  function arg(name) {
@@ -52,97 +63,110 @@ function gitEmail() {
52
63
  }
53
64
  }
54
65
 
66
+ /** Quita de settings.json los hooks de la versión anterior (ahora los trae el plugin). */
67
+ function cleanLegacyHooks() {
68
+ if (!existsSync(SETTINGS)) return 0;
69
+ const { settings, removed } = withoutLegacyHooks(
70
+ JSON.parse(readFileSync(SETTINGS, 'utf8')),
71
+ );
72
+ if (removed)
73
+ writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + '\n');
74
+ return removed;
75
+ }
76
+
77
+ function cleanLegacy() {
78
+ const removed = cleanLegacyHooks();
79
+ if (removed)
80
+ console.log(
81
+ `🧹 ${removed} hooks de la versión anterior quitados de ${SETTINGS}`,
82
+ );
83
+ const backup = join(BA_DIR, 'legacy-skills', String(Date.now()));
84
+ const moved = moveLegacySkills(SKILLS_DEST, backup);
85
+ if (moved.length)
86
+ console.log(`🧹 skills sueltas movidas a ${backup}: ${moved.join(', ')}`);
87
+ }
88
+
55
89
  function install() {
56
90
  const url =
57
91
  arg('url') || process.env.HOOK_STREAM_URL || 'https://api.battlearena.dev';
58
92
  const key = arg('api-key') || process.env.HOOK_STREAM_API_KEY || '';
59
93
 
94
+ if (!hasClaudeCli()) {
95
+ console.error(
96
+ '❌ No encontré el comando `claude` (Claude Code CLI). Instálalo y vuelve a correr este instalador:\n' +
97
+ ' https://docs.claude.com/en/docs/claude-code/setup',
98
+ );
99
+ process.exit(1);
100
+ }
60
101
  if (!gitEmail()) {
61
102
  console.log(
62
103
  '⚠️ git config user.email vacío — configúralo: es tu identidad en el arena.',
63
104
  );
64
105
  }
65
106
 
66
- // 1) Hooks → ~/.claude/battle-arena/hooks (ubicación estable)
107
+ // 1) Scripts de los hooks → ~/.claude/battle-arena/hooks (las skills los buscan ahí)
67
108
  mkdirSync(HOOKS_DEST, { recursive: true });
68
109
  for (const f of HOOK_FILES) cpSync(join(PKG_DIR, f), join(HOOKS_DEST, f));
69
110
  cpSync(join(PKG_DIR, 'lib'), join(HOOKS_DEST, 'lib'), { recursive: true });
70
- console.log(`✅ hooks copiados a ${HOOKS_DEST}`);
71
-
72
- // 2) Skills del paquete → ~/.claude/skills (cada subcarpeta de skill/*)
73
- const skillsRoot = join(PKG_DIR, 'skill');
74
- if (existsSync(skillsRoot)) {
75
- mkdirSync(SKILLS_DEST, { recursive: true });
76
- const skillDirs = readdirSync(skillsRoot, { withFileTypes: true }).filter(
77
- (d) => d.isDirectory(),
78
- );
79
- for (const d of skillDirs) {
80
- cpSync(join(skillsRoot, d.name), join(SKILLS_DEST, d.name), {
81
- recursive: true,
82
- });
83
- console.log(`✅ skill ${d.name} instalada en ${SKILLS_DEST}`);
84
- }
85
- }
86
111
 
87
- // 3) Config (url + api-key) → ~/.claude/battle-arena/config.json. Los hooks lo leen de ahí.
88
- // Evita hornear `VAR=val node ...` en el comando, sintaxis que NO existe en Windows
89
- // (cmd/PowerShell). Así el comando es solo `node "<ruta>"`, idéntico en los 3 SOs.
112
+ // 2) El plugin (hooks, skills y Scorecard) y su marketplace local, junto a los scripts
113
+ copyPlugin(PKG_DIR, HOOKS_DEST);
114
+
115
+ // 3) Config (url + api-key) → ~/.claude/battle-arena/config.json. Los hooks y el Scorecard lo leen
116
+ // de ahí; así el comando es solo `node "<ruta>"`, idéntico en los 3 SOs.
90
117
  writeFileSync(
91
118
  join(BA_DIR, 'config.json'),
92
119
  JSON.stringify({ url, apiKey: key || undefined }, null, 2) + '\n',
93
120
  );
94
121
 
95
- // 4) Hooks en ~/.claude/settings.json (merge idempotente, sin pisar otros)
96
- const cmd = (f) => `node "${join(HOOKS_DEST, f)}"`;
97
- const map = {
98
- SessionStart: ['session-start.mjs'],
99
- UserPromptSubmit: [
100
- 'emit.mjs',
101
- 'check-connection.mjs',
102
- 'objective-nudge.mjs',
103
- 'execution-coach-nudge.mjs',
104
- ],
105
- PreToolUse: ['emit.mjs'],
106
- PostToolUse: ['emit.mjs', 'pr-review-worktree.mjs'],
107
- Notification: ['emit.mjs'],
108
- Stop: ['emit.mjs'],
109
- SubagentStop: ['emit.mjs'],
110
- PreCompact: ['emit.mjs'],
111
- SessionEnd: ['emit.mjs'],
112
- };
113
-
114
- mkdirSync(CLAUDE, { recursive: true });
115
- const settings = existsSync(SETTINGS)
116
- ? JSON.parse(readFileSync(SETTINGS, 'utf8'))
117
- : {};
118
- settings.hooks = settings.hooks || {};
119
- for (const [event, scripts] of Object.entries(map)) {
120
- const groups = (settings.hooks[event] = settings.hooks[event] || []);
121
- // quita entradas previas del arena (idempotente) y reescribe en su grupo
122
- for (const g of groups)
123
- g.hooks = (g.hooks || []).filter(
124
- (h) =>
125
- !/battle-arena[/\\]hooks|claude-hooks[/\\]/.test(h.command || ''),
126
- );
127
- settings.hooks[event] = groups.filter((g) => (g.hooks || []).length > 0);
128
- settings.hooks[event].push({
129
- hooks: scripts.map((f) => ({ type: 'command', command: cmd(f) })),
130
- });
122
+ // 4) Registrar e instalar el plugin (idempotente). Va ANTES de limpiar lo anterior: si falla,
123
+ // la instalación vieja sigue funcionando intacta.
124
+ try {
125
+ registerPlugin(HOOKS_DEST);
126
+ } catch (error) {
127
+ console.error(
128
+ `❌ No pude instalar el plugin (tu instalación anterior sigue igual): ${String(error.stderr || error.message).trim()}`,
129
+ );
130
+ process.exit(1);
131
131
  }
132
- writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + '\n');
133
- console.log(`✅ hooks registrados en ${SETTINGS}`);
132
+
133
+ // 5) Ya con el plugin activo, lo que dejó la versión anterior: hooks en settings.json y skills
134
+ // sueltas (no duplicar cada evento)
135
+ cleanLegacy();
136
+
137
+ console.log(
138
+ `✅ plugin ${PLUGIN_ID} instalado (se lee de ${join(HOOKS_DEST, 'plugin')})`,
139
+ );
134
140
  console.log(` colector: ${url}${key ? '' : ' (sin api-key)'}`);
135
141
  console.log(
136
- '\nListo. Abre Claude Code en cualquier repo y verás el handshake del arena.',
142
+ '\nListo. Abre Claude Code: /battlearena:conquer arma tu plan y /scorecard abre tu día.' +
143
+ '\nSi ya tenías una sesión abierta, corre /reload-plugins.',
144
+ );
145
+ }
146
+
147
+ function uninstall() {
148
+ const result = hasClaudeCli()
149
+ ? unregisterPlugin()
150
+ : { plugin: false, marketplace: false };
151
+ cleanLegacyHooks();
152
+ console.log(
153
+ result.plugin
154
+ ? `✅ plugin ${PLUGIN_ID} desinstalado`
155
+ : `ℹ️ el plugin ${PLUGIN_ID} no estaba instalado`,
156
+ );
157
+ console.log(
158
+ ` Tu config (${join(BA_DIR, 'config.json')}) se queda; bórrala si ya no usarás Battle Arena.`,
137
159
  );
138
160
  }
139
161
 
162
+ const COMMANDS = { install, uninstall };
140
163
  const command = process.argv[2];
141
- if (command === 'install') {
142
- install();
143
- } else {
164
+ const run = COMMANDS[command];
165
+ if (!run) {
144
166
  console.log(
145
- 'Uso: npx @thebattlearena/battle-arena-hooks install [--url <colector>] [--api-key <key>]',
167
+ 'Uso: npx @thebattlearena/battle-arena-hooks install [--url <colector>] [--api-key <key>]\n' +
168
+ ' npx @thebattlearena/battle-arena-hooks uninstall',
146
169
  );
147
170
  process.exit(command ? 1 : 0);
148
171
  }
172
+ run();
@@ -36,7 +36,7 @@ const SELF_ASSESS =
36
36
  'Battle Arena — antes de responder, juzga si el pedido del usuario está claramente vago, ' +
37
37
  'subespecificado o confuso: sin objetivo claro, sin criterio de aceptación, o sin saber para ' +
38
38
  'quién/para qué es (p. ej. "quiero un código para desplegar el proyecto" sin decir qué app, ' +
39
- 'entorno ni cómo se ve "listo"). Si CLARAMENTE lo está, invoca el skill `execution-coach` para ' +
39
+ 'entorno ni cómo se ve "listo"). Si CLARAMENTE lo está, invoca el skill `battlearena:execution-coach` para ' +
40
40
  'interceptar con preguntas mayéuticas antes de codear. Sé conservador: ante la duda, o si el ' +
41
41
  'pedido ya es concreto, sigue normal sin interrumpir.';
42
42
 
@@ -228,7 +228,7 @@ process.stdin.on('end', () => {
228
228
  let context = SELF_ASSESS;
229
229
  if (signals.length) {
230
230
  context +=
231
- '\n\nAdemás, señales deterministas en esta sesión (revísalas con `execution-coach` si ' +
231
+ '\n\nAdemás, señales deterministas en esta sesión (revísalas con `battlearena:execution-coach` si ' +
232
232
  'siguen vigentes):\n' +
233
233
  signals.map((s) => `- ${s}`).join('\n');
234
234
  }