@thebattlearena/corner 0.8.0 → 0.9.1-beta.70
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/README.md +38 -5
- package/fonts/battle-arena-icons/BattleArenaIcons.ttf +0 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/icons.generated.ts +2 -0
- package/plugin/hooks/pane.tsx +16 -2
- package/plugin/hooks/register.tsx +62 -33
- package/plugin/hooks/ring-io.ts +59 -1
- package/plugin/hooks/ring-pane.tsx +125 -35
- package/plugin/hooks/ring.ts +73 -38
- package/plugin/hooks/sparring.ts +139 -0
- package/plugin/icons/rudo-amber-2x1.png +0 -0
- package/plugin/icons/rudo-amber-4x2.png +0 -0
- package/plugin/icons/rudo-amber-5x2.png +0 -0
- package/plugin/icons/rudo-amber-6x2.png +0 -0
- package/plugin/icons/rudo-grey-2x1.png +0 -0
- package/plugin/icons/rudo-grey-4x2.png +0 -0
- package/plugin/icons/rudo-grey-5x2.png +0 -0
- package/plugin/icons/rudo-grey-6x2.png +0 -0
- package/plugin/icons/rudo-pink-2x1.png +0 -0
- package/plugin/icons/rudo-pink-4x2.png +0 -0
- package/plugin/icons/rudo-pink-5x2.png +0 -0
- package/plugin/icons/rudo-pink-6x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-2x1.png +0 -0
- package/plugin/icons/rudo-pink-dim-4x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-5x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-6x2.png +0 -0
- package/plugin/icons/rudo-red-2x1.png +0 -0
- package/plugin/icons/rudo-red-4x2.png +0 -0
- package/plugin/icons/rudo-red-5x2.png +0 -0
- package/plugin/icons/rudo-red-6x2.png +0 -0
- package/plugin/tests/ring-pane.test.tsx +154 -0
- package/plugin/tests/ring.test.ts +13 -4
- package/plugin/tests/sparring.test.ts +204 -0
- package/plugin/types/index.d.ts +25 -1
- package/ring/.claude-plugin/plugin.json +1 -1
- package/ring/agents/rudo.md +95 -0
- package/ring/scripts/chronicle.mjs +24 -0
- package/ring/scripts/config.mjs +47 -1
- package/ring/scripts/fight.mjs +238 -0
- package/ring/scripts/judges-card.mjs +53 -9
- package/ring/scripts/live-hook.mjs +42 -15
- package/ring/scripts/live.mjs +96 -1
- package/ring/scripts/mask-guard.mjs +184 -6
- package/ring/scripts/monitors.mjs +2 -1
- package/ring/scripts/play-by-play.mjs +39 -9
- package/ring/scripts/round-bell.mjs +10 -6
- package/ring/scripts/sparring-chronicle.mjs +470 -0
- package/ring/scripts/sparring.mjs +1161 -0
- package/ring/skills/play-by-play/SKILL.md +49 -3
- package/ring/skills/play-by-play/templates/chronicle.html +7 -0
- package/ring/skills/referee/SKILL.md +46 -2
- package/ring/skills/sparring/SKILL.md +408 -0
- package/ring/skills/sparring/references/host-signal.md +86 -0
- package/ring/skills/sparring/references/lessons.md +50 -0
- package/ring/skills/sparring/templates/pr-body.md +34 -0
- package/ring/skills/sparring/templates/rudo-brief.md +47 -0
- package/ring/skills/sparring/templates/sparring-card.md +33 -0
- package/ring/skills/weigh-in/SKILL.md +21 -3
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -163,6 +163,10 @@ type Setup = {
|
|
|
163
163
|
remote?: string;
|
|
164
164
|
/** Aquí se anotan los `argv` de cada `process.run`. */
|
|
165
165
|
runs?: string[][];
|
|
166
|
+
/** Sparrings en `<RING_HOME>/sparrings/<slug>/`: su `sparring.json` y su feed. */
|
|
167
|
+
sparrings?: Record<string, { state: object; live: object[] }>;
|
|
168
|
+
/** El id de la sesión (`$.session.id()`). */
|
|
169
|
+
sessionId?: string;
|
|
166
170
|
};
|
|
167
171
|
|
|
168
172
|
const engine = (on: On, setup: Setup = {}) => {
|
|
@@ -179,6 +183,8 @@ const engine = (on: On, setup: Setup = {}) => {
|
|
|
179
183
|
termProgram,
|
|
180
184
|
remote = 'git@github.com:julio-supervisor/agents-studio-be.git\n',
|
|
181
185
|
runs = [],
|
|
186
|
+
sparrings = {},
|
|
187
|
+
sessionId = 'sesion-1',
|
|
182
188
|
} = setup;
|
|
183
189
|
const files: Record<string, string> = {
|
|
184
190
|
'/Users/test/.claude/battle-arena/config.json': JSON.stringify({
|
|
@@ -196,6 +202,15 @@ const engine = (on: On, setup: Setup = {}) => {
|
|
|
196
202
|
[`${BOUT}/live.jsonl`]: live.map((e) => JSON.stringify(e)).join('\n'),
|
|
197
203
|
[`${BOUT}/title-defense.md`]: CATALOGUE,
|
|
198
204
|
[`${ROOT}/.claude/ring.json`]: JSON.stringify({ monitors }),
|
|
205
|
+
...Object.fromEntries(
|
|
206
|
+
Object.entries(sparrings).flatMap(([slug, { state, live: feed }]) => [
|
|
207
|
+
[`${HOME}/sparrings/${slug}/sparring.json`, JSON.stringify(state)],
|
|
208
|
+
[
|
|
209
|
+
`${HOME}/sparrings/${slug}/live.jsonl`,
|
|
210
|
+
feed.map((e) => JSON.stringify(e)).join('\n'),
|
|
211
|
+
],
|
|
212
|
+
]),
|
|
213
|
+
),
|
|
199
214
|
};
|
|
200
215
|
const dirs: Record<string, string[]> = {
|
|
201
216
|
[HOME]: ['julio-supervisor'],
|
|
@@ -210,6 +225,13 @@ const engine = (on: On, setup: Setup = {}) => {
|
|
|
210
225
|
'round-15-b.md',
|
|
211
226
|
'round6-since.txt',
|
|
212
227
|
],
|
|
228
|
+
[`${HOME}/sparrings`]: Object.keys(sparrings),
|
|
229
|
+
...Object.fromEntries(
|
|
230
|
+
Object.keys(sparrings).map((slug) => [
|
|
231
|
+
`${HOME}/sparrings/${slug}`,
|
|
232
|
+
['sparring.json', 'live.jsonl', 'card.md'],
|
|
233
|
+
]),
|
|
234
|
+
),
|
|
213
235
|
};
|
|
214
236
|
on('session.start', (_, e) => ({ cwd: e.cwd }));
|
|
215
237
|
on('clock.now', () => ({ value: NOW }));
|
|
@@ -252,6 +274,7 @@ const engine = (on: On, setup: Setup = {}) => {
|
|
|
252
274
|
on('ui.open', () => ({ value: { isPlaced: true } }));
|
|
253
275
|
on('ui.toast', () => ({ value: undefined }));
|
|
254
276
|
on('session.root', () => ({ value: ROOT }));
|
|
277
|
+
on('session.id', () => ({ value: sessionId }));
|
|
255
278
|
on('env.get', (_, e) => {
|
|
256
279
|
if (e.name === 'TERM_PROGRAM') return { value: termProgram };
|
|
257
280
|
return { value: e.name === 'RING_HOME' ? HOME : '/Users/test' };
|
|
@@ -1346,3 +1369,134 @@ for (const termProgram of [undefined, 'Apple_Terminal'])
|
|
|
1346
1369
|
expect((await ui.find({ key: 'ring-chip-bateria' }))?.text).toContain('⚠');
|
|
1347
1370
|
await ui.unmount();
|
|
1348
1371
|
});
|
|
1372
|
+
|
|
1373
|
+
const SPARRING = {
|
|
1374
|
+
slug: 'tira-morada',
|
|
1375
|
+
verdict: { call: 'rematch', note: 'oculta los totales', at: ago(7200) },
|
|
1376
|
+
history: [{ call: 'rematch' }],
|
|
1377
|
+
repos: [
|
|
1378
|
+
{
|
|
1379
|
+
repo: 'acme/api',
|
|
1380
|
+
worktree: '/wt/api-sparring',
|
|
1381
|
+
pr: 7,
|
|
1382
|
+
url: 'https://github.com/acme/api/pull/7',
|
|
1383
|
+
},
|
|
1384
|
+
{ repo: 'acme/web', worktree: '/wt/web-sparring', pr: null, url: null },
|
|
1385
|
+
],
|
|
1386
|
+
sessions: [{ id: 'sesion-1', role: 'referee' }],
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const SPARRING_LIVE = [
|
|
1390
|
+
{
|
|
1391
|
+
at: ago(900),
|
|
1392
|
+
actor: 'arbitro',
|
|
1393
|
+
state: 'done',
|
|
1394
|
+
label: 'abrió el PR',
|
|
1395
|
+
id: 'p',
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
at: ago(240),
|
|
1399
|
+
actor: 'rudo',
|
|
1400
|
+
state: 'active',
|
|
1401
|
+
label: 'tira-morada acme/web',
|
|
1402
|
+
id: 'r1',
|
|
1403
|
+
detail: 'tira-morada acme/web',
|
|
1404
|
+
},
|
|
1405
|
+
];
|
|
1406
|
+
|
|
1407
|
+
test('sparring: su sección con la ronda, el veredicto, sus repos y el rudo trabajando', async ($, on) => {
|
|
1408
|
+
engine(on, {
|
|
1409
|
+
sparrings: { 'tira-morada': { state: SPARRING, live: SPARRING_LIVE } },
|
|
1410
|
+
});
|
|
1411
|
+
await start($);
|
|
1412
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' });
|
|
1413
|
+
const row = 'sparring-tira-morada-row';
|
|
1414
|
+
expect(await ui.find({ key: row })).toBeTruthy();
|
|
1415
|
+
expect(
|
|
1416
|
+
await ui.find({ type: 'Text', text: 'Sparring tira-morada' }),
|
|
1417
|
+
).toBeTruthy();
|
|
1418
|
+
// Va tras los puntos del plan, como un combate sin punto anfitrión.
|
|
1419
|
+
const tree = await texts(ui);
|
|
1420
|
+
expect(tree.indexOf('Programar lotes desde el panel')).toBeLessThan(
|
|
1421
|
+
tree.indexOf('Sparring tira-morada'),
|
|
1422
|
+
);
|
|
1423
|
+
expect(await ui.find({ key: 'sparring-tira-morada-chip-rudo' })).toBeFalsy();
|
|
1424
|
+
await ui.press({ key: row });
|
|
1425
|
+
expect(await ui.find({ type: 'Text', text: 'ronda 2' })).toBeTruthy();
|
|
1426
|
+
expect(
|
|
1427
|
+
await ui.find({ type: 'Text', text: 'último veredicto: rematch' }),
|
|
1428
|
+
).toBeTruthy();
|
|
1429
|
+
expect(
|
|
1430
|
+
await ui.find({ key: 'sparring-tira-morada-repo-acme/api', text: /#7/ }),
|
|
1431
|
+
).toBeTruthy();
|
|
1432
|
+
expect(
|
|
1433
|
+
await ui.find({
|
|
1434
|
+
key: 'sparring-tira-morada-repo-acme/web',
|
|
1435
|
+
text: /sin PR/,
|
|
1436
|
+
}),
|
|
1437
|
+
).toBeTruthy();
|
|
1438
|
+
// La tira: árbitro, rudo y juez; el rudo trabaja (su barrita), el combate sigue con la suya.
|
|
1439
|
+
for (const id of ['arbitro', 'rudo', 'juez'])
|
|
1440
|
+
expect(
|
|
1441
|
+
await ui.find({ key: `sparring-tira-morada-chip-${id}` }),
|
|
1442
|
+
).toBeTruthy();
|
|
1443
|
+
expect(
|
|
1444
|
+
await ui.find({ key: 'sparring-tira-morada-chip-luchador' }),
|
|
1445
|
+
).toBeFalsy();
|
|
1446
|
+
expect(
|
|
1447
|
+
await ui.find({
|
|
1448
|
+
key: 'sparring-tira-morada-chip-rudo',
|
|
1449
|
+
text: /[▰▱]{4}/,
|
|
1450
|
+
}),
|
|
1451
|
+
).toBeTruthy();
|
|
1452
|
+
expect(
|
|
1453
|
+
await ui.find({ type: 'Text', text: 'tira-morada acme/web' }),
|
|
1454
|
+
).toBeTruthy();
|
|
1455
|
+
// Abrir la ficha del rudo no abre ninguna del combate.
|
|
1456
|
+
await ui.press({ key: 'sparring-tira-morada-chip-rudo' });
|
|
1457
|
+
expect(await ui.find({ key: 'sparring-tira-morada-field-en' })).toBeTruthy();
|
|
1458
|
+
expect(await ui.find({ key: 'ring-field-en' })).toBeFalsy();
|
|
1459
|
+
await ui.unmount();
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
test('sparring: una pregunta abierta enciende al juez; uno con ko o de otra sesión no se pinta', async ($, on) => {
|
|
1463
|
+
engine(on, {
|
|
1464
|
+
sparrings: {
|
|
1465
|
+
'tira-morada': {
|
|
1466
|
+
state: SPARRING,
|
|
1467
|
+
live: [
|
|
1468
|
+
...SPARRING_LIVE,
|
|
1469
|
+
{
|
|
1470
|
+
at: ago(30),
|
|
1471
|
+
actor: 'juez',
|
|
1472
|
+
state: 'waiting',
|
|
1473
|
+
label: 'te toca decidir',
|
|
1474
|
+
id: 'q1',
|
|
1475
|
+
},
|
|
1476
|
+
],
|
|
1477
|
+
},
|
|
1478
|
+
ganada: {
|
|
1479
|
+
state: {
|
|
1480
|
+
...SPARRING,
|
|
1481
|
+
slug: 'ganada',
|
|
1482
|
+
verdict: { call: 'ko', note: 'x' },
|
|
1483
|
+
},
|
|
1484
|
+
live: [],
|
|
1485
|
+
},
|
|
1486
|
+
ajena: {
|
|
1487
|
+
state: { ...SPARRING, slug: 'ajena', sessions: [{ id: 'otra' }] },
|
|
1488
|
+
live: [],
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
});
|
|
1492
|
+
await start($);
|
|
1493
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' });
|
|
1494
|
+
expect(await ui.find({ key: 'sparring-ganada-row' })).toBeFalsy();
|
|
1495
|
+
expect(await ui.find({ key: 'sparring-ajena-row' })).toBeFalsy();
|
|
1496
|
+
await ui.press({ key: 'sparring-tira-morada-row' });
|
|
1497
|
+
expect(
|
|
1498
|
+
await ui.find({ key: 'sparring-tira-morada-chip-juez', text: 'tú' }),
|
|
1499
|
+
).toBeTruthy();
|
|
1500
|
+
expect(await ui.find({ type: 'Text', text: /TE TOCA DECIDIR/ })).toBeTruthy();
|
|
1501
|
+
await ui.unmount();
|
|
1502
|
+
});
|
|
@@ -416,13 +416,15 @@ test('paleta: cada actor con su color; un monitor pide uno de la paleta o va en
|
|
|
416
416
|
e2e: 'pink',
|
|
417
417
|
mal: 'green',
|
|
418
418
|
});
|
|
419
|
-
|
|
420
|
-
expect(
|
|
419
|
+
// El rudo (sparring) no va en la tira de un combate, pero tiene su color: el rosa.
|
|
420
|
+
expect(ACTOR_COLOR.rudo).toBe('pink');
|
|
421
|
+
expect(Object.keys(ACTOR_COLOR)).toHaveLength(7);
|
|
422
|
+
expect(new Set(Object.values(ACTOR_COLOR)).size).toBe(7);
|
|
421
423
|
expect(Object.keys(PALETTE)).toContain('green');
|
|
422
424
|
});
|
|
423
425
|
|
|
424
|
-
test('los íconos salen de la fuente: los
|
|
425
|
-
expect(Object.keys(ICONS)).toHaveLength(
|
|
426
|
+
test('los íconos salen de la fuente: los 59, con su glifo, sus letras y sus PNG', () => {
|
|
427
|
+
expect(Object.keys(ICONS)).toHaveLength(59);
|
|
426
428
|
expect(ICONS.trofeo).toEqual({
|
|
427
429
|
glyph: String.fromCodePoint(0xfba57),
|
|
428
430
|
alt: 'TR',
|
|
@@ -431,6 +433,13 @@ test('los íconos salen de la fuente: los 58, con su glifo, sus letras y sus PNG
|
|
|
431
433
|
cells: 1,
|
|
432
434
|
});
|
|
433
435
|
expect(ICONS.luchador.tints).toEqual(['blue', 'red', 'amber']);
|
|
436
|
+
expect(ICONS.rudo).toEqual({
|
|
437
|
+
glyph: String.fromCodePoint(0xfba16),
|
|
438
|
+
alt: 'RU',
|
|
439
|
+
png: 'rudo',
|
|
440
|
+
tints: ['pink', 'red', 'amber'],
|
|
441
|
+
cells: 1,
|
|
442
|
+
});
|
|
434
443
|
expect(
|
|
435
444
|
monitorsOf({ monitors: [{ id: 'copa', label: 'Copa', icon: 'trofeo' }] }),
|
|
436
445
|
).toEqual([{ id: 'copa', label: 'Copa', icon: 'trofeo' }]);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { expect, test } from 'claude-code/testing';
|
|
2
|
+
|
|
3
|
+
import type { RingEvent } from '../types';
|
|
4
|
+
import type { SparringFile } from '../hooks/sparring';
|
|
5
|
+
import { deriveRing, monitorsOf, parseEvents } from '../hooks/ring';
|
|
6
|
+
import {
|
|
7
|
+
deriveSparring,
|
|
8
|
+
isActiveSparring,
|
|
9
|
+
roundOf,
|
|
10
|
+
SPARRING_FRESH_MS,
|
|
11
|
+
sparringOf,
|
|
12
|
+
sparringsFor,
|
|
13
|
+
} from '../hooks/sparring';
|
|
14
|
+
|
|
15
|
+
const NOW = Date.parse('2026-10-24T20:00:00Z');
|
|
16
|
+
const ago = (seconds: number) => new Date(NOW - seconds * 1000).toISOString();
|
|
17
|
+
const HOUR = 3_600_000;
|
|
18
|
+
|
|
19
|
+
const file = (slug: string, extra: Partial<SparringFile> = {}) => ({
|
|
20
|
+
slug,
|
|
21
|
+
verdict: null,
|
|
22
|
+
repos: [
|
|
23
|
+
{
|
|
24
|
+
repo: 'acme/web',
|
|
25
|
+
worktree: `/wt/web-sparring-${slug}`,
|
|
26
|
+
pr: null,
|
|
27
|
+
url: null,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
sessions: [{ id: 's1' }],
|
|
31
|
+
...extra,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('activo: sin veredicto final (un rematch sigue) y tocado en las últimas 12 h', () => {
|
|
35
|
+
expect(isActiveSparring(file('a'), NOW - HOUR, NOW)).toBe(true);
|
|
36
|
+
expect(
|
|
37
|
+
isActiveSparring(
|
|
38
|
+
file('a', { verdict: { call: 'rematch', note: 'x' } }),
|
|
39
|
+
NOW,
|
|
40
|
+
NOW,
|
|
41
|
+
),
|
|
42
|
+
).toBe(true);
|
|
43
|
+
expect(
|
|
44
|
+
isActiveSparring(
|
|
45
|
+
file('a', { verdict: { call: 'ko', note: 'x' } }),
|
|
46
|
+
NOW,
|
|
47
|
+
NOW,
|
|
48
|
+
),
|
|
49
|
+
).toBe(false);
|
|
50
|
+
expect(
|
|
51
|
+
isActiveSparring(
|
|
52
|
+
file('a', { verdict: { call: 'discard', note: 'x' } }),
|
|
53
|
+
NOW,
|
|
54
|
+
NOW,
|
|
55
|
+
),
|
|
56
|
+
).toBe(false);
|
|
57
|
+
expect(isActiveSparring(file('a'), NOW - SPARRING_FRESH_MS, NOW)).toBe(false);
|
|
58
|
+
expect(isActiveSparring({ slug: 3 }, NOW, NOW)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('qué sparrings muestra la sesión: los que registró; sin ninguno, los de su carpeta', () => {
|
|
62
|
+
const found = [
|
|
63
|
+
{ state: file('vieja'), touched: NOW - 2 * HOUR },
|
|
64
|
+
{ state: file('nueva'), touched: NOW - HOUR },
|
|
65
|
+
{ state: file('ajena', { sessions: [{ id: 's2' }] }), touched: NOW },
|
|
66
|
+
{
|
|
67
|
+
state: file('ganada', { verdict: { call: 'ko', note: 'x' } }),
|
|
68
|
+
touched: NOW,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
const slugs = (list: typeof found) => list.map((s) => s.state.slug);
|
|
72
|
+
expect(
|
|
73
|
+
slugs(sparringsFor(found, { sessionId: 's1', root: '/repo', now: NOW })),
|
|
74
|
+
).toEqual(['nueva', 'vieja']);
|
|
75
|
+
// Otra sesión que no registró ninguno: los que trabajan en su carpeta (o debajo de ella).
|
|
76
|
+
expect(
|
|
77
|
+
slugs(
|
|
78
|
+
sparringsFor(found, {
|
|
79
|
+
sessionId: 's9',
|
|
80
|
+
root: '/wt/web-sparring-ajena/src',
|
|
81
|
+
now: NOW,
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
).toEqual(['ajena']);
|
|
85
|
+
expect(
|
|
86
|
+
slugs(
|
|
87
|
+
sparringsFor(found, {
|
|
88
|
+
sessionId: null,
|
|
89
|
+
root: '/wt/web-sparring-ganada',
|
|
90
|
+
now: NOW,
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
).toEqual([]);
|
|
94
|
+
// `/wt/web-sparring-nuevas` no es la carpeta de `nueva`.
|
|
95
|
+
expect(
|
|
96
|
+
slugs(
|
|
97
|
+
sparringsFor(found, {
|
|
98
|
+
sessionId: null,
|
|
99
|
+
root: '/wt/web-sparring-nuevas',
|
|
100
|
+
now: NOW,
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('la ronda: veredictos de history + 1; los de antes de history, por su veredicto', () => {
|
|
107
|
+
expect(roundOf(file('a'))).toBe(1);
|
|
108
|
+
expect(
|
|
109
|
+
roundOf(
|
|
110
|
+
file('a', {
|
|
111
|
+
verdict: { call: 'rematch', note: 'x' },
|
|
112
|
+
history: [{}, {}],
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
).toBe(3);
|
|
116
|
+
expect(roundOf(file('a', { verdict: { call: 'rematch', note: 'x' } }))).toBe(
|
|
117
|
+
2,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('el sparring: sus repos con su PR, la ronda, el último veredicto y su tira (árbitro, rudo, juez)', () => {
|
|
122
|
+
const events = parseEvents(
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
at: ago(900),
|
|
126
|
+
actor: 'arbitro',
|
|
127
|
+
state: 'done',
|
|
128
|
+
label: 'abrió el PR',
|
|
129
|
+
pr: 12,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
at: ago(300),
|
|
133
|
+
actor: 'rudo',
|
|
134
|
+
state: 'active',
|
|
135
|
+
label: 'tira-morada acme/web',
|
|
136
|
+
id: 'r1',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
at: ago(60),
|
|
140
|
+
actor: 'juez',
|
|
141
|
+
state: 'waiting',
|
|
142
|
+
label: 'te toca decidir',
|
|
143
|
+
id: 'q1',
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
.map((e) => JSON.stringify(e))
|
|
147
|
+
.join('\n'),
|
|
148
|
+
[],
|
|
149
|
+
);
|
|
150
|
+
expect(events).toHaveLength(3);
|
|
151
|
+
const sparring = sparringOf(
|
|
152
|
+
file('tira-morada', {
|
|
153
|
+
verdict: { call: 'rematch', note: 'oculta los totales' },
|
|
154
|
+
history: [{}],
|
|
155
|
+
repos: [
|
|
156
|
+
{ repo: 'acme/api', pr: 7, url: 'https://github.com/acme/api/pull/7' },
|
|
157
|
+
{ repo: 'acme/web' },
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
events,
|
|
161
|
+
);
|
|
162
|
+
expect(sparring).toMatchObject({
|
|
163
|
+
slug: 'tira-morada',
|
|
164
|
+
round: 2,
|
|
165
|
+
verdict: { call: 'rematch', note: 'oculta los totales' },
|
|
166
|
+
repos: [
|
|
167
|
+
{ repo: 'acme/api', pr: 7, url: 'https://github.com/acme/api/pull/7' },
|
|
168
|
+
{ repo: 'acme/web', pr: null, url: null },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
const view = deriveSparring(sparring, NOW);
|
|
172
|
+
expect(view.chips.map((c) => [c.id, c.color, c.mode])).toEqual([
|
|
173
|
+
['arbitro', 'slate', 'done'],
|
|
174
|
+
['rudo', 'pink', 'active'],
|
|
175
|
+
['juez', 'orange', 'waiting'],
|
|
176
|
+
]);
|
|
177
|
+
expect(view.running).toBe(true);
|
|
178
|
+
expect(view.waiting).toBe(true);
|
|
179
|
+
expect(view.live?.id).toBe('juez');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('el rudo no entra a la tira de un combate, y ningún monitor puede llamarse rudo', () => {
|
|
183
|
+
const rudo: RingEvent = {
|
|
184
|
+
at: ago(10),
|
|
185
|
+
actor: 'rudo',
|
|
186
|
+
state: 'active',
|
|
187
|
+
label: 'x',
|
|
188
|
+
};
|
|
189
|
+
const view = deriveRing(
|
|
190
|
+
{
|
|
191
|
+
slug: 'acme/api',
|
|
192
|
+
pr: 1,
|
|
193
|
+
playByPlayUrl: null,
|
|
194
|
+
defense: null,
|
|
195
|
+
bells: 0,
|
|
196
|
+
events: [rudo],
|
|
197
|
+
monitors: [],
|
|
198
|
+
commands: [],
|
|
199
|
+
},
|
|
200
|
+
NOW,
|
|
201
|
+
);
|
|
202
|
+
expect(view.chips.map((c) => c.id)).not.toContain('rudo');
|
|
203
|
+
expect(monitorsOf({ monitors: [{ id: 'rudo', label: 'Rudo' }] })).toEqual([]);
|
|
204
|
+
});
|
package/plugin/types/index.d.ts
CHANGED
|
@@ -247,7 +247,8 @@ export type RingActorId =
|
|
|
247
247
|
| 'esquinero'
|
|
248
248
|
| 'campana'
|
|
249
249
|
| 'copilot'
|
|
250
|
-
| 'juez'
|
|
250
|
+
| 'juez'
|
|
251
|
+
| 'rudo';
|
|
251
252
|
|
|
252
253
|
export type RingEventState = 'active' | 'waiting' | 'done' | 'failed';
|
|
253
254
|
|
|
@@ -314,6 +315,27 @@ export type RingBout = {
|
|
|
314
315
|
commands: string[];
|
|
315
316
|
};
|
|
316
317
|
|
|
318
|
+
/** Un repo de un sparring: su PR (borrador) cuando ya se abrió. */
|
|
319
|
+
export type RingSparringRepo = {
|
|
320
|
+
repo: string;
|
|
321
|
+
pr: number | null;
|
|
322
|
+
url: string | null;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Un sparring activo, leído de `~/.claude/ring/sparrings/<slug>/` (`sparring.json` y su
|
|
327
|
+
* `live.jsonl`): no es un combate, pero se pinta como uno, con su propia tira (árbitro, rudo, juez).
|
|
328
|
+
*/
|
|
329
|
+
export type RingSparring = {
|
|
330
|
+
slug: string;
|
|
331
|
+
repos: RingSparringRepo[];
|
|
332
|
+
/** La que se pelea: veredictos de `history` + 1. */
|
|
333
|
+
round: number;
|
|
334
|
+
/** El último veredicto (un `rematch`, si sigue activo); `null` mientras el juez no dicta. */
|
|
335
|
+
verdict: { call: string; note: string } | null;
|
|
336
|
+
events: RingEvent[];
|
|
337
|
+
};
|
|
338
|
+
|
|
317
339
|
/** Si la terminal dibuja las imágenes de las fichas (`null` sin probar) y por qué no. */
|
|
318
340
|
export type RingImages = { draws: boolean | null; reason: string | null };
|
|
319
341
|
|
|
@@ -327,6 +349,8 @@ export type IconsDiag = {
|
|
|
327
349
|
|
|
328
350
|
export type RingSnapshot = {
|
|
329
351
|
bout: RingBout | null;
|
|
352
|
+
/** Los sparrings activos de esta sesión (o de su carpeta); vacío sin ninguno. */
|
|
353
|
+
sparrings?: RingSparring[];
|
|
330
354
|
readAt: number;
|
|
331
355
|
/** `iconFont: true` en la config: la fuente Battle Arena Icons está instalada. */
|
|
332
356
|
iconFont?: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "A way of working for backend, frontend and mobile repos, each at its own venue: a referee session at high effort, one long-lived wrestler at medium, read-only cornermen at high, small do-not-merge review PRs, rebounds, a numbered rulebook and a durable judges card.",
|
|
4
|
+
"description": "A way of working for backend, frontend and mobile repos, each at its own venue: a referee session at high effort, one long-lived wrestler at medium, read-only cornermen at high, small do-not-merge review PRs, rebounds, a numbered rulebook and a durable judges card, plus sparring drafts (a rudo at medium) that prove a feature is worth building before it is built well.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Battle Arena"
|
|
7
7
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rudo
|
|
3
|
+
description: The ring's sparring executor. Builds a quick, deliberately low-quality draft of a feature in its own worktree, only to prove whether it is worth building; never merged. Launched and directed by the ring referee from `/ring:sparring`; takes every doubt to the referee (never to the human), works inside a timebox and lists every shortcut it took. Suspends the style and test rules, never the live-operation ones.
|
|
4
|
+
tools: Read, Grep, Glob, Edit, Write, Bash
|
|
5
|
+
effort: medium
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the rudo: in lucha libre the rudo wins however he can, the técnico wins clean. You are
|
|
9
|
+
the first one in. Your job is to make the feature **work well enough for the judge to try it, in
|
|
10
|
+
the whole experience it lives in**, inside the timebox, and to say honestly which corners you cut. If the judge calls it a KO, a ring
|
|
11
|
+
bout refactors your branch later; nobody expects clean code from you.
|
|
12
|
+
|
|
13
|
+
## What you may skip
|
|
14
|
+
|
|
15
|
+
- The rulebook's style and design rules (R): layers, patterns, specifications, mappers, file
|
|
16
|
+
size, naming polish. Inline what is faster.
|
|
17
|
+
- The test rules (T): no coverage needed. Only the typecheck gate must pass.
|
|
18
|
+
- Cornermen, rebounds and round-bells: there are none in a sparring.
|
|
19
|
+
- Contract first: the API or model change is a draft too, reviewed later in the bout.
|
|
20
|
+
|
|
21
|
+
## What you never skip
|
|
22
|
+
|
|
23
|
+
- **Live operation (rule F):** no real money, messages, calls or customer events. Sandbox or test
|
|
24
|
+
modes only. The one exception, when your brief grants it: a real send to the judge's own
|
|
25
|
+
destinations, read from `SPARRING_ALLOWED_<KIND>` in `.env.sparring` (comma-separated, filled by
|
|
26
|
+
the judge). Closed by default: an empty list sends nothing; any other destination is recorded
|
|
27
|
+
with `{ sent: false, reason: "not_allowlisted" }`. Never write a destination in code, a fixture,
|
|
28
|
+
a commit or a log; log only whether it was sent.
|
|
29
|
+
- **Secrets:** credentials are names in your brief; their values live in
|
|
30
|
+
`<worktree>/.env.sparring`, filled by the judge. Never commit, print, echo, log or paste a
|
|
31
|
+
value, nor put one in code, a fixture or a commit message. `mask-guard` blocks committing
|
|
32
|
+
`.env.sparring`.
|
|
33
|
+
- **Database:** additive only (new tables or columns, English names), never against production.
|
|
34
|
+
Never drop or alter what exists.
|
|
35
|
+
- Customer data and PII stay out of fixtures and seeds.
|
|
36
|
+
- **Never write to a shared database** (dev, staging). When the proof needs seeded data, prepare
|
|
37
|
+
it in `<sparring dir>/tools/`: synthetic rows marked with the slug, values that reach nobody
|
|
38
|
+
(unassigned phone prefixes, `example.invalid` emails), a dry run, and a cleanup that deletes only
|
|
39
|
+
the marked rows. Report the commands; the judge runs them. If a write is blocked, do not retry it
|
|
40
|
+
another way.
|
|
41
|
+
- Stage by explicit paths (never `git add -A` or `.`); no `--no-verify`; when the brief's
|
|
42
|
+
`forbidAiTrailers` is true (the default), no AI attribution trailers.
|
|
43
|
+
- Push only to `sparring/<slug>`, never force. No ready PRs, merges or base changes: `mask-guard`
|
|
44
|
+
blocks them on a sparring branch, bout or not.
|
|
45
|
+
- Write only inside your worktree; a helper goes in the sparring dir's `tools/`, never in the repo.
|
|
46
|
+
- One heavy command at a time, through `node "<ring>/scripts/one-at-a-time.mjs" run -- <command>`.
|
|
47
|
+
- **Background processes by PID only.** Note the PID of every server, watcher or `tail`/`cat` you
|
|
48
|
+
leave running (`echo $!` right after starting it) and stop it with `kill <pid>` before you
|
|
49
|
+
report. Never `pkill`, `killall` or `kill $(pgrep …)`: other sessions on the machine run
|
|
50
|
+
processes with the same name. Whatever you leave running for the judge to try (the server
|
|
51
|
+
of the proof), list it in your report with its PID.
|
|
52
|
+
|
|
53
|
+
## How you work
|
|
54
|
+
|
|
55
|
+
1. `git -C <worktree> status` must be clean before you start. Every git command is
|
|
56
|
+
`git -C <worktree> …`.
|
|
57
|
+
2. Build the **full screen or flow** of your brief, the way the design draws it, **working**: the
|
|
58
|
+
judge must see the whole experience and every visible control must do something (search,
|
|
59
|
+
filters, tabs, loaders, pagination or infinite scroll, actions and the alert when they fail).
|
|
60
|
+
Data comes from the back, built quick and dirty; a mock is only for what cannot be had yet.
|
|
61
|
+
Never a placeholder that says "comes later", never a button that does nothing. Speed comes
|
|
62
|
+
from the quality you skip, not from the scope or the depth you cut. Before you report, click
|
|
63
|
+
every control in a browser and list any that still does nothing.
|
|
64
|
+
- **Designs** in the brief are followed as closely as the timebox allows. A `.pen` file is read
|
|
65
|
+
only through the pencil MCP tools, never with Read or Grep (it is encrypted).
|
|
66
|
+
- **Other repos of the sparring:** when you produce what another repo consumes (an SDK, an
|
|
67
|
+
endpoint), leave it in `<sparring dir>/tools/` (an SDK as an `npm pack` tarball) and report its
|
|
68
|
+
path, your port and one `curl` that proves it. When you consume, install it from there, never
|
|
69
|
+
from a registry the sparring published to; until it arrives, a mock behind an env var is a
|
|
70
|
+
shortcut to list. Locally, accept requests from any `http://localhost:<port>` origin (CORS),
|
|
71
|
+
never one fixed port, and list it as a shortcut.
|
|
72
|
+
3. Keep a running list of **shortcuts** as you take them ("hardcoded the workspace id", "no
|
|
73
|
+
pagination", "mapping inline in the repository"): it becomes the first rebound of the bout.
|
|
74
|
+
4. Commit often, conventional messages in the language of the branch's previous commits. Push to
|
|
75
|
+
`sparring/<slug>` when something is worth trying.
|
|
76
|
+
5. When the timebox runs out, stop where you are, push what works and report.
|
|
77
|
+
6. Every doubt goes to the referee in your report, with two options each. Never to the human.
|
|
78
|
+
|
|
79
|
+
## What you return (every time you push or stop)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Sparring: <slug> · tip <sha> (pushed to sparring/<slug>)
|
|
83
|
+
Proof of value: reachable ✓ | partial: <what is missing> | not reached: <why>
|
|
84
|
+
How to try it: <how to start it, its port, env vars, sample input>
|
|
85
|
+
Dead controls: none | <control — why> (from your click-through)
|
|
86
|
+
Judge opens: <exact URL of each screen> · enters as <user / workspace / account>
|
|
87
|
+
Left running: none | <process, PID, port> (only what the judge needs to try it)
|
|
88
|
+
Hand-off: none | <path in tools/, port, curl> (producer) | <what you installed from tools/> (consumer)
|
|
89
|
+
Shortcuts: <one line each>
|
|
90
|
+
Credentials used (names only): <NAME, … | none>
|
|
91
|
+
Schema changes: none | <additive change, tables/columns>
|
|
92
|
+
Gate: typecheck ✓/✗ (exact command)
|
|
93
|
+
Questions for the referee: <numbered, 2 options each>
|
|
94
|
+
Time used: <h:mm> of <timebox>
|
|
95
|
+
```
|
|
@@ -21,6 +21,20 @@
|
|
|
21
21
|
// gh events ready_for_review → juez; review_requested to Copilot → campana (closes a round)
|
|
22
22
|
// round-bells a round-bell PR opened → dado
|
|
23
23
|
// --extra any of the types, as given, marked provided
|
|
24
|
+
//
|
|
25
|
+
// A fight that starts at a sparring (sparring-chronicle.mjs, fight.mjs) uses the same eight types,
|
|
26
|
+
// no new one. Its sources, classified:
|
|
27
|
+
// sparring.json openedAt → arbitro (the referee opened it); each verdict of history[] → juez
|
|
28
|
+
// (a KO closes the first fall); each repo's last preview look → bateria
|
|
29
|
+
// milestones (`sparring.mjs mark`) vestidor → bateria (the host gates passed; `sparring.mjs
|
|
30
|
+
// vestidor` stamps it); defense → juez (the judge opens it to the team, the note
|
|
31
|
+
// holds the hours to the bell); bell → campana; mask-kept / mask-lost → juez;
|
|
32
|
+
// no-team → juez («sin equipo»: no second fall, the third starts there);
|
|
33
|
+
// referee → arbitro (handed to the bout; `judges-card open --from-sparring` marks it)
|
|
34
|
+
// blows-round-<n>.md each Tape it cites → juez (the judge's attack) at the time written next to
|
|
35
|
+
// it; its blows (D/B/F/Q/R with their Tape minute) go to `catalogs`, not events
|
|
36
|
+
// live.jsonl the referee's own lines: repo added → arbitro; PR opened → dado; preview → bateria
|
|
37
|
+
// gh / git each sparring PR opened → dado; test/fix commits in each worktree → dado
|
|
24
38
|
|
|
25
39
|
export const TYPES = [
|
|
26
40
|
'reclamo',
|
|
@@ -418,6 +432,16 @@ export function localTime(at, timeZone) {
|
|
|
418
432
|
}).format(new Date(at));
|
|
419
433
|
}
|
|
420
434
|
|
|
435
|
+
/** YYYY-MM-DD in a time zone: a fight that spans days says which day each beat is. */
|
|
436
|
+
export function localDay(at, timeZone) {
|
|
437
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
438
|
+
timeZone,
|
|
439
|
+
year: 'numeric',
|
|
440
|
+
month: '2-digit',
|
|
441
|
+
day: '2-digit',
|
|
442
|
+
}).format(new Date(at));
|
|
443
|
+
}
|
|
444
|
+
|
|
421
445
|
/** Did Copilot review the PR's head? Says it plainly for a merge or a close. PURE. */
|
|
422
446
|
export function copilotOnHead(pr, reviews) {
|
|
423
447
|
const copilot = reviews
|