@thebattlearena/corner 0.10.1-beta.84 → 0.10.1-beta.85

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thebattlearena/corner",
3
- "version": "0.10.1-beta.84",
3
+ "version": "0.10.1-beta.85",
4
4
  "description": "Corner: tu esquina de Battle Arena en Claude Code (el plugin `battlearena`). Hooks de tracking, objetivo del turno, plan de batalla /battlearena:conquer, Ringside (el panel de tu día), el ring y la radio. Zero-dependency.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -872,11 +872,13 @@ export function readyForTitleDefense(pr, view) {
872
872
  reason: `could not tell whether PR #${pr} is a draft (${error.message}); title-defense does not start`,
873
873
  };
874
874
  }
875
- if (answer?.isDraft === false) return { ok: true, reason: null };
875
+ if (answer?.isDraft === false)
876
+ return { ok: true, reason: null, nodeId: answer.id ?? null };
876
877
  if (answer?.isDraft === true)
877
878
  return {
878
879
  ok: false,
879
880
  draft: true,
881
+ nodeId: answer.id ?? null,
880
882
  reason: `PR #${pr} is still a draft; title-defense starts when you mark it ready for review`,
881
883
  };
882
884
  return { ok: false, reason: `gh did not say whether PR #${pr} is a draft` };
@@ -1005,7 +1007,7 @@ function titleDefense([pr, ...argv]) {
1005
1007
  const gate = clear
1006
1008
  ? { ok: true }
1007
1009
  : readyForTitleDefense(pr, () =>
1008
- execFileSync('gh', ['pr', 'view', String(pr), '--json', 'isDraft'], {
1010
+ execFileSync('gh', ['pr', 'view', String(pr), '--json', 'isDraft,id'], {
1009
1011
  cwd: state.worktree,
1010
1012
  encoding: 'utf8',
1011
1013
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -1030,7 +1032,18 @@ function titleDefense([pr, ...argv]) {
1030
1032
  throw new Error(
1031
1033
  gate.draft ? `${gate.reason} (--keep-draft was passed)` : gate.reason,
1032
1034
  );
1033
- const next = withTitleDefense(state, clear ? null : Number(argv[index + 1]));
1035
+ const flagged = withTitleDefense(
1036
+ state,
1037
+ clear ? null : Number(argv[index + 1]),
1038
+ );
1039
+ // The PR's GraphQL node id: mask-guard ties a requestReviewsByLogin (`-F id=<node>`) to this bout.
1040
+ const next =
1041
+ clear || !gate.nodeId
1042
+ ? flagged
1043
+ : {
1044
+ ...flagged,
1045
+ titleDefense: { ...flagged.titleDefense, nodeId: gate.nodeId },
1046
+ };
1034
1047
  writeState(dir, next);
1035
1048
  console.log(
1036
1049
  clear
@@ -13,6 +13,7 @@
13
13
  import { boutArchGate } from './arch-gate.mjs';
14
14
  import { boutFormatGate } from './format-gate.mjs';
15
15
  import { boutLeases, commitLeaseRefusal, wrestlerBlocks } from './lanes.mjs';
16
+ import { reviewInputs, roundsAwaitingHuman } from './review-blocks.mjs';
16
17
  import { execFileSync } from 'node:child_process';
17
18
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
18
19
  import { homedir } from 'node:os';
@@ -955,6 +956,19 @@ const requestsCopilotByLogin = (gh) =>
955
956
  /\brequestReviewsByLogin\b/.test(graphqlText(gh)) &&
956
957
  /copilot-pull-request-reviewer/.test(graphqlText(gh));
957
958
 
959
+ /**
960
+ * The PR node id a GraphQL mutation targets: `-F id=<node>` / `-f id=…` or a `pullRequestId:
961
+ * "<node>"` inside the query. PURE.
962
+ */
963
+ export function graphqlNodeId(gh) {
964
+ const text = graphqlText(gh);
965
+ return (
966
+ /(?:^|\s)-[fF]\s+(?:id|pullRequestId)=(\S+)/.exec(text)?.[1] ??
967
+ /pullRequestId:\s*"([^"]+)"/.exec(text)?.[1] ??
968
+ null
969
+ );
970
+ }
971
+
958
972
  /** `node <…>/judges-card.mjs close <pr>`: the PR it closes, else null. */
959
973
  function closesBout(words) {
960
974
  const closing =
@@ -980,9 +994,13 @@ function chronicleBehind(ctx, where, pr) {
980
994
  }
981
995
  const [first] = stale;
982
996
  if (!first) return null;
997
+ const unscoped =
998
+ pr === null
999
+ ? ` This command could not be tied to one bout, so every active bout of the repo was checked: #${first.pr} is the one behind, not necessarily the PR this command is for.`
1000
+ : '';
983
1001
  return {
984
1002
  block: true,
985
- reason: `The play-by-play is behind the bout (records changed after ${first.publishedAt ?? 'a publish that was never stamped'}). Run /ring:play-by-play ${first.pr} and publish to the same link first.`,
1003
+ reason: `The play-by-play is behind the bout (records changed after ${first.publishedAt ?? 'a publish that was never stamped'}). Run /ring:play-by-play ${first.pr} and publish to the same link first.${unscoped}`,
986
1004
  };
987
1005
  }
988
1006
 
@@ -1376,8 +1394,19 @@ function judgeSegment(segment, words, cwd, ctx) {
1376
1394
  const bell = gh && requestsCopilotByLogin(gh);
1377
1395
  const where = { git, gh, dir: cwd };
1378
1396
  if ((bell || closing !== null) && ctx.anyBout() && ctx.boutActive(where)) {
1379
- const behind = chronicleBehind(ctx, where, bell ? prNumberOf(gh) : closing);
1397
+ // A GraphQL re-request names its PR by node id: title-defense stored it when it set the flag.
1398
+ const bellPr = bell
1399
+ ? (prNumberOf(gh) ?? ctx.prOfNode?.(where, graphqlNodeId(gh)) ?? null)
1400
+ : null;
1401
+ const behind = chronicleBehind(ctx, where, bell ? bellPr : closing);
1380
1402
  if (behind) return behind;
1403
+ // Copilot gets no new round until the human validated the last one (its plan block).
1404
+ const unreviewed = bell ? ctx.roundsAwaitingHuman?.(where, bellPr) : [];
1405
+ if (unreviewed?.length)
1406
+ return {
1407
+ block: true,
1408
+ reason: `The human has not validated ${unreviewed.join(', ')} yet: Copilot gets the next round only after the human signs that round in PR Review Lab (a pass). Wait for the verdict (the lookout wakes you), apply it with judges-card.mjs summary <pr>, then re-request.`,
1409
+ };
1381
1410
  }
1382
1411
  const ambiguous =
1383
1412
  git?.sub === 'commit' &&
@@ -1721,6 +1750,26 @@ function realContext(cwd, agentId = null) {
1721
1750
  .filter((bout) => bout.stale)
1722
1751
  .map(({ pr: stalePr, publishedAt }) => ({ pr: stalePr, publishedAt }));
1723
1752
  },
1753
+ // The Copilot round blocks the human has not validated, of the bout of `pr` (every bout when null).
1754
+ roundsAwaitingHuman: (where, pr) => {
1755
+ const slug = slugWhere(where);
1756
+ if (!slug) return [];
1757
+ return boutsOf(slug)
1758
+ .filter(({ state }) => pr === null || state.pr === pr)
1759
+ .flatMap(({ dir }) => {
1760
+ const { plan, log, verdicts } = reviewInputs(dir);
1761
+ return roundsAwaitingHuman(plan, log, verdicts);
1762
+ });
1763
+ },
1764
+ // The bout whose title-defense flag carries this PR node id, or null.
1765
+ prOfNode: (where, nodeId) => {
1766
+ const slug = slugWhere(where);
1767
+ if (!slug || !nodeId) return null;
1768
+ return (
1769
+ boutsOf(slug).find(({ state }) => state.titleDefense?.nodeId === nodeId)
1770
+ ?.state.pr ?? null
1771
+ );
1772
+ },
1724
1773
  titleDefenseFlags: (where) => {
1725
1774
  const slug = slugWhere(where);
1726
1775
  if (!slug) return [];
@@ -287,6 +287,19 @@ export function lookaheadRefusal({ globs, waitingFiles, running }) {
287
287
  return null;
288
288
  }
289
289
 
290
+ /**
291
+ * The Copilot rounds of title-defense the human has not validated yet: a block «Copilot R<k>…»
292
+ * still «en curso», or waiting on the judge with no later verdict. Copilot is not asked for another
293
+ * round until they are empty. PURE.
294
+ */
295
+ export function roundsAwaitingHuman(plan, log, verdicts) {
296
+ const isRound = (item) => /^Copilot R\d+/i.test(item.title);
297
+ const waiting = blocksAwaitingReview(plan, log, verdicts).filter(isRound);
298
+ return [...runningBlocks(plan).filter(isRound), ...waiting].map(
299
+ (item) => `B${item.n} «${item.title}»`,
300
+ );
301
+ }
302
+
290
303
  /** The blocks «en curso» right now. PURE. */
291
304
  export const runningBlocks = (plan) =>
292
305
  parsePlan(plan)
@@ -94,7 +94,15 @@ list, judges card, `decisions.md` and `rebounds.md`, and the same `mask-guard` r
94
94
  result is a new rebound; a fix is not done until the reproduction says so (PR26). If it cannot
95
95
  run (environment down, scenario missing), the thread stays open and is listed for the human in
96
96
  the judges card: never resolve it on unit tests alone.
97
- 6. **A round-bell for the fixes** (§2.6), one per concept.
97
+ 6. **A round-bell for the fixes** (§2.6), one per concept. **The round is a block of the plan**,
98
+ so the human reviews it in PR Review Lab like any other: before the wrestler starts, `judges-card.mjs
99
+ plan <pr> add "Defensa del título" "Copilot R<k>: <one line of what it fixes>"`, then `plan <pr>
100
+ <n> "en curso" --minutes <m> --priority 1` (with `--files` when blocks run side by side), and
101
+ `plan <pr> <n> "espera al juez"` once its range is approved. The human's verdict applies as for
102
+ any block: a fail comes back as the rebound of the next round. A round with nothing to fix adds
103
+ no block. **Copilot gets the next round only after the human passed this one**: mask-guard
104
+ refuses the re-request while a «Copilot R<k>» block is running or waits on the judge; the lookout
105
+ wakes you when the verdict lands.
98
106
  7. **CI red**: diagnose before touching code: `gh run view <id> --log-failed`. Tell a PR failure
99
107
  from a gate failure (coverage of an untested file; a size ceiling, which goes to the human).
100
108
  Never "fix" a gate by weakening it. A PR failure goes through steps 3 to 6 like a finding.