@thebattlearena/corner 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +38 -5
  2. package/fonts/battle-arena-icons/BattleArenaIcons.ttf +0 -0
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/hooks/icons.generated.ts +2 -0
  6. package/plugin/hooks/pane.tsx +16 -2
  7. package/plugin/hooks/register.tsx +62 -33
  8. package/plugin/hooks/ring-io.ts +59 -1
  9. package/plugin/hooks/ring-pane.tsx +125 -35
  10. package/plugin/hooks/ring.ts +73 -38
  11. package/plugin/hooks/sparring.ts +139 -0
  12. package/plugin/icons/rudo-amber-2x1.png +0 -0
  13. package/plugin/icons/rudo-amber-4x2.png +0 -0
  14. package/plugin/icons/rudo-amber-5x2.png +0 -0
  15. package/plugin/icons/rudo-amber-6x2.png +0 -0
  16. package/plugin/icons/rudo-grey-2x1.png +0 -0
  17. package/plugin/icons/rudo-grey-4x2.png +0 -0
  18. package/plugin/icons/rudo-grey-5x2.png +0 -0
  19. package/plugin/icons/rudo-grey-6x2.png +0 -0
  20. package/plugin/icons/rudo-pink-2x1.png +0 -0
  21. package/plugin/icons/rudo-pink-4x2.png +0 -0
  22. package/plugin/icons/rudo-pink-5x2.png +0 -0
  23. package/plugin/icons/rudo-pink-6x2.png +0 -0
  24. package/plugin/icons/rudo-pink-dim-2x1.png +0 -0
  25. package/plugin/icons/rudo-pink-dim-4x2.png +0 -0
  26. package/plugin/icons/rudo-pink-dim-5x2.png +0 -0
  27. package/plugin/icons/rudo-pink-dim-6x2.png +0 -0
  28. package/plugin/icons/rudo-red-2x1.png +0 -0
  29. package/plugin/icons/rudo-red-4x2.png +0 -0
  30. package/plugin/icons/rudo-red-5x2.png +0 -0
  31. package/plugin/icons/rudo-red-6x2.png +0 -0
  32. package/plugin/tests/ring-pane.test.tsx +154 -0
  33. package/plugin/tests/ring.test.ts +13 -4
  34. package/plugin/tests/sparring.test.ts +204 -0
  35. package/plugin/types/index.d.ts +25 -1
  36. package/ring/.claude-plugin/plugin.json +1 -1
  37. package/ring/agents/rudo.md +95 -0
  38. package/ring/scripts/chronicle.mjs +24 -0
  39. package/ring/scripts/config.mjs +47 -1
  40. package/ring/scripts/fight.mjs +238 -0
  41. package/ring/scripts/judges-card.mjs +53 -9
  42. package/ring/scripts/live-hook.mjs +42 -15
  43. package/ring/scripts/live.mjs +96 -1
  44. package/ring/scripts/mask-guard.mjs +184 -6
  45. package/ring/scripts/monitors.mjs +2 -1
  46. package/ring/scripts/play-by-play.mjs +39 -9
  47. package/ring/scripts/round-bell.mjs +10 -6
  48. package/ring/scripts/sparring-chronicle.mjs +470 -0
  49. package/ring/scripts/sparring.mjs +1161 -0
  50. package/ring/skills/play-by-play/SKILL.md +49 -3
  51. package/ring/skills/play-by-play/templates/chronicle.html +7 -0
  52. package/ring/skills/referee/SKILL.md +46 -2
  53. package/ring/skills/sparring/SKILL.md +408 -0
  54. package/ring/skills/sparring/references/host-signal.md +86 -0
  55. package/ring/skills/sparring/references/lessons.md +50 -0
  56. package/ring/skills/sparring/templates/pr-body.md +34 -0
  57. package/ring/skills/sparring/templates/rudo-brief.md +47 -0
  58. package/ring/skills/sparring/templates/sparring-card.md +33 -0
  59. package/ring/skills/weigh-in/SKILL.md +21 -3
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: play-by-play
3
- description: Narrates a ring bout as a Battle Arena sports chronicle (rounds, blows, fouls, bells) with the real time of each event, and publishes it as a private Artifact page that keeps one stable link for the life of the bout. Invoke with `/ring:play-by-play <pr>`, "write the chronicle of the bout", "update the play-by-play".
4
- argument-hint: "<pr>"
3
+ description: Narrates a ring bout as a Battle Arena sports chronicle (rounds, blows, fouls, bells) with the real time of each event, and publishes it as a private Artifact page that keeps one stable link for the life of the bout. A fight that starts at a sparring is one chronicle from the sparring's opening, two out of three falls. Invoke with `/ring:play-by-play <pr>`, `/ring:play-by-play --sparring <slug>`, "write the chronicle of the bout", "update the play-by-play".
4
+ argument-hint: "<pr> | --sparring <slug>"
5
5
  ---
6
6
 
7
7
  # play-by-play
@@ -25,6 +25,37 @@ re-request closes it), `events` (type, real time `at`, `local` HH:MM, title, det
25
25
  `falta`, `juez`, `arbitro`, `bateria`, `campana`. The classification rules are at the top of
26
26
  `$RING/scripts/chronicle.mjs`.
27
27
 
28
+ ### A fight that starts at a sparring
29
+
30
+ The chronicle starts at the sparring, so its material never ends up scattered:
31
+
32
+ ```bash
33
+ node "$RING/scripts/play-by-play.mjs" --sparring <slug> --tz <the human's time zone> [--extra <json|file>] [--no-gh]
34
+ ```
35
+
36
+ It prints the same shape plus `falls`, `catalogs`, `bouts` and the `page` to write. The fight is
37
+ told in three falls, and every event carries its `fall`, its `round` and its `day` (a fight spans
38
+ days):
39
+
40
+ 1. **First fall, the rudo against the judge** (rounds `s1`, `s2`…): from `open` to the KO, one
41
+ round per verdict of `history[]` (a rematch closes one and opens the next). The judge's Tapes
42
+ are attacks at the time the catalog writes next to them; each round's catalog
43
+ (`blows-round-<n>.md`) is in `catalogs`, every D/B/F/Q blow with its Tape minute, to narrate
44
+ them by minute of the Tape, never by an invented clock time.
45
+ 2. **Second fall, the defense of the técnicos** (round `defense`): from the KO (or the first
46
+ milestone of the defense, if it came first) through the vestidor (the host gates), the
47
+ defense opened to the team, its Tapes, the bell and whether the rudo keeps or loses the mask.
48
+ With «sin equipo» (`no-team`) there is no second fall: that fall comes `skipped` and the
49
+ third starts at the judge's call; tell it in one line, never as an empty fall.
50
+ 3. **Third fall, the referee**: from the hand-off, every bout opened `--from-sparring <slug>`,
51
+ each with its own rounds (`<pr>/<n>`, as a bout's chronicle has them).
52
+
53
+ The milestones exist only if the sparring's referee marked them when they happened
54
+ (`sparring.mjs vestidor <slug>`, `sparring.mjs mark <slug> defense|bell|mask-kept|mask-lost|no-team`); `judges-card open
55
+ --from-sparring` marks the hand-off. A moment nobody marked is not in the JSON: say it as a gap,
56
+ or bring its time through `--extra` when someone provided it.
57
+ `/ring:play-by-play <pr>` on a bout opened from a sparring prints the same whole fight.
58
+
28
59
  Times the records do not hold (a battery run the host logs elsewhere, a screenshot the human
29
60
  sent) enter only through `--extra` (an array of `{ at, type, title, detail, ref }`, inline or as
30
61
  the path of a JSON file), and they stay marked `provided`.
@@ -45,7 +76,9 @@ dark mode, no external scripts, only the Google Fonts stylesheet). Keep its stru
45
76
  - masthead with the live status, headline with the PR number, a two-sentence dek, the byline;
46
77
  - the fight card (champion vs challenger) and the crew line;
47
78
  - the tally (duration, Copilot findings, commits, fouls) and the legend of the eight tags;
48
- - one section per round, each beat with its time in the margin and its source in `.ref`;
79
+ - one section per round, each beat with its time in the margin and its source in `.ref`; in a
80
+ fight from a sparring, the rounds go under a header per fall (`h3.fall`), the card is the rudo
81
+ against the judge, and the first beat of each day says the day;
49
82
  - exactly two pull quotes in the whole chronicle;
50
83
  - the judges' card table and the sources footer.
51
84
 
@@ -62,6 +95,15 @@ link (a second, different link is refused), and every time it stamps `playByPlay
62
95
 
63
96
  Then offer, once, to put the link in the PR body; do it only with the human's yes.
64
97
 
98
+ **A fight from a sparring has one page and one link.** Write it to
99
+ `<sparring dir>/play-by-play.html` (the JSON's `page`), always that path, whether it is refreshed
100
+ from the sparring or from one of its bouts. If `sparring.json` already has `playByPlayUrl`,
101
+ publish with `url` set to it. After every publish from the sparring, run
102
+ `node "$RING/scripts/sparring.mjs" play-by-play-url <slug> <url>`; from a bout,
103
+ `judges-card.mjs play-by-play-url <pr> <url>` as always, which refuses any link but the fight's and
104
+ also stamps the sparring. The sparring keeps its first link for the whole fight, through every
105
+ bout of every repo.
106
+
65
107
  ## Rules
66
108
 
67
109
  - **No invented times.** A gap is said as a gap: the beat goes where it must have happened, with
@@ -81,3 +123,7 @@ Then offer, once, to put the link in the PR body; do it only with the human's ye
81
123
 
82
124
  The referee and title-defense refresh it after each round-bell and at each bell (the Copilot
83
125
  re-request); `close` publishes the final edition, with the status "Final".
126
+
127
+ In a sparring, the referee of the sparring publishes it first at `open` and refreshes it after
128
+ each verdict, after the vestidor, when the defense opens and at the bell. Those refreshes report;
129
+ like every chronicle, they decide nothing about the sparring.
@@ -58,6 +58,7 @@ h1 em{font-style:normal;color:var(--red)}
58
58
  blockquote{margin:22px 0 22px 74px;padding:0 0 0 18px;border-left:4px solid var(--gold);font-size:21px;line-height:1.45;font-style:italic;max-width:56ch}
59
59
  @media (max-width:560px){blockquote{margin-left:0}.beat{grid-template-columns:minmax(0,1fr);gap:2px}}
60
60
  .ref{display:block;font-family:var(--f-mono);font-size:12px;color:var(--muted);margin-top:4px;overflow-wrap:anywhere}
61
+ h3.fall{font-family:var(--f-label);font-weight:700;font-size:15px;letter-spacing:.1em;text-transform:uppercase;color:var(--red);margin:56px 0 -28px;border-top:3px double var(--ink);padding-top:10px}
61
62
  h3.section{font-family:var(--f-display);font-weight:400;font-size:24px;text-transform:uppercase;margin:52px 0 10px;border-top:3px solid var(--ink);padding-top:12px}
62
63
  .tbl{overflow-x:auto;background:var(--paper);border:1px solid var(--rule)}
63
64
  table{border-collapse:collapse;width:100%;min-width:600px;font-family:var(--f-label);font-size:15.5px}
@@ -78,6 +79,10 @@ footer{margin-top:44px;font-family:var(--f-label);font-size:14px;color:var(--mut
78
79
  t-bateria, t-campana; a foul beat also gets class "falta";
79
80
  - .ref under each beat says where the time came from (decision id, sha, review, run);
80
81
  - exactly two blockquotes (pull quotes) in the whole chronicle;
82
+ - a fight from a sparring (--sparring): one <h3 class="fall"> before the first round of each
83
+ fall of the JSON (first fall: the rudo against the judge; second: the defense of the técnicos;
84
+ third: the referee), the card is the rudo against the judge, and the first beat of each day
85
+ says the day (the event's `day`) in its prose;
81
86
  - no scripts; only the Google Fonts stylesheet above loads from outside.
82
87
  -->
83
88
  <div class="wrap">
@@ -105,6 +110,8 @@ footer{margin-top:44px;font-family:var(--f-label);font-size:14px;color:var(--mut
105
110
  <span class="tag t-reclamo">{{Reclamo de Copilot}}</span><span class="tag t-dado">{{Golpe dado}}</span><span class="tag t-recibido">{{Golpe recibido}}</span><span class="tag t-falta">{{Falta}}</span><span class="tag t-arbitro">{{Árbitro}}</span><span class="tag t-juez">{{Juez}}</span><span class="tag t-bateria">{{Batería}}</span><span class="tag t-campana">{{Campana}}</span>
106
111
  </div>
107
112
 
113
+ <!-- In a fight from a sparring, before the first round of each fall:
114
+ <h3 class="fall">{{e.g. "Primera caída · el rudo contra el juez"}}</h3> -->
108
115
  <!-- One section per round of the JSON; round 0 is the preview ("La previa"). -->
109
116
  <section class="round">
110
117
  <h2>{{ROUND NAME, e.g. "Round 1 · siete golpes de entrada"}}</h2>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: referee
3
3
  description: Orchestrates a refactor or a large delivery on a backend, frontend or mobile PR as its referee, at the venue the PR belongs to. One long-lived wrestler (effort medium) writes in its own worktree; you (effort high) review each commit range with read-only cornermen against a numbered rulebook, send one rebound package at a time, open small do-not-merge round-bell PRs so the human reviews early, decide design doubts yourself from the rulebook and precedents (escalating only a short hard list), and keep a durable judges card so any new session can resume. Invoke with `/ring:referee start <pr>`, `/ring:referee resume [<pr>]`, `/ring:referee close`, "referee the refactor of #N", "resume the bout".
4
- argument-hint: "start <pr> | resume [<pr>] | close"
4
+ argument-hint: "start <pr> | start --from-sparring <slug> | resume [<pr>] | close"
5
5
  ---
6
6
 
7
7
  # referee
@@ -79,6 +79,50 @@ list reaches the human.
79
79
 
80
80
  ## 1 · `start <pr>`
81
81
 
82
+ **`--from-sparring <slug>`**: the bout cleans the deliverable of a sparring the judge called KO
83
+ (`/ring:sparring`). Before step 1, read `~/.claude/ring/sparrings/<slug>/card.md`
84
+ (`node "$RING/scripts/sparring.mjs" list` names them) and refuse unless its verdict is `ko`. A
85
+ sparring that spanned several repos gets one bout per repo; this one covers this session's repo.
86
+ **The `<pr>` is not needed**: unless the sparring was already promoted (its `sparring.json` names a
87
+ `mainevent` for this repo), run `node "$RING/scripts/sparring.mjs" promote <slug> --repo
88
+ <owner/repo>`. It creates `release/<slug>` from the repo's base, `mainevent/<slug>` from the
89
+ sparring tip, and opens the deliverable PR mainevent → release as a draft; that PR is the bout's
90
+ `<pr>` and `release/<slug>` its **stack base**. The branches:
91
+
92
+ ```
93
+ main
94
+ └─ release/<slug> reaches main in one PR at the end (the release exception of the line ceiling)
95
+ └─ mainevent/<slug> PR → release; the wrestler cleans everything here, no line ceiling
96
+ └─ ring/<pr>/round-<n> round-bells, do-not-merge as always: the human reads each concept
97
+ sparring/<slug> do-not-merge, never merged: the evidence of what the KO saw
98
+ ```
99
+
100
+ Never push `sparring/*` to another branch by hand (mask-guard refuses it): `promote` is the only
101
+ way out, and it refuses without a KO. Then the bout runs as always, with three inputs from the card:
102
+
103
+ - **What the judge validated is the behavior that must not change**: write it as numbered
104
+ invariants for the concept-mode cornerman, and add characterization tests first (T rules).
105
+ - **The rudo's shortcuts for this repo are the first rebound package**, blockers first.
106
+ - **The contract the sparring drafted goes through §2b** like any other: nothing of it is approved
107
+ because the sparring used it. Its credentials (by name) are a hard-list item: taking a sandbox
108
+ key's variable to production is the human's.
109
+ - **Open it with `--from-sparring <slug>`** (step 5): `judges-card.mjs open` refuses a sparring
110
+ that is not a KO, records `fromSparring` and marks the hand-off on the sparring. The bout is then
111
+ the third fall of the sparring's chronicle: one page and one link for the whole fight
112
+ (`/ring:play-by-play`).
113
+ - **That contract is shown to the judge first, without the judge asking for it.** Right after
114
+ opening the bout (step 5) and before the first wrestler brief: list every file under
115
+ `config.apiSpec` and `config.dbModels` that changes between the stack base and the PR head
116
+ (`git diff --name-only <base>...<head> -- <those paths>`), have a cornerman review it (§2b
117
+ step 3), draw the diagram of every migration (§2b step 4), serve `contract-preview.mjs`, and
118
+ escalate it all as **one** contract escalation: the preview URL, the spec diff, each migration
119
+ with its backfill, and your recommendation. Until `approve-contract` records the judge's yes for
120
+ exactly those files (already committed, so the hashed content is the one at the PR head), the
121
+ whole contract zone is frozen: the wrestler cleans only what does not touch those files, and no
122
+ rebound changes them. Starting the cleanup before that escalation is a foul; log it in the
123
+ judges card.
124
+
125
+
82
126
  1. `gh pr view <pr> --json number,title,url,headRefName,baseRefName,files`. The **stack base** is
83
127
  `baseRefName` unless the human names another one; ask if the PR is part of a stack.
84
128
  2. **Venue.** `gh pr diff <pr> --name-only | node "$RING/scripts/config.mjs" venue`, or
@@ -93,7 +137,7 @@ list reaches the human.
93
137
  `git worktree add --detach <abs path> origin/<headRefName>` (after `git fetch origin`). Never
94
138
  launch the wrestler with `isolation: "worktree"`: that creates another tree on another branch.
95
139
  5. Open the bout: `node "$RING/scripts/judges-card.mjs" open --pr <pr> --worktree <abs path>
96
- --branch <headRefName> --base <stack base> --venue <venue>`. This activates `mask-guard` for this repo.
140
+ --branch <headRefName> --base <stack base> --venue <venue> [--from-sparring <slug>]`. This activates `mask-guard` for this repo.
97
141
  It refuses to open when `.claude/ring.json` leaves `apiSpec` or `dbModels` empty while the repo
98
142
  has specs or migrations: fix the config (paths or globs; `null` means "detect") rather than
99
143
  passing `--allow-empty-contract`, which is only for a bout that truly has no contract. Then
@@ -0,0 +1,408 @@
1
+ ---
2
+ name: sparring
3
+ description: Opens a sparring, a quick and deliberately low-quality draft (one PR per repo it touches, back and front included) that only proves whether a feature is worth building, before anyone pays for building it well. A rudo per repo (effort medium) writes it inside a timebox in its own worktree; the draft PR (labels sparring + do-not-merge) is the event the host project's CI catches to deploy a preview; credentials travel by name only. The judge calls KO, rematch or discard, in two out of three falls: a KO passes the host gates (vestidor), faces the team's Tapes until a bell (unless «sin equipo»), keeps or loses the mask, and then hands each branch to a ring bout. Invoke with `/ring:sparring <idea | Linear ticket>`, `/ring:sparring preview <slug>`, `/ring:sparring verdict <slug>`, `/ring:sparring list`, "make a quick draft of this to try it", "spar this feature first".
4
+ argument-hint: '<idea | ticket> | preview <slug> | verdict <slug> | vestidor <slug> | list'
5
+ ---
6
+
7
+ # sparring
8
+
9
+ A sparring answers one question, "is this worth building?", with evidence. The code is
10
+ disposable in quality, not in behavior: if the judge calls a KO, a ring bout refactors the same
11
+ branch and keeps what the judge validated. You are the referee of the sparring; the rudo writes.
12
+
13
+ **The judge sees the whole experience, working, never an isolated or superficial piece.** A
14
+ sparring shows the full screen or flow the feature lives in (the list, the detail, the filters and
15
+ the new piece, as the design draws them), and **every control on it works**: search, filters, tabs,
16
+ loaders, pagination or infinite scroll, actions and their errors. The data comes from the back,
17
+ built quick and dirty for the sparring; a mock is only for what cannot be had yet (an external
18
+ provider, data no environment has). A widget on an empty page, or a screen of buttons that do
19
+ nothing, leaves the judge without what they need to call the verdict. The code is thrown away, so
20
+ cutting scope or depth saves nothing that matters: the speed comes from the quality you skip.
21
+
22
+ ## Pace
23
+
24
+ A sparring is careless code that puts working first, so it is fast or it is wrong. The judge
25
+ clicks a first working version of the whole screen **within 90 minutes** of the interview; the
26
+ rest of the timebox goes to the controls that are still dead. Where time goes, and what you cut:
27
+
28
+ - **The interview is one message with your proposed answers**; the judge answers with "yes" or a
29
+ correction. Everything that does not change the hypothesis, the proof, the scope or a hard stop,
30
+ you decide without asking, then and while the rudos work.
31
+ - **Contract first, in the first 15 minutes.** With several repos, the producer writes the contract
32
+ (routes, shapes, error codes) to `<sparring dir>/contrato.md` before implementing, and every rudo
33
+ starts at once against it. The consumer never waits for the producer's code.
34
+ - **Ceremony the rudo skips:** migrations in the repo (tables come from a SQL in `tools/`), OpenAPI
35
+ and generated types until the last push, typecheck after every edit (only before each push),
36
+ tests, wiki, DTOs, CQRS, ports, feature flags. Each one goes on the shortcuts list.
37
+ - **Data is seeded locally first**, without asking: synthetic rows, enough to look like the design.
38
+ A shared database only when the judge asks for it.
39
+ - **Report early, not only at the end:** each rudo reports its first clickable version and then
40
+ every hour, so a wrong direction is caught at minute 60, not at hour 5.
41
+
42
+ ## 0 · Every time you are invoked
43
+
44
+ 1. **Paths.** `RING` is the plugin root: two directories above this skill's base directory
45
+ (`${CLAUDE_PLUGIN_ROOT}` when it is expanded). Script: `$RING/scripts/sparring.mjs`. Templates:
46
+ `$RING/skills/sparring/templates/`. The host contract:
47
+ `$RING/skills/sparring/references/host-signal.md`.
48
+ 2. **The repos are the sparring's, not the session's.** A sparring may target repos other than the
49
+ checkout this session runs in, and may span several (a back and its front). Every script call
50
+ names its repo by worktree; never rely on the session's directory.
51
+ 3. **Config, per repo.** `node "$RING/scripts/config.mjs" show` run from inside each repo's
52
+ worktree: read `config.sparring` (label, `previewEnvironment`, `previewTimeoutMin`,
53
+ `staleDays`) and `config.gates.typecheck`. A repo without `previewEnvironment` has no preview:
54
+ say it once and plan for a local test.
55
+ 4. **State** lives in `~/.claude/ring/sparrings/<slug>/` (`sparring.json`, `card.md`, `tools/`),
56
+ one directory per sparring whatever repos it spans. It is not a bout: no lookout, no cornermen,
57
+ no round-bells. A sparring recorded per repo by an older version is merged into one the first time a command reads it; its `tools/` and notes move along, with a symlink left at the old path.
58
+ 5. **Lessons.** Read `$RING/skills/sparring/references/lessons.md`: what earlier judges asked for
59
+ when they called rematch or discard. Plan with them, and paste the ones that apply into every
60
+ rudo brief.
61
+ 6. **Chronicle.** The fight's chronicle starts at `open`, not at the referee bout: publish it then
62
+ (`/ring:play-by-play --sparring <slug>`) and refresh it after each verdict, after the vestidor,
63
+ when the defense opens, at the bell and at the mask, marking each of those moments first (§4:
64
+ `sparring.mjs vestidor <slug>` stamps the vestidor; `sparring.mjs mark <slug>
65
+ defense|bell|mask-kept|mask-lost|no-team` the rest).
66
+
67
+ ## 1 · `/ring:sparring <idea | ticket>`
68
+
69
+ 1. **Read the input first.** When the idea comes with a plan, a ticket or an artifact, check what
70
+ of it already exists in the code before asking anything: that decides the scope question.
71
+ 2. **Short interview** (one message, numbered, answerable in one line each). Skip what the idea or
72
+ the ticket already says:
73
+ - **Hypothesis:** "if X exists, the team / customer does Y".
74
+ - **Proof of value:** what the judge has to _see_ to say yes. Observable and unambiguous; "it
75
+ looks good" is not one.
76
+ - **The full screen the judge will see:** which screen or flow, complete, the feature lives in,
77
+ and what of it already exists. What is missing (the screen the idea stands on, an endpoint, a
78
+ table) is built quick and dirty or mocked so the whole experience is there; never a
79
+ placeholder. Ask only what stays out on purpose.
80
+ - **Repos:** which ones it touches (back, front, both) and their base branches (default: each
81
+ repo's default branch). All of them belong to this one sparring.
82
+ - **Designs:** a `.pen` frame, a Figma link or images the rudo has to follow, if any.
83
+ - **Timebox:** 3 h at most. If it does not fit, split it and spar the riskiest part first.
84
+ - **Credentials:** which provider lab it needs and the NAMES of the variables
85
+ (`KAPSO_API_KEY`). Never ask for, accept or repeat a value; if the human pastes one, do not
86
+ echo it and tell them to rotate it.
87
+ - **Real sends to the judge:** whether the proof needs a message, call or email that really
88
+ leaves through the provider (§1.8). Only to the judge's own test destinations.
89
+ 3. **Hard stops before starting** (escalate, do not open): a production credential, real money,
90
+ messages or calls to customers, customer data or PII in fixtures, or a destructive schema
91
+ change. Those break rule F or the additive-only rule. The one exception is §1.8: real sends to
92
+ the judge's own allowlisted destinations, with their explicit yes.
93
+ 4. **A worktree per repo**, outside every main checkout, e.g. a sibling `<repo>-sparring-<slug>`:
94
+
95
+ ```bash
96
+ git -C <repo checkout> fetch origin
97
+ git -C <repo checkout> worktree add -b sparring/<slug> <abs path> origin/<base>
98
+ ```
99
+
100
+ 5. **Open it** with the first repo, then add the others. Credential names are validated: anything
101
+ that is not `UPPER_SNAKE_CASE` is refused, so a value never lands in the state.
102
+
103
+ ```bash
104
+ node "$RING/scripts/sparring.mjs" open <slug> --worktree <abs path> --base <base> \
105
+ --hypothesis "<…>" --proof "<…>" [--credentials NAME,NAME]
106
+ node "$RING/scripts/sparring.mjs" add-repo <slug> --worktree <abs path of the other repo> --base <base>
107
+ ```
108
+
109
+ 6. **Credentials, locally:** give the judge the path `<worktree>/.env.sparring` of each repo that
110
+ needs them and the names to fill. They fill it; you never read it back. For a host's preview,
111
+ give the judge the command to load each missing secret into that host's store
112
+ (`gh secret set <NAME> --env <previewEnvironment>`); loading secrets is theirs.
113
+ 7. **Launch a rudo per repo**, all in the background, each with `templates/rudo-brief.md` filled
114
+ for its repo (scope, designs, the other repos and what they exchange):
115
+
116
+ ```
117
+ Agent{subagent_type: "ring:rudo", effort: "medium", run_in_background: true,
118
+ description: "Rudo: <slug> <repo>", prompt: <the brief>}
119
+ ```
120
+
121
+ Never `isolation: "worktree"`. When one repo produces what another consumes (the back's SDK or
122
+ endpoint for the front), say so in both briefs: the producer goes first and leaves the hand-off
123
+ in `<sparring dir>/tools/` (§2).
124
+
125
+ 8. **Real sends to the judge** (a message, call or email that really leaves through the provider),
126
+ only when the proof needs it and the judge said yes. The destinations are **the judge's own and
127
+ change with every judge**: they are never asked for in chat, never written in code, a fixture,
128
+ the card, a commit or a log. The judge fills them in `<worktree>/.env.sparring`, one variable
129
+ per kind, `SPARRING_ALLOWED_<KIND>` (`SPARRING_ALLOWED_PHONES`, `SPARRING_ALLOWED_EMAILS`,
130
+ `SPARRING_ALLOWED_CHAT_IDS`…), comma-separated. The code that sends:
131
+ - **closed by default:** an empty or missing variable sends nothing;
132
+ - sends only when the destination is on the list, normalized the same way on both sides (for
133
+ phones, digits only);
134
+ - records every other attempt without sending it (`{ sent: false, reason: "not_allowlisted" }`),
135
+ so the screen still works with seeded destinations;
136
+ - logs whether it sent, never the destination.
137
+
138
+ The provider's credentials go by name like any other (§1.6); a production key is still a hard
139
+ stop unless the judge explicitly accepts it for these sends, and then the allowlist is the only
140
+ guard, so say so. The variables are listed in the card by name, never their values. The
141
+ allowlist is per sparring and per judge: a later judge fills their own.
142
+
143
+ ## 2 · While the rudos work
144
+
145
+ - Their doubts come to you: decide them for speed (the fastest way to show the whole experience)
146
+ and only take to the human what changes the hypothesis, the proof, the scope or the hard stops.
147
+ - Check each report against the hard stops, not against style: a secret in a diff
148
+ (`git -C <wt> diff <base>...HEAD`), a non-additive migration, a real provider endpoint outside
149
+ sandbox mode. Any of those goes back to that rudo before anything else.
150
+ - **Hand-off between repos.** Nothing is published to a registry for a sparring. The producer
151
+ leaves what the consumer needs in `<sparring dir>/tools/` (an SDK as an `npm pack` tarball, a
152
+ fixture, a mock) and reports its path, the port it serves on and one `curl` that proves it; you
153
+ forward that to the consumer's rudo, which installs it from there.
154
+ - **Seeding a shared database** (dev, staging) when the proof needs data that does not exist yet,
155
+ because the feature is new: only with the judge's explicit yes. The rudo prepares it, never runs
156
+ it: synthetic rows only, each one marked with the sparring's slug, values that can reach nobody
157
+ (phones with an unassigned prefix, `example.invalid` emails), a dry run that prints what it
158
+ would write, and a cleanup that deletes only the marked rows, ready **before** seeding. Write the
159
+ seed and the cleanup commands under "Seed" in the card's notes (below the notes marker, which every rewrite keeps). **The judge runs the seed**, with `!` in
160
+ their session; the referee and the rudo never write to a shared database, and a write the
161
+ session's permissions block is never retried another way. Cleaning up is pending until the
162
+ verdict.
163
+ - At the end of the timebox each rudo stops. A second timebox is the human's call (§4 rematch).
164
+
165
+ ## 3 · The PRs and the preview
166
+
167
+ 1. When a rudo pushed something worth trying, open its repo's PR (`--repo` can be left out only
168
+ when the sparring has one repo). Each PR body links the other repos of the sparring:
169
+
170
+ ```bash
171
+ node "$RING/scripts/sparring.mjs" pr <slug> --repo <owner/repo> --title "<feature>" \
172
+ --how-to-test "<steps from the rudo>" --shortcuts "<the rudo's list>"
173
+ ```
174
+
175
+ It opens the draft with the sparring and do-not-merge labels (created in that repo when it
176
+ lacks them) and the credentials block (names only) and warning. That PR is the event the
177
+ repo's CI catches.
178
+
179
+ 2. Previews: `node "$RING/scripts/sparring.mjs" preview <slug> --wait` with
180
+ `run_in_background: true` looks at every repo (or one, with `--repo`), each bounded by its own
181
+ `previewTimeoutMin`. Give the judge a link only when it printed one; otherwise say exactly what
182
+ it printed (no preview configured, pending, failure, timed out). Never say "ready to try"
183
+ without the link in hand.
184
+ 3. **Local test** for the repos without a preview: one runbook for the judge, in order. The
185
+ producer first (how to start it, its port, `.env.sparring`, the `curl` that proves it), then
186
+ each consumer pointed at it (the env var with the producer's URL, how to install the hand-off
187
+ from `tools/`, how to start it, the URL to open). Ports must not collide with what the judge
188
+ already runs; ask when unsure. Locally, the producer accepts requests from any
189
+ `http://localhost:<port>` origin (CORS), never a single fixed port; that is a shortcut to list.
190
+ 4. **Before announcing it, click everything.** A browser pass (Playwright or the like) over each
191
+ screen at the sizes the design draws, clicking every visible control and listing the ones that
192
+ do nothing, have no loader or fail silently. A dead control goes back to its rudo; the
193
+ sparring is not announced until the list is empty or the judge accepted what is left.
194
+ 5. Tell the judge, in one short message: **the exact URL of each screen to open** (with its path,
195
+ not just the host), **the user, workspace or account to enter with**, what to look at first,
196
+ the links or the runbook, the hypothesis, the proof of value to check, and the credentials
197
+ warning. A sparring with a front is never announced without the screen's URL.
198
+
199
+ ## 4 · `verdict <slug>`: two out of three falls
200
+
201
+ The fight has three falls, in this order: the rudo against the judge (verdicts), the defense of the
202
+ técnicos (the team's Tapes, after the vestidor) and the técnico (the referee bout). Mark each
203
+ moment when it happens (`sparring.mjs mark`, §0.6) so the chronicle has its real time.
204
+
205
+ ### First fall · the rudo against the judge
206
+
207
+ #### The judge attacks with a Tape
208
+
209
+ When the rudos deliver, the judge attacks: they record their review as a Tape session
210
+ (`battlearena.dev/t/<id>`), voice over their own clicks, and hand it to the battle so it knows every
211
+ blow it took. That Tape is the evidence of the verdict and the brief of the next round.
212
+
213
+ 1. **Read it yourself**, with the Tape tools (`qa_session_overview`, `qa_session_timeline` with
214
+ speech, `qa_session_errors`, `qa_session_frame` for what was on screen). Never work from a
215
+ summary someone else wrote.
216
+ 2. **One voice: you, the referee of the sparring, write the catalog of blows.** Nobody else
217
+ (another session, a monitor, a rudo) sends their own version to the rudos; if one reaches you,
218
+ reconcile it into yours before anything goes out. Two catalogs of one verdict make the rudos
219
+ fight different fights.
220
+ 3. **The catalog** goes to `<sparring dir>/blows-round-<n>.md`, each blow with the Tape minute and
221
+ the judge's words when they matter:
222
+ - **Decisions** of the judge (D1, D2…): product calls the rudos apply, never reopen.
223
+ - **Bugs** (B1…): what broke, how to reproduce it.
224
+ - **Missing** (F1…): what the judge expected to find.
225
+ - **Questions for the judge** (Q1…): design doubts the rudos answer with a proposal, not a
226
+ question back.
227
+ - **Noise**: what failed on screen but is not the sparring's (a third-party widget, an
228
+ unrelated 401); listed so nobody fixes it.
229
+
230
+ Then split it **by repo**: each rudo gets only its own blows, plus the decisions that touch it.
231
+
232
+ 4. Record the verdict with the Tape as evidence (`--note "Tape <url> (<date time>): D1…; …"`) and
233
+ link the catalog in the card's notes. The card keeps every round (earlier verdicts stay under
234
+ "Earlier rounds").
235
+
236
+ #### The call
237
+
238
+ Ask the judge for the call and the evidence (a Tape session, screenshots, what they saw), then
239
+ `node "$RING/scripts/sparring.mjs" verdict <slug> <ko|rematch|discard> --note "<why · evidence>"`.
240
+
241
+ - **rematch:** adjust and try again inside a new timebox; send the rudo what changes.
242
+ - **discard:** list the PRs for the human to close (`gh pr close <n> --delete-branch` is theirs).
243
+ The card keeps why: a discarded idea is a result too.
244
+ - **ko:** it is worth building, and the first fall is over. It does **not** go to the referee yet:
245
+ ask the judge, in the same message, whether the team defends it (default) or it goes on «sin
246
+ equipo». With «sin equipo», `node "$RING/scripts/sparring.mjs" mark <slug> no-team` and jump to
247
+ the third fall: no vestidor, no defense. Otherwise, the vestidor.
248
+
249
+ After a **rematch** or a **discard**, distill the judge's reason into a rule that holds for the
250
+ next sparring and append it to `references/lessons.md` (rule, date, slug), unless a lesson already
251
+ says it. That is how one judge's feedback reaches every later sparring, not only this one.
252
+
253
+ ### The vestidor (after the KO, before the team)
254
+
255
+ Until the KO the sparring stays local, fast and ugly; nothing of what follows is prepared earlier,
256
+ so the rudo neither worries about it nor spends time on it. Now it has to look serious, whatever
257
+ the repo: **every repo of the sparring passes its host's own gates**, so each PR could be deployed
258
+ if needed.
259
+
260
+ 1. `node "$RING/scripts/sparring.mjs" vestidor <slug>` (refused unless the last verdict is `ko`).
261
+ In each repo's worktree it runs that repo's top-level gates from `.claude/ring.json` (or
262
+ autodetected): `typecheck`, `lint` and `build` when configured, one at a time through the
263
+ machine lock; `test` never (it takes paths; the referee bout runs the tests). It prints each
264
+ gate with its exit code, keeps the logs in `<sparring dir>/vestidor/`, and stamps the
265
+ `vestidor` milestone only when all of them passed; otherwise it stamps nothing and exits 1.
266
+ 2. A red gate goes back to its rudo. A lint rule that only blocks the build may be switched off on
267
+ the sparring branch, listed as a shortcut. **Typecheck is never switched off.** Run the vestidor
268
+ again until it passes.
269
+ 3. **Now, and only now, the sharing prep**, as one step, expecting all of it to bite at once:
270
+ - the schema in additive migrations (or loaded into the shared database), not only a `tools/` SQL;
271
+ - any SDK or hand-off the front needs, vendored on the branch when it is not published;
272
+ - identity per judge from their session, never one shared operator;
273
+ - a seed of the shared environment with enough data to look like the local run (§2: the judge
274
+ runs it). Real data only with the judge's own yes, given in your session, for that environment;
275
+ - the allowlist of real sends stays closed for everyone but the judge.
276
+ 4. **Deploy through the host's own mechanism** (its preview or sandbox workflow), never one of the
277
+ ring's. Click everything with a logged-in session before sharing the URLs.
278
+ 5. **A short guide for the team:** the URLs with their path, how to log in, what to try first,
279
+ the hypothesis, how to record a Tape and where to send it (ask the judge where).
280
+
281
+ ### Second fall · the defense of the técnicos
282
+
283
+ The judge opens the arena to the team («¡a la defensa de los técnicos!»), mandatory unless they
284
+ said «sin equipo». The team attacks with a storm of Tapes until **the bell**.
285
+
286
+ 1. **The bell.** The judge sets the time limit when opening the defense:
287
+ `node "$RING/scripts/sparring.mjs" mark <slug> defense --note "<hours>h"`. Battle Arena will keep
288
+ it (the ring API, draft PR #779: `POST /v1/ring/bouts/:id/stage` with `defenseHours`); until
289
+ that is merged and this plugin calls it, the milestone's note is the only record of the limit:
290
+ tell the judge so.
291
+ 2. **The rudo is on defense now**, correcting under fire what the judge accepts as "apply now". Its
292
+ fear shows only in the live feed, the Ringside and the chronicle, never in a commit or a PR.
293
+ 3. **Every Tape goes through the same funnel**: read it yourself (first fall, step 1) and fold it
294
+ into one catalog of blows for the defense (`blows-round-<n>.md`, the next round, with the
295
+ judge who gave each blow). Duplicates merge and conflicts between judges go to the main judge as
296
+ a question; the rudos never get one catalog per teammate.
297
+ 4. **Triage before any score.** Nothing the team said reaches a rudo, and nothing counts on the
298
+ scoreboard, until the main judge has triaged it. Analyze every point of every Tape (none is
299
+ omitted, not even the ones that look off-topic or are duplicates) and propose, ranked by
300
+ criticality (blocker: unusable or wrong data · high: breaks or confuses a main flow · medium ·
301
+ low: cosmetic or nice-to-have), a recommendation for each: apply now, apply in the referee bout,
302
+ ask the judge, or out of scope. A recommendation is never a decision: **only the judge discards**.
303
+ Points you would drop stay in the list, marked, with the reason, for the judge to rule on. What
304
+ the judge accepts becomes the defense's catalog of blows; the rest is recorded with the judge's
305
+ ruling, so it is not lost. Triage Tapes as they arrive; at the bell every point is ruled.
306
+ **The triage page is where the judge rules.** Publish it as a private Artifact, in the
307
+ play-by-play style (one beat per blow, grouped by criticality as rounds, the Tape's start time in
308
+ the margin). Each blow carries:
309
+ - who gave it;
310
+ - its source: every cited minute is a ▶ link to the Battle Arena library's QA view at that
311
+ second (`/labs/biblioteca?sesion=<recording id>&vista=qa&t=<seconds>`);
312
+ - its recommendation;
313
+ - the judge's ruling.
314
+
315
+ The ruling lives in the page's `db`, in collection `fallos`, one document per blow ID. Only the
316
+ judge writes it (rule `{path:"fallos", read:"view", write:"admin"}`); everyone the page is shared
317
+ with reads it. A ruling is one of:
318
+ - **Aplica**, with the judge's observation **mandatory** (what to apply and how);
319
+ - **No aplica**, with an **optional** observation.
320
+
321
+ Then the judge marks **Aplicado al Rudo** once the rudo's correction is in, and the page stamps
322
+ the time.
323
+
324
+ You read `fallos` with `ArtifactData`: only `aplica` blows, each with its observation, go to the
325
+ rudos. You tell the judge when one is corrected so they can mark it applied. Never write a
326
+ ruling yourself.
327
+
328
+ **The judge to the técnicos.** The page closes with one card per teammate who got in the ring:
329
+ - their Tapes;
330
+ - their blows;
331
+ - how many apply, do not apply, or are still pending. These numbers are computed live from
332
+ `fallos`.
333
+
334
+ On each card, the judge leaves their feedback to that person: which blows counted, what was
335
+ missing, and how to attack better next time. It goes in collection `feedback`, one document per
336
+ person, and only the judge writes it (`{path:"feedback", read:"view", write:"admin"}`). Nothing
337
+ moves to the rudos until the judge says the triage is complete.
338
+
339
+ **The fight report.** When the judge says the review is complete, read `fallos` and `feedback`
340
+ and build a PDF report in the Lotería style of the host (its paper, ink, picado and card
341
+ tokens). Render it as HTML and print it with headless Chrome. Look at the pages before handing
342
+ it over. The report has four parts:
343
+ - **The poster:** who got in the ring, the tally (Tapes, blows, apply, do not apply), a table by
344
+ criticality, and the blows that did not apply with the judge's reason.
345
+ - **One card per técnico:** Tapes, blows, apply and do not apply. The judge's feedback is
346
+ polished into «Lo mejor» and «Para la próxima», faithful to what the judge wrote and without
347
+ typos. Say which lines you added where the judge's note had nothing for that part.
348
+ - **Next steps by workstream:** product decisions, research before design, design rounds in the
349
+ host's design tool (with the number of proposals asked for), and direct fixes to the Rudo.
350
+ Each blow that applies is one row: ID, topic, and a short action you write from the judge's
351
+ observation. A blow that only refers to another one names it. Check that every blow that
352
+ applies appears at least once.
353
+ - **The close of the second fall:** hand off to the arbiter, the bell, the mask, and the third
354
+ fall.
355
+
356
+ The judge's original notes stay in the db. The PDF goes to the judge for validation, and
357
+ nothing reaches the arbiter until the judge approves it.
358
+
359
+ 5. **At the bell**: `node "$RING/scripts/sparring.mjs" mark <slug> bell --note "<Tapes · accepted
360
+ blows>"`. Late Tapes go to the referee bout as notes, not to the scoreboard.
361
+ 6. **The mask.** The scoreboard counts only the accepted blows; the team's Tapes are evidence, not
362
+ votes. The judge looks at it and decides (never you, never a count, never the server):
363
+ - **mask kept** (`mark <slug> mask-kept --note "<scoreboard>"`): the team found little; the rudo
364
+ reaches the referee as a champion, with a short first package;
365
+ - **mask lost** (`mark <slug> mask-lost --note "<scoreboard>"`): badly hurt; the accepted
366
+ catalog becomes the referee's first rebound.
367
+
368
+ ### Third fall · the técnico
369
+
370
+ The next step is `/ring:referee start --from-sparring <slug>`, one bout per repo (a session in each
371
+ repo). It promotes the sparring: `sparring.mjs promote <slug>` (refused unless the last verdict is
372
+ `ko`) creates `release/<slug>` from the repo's base, `mainevent/<slug>` from the sparring tip, and
373
+ opens the deliverable PR mainevent → release as a draft; nobody creates a branch by hand, and
374
+ pushing `sparring/*` elsewhere stays blocked. The sparring and its PR stay do-not-merge, as the
375
+ evidence of what the KO saw; the bout cleans `mainevent/<slug>` and shows the judge the sparring's
376
+ contract before any cleanup. `judges-card.mjs open
377
+ --from-sparring` refuses a sparring that is not a KO, warns when neither `vestidor` nor `no-team`
378
+ was marked, and stamps the `referee` milestone itself (`mark <slug> referee` only when a bout opens
379
+ some other way). The behavior the judge validated is the invariant the bout keeps; the defense's
380
+ catalog is its first rebound when the mask was lost, a short first package when it was kept.
381
+
382
+ The card and the PR body always carry the fixed warning: the credentials it used are assumed
383
+ rotated. Nobody follows it up.
384
+
385
+ ## 5 · `list`
386
+
387
+ `node "$RING/scripts/sparring.mjs" list`: every sparring on this machine with its repos and PRs,
388
+ verdict and age.
389
+ Those over `staleDays` without a verdict show `STALE`: bring them to the judge for a call.
390
+
391
+ ## Hard guards
392
+
393
+ `mask-guard` enforces these on every session, sparring or bout or neither:
394
+
395
+ - `.env.sparring` is never committed;
396
+ - a `sparring/*` branch is never merged (`git merge sparring/…`) nor pushed to any branch that is
397
+ not itself `sparring/*`;
398
+ - a PR from `sparring/*` is created only as a draft with the do-not-merge label, and never taken out
399
+ of draft (`gh pr ready`) or merged (`gh pr merge`).
400
+
401
+ And these are yours:
402
+
403
+ - No credential value in any message, file, PR, commit or log. Names only.
404
+ - Sandbox or test mode of every provider; production keys are an escalation, never a shortcut.
405
+ - A real send only to the judge's own allowlisted destinations (§1.8), closed by default; never a
406
+ destination in a file, a commit or a log.
407
+ - No files in a checkout the sparring does not own; helpers go in `<sparring dir>/tools/`.
408
+ - Every PR is a draft; closing, merging and base changes are the human's.