@thebattlearena/corner 0.10.1-beta.78 → 0.10.1-beta.80
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 +69 -1
- package/package.json +2 -2
- package/ring/agents/cornerman.md +7 -0
- package/ring/hooks/hooks.json +1 -1
- package/ring/scripts/arch-gate.mjs +407 -0
- package/ring/scripts/brief-guard.mjs +46 -2
- package/ring/scripts/brief.mjs +46 -2
- package/ring/scripts/config.mjs +24 -0
- package/ring/scripts/engram-memory.mjs +213 -0
- package/ring/scripts/engram.mjs +318 -0
- package/ring/scripts/findings.mjs +37 -4
- package/ring/scripts/judges-card.mjs +180 -7
- package/ring/scripts/mask-guard.mjs +33 -0
- package/ring/scripts/relay.mjs +127 -0
- package/ring/scripts/review-blocks.mjs +232 -0
- package/ring/scripts/tech-plan-log.mjs +34 -2
- package/ring/skills/no-surrender/SKILL.md +5 -0
- package/ring/skills/referee/SKILL.md +24 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The human review by blocks with PR Review Lab: each point of the technical plan is a block (B<n>
|
|
2
|
+
// is point n), with its priority, estimate, author and range in plan.jsonl; every `humanReviewEvery`
|
|
3
|
+
// blocks waiting on the judge, brief.mjs briefs no wrestler until the human reviews that cut in
|
|
4
|
+
// PR Review Lab and signs a verdict with a Tape. PR Review Lab appends the verdicts to
|
|
5
|
+
// `<bout dir>/review-verdicts.jsonl`; the ring reads them: a pass moves the blocks to «hecho», a
|
|
6
|
+
// fail sends them back with the feedback and the Tape as a rebound for the next wrestler.
|
|
7
|
+
// Zero-dependency.
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { readState, writeState } from './config.mjs';
|
|
13
|
+
import { parsePlan, parsePlanLog, PLAN, PLAN_LOG } from './tech-plan.mjs';
|
|
14
|
+
|
|
15
|
+
export const VERDICTS = 'review-verdicts.jsonl';
|
|
16
|
+
const WAITING = 'espera al juez';
|
|
17
|
+
const TAPE = /^https:\/\/(?:[\w-]+\.)?battlearena\.dev\/t\/[\w-]+$/;
|
|
18
|
+
|
|
19
|
+
/** What PR Review Lab reads of a block, as its last line of plan.jsonl carries it. */
|
|
20
|
+
const FACTS = [
|
|
21
|
+
'priority',
|
|
22
|
+
'estimateMin',
|
|
23
|
+
'email',
|
|
24
|
+
'range',
|
|
25
|
+
'startSha',
|
|
26
|
+
'startedAt',
|
|
27
|
+
'endedAt',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** The facts of point `n` so far: each field from the last plan.jsonl line that has it. PURE. */
|
|
31
|
+
export function blockFacts(log, n) {
|
|
32
|
+
return log
|
|
33
|
+
.filter((line) => line.n === n)
|
|
34
|
+
.reduce(
|
|
35
|
+
(facts, line) => ({
|
|
36
|
+
...facts,
|
|
37
|
+
...Object.fromEntries(
|
|
38
|
+
FACTS.filter((key) => line[key] != null).map((key) => [
|
|
39
|
+
key,
|
|
40
|
+
line[key],
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
{},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The facts a state change adds: entering «en curso» stamps the start (the first time: a block sent
|
|
50
|
+
* back keeps its start, so its next review shows the whole block), and «espera al juez» the end and
|
|
51
|
+
* the range from that start to the worktree's head. PURE.
|
|
52
|
+
*/
|
|
53
|
+
export function factsForState(previous, to, { at, head, email, range }) {
|
|
54
|
+
if (to === 'en curso')
|
|
55
|
+
return {
|
|
56
|
+
startedAt: previous.startedAt ?? at,
|
|
57
|
+
startSha: previous.startSha ?? head,
|
|
58
|
+
email: previous.email ?? email,
|
|
59
|
+
// Back at work: no end and no range until it waits on the judge again.
|
|
60
|
+
endedAt: null,
|
|
61
|
+
range: null,
|
|
62
|
+
};
|
|
63
|
+
if (to === WAITING) {
|
|
64
|
+
const from = previous.startSha;
|
|
65
|
+
return {
|
|
66
|
+
endedAt: at,
|
|
67
|
+
range: range ?? (from && head ? `${from}..${head}` : null),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Checks an estimate: priority 1–4 and whole minutes. PURE. */
|
|
74
|
+
export function estimateFacts({ priority, minutes }) {
|
|
75
|
+
const facts = {};
|
|
76
|
+
if (priority != null) {
|
|
77
|
+
if (![1, 2, 3, 4].includes(Number(priority)))
|
|
78
|
+
throw new Error('--priority is 1 (first) to 4 (last)');
|
|
79
|
+
facts.priority = Number(priority);
|
|
80
|
+
}
|
|
81
|
+
if (minutes != null) {
|
|
82
|
+
if (!/^\d+$/.test(String(minutes)) || Number(minutes) < 1)
|
|
83
|
+
throw new Error('--minutes is a whole number of minutes');
|
|
84
|
+
facts.estimateMin = Number(minutes);
|
|
85
|
+
}
|
|
86
|
+
if (!Object.keys(facts).length)
|
|
87
|
+
throw new Error(
|
|
88
|
+
'usage: plan <pr> estimate <n> --minutes <m> [--priority 1-4]',
|
|
89
|
+
);
|
|
90
|
+
return facts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The verdicts PR Review Lab signed, in order; a torn line is skipped. PURE. */
|
|
94
|
+
export function parseVerdicts(text) {
|
|
95
|
+
return parsePlanLog(text).filter(
|
|
96
|
+
(verdict) =>
|
|
97
|
+
['pass', 'fail'].includes(verdict.verdict) &&
|
|
98
|
+
Array.isArray(verdict.blocks) &&
|
|
99
|
+
verdict.at,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `["B3", "B4"]` (or numbers) as point numbers. PURE. */
|
|
104
|
+
export const blockNumbers = (blocks) =>
|
|
105
|
+
blocks.map((block) => Number(/\d+/.exec(String(block))?.[0])).filter(Boolean);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The blocks waiting for the human: in «espera al juez», with a range to review, and no verdict
|
|
109
|
+
* signed after the block's last change. PURE.
|
|
110
|
+
*/
|
|
111
|
+
export function blocksAwaitingReview(plan, log, verdicts) {
|
|
112
|
+
return parsePlan(plan)
|
|
113
|
+
.groups.flatMap((group) => group.items)
|
|
114
|
+
.filter((item) => item.state === WAITING)
|
|
115
|
+
.filter((item) => blockFacts(log, item.n).range)
|
|
116
|
+
.filter((item) => {
|
|
117
|
+
const last = log.filter((line) => line.n === item.n).at(-1)?.at ?? '';
|
|
118
|
+
return !verdicts.some(
|
|
119
|
+
(verdict) =>
|
|
120
|
+
blockNumbers(verdict.blocks).includes(item.n) && verdict.at > last,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Why the next wrestler waits for the human's review, or null. PURE. */
|
|
126
|
+
export function reviewDue(waiting, every) {
|
|
127
|
+
if (!every || waiting.length < every) return null;
|
|
128
|
+
return `the human review is due: ${waiting.map((item) => `B${item.n} «${item.title}»`).join(', ')} wait on the judge (humanReviewEvery ${every}). The bell rang in PR Review Lab (bout mode): the human reviews that cut in one diff and signs a verdict with a Tape; brief.mjs reads it and goes on`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What a verdict does to the bout: the plan state of each block, the decisions line (the judge's
|
|
133
|
+
* verdict, which Engram learns) and, for a fail, the rebound with the feedback and the Tape. PURE.
|
|
134
|
+
*/
|
|
135
|
+
export function verdictEffects(verdict) {
|
|
136
|
+
const blocks = blockNumbers(verdict.blocks);
|
|
137
|
+
const names = blocks.map((n) => `B${n}`).join(' + ');
|
|
138
|
+
const pass = verdict.verdict === 'pass';
|
|
139
|
+
if (!TAPE.test(String(verdict.tape ?? '')) || !verdict.feedback?.trim())
|
|
140
|
+
throw new Error(
|
|
141
|
+
`the verdict of ${names} at ${verdict.at} has no Tape or no feedback: PR Review Lab must not write it without both`,
|
|
142
|
+
);
|
|
143
|
+
const who = verdict.reviewer ? ` (${verdict.reviewer})` : '';
|
|
144
|
+
return {
|
|
145
|
+
blocks,
|
|
146
|
+
state: pass ? 'hecho' : 'en curso',
|
|
147
|
+
decision: `revisión humana de ${names}${who}: ${pass ? 'pasa' : 'no pasa'} · ${verdict.feedback.trim()} · Tape ${verdict.tape} · ${verdict.range ?? ''}`,
|
|
148
|
+
rebound: pass
|
|
149
|
+
? null
|
|
150
|
+
: `Revisión humana de ${names}${who} no pasa (Tape ${verdict.tape}, rango ${verdict.range ?? 'del corte'}): ${verdict.feedback.trim()}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const read = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
155
|
+
|
|
156
|
+
/** The author of a block: the git user of the worktree that writes it. */
|
|
157
|
+
export function authorEmail(worktree) {
|
|
158
|
+
try {
|
|
159
|
+
return (
|
|
160
|
+
execFileSync('git', ['-C', worktree, 'config', 'user.email'], {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
|
+
}).trim() || null
|
|
164
|
+
);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A bout's plan, its log and PR Review Lab's verdicts, as read now. */
|
|
171
|
+
export function reviewInputs(dir) {
|
|
172
|
+
return {
|
|
173
|
+
plan: read(join(dir, PLAN)),
|
|
174
|
+
log: parsePlanLog(read(join(dir, PLAN_LOG))),
|
|
175
|
+
verdicts: parseVerdicts(read(join(dir, VERDICTS))),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Applies the verdicts the bout has not handled yet (state.handledVerdicts holds their `at`):
|
|
181
|
+
* `setState(n, state, ref)` moves each block, and the decision and the rebound are appended.
|
|
182
|
+
* Returns the state to write and what was applied.
|
|
183
|
+
*/
|
|
184
|
+
export function applyVerdicts(dir, state, setState) {
|
|
185
|
+
const handled = new Set(state.handledVerdicts ?? []);
|
|
186
|
+
const fresh = reviewInputs(dir).verdicts.filter(
|
|
187
|
+
(verdict) => !handled.has(verdict.at),
|
|
188
|
+
);
|
|
189
|
+
const nextId = () =>
|
|
190
|
+
Math.max(
|
|
191
|
+
0,
|
|
192
|
+
...[...read(join(dir, 'decisions.md')).matchAll(/ · D(\d+) · /g)].map(
|
|
193
|
+
(match) => Number(match[1]),
|
|
194
|
+
),
|
|
195
|
+
) + 1;
|
|
196
|
+
const applied = fresh.map((verdict) => {
|
|
197
|
+
const effects = verdictEffects(verdict);
|
|
198
|
+
for (const n of effects.blocks)
|
|
199
|
+
setState(n, effects.state, `review ${verdict.tape}`);
|
|
200
|
+
appendFileSync(
|
|
201
|
+
join(dir, 'decisions.md'),
|
|
202
|
+
`- ${new Date().toISOString()} · D${nextId()} · judge · ${effects.decision}\n`,
|
|
203
|
+
);
|
|
204
|
+
if (effects.rebound)
|
|
205
|
+
appendFileSync(
|
|
206
|
+
join(dir, 'rebounds.md'),
|
|
207
|
+
`- [ ] ${new Date().toISOString()} · ${effects.rebound}\n`,
|
|
208
|
+
);
|
|
209
|
+
handled.add(verdict.at);
|
|
210
|
+
return `${verdict.verdict} ${effects.blocks.map((n) => `B${n}`).join('+')} (${verdict.at})`;
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
state: applied.length ? { ...state, handledVerdicts: [...handled] } : state,
|
|
214
|
+
applied,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The review turn of a bout, before a wrestler is briefed or the referee resumes: applies the new
|
|
220
|
+
* verdicts, keeps `pauseEvery` in state.json for PR Review Lab, and says whether the human's review
|
|
221
|
+
* is due. `setState(n, state, ref)` moves a block of the plan.
|
|
222
|
+
*/
|
|
223
|
+
export function reviewTurn(dir, every, setState) {
|
|
224
|
+
const before = readState(dir);
|
|
225
|
+
const { state, applied } = applyVerdicts(dir, before, setState);
|
|
226
|
+
const next = { ...state, pauseEvery: every ?? null };
|
|
227
|
+
if (applied.length || before.pauseEvery !== next.pauseEvery)
|
|
228
|
+
writeState(dir, next);
|
|
229
|
+
const { plan, log, verdicts } = reviewInputs(dir);
|
|
230
|
+
const waiting = blocksAwaitingReview(plan, log, verdicts);
|
|
231
|
+
return { applied, waiting, due: reviewDue(waiting, every) };
|
|
232
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Reads and writes a bout's technical plan (tech-plan.mjs): `judges-card.mjs plan` changes a
|
|
2
2
|
// point's state or adds one, `page-verdicts` adds each judge's «se mejora», and `play-by-play.mjs
|
|
3
3
|
// embed --plan` puts the plan into the chronicle. Zero-dependency.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import {
|
|
5
6
|
appendFileSync,
|
|
6
7
|
existsSync,
|
|
@@ -11,6 +12,7 @@ import { join } from 'node:path';
|
|
|
11
12
|
|
|
12
13
|
import { readState } from './config.mjs';
|
|
13
14
|
import { scriptJson } from './contract-endpoints.mjs';
|
|
15
|
+
import { authorEmail, blockFacts, factsForState } from './review-blocks.mjs';
|
|
14
16
|
import {
|
|
15
17
|
parsePlan,
|
|
16
18
|
parsePlanLog,
|
|
@@ -32,18 +34,48 @@ export function setPlanState(
|
|
|
32
34
|
state,
|
|
33
35
|
ref = null,
|
|
34
36
|
at = new Date().toISOString(),
|
|
37
|
+
{ range = null, estimate = null } = {},
|
|
35
38
|
) {
|
|
36
39
|
const file = join(dir, PLAN);
|
|
37
40
|
const markdown = read(file);
|
|
38
41
|
if (markdown === null) throw new Error(`no ${PLAN} in ${dir}`);
|
|
39
42
|
const item = planItem(parsePlan(markdown), target);
|
|
40
43
|
const next = planState(state);
|
|
44
|
+
// Each line carries every fact of the block so far (PR Review Lab reads the last one): the
|
|
45
|
+
// priority and estimate, and the start, end, author and range a state change stamps.
|
|
46
|
+
const previous = blockFacts(parsePlanLog(read(join(dir, PLAN_LOG))), item.n);
|
|
47
|
+
const { worktree } = readState(dir);
|
|
48
|
+
const facts = {
|
|
49
|
+
...previous,
|
|
50
|
+
...(estimate ?? {}),
|
|
51
|
+
// Only a change of state stamps a start or an end: an estimate keeps the block's range.
|
|
52
|
+
...(next === item.state && !range
|
|
53
|
+
? {}
|
|
54
|
+
: factsForState(previous, next, {
|
|
55
|
+
at,
|
|
56
|
+
head: worktreeHead(worktree),
|
|
57
|
+
email: authorEmail(worktree),
|
|
58
|
+
range,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
41
61
|
writeFileSync(file, withPlanState(markdown, item, next));
|
|
42
62
|
appendFileSync(
|
|
43
63
|
join(dir, PLAN_LOG),
|
|
44
|
-
`${JSON.stringify({ at, n: item.n, title: item.title, from: item.state, to: next, ref })}\n`,
|
|
64
|
+
`${JSON.stringify({ at, n: item.n, title: item.title, from: item.state, to: next, ref, ...Object.fromEntries(Object.entries(facts).filter(([, value]) => value != null)) })}\n`,
|
|
45
65
|
);
|
|
46
|
-
return { ...item, from: item.state, to: next };
|
|
66
|
+
return { ...item, from: item.state, to: next, facts };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The worktree's head, or null when it is gone. */
|
|
70
|
+
function worktreeHead(worktree) {
|
|
71
|
+
try {
|
|
72
|
+
return execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
75
|
+
}).trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
/** Adds a point to the plan (creating the file when it does not exist); returns whether it was new. */
|
|
@@ -43,6 +43,11 @@ theirs may be decided by you. It runs on top of an active bout: without one, say
|
|
|
43
43
|
- **Keep the cycle**: every range reviewed by a cornerman at effort high, filtered against the
|
|
44
44
|
base, one rebound package, round-bells opened as concepts close (they wait for the human; that
|
|
45
45
|
is fine).
|
|
46
|
+
- **A fresh wrestler per concept, and a relay every few**: a long night is where a wrestler's and a
|
|
47
|
+
referee's context grow stale. Close each approved concept with `judges-card.mjs concept`, brief
|
|
48
|
+
the next one to a new wrestler (`--mode resume`), and when `brief.mjs` says a relay is due,
|
|
49
|
+
rewrite the card's «How to resume» and run `judges-card.mjs relay` before going on. When the
|
|
50
|
+
session compacts, the SessionStart hook puts that section back: it is all you keep.
|
|
46
51
|
- **Red gates are classified**, not silenced: code (rebound to the wrestler), environment (note it
|
|
47
52
|
and retry once), harness (note it). A coverage gap is fixed with real tests, never with a test
|
|
48
53
|
that only imports a file (T8). A flag decision and a PR over the host's size ceiling (how to
|
|
@@ -201,6 +201,23 @@ way out, and it refuses without a KO. Then the bout runs as always, with three i
|
|
|
201
201
|
and `claims`; `designReference` at a UI venue), over ranges already reviewed one by one. It
|
|
202
202
|
also reads the concept as one piece through the venue's lenses: what no single range shows;
|
|
203
203
|
- a design review: `--mode batch --files <f1,f2,…>`.
|
|
204
|
+
**A concept ends its wrestler.** When the concept review approves it, run
|
|
205
|
+
`judges-card.mjs concept <pr> "<concept>" --range <first>..<last>`: it records the concept and
|
|
206
|
+
retires the wrestlers that carried it (a long-lived wrestler drags the earlier concepts'
|
|
207
|
+
context and repeats their mistakes). The next concept goes to a fresh wrestler:
|
|
208
|
+
`brief.mjs wrestler <pr> --mode resume --vars <file>` with `nextConcept`; brief-guard warns
|
|
209
|
+
(or, under `briefGuard: "block"`, refuses) a SendMessage to a retired one. Every `relayEvery`
|
|
210
|
+
concepts (4 by default) you relay first: rewrite the card's «How to resume» (where the bout is,
|
|
211
|
+
what is next, what waits on the human) and run `judges-card.mjs relay <pr>`; until then
|
|
212
|
+
`brief.mjs` briefs no wrestler. That is what a compaction or a new session resumes from.
|
|
213
|
+
**Human review by blocks**: each point of the technical plan is a block. Give each its estimate
|
|
214
|
+
and priority when the plan is made (`judges-card.mjs plan <pr> estimate <n> --minutes <m>
|
|
215
|
+
--priority <1-4>`), move it to «en curso» when its wrestler starts and to «espera al juez» when
|
|
216
|
+
its review passes (that stamps the author, the times and the range PR Review Lab reviews). Every
|
|
217
|
+
`humanReviewEvery` blocks waiting, brief.mjs stops: the human reviews that cut in PR Review Lab
|
|
218
|
+
(bout mode) and signs a verdict with a Tape. The next brief applies it: a pass closes the
|
|
219
|
+
blocks, a fail comes back as a rebound the next wrestler fixes first. Do not work around the
|
|
220
|
+
pause.
|
|
204
221
|
If the wrestler claimed something ("does not change counter X"), hand the claim to the
|
|
205
222
|
cornerman to verify against the base. The cornerman always checks the format of the range
|
|
206
223
|
(`format-gate.mjs check`); an unclean file is a finding like any other, fixed by the wrestler
|
|
@@ -504,8 +521,10 @@ and runs the fixes through this same cycle (§2).
|
|
|
504
521
|
who, when), validation (SHA, command, result), what is not verified yet. Show it to the human;
|
|
505
522
|
apply it with `gh pr edit <pr> --body-file …` only when they say so, then check it with
|
|
506
523
|
`gh pr view <pr> --json title,body`.
|
|
507
|
-
8. **Precedents**: show the human the proposals in "For the rulebook / precedents"
|
|
508
|
-
|
|
524
|
+
8. **Precedents**: show the human the proposals in "For the rulebook / precedents" (the `X0`
|
|
525
|
+
findings land there too) and record each answer with
|
|
526
|
+
`judges-card.mjs proposal <pr> <n> accept|reject --why "<what they said>"`: `judges-card.mjs
|
|
527
|
+
close` refuses while one has no decision. For the ones they accept, offer a small separate PR that appends them to the host precedents file
|
|
509
528
|
(`config.precedentsPath`, or a new `.claude/ring-precedents.md`) with the next HPn ids. Never
|
|
510
529
|
commit that file on your own.
|
|
511
530
|
9. **List, never close**: `node "$RING/scripts/round-bell.mjs" list <pr>`, `git worktree list`
|
|
@@ -522,7 +541,9 @@ and runs the fixes through this same cycle (§2).
|
|
|
522
541
|
|
|
523
542
|
1. Without `<pr>`: `node "$RING/scripts/judges-card.mjs" list`; take the only active bout or ask.
|
|
524
543
|
2. `node "$RING/scripts/judges-card.mjs" summary <pr>`: state (with the bout's venue),
|
|
525
|
-
unreviewed commits, open rebounds, decisions and the judges card
|
|
544
|
+
unreviewed commits, open rebounds, decisions and the judges card, and, when Engram answers,
|
|
545
|
+
the repo's latest fouls, verdicts and precedents from earlier bouts (do not repeat those fouls;
|
|
546
|
+
they are context, never rules). Read the rulebook and both precedent files in full again (§0.4).
|
|
526
547
|
3. Review the unreviewed commits first (§2.1–2.4).
|
|
527
548
|
4. A new session cannot talk to the old wrestler: its agent id does not survive. Relaunch it
|
|
528
549
|
**without `isolation`**, on the existing worktree, with
|