alissa-tools-github-revloop 0.31.3__tar.gz → 0.31.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {alissa_tools_github_revloop-0.31.3/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.4}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/alissa.py +92 -19
  3. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/loop.py +86 -18
  4. alissa_tools_github_revloop-0.31.4/src/main/alissa/tools/github/revloop/version +1 -0
  5. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_revloop-0.31.3/src/main/alissa/tools/github/revloop/version +0 -1
  7. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/LICENSE +0 -0
  8. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/MANIFEST.in +0 -0
  9. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/NOTICE +0 -0
  10. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/README.md +0 -0
  11. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/requirements.txt +0 -0
  12. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/setup.cfg +0 -0
  13. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/setup.py +0 -0
  14. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  15. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  16. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
  17. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  18. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/config.py +0 -0
  19. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
  20. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  21. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
  22. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  23. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
  24. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  25. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/state.py +0 -0
  26. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/trust.py +0 -0
  27. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/version.py +0 -0
  28. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
  29. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
  30. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
  31. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
  32. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
  33. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
  34. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
  35. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
  36. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  37. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
  38. {alissa_tools_github_revloop-0.31.3 → alissa_tools_github_revloop-0.31.4}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.3
3
+ Version: 0.31.4
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -239,7 +239,7 @@ _VERDICT_RE = re.compile(
239
239
  # The envelope's merge-readiness judgment (issue #130). The alissa-code-review
240
240
  # skill writes it as one list line in the verdict envelope:
241
241
  # - **Merge-Readiness:** auto
242
- # - **Merge-Readiness:** operator — touches convex/schema.ts
242
+ # - **Merge-Readiness:** operator — schema-migration: removes users.legacyId
243
243
  # Tolerant of the markdown around the label -- an optional bullet, optional
244
244
  # `**` bold around the label (with the colon inside or outside it), optional
245
245
  # bold around the value -- but NOT of the value's case: `Auto` is not a
@@ -255,21 +255,83 @@ _READINESS_RE = re.compile(
255
255
  re.MULTILINE,
256
256
  )
257
257
 
258
+ # The operator reason's CLASS (issue #142). An `operator` judgment names WHY
259
+ # the merge is a human's, and until now it did so in prose the merge edge
260
+ # could not act on: of 14 operator verdicts on ~40 studio PRs, 3 named real
261
+ # merge risk and 11 were "the PR body lists an unverified item" -- validation
262
+ # work the human does at gate 2 anyway, not a reason to withhold the merge.
263
+ # So the reason now LEADS with one token from this closed enum, in the
264
+ # consumer's grammar:
265
+ #
266
+ # Merge-Readiness: auto
267
+ # Merge-Readiness: operator — <class>: <one-line reason>
268
+ #
269
+ # The order below is the SEVERITY order: when more than one row applies, the
270
+ # reviewer names the one nearest the top (the most severe wins), and exactly
271
+ # one. `auto` never carries a class. An operator line whose reason does not
272
+ # lead with a recognised token is still valid grammar -- it parses, value and
273
+ # reason intact -- and reads as UNCLASSED (`klass=None`); the consumer treats
274
+ # unclassed as a hard hold (fail closed), so a reviewer that forgets the
275
+ # class loses throughput, never safety. The token is matched exactly as
276
+ # written here (lowercase, hyphenated): `Schema-Migration:` is not a class
277
+ # this daemon reads, for the same reason `Auto` is not a value it carries.
278
+ READINESS_CLASSES: "tuple[str, ...]" = (
279
+ "schema-migration", # schema shape changes needing a migration / removal / rename on live data
280
+ "data-backfill", # one-off writes over existing rows (backfills, stored-shape version bumps)
281
+ "secrets-env", # new / changed env vars, secrets, credentials the deploy must carry
282
+ "infra-deploy", # Dockerfiles, workflows, Railway config, base-image pins -- what runs
283
+ "billing", # credit charging, pricing, tiers, quotas
284
+ "security", # auth, permission gates, redaction, sandboxing flagged operator-worthy
285
+ "unverified-ux", # the PR body's operator gate is a human look at a screen / copy / mockup
286
+ "unverified-runtime", # the PR body's operator gate is a live smoke / real-model run / replay
287
+ "release-act", # the merge itself is a release (VERSION bump -> publish, tag, npm)
288
+ )
289
+ _READINESS_CLASS_RE = re.compile(
290
+ r"^(" + "|".join(re.escape(klass) for klass in READINESS_CLASSES) + r")[ \t]*:"
291
+ )
292
+
258
293
  # The reason lands in a GitHub review body (as a line-anchored trailer) and in
259
294
  # a log line, so it is bounded and flattened at the parser: one line, no
260
295
  # backticks (a fence would swallow the trailer and everything after it), and
261
- # no more than this many characters.
296
+ # no more than this many characters. The class prefix is PART of the reason
297
+ # and survives the flattening untouched: the native trailer must stay
298
+ # byte-equal to the envelope's line, class included.
262
299
  MAX_READINESS_REASON_CHARS = 200
263
300
 
264
301
 
265
302
  def clean_readiness_reason(text: object) -> str:
266
- """Flatten a Merge-Readiness reason to one bounded, backtick-free line."""
303
+ """Flatten a Merge-Readiness reason to one bounded, backtick-free line.
304
+
305
+ The `<class>:` prefix, when present, is kept intact at the head of the
306
+ line -- classify_readiness_reason reads it off the cleaned reason, and the
307
+ emitter copies the cleaned reason whole.
308
+ """
267
309
  if not isinstance(text, str):
268
310
  return ""
269
311
  flat = " ".join(text.replace("`", "").split())
270
312
  return flat[:MAX_READINESS_REASON_CHARS].rstrip()
271
313
 
272
314
 
315
+ def classify_readiness_reason(value: object, reason: object) -> "str | None":
316
+ """The READINESS_CLASSES token an `operator` reason leads with, or None.
317
+
318
+ None for `auto` (never classed), for an operator reason that leads with
319
+ no recognised token (UNCLASSED -- the consumer's hard hold), and for a
320
+ token in the wrong case or spelling. The reason itself is never altered:
321
+ the class is read off it, not cut out of it.
322
+ """
323
+ if value != READINESS_OPERATOR or not isinstance(reason, str):
324
+ return None
325
+ match = _READINESS_CLASS_RE.match(reason)
326
+ return match.group(1) if match else None
327
+
328
+
329
+ def _classed(value: "str | None", raw_reason: object) -> "tuple[str | None, str, str | None]":
330
+ """The `(value, reason, klass)` triple both parsers hand back."""
331
+ reason = clean_readiness_reason(raw_reason)
332
+ return (value, reason, classify_readiness_reason(value, reason))
333
+
334
+
273
335
  # The trailer's own grammar (issue #134) -- the bare-line sibling of
274
336
  # _READINESS_RE. This is the line a native review ENDS with, whoever wrote
275
337
  # it: the daemon's emitter (loop.readiness_trailer) when it posts the verdict
@@ -288,37 +350,42 @@ _TRAILER_RE = re.compile(
288
350
  )
289
351
 
290
352
 
291
- def parse_trailer(body: object) -> "tuple[str | None, str]":
292
- """`(value, reason)` from the first bare `Merge-Readiness:` line in a
293
- review body, or `(None, "")` when no line matches the trailer grammar.
353
+ def parse_trailer(body: object) -> "tuple[str | None, str, str | None]":
354
+ """`(value, reason, klass)` from the first bare `Merge-Readiness:` line in
355
+ a review body, or `(None, "", None)` when no line matches the trailer
356
+ grammar.
294
357
 
295
358
  Strict where parse_readiness is tolerant: the line inside backticks
296
359
  mid-sentence (studio #1258) and the envelope's `- **Merge-Readiness:**`
297
360
  bullet both read as MISSING here, because the consumer's regex is what
298
361
  decides whether the merge edge sees the judgment at all. CRLF bodies (a
299
362
  review typed into the web form) are normalised first; the grammar itself
300
- is unchanged.
363
+ is unchanged. `klass` is the READINESS_CLASSES token an operator reason
364
+ leads with, or None (auto, or an unclassed operator -- issue #142).
301
365
  """
302
366
  if not isinstance(body, str):
303
- return (None, "")
367
+ return (None, "", None)
304
368
  match = _TRAILER_RE.search(body.replace("\r\n", "\n"))
305
369
  if match is None:
306
- return (None, "")
307
- return (match.group(1), clean_readiness_reason(match.group(2)))
370
+ return (None, "", None)
371
+ return _classed(match.group(1), match.group(2))
308
372
 
309
373
 
310
- def parse_readiness(blob: object) -> "tuple[str | None, str]":
311
- """`(value, reason)` from the first Merge-Readiness line in `blob`.
374
+ def parse_readiness(blob: object) -> "tuple[str | None, str, str | None]":
375
+ """`(value, reason, klass)` from the first Merge-Readiness line in `blob`.
312
376
 
313
377
  `value` is READINESS_AUTO / READINESS_OPERATOR, or None when no line
314
- parses; `reason` is the cleaned trailing text (empty when there is none).
378
+ parses; `reason` is the cleaned trailing text (empty when there is none),
379
+ class prefix included; `klass` is the READINESS_CLASSES token that reason
380
+ leads with, or None when the value is `auto`, the reason is unclassed, or
381
+ nothing parsed (issue #142).
315
382
  """
316
383
  if not isinstance(blob, str):
317
- return (None, "")
384
+ return (None, "", None)
318
385
  match = _READINESS_RE.search(blob)
319
386
  if match is None:
320
- return (None, "")
321
- return (match.group(1), clean_readiness_reason(match.group(2)))
387
+ return (None, "", None)
388
+ return _classed(match.group(1), match.group(2))
322
389
 
323
390
 
324
391
  @dataclass(frozen=True)
@@ -580,12 +647,17 @@ class VerdictEnvelope:
580
647
  envelope has no parseable `Merge-Readiness` line -- the case the native
581
648
  post fails closed on. `readiness_reason` is already one bounded,
582
649
  backtick-free line (see clean_readiness_reason); empty when the envelope
583
- gave none.
650
+ gave none. `readiness_class` is the READINESS_CLASSES token an operator
651
+ reason leads with (issue #142), or None: for `auto`, for an unclassed
652
+ operator reason, and for a missing line alike -- the reason keeps the
653
+ prefix either way, so the class is never lost between here and the
654
+ native trailer.
584
655
  """
585
656
 
586
657
  verdict: str
587
658
  readiness: "str | None" = None
588
659
  readiness_reason: str = ""
660
+ readiness_class: "str | None" = None
589
661
 
590
662
 
591
663
  @dataclass(frozen=True)
@@ -1086,9 +1158,9 @@ class Alissa:
1086
1158
  continue
1087
1159
  match = _VERDICT_RE.search(blob)
1088
1160
  if match:
1089
- readiness, reason = parse_readiness(content)
1161
+ readiness, reason, klass = parse_readiness(content)
1090
1162
  if readiness is None:
1091
- readiness, reason = parse_readiness(title)
1163
+ readiness, reason, klass = parse_readiness(title)
1092
1164
  found.append(
1093
1165
  (Alissa._created_key(item.get("createdAt")),
1094
1166
  index,
@@ -1096,6 +1168,7 @@ class Alissa:
1096
1168
  verdict=match.group(1).lower(),
1097
1169
  readiness=readiness,
1098
1170
  readiness_reason=reason,
1171
+ readiness_class=klass,
1099
1172
  ))
1100
1173
  )
1101
1174
  break
@@ -427,13 +427,21 @@ NATIVE_VERDICT_BODY = (
427
427
  # the last non-empty line before the hidden verdict marker --
428
428
  #
429
429
  # Merge-Readiness: auto
430
- # Merge-Readiness: operator — <one-line reason>
430
+ # Merge-Readiness: operator — <class>: <one-line reason>
431
431
  #
432
432
  # The rules that make it safe to consume: it is emitted on APPROVE events
433
433
  # ONLY (a request_changes envelope with a stray `auto` line carries nothing;
434
434
  # an approve the checks gate downgraded carries nothing), and an envelope
435
435
  # with no parseable line fails CLOSED to `operator`, with a reason that tells
436
- # the operator why the merge waited on them.
436
+ # the operator why the merge waited on them -- and NO class (issue #142): the
437
+ # fallback is unclassed by construction, the consumer's hard hold.
438
+ #
439
+ # The operator reason leads with one token from alissa.READINESS_CLASSES
440
+ # (issue #142) so the consumer can apply a POLICY to the hold instead of
441
+ # holding every operator alike. The emitter copies the envelope's cleaned
442
+ # reason whole, class prefix included, so the native line is byte-equal to
443
+ # the envelope's; it never adds, drops or rewrites a class -- an unclassed
444
+ # envelope posts unclassed, and the narration says so.
437
445
  #
438
446
  # The label and the grammar live in alissa.py (READINESS_TRAILER_LABEL,
439
447
  # parse_trailer): the emitter below builds from the label the regex is built
@@ -441,6 +449,21 @@ NATIVE_VERDICT_BODY = (
441
449
  # same regex (issue #134) -- one grammar, two writers, no drift.
442
450
  READINESS_MISSING_REASON = "envelope carries no Merge-Readiness line"
443
451
 
452
+ # What the log line and the activity row say about an operator judgment whose
453
+ # reason leads with no recognised class (issue #142). Named because it is a
454
+ # state the merge policy acts on (hard hold), not an absence.
455
+ READINESS_UNCLASSED = "unclassed"
456
+
457
+
458
+ def readiness_class_term(value: "str | None", klass: "str | None") -> str:
459
+ """` class=<klass>` (or ` class=unclassed`) for an operator judgment, and
460
+ nothing at all for `auto` or a missing one -- `auto` never carries a
461
+ class, so naming one there would be noise the consumer must never read.
462
+ """
463
+ if value != READINESS_OPERATOR:
464
+ return ""
465
+ return f" class={klass or READINESS_UNCLASSED}"
466
+
444
467
 
445
468
  def readiness_trailer(envelope: "VerdictEnvelope | None") -> str:
446
469
  """The line the native APPROVE carries for `envelope`'s judgment.
@@ -449,7 +472,9 @@ def readiness_trailer(envelope: "VerdictEnvelope | None") -> str:
449
472
  the envelope onto the grammar. `auto` is emitted bare -- the consumer
450
473
  keys on the value, and an operator reads a reason only when the merge is
451
474
  theirs to make. A missing or unparseable judgment (None envelope, or one
452
- whose readiness did not parse) is `operator` with a reason saying so.
475
+ whose readiness did not parse) is `operator` with a reason saying so and
476
+ no class. The reason is copied whole, so an envelope's `<class>:` prefix
477
+ reaches the native line byte for byte (issue #142).
453
478
  """
454
479
  if envelope is not None and envelope.readiness == READINESS_AUTO:
455
480
  return f"{READINESS_TRAILER_LABEL} {READINESS_AUTO}"
@@ -679,18 +704,53 @@ _RECORD_THE_CAP = (
679
704
  # envelope without the line posts as `operator` -- so a reviewer who skips it
680
705
  # has silently withheld auto-merge. The two grammar sentences are the
681
706
  # README's ("The `Merge-Readiness` trailer on a native approve"), verbatim.
707
+ #
708
+ # The operator form carries a CLASS (issue #142): `operator — <class>:
709
+ # <reason>`, one token from alissa.READINESS_CLASSES, so the merge edge can
710
+ # apply a policy to the hold. The compact table below is the ONE place the
711
+ # directive states the enum (each token exactly once -- test_readiness pins
712
+ # it), in severity order with the most-severe rule and the reasoning the
713
+ # reviewer needs to classify honestly: an unverified-* hold is validation
714
+ # work, not merge risk, and saying so lets the policy merge it. The table's
715
+ # rows are the README's, condensed; the README is the long form.
682
716
  _MERGE_READINESS_LINE = (
683
717
  "Your verdict envelope MUST carry the skill's `- **Merge-Readiness:** auto | "
684
- "operator — <reason>` line, AND your OWN native review (every `gh pr review` "
685
- "form and the reviews-API POST alike) MUST END with the bare line "
686
- "`Merge-Readiness: auto` or `Merge-Readiness: operator — <one-line reason>` "
687
- "as its last non-empty line — plain text at the start of the line, not in "
688
- "backticks, not bold, not mid-sentence — byte-equal in value and reason to "
689
- "the envelope's line; `auto` only on an APPROVE of the reviewed head. The "
690
- "merge edge reads that line off the review and holds an approve without "
691
- "it. The daemon copies the envelope's line onto a native review only when "
692
- "it posts the verdict itself, and an envelope without the line posts as "
693
- "`operator`. "
718
+ "operator — <class>: <reason>` line, AND your OWN native review (every `gh pr "
719
+ "review` form and the reviews-API POST alike) MUST END with the bare line "
720
+ "`Merge-Readiness: auto` or `Merge-Readiness: operator — <class>: <one-line "
721
+ "reason>` as its last non-empty line — plain text at the start of the line, "
722
+ "not in backticks, not bold, not mid-sentence — byte-equal in value, class "
723
+ "and reason to the envelope's line; `auto` only on an APPROVE of the "
724
+ "reviewed head. `<class>` is exactly ONE token from this closed enum, "
725
+ "lowercase and hyphenated, followed by a colon and the reason; the rows are "
726
+ "in severity order, most severe first: "
727
+ "`schema-migration` (schema shape changes that need a migration, removal or "
728
+ "rename on live data); "
729
+ "`data-backfill` (one-off writes over existing rows — backfills, version "
730
+ "bumps of stored shapes); "
731
+ "`secrets-env` (new or changed env vars, secrets or credentials the deploy "
732
+ "must carry); "
733
+ "`infra-deploy` (Dockerfiles, workflows, Railway config, base-image pins — "
734
+ "anything that changes what runs); "
735
+ "`billing` (credit charging, pricing, tiers, quotas); "
736
+ "`security` (auth, permission gates, redaction, sandboxing — anything the "
737
+ "rubric's third dimension flagged as operator-worthy); "
738
+ "`unverified-ux` (the PR body's operator gate is a human look at a screen, "
739
+ "copy or mockup); "
740
+ "`unverified-runtime` (the PR body's operator gate is a live smoke, "
741
+ "real-model run or replay the worker could not run); "
742
+ "`release-act` (the merge itself is a release — VERSION bump → publish, tag, "
743
+ "npm). "
744
+ "When more than one row applies, name the MOST SEVERE one — the row listed "
745
+ "first — and only it. Classify honestly, not defensively: an unverified-* "
746
+ "hold is validation work the human does at gate 2 anyway, not merge risk, "
747
+ "and the merge policy may merge it while it keeps holding every row above; "
748
+ "an operator line whose reason leads with no recognised token still parses "
749
+ "but reads as UNCLASSED — a hard hold that costs throughput, never safety. "
750
+ "`auto` never carries a class. The merge edge reads that line off the "
751
+ "review and holds an approve without it. The daemon copies the envelope's "
752
+ "line onto a native review only when it posts the verdict itself, and an "
753
+ "envelope without the line posts as `operator` with no class. "
694
754
  )
695
755
 
696
756
  # -- the reviewer session's own CI gate (issue #84) ---------------------------
@@ -3732,9 +3792,14 @@ class ReviewWatcher:
3732
3792
  pr.full_name, pr.number, judged, int(time.time()), url or ""
3733
3793
  )
3734
3794
  # The readiness carried on the post, in the same three records. Only
3735
- # an APPROVE carries one, so only an APPROVE names it.
3795
+ # an APPROVE carries one, so only an APPROVE names it. The class is
3796
+ # read BACK off the emitted line with the shared grammar (issue
3797
+ # #142), so what the narration names is what the consumer will parse
3798
+ # -- an unclassed operator says `class=unclassed`, the hard hold.
3736
3799
  readiness_value = trailer[len(READINESS_TRAILER_LABEL):].strip()
3737
- readiness_note = f" readiness={readiness_value}" if trailer else ""
3800
+ posted_value, _, posted_class = parse_trailer(trailer)
3801
+ class_term = readiness_class_term(posted_value, posted_class)
3802
+ readiness_note = f" readiness={readiness_value}{class_term}" if trailer else ""
3738
3803
  log.info(
3739
3804
  "%s round %d closed: native %s review submitted as %s (%s)%s%s",
3740
3805
  pr.slug, round_, event, self.github.login, url or "no url",
@@ -3744,7 +3809,7 @@ class ReviewWatcher:
3744
3809
  pr,
3745
3810
  f"- {_now()} — round {round_} — native `{event}` review submitted "
3746
3811
  f"as `{self.github.login}` (verdict of record){gate_note}"
3747
- + (f" — merge-readiness: `{readiness_value}`" if trailer else ""),
3812
+ + (f" — merge-readiness: `{readiness_value}`{class_term}" if trailer else ""),
3748
3813
  )
3749
3814
  if event == EVENT_COMMENT:
3750
3815
  # A degraded verdict takes the PR out of the loop (the review
@@ -4458,7 +4523,7 @@ class ReviewWatcher:
4458
4523
  if self.state.readiness_observed(pr.full_name, pr.number, pr.head_sha) is not None:
4459
4524
  return
4460
4525
 
4461
- value, reason = parse_trailer(newest.body)
4526
+ value, reason, klass = parse_trailer(newest.body)
4462
4527
  head7 = pr.head_sha[:7]
4463
4528
  if value is None:
4464
4529
  readiness = READINESS_MISSING
@@ -4476,7 +4541,10 @@ class ReviewWatcher:
4476
4541
  )
4477
4542
  else:
4478
4543
  readiness = value
4479
- term = f"{value} — {reason}" if reason else value
4544
+ # The class rides along (issue #142): `class=<token>` when the
4545
+ # operator reason leads with one, `class=unclassed` when it does
4546
+ # not, nothing for `auto`.
4547
+ term = (f"{value} — {reason}" if reason else value) + readiness_class_term(value, klass)
4480
4548
  log.info(
4481
4549
  "%s approve at %s by %s carries readiness=%s",
4482
4550
  pr.slug, head7, self.github.login, term,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.3
3
+ Version: 0.31.4
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera