alissa-tools-github-revloop 0.12.0__tar.gz → 0.14.0__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.
- {alissa_tools_github_revloop-0.12.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.14.0}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/setup.py +1 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/loop.py +114 -3
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/state.py +488 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/version +1 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/__init__.py +36 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/__main__.py +130 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/auth.py +200 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/page.py +588 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/server.py +355 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/sources.py +615 -0
- alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/sysinfo.py +174 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +7 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +1 -0
- alissa_tools_github_revloop-0.12.0/src/main/alissa/tools/github/revloop/state.py +0 -218
- alissa_tools_github_revloop-0.12.0/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/README.md +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -50,6 +50,7 @@ setup(
|
|
|
50
50
|
"console_scripts": [
|
|
51
51
|
"alissa-revloop=alissa.tools.github.revloop.__main__:main",
|
|
52
52
|
"alissa-pr-review=alissa.tools.github.revloop.prreview:main",
|
|
53
|
+
"alissa-revloop-ui=alissa.tools.github.revloop.webui.__main__:main",
|
|
53
54
|
]
|
|
54
55
|
},
|
|
55
56
|
install_requires=requirements,
|
|
@@ -14,6 +14,7 @@ import logging
|
|
|
14
14
|
import re
|
|
15
15
|
import secrets
|
|
16
16
|
import time
|
|
17
|
+
from collections import Counter
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from enum import Enum
|
|
19
20
|
from pathlib import Path
|
|
@@ -185,6 +186,19 @@ class Decision:
|
|
|
185
186
|
action: Action
|
|
186
187
|
reason: str = ""
|
|
187
188
|
round: int | None = None
|
|
189
|
+
# Descriptive metadata for the poll-snapshot exhaust (see
|
|
190
|
+
# ReviewWatcher._stage_record). Populated where a decision names a concrete
|
|
191
|
+
# reviewer -- the SPAWNED path and the liveness-deferral IN_FLIGHT paths --
|
|
192
|
+
# and left at its default elsewhere. Purely observational: nothing in the
|
|
193
|
+
# decision logic reads these, so they never change which branch is taken.
|
|
194
|
+
# `deferred` marks an IN_FLIGHT that is a liveness deferral (a live session
|
|
195
|
+
# holding the respawn back) rather than a freshly-enqueued round;
|
|
196
|
+
# `reenqueued` marks a SPAWNED that respawned a round whose prior session
|
|
197
|
+
# was presumed dead (the "stale-re-enqueued" summary bucket).
|
|
198
|
+
session: str | None = None
|
|
199
|
+
task_ref: str | None = None
|
|
200
|
+
deferred: bool = False
|
|
201
|
+
reenqueued: bool = False
|
|
188
202
|
|
|
189
203
|
|
|
190
204
|
def session_name(pr: PullRequest, round_: int) -> str:
|
|
@@ -323,6 +337,8 @@ class ReviewWatcher:
|
|
|
323
337
|
Action.IN_FLIGHT,
|
|
324
338
|
f"round {round_} is stale but liveness is unprobeable — deferring",
|
|
325
339
|
round_,
|
|
340
|
+
session=session,
|
|
341
|
+
deferred=True,
|
|
326
342
|
)
|
|
327
343
|
|
|
328
344
|
ses = live.get(session)
|
|
@@ -356,6 +372,8 @@ class ReviewWatcher:
|
|
|
356
372
|
f"{session} is still {life} — not "
|
|
357
373
|
f"respawning over a live reviewer",
|
|
358
374
|
round_,
|
|
375
|
+
session=session,
|
|
376
|
+
deferred=True,
|
|
359
377
|
)
|
|
360
378
|
|
|
361
379
|
def _convergence_reason(
|
|
@@ -405,9 +423,14 @@ class ReviewWatcher:
|
|
|
405
423
|
|
|
406
424
|
# -- reap sweep --------------------------------------------------------
|
|
407
425
|
|
|
408
|
-
def sweep_sessions(self) ->
|
|
426
|
+
def sweep_sessions(self) -> int:
|
|
409
427
|
"""Kill the managed session of every finished round. Runs every poll.
|
|
410
428
|
|
|
429
|
+
Returns the number of sessions actually reaped this pass (0 in
|
|
430
|
+
`--dry-run`, where the sweep only logs) -- the poll-snapshot exhaust
|
|
431
|
+
records it, and it is the one count the snapshot cannot derive from
|
|
432
|
+
the per-PR Decision list.
|
|
433
|
+
|
|
411
434
|
The predecessor of this sweep ran inside evaluate(), which is fed by
|
|
412
435
|
the review-requested:@me search -- and submitting a review CLEARS the
|
|
413
436
|
request, so a finished round's PR vanished from the search at exactly
|
|
@@ -435,7 +458,7 @@ class ReviewWatcher:
|
|
|
435
458
|
sessions = self.alissa.list_review_sessions()
|
|
436
459
|
except CommandError as exc:
|
|
437
460
|
log.warning("reap sweep skipped: could not list sessions: %s", exc)
|
|
438
|
-
return
|
|
461
|
+
return 0
|
|
439
462
|
|
|
440
463
|
# Per-sweep memos. The PR fetch is keyed per distinct PR; the round
|
|
441
464
|
# count additionally keys on the task ref, because two spawns of one
|
|
@@ -443,6 +466,7 @@ class ReviewWatcher:
|
|
|
443
466
|
# task existed carries None). None = undecidable this pass.
|
|
444
467
|
prs: dict[tuple[str, int], PullRequest | None] = {}
|
|
445
468
|
completed_cache: dict[tuple[str, int, str | None], float | None] = {}
|
|
469
|
+
reaped = 0
|
|
446
470
|
|
|
447
471
|
for ses in sessions:
|
|
448
472
|
if not ses.is_idle:
|
|
@@ -486,10 +510,12 @@ class ReviewWatcher:
|
|
|
486
510
|
# The live list is the authority; gating on the reaps table would
|
|
487
511
|
# spare any session killed behind the ledger's back.
|
|
488
512
|
self.state.record_reap(ses.name)
|
|
513
|
+
reaped += 1
|
|
489
514
|
log.info(
|
|
490
515
|
"reaped finished reviewer session %s (round %d done)",
|
|
491
516
|
ses.name, row["round"],
|
|
492
517
|
)
|
|
518
|
+
return reaped
|
|
493
519
|
|
|
494
520
|
def _sweep_pr(self, repo_slug: str, number: int) -> PullRequest | None:
|
|
495
521
|
"""One PR fetch for the sweep; the caller memoizes per distinct PR.
|
|
@@ -596,6 +622,9 @@ class ReviewWatcher:
|
|
|
596
622
|
Action.SPAWNED,
|
|
597
623
|
f"session {name} → {task.ref if task else 'no task'}",
|
|
598
624
|
round_,
|
|
625
|
+
session=name,
|
|
626
|
+
task_ref=task.ref if task else None,
|
|
627
|
+
reenqueued=reenqueued,
|
|
599
628
|
)
|
|
600
629
|
|
|
601
630
|
def _ensure_hub(self, pr: PullRequest) -> tuple[Path, str | None]:
|
|
@@ -778,7 +807,8 @@ class ReviewWatcher:
|
|
|
778
807
|
# needs the slot a finished session is squatting on. Deliberately not
|
|
779
808
|
# inside the per-request loop below — the sweep must reach sessions
|
|
780
809
|
# whose PR no longer appears in the search at all.
|
|
781
|
-
|
|
810
|
+
started = time.monotonic()
|
|
811
|
+
reaped = self.sweep_sessions()
|
|
782
812
|
|
|
783
813
|
requests = self.github.review_requests(self.config.repos)
|
|
784
814
|
log.info("%d PR(s) with a review pending from %s", len(requests), self.github.login)
|
|
@@ -799,8 +829,89 @@ class ReviewWatcher:
|
|
|
799
829
|
level = logging.INFO if decision.action != Action.SKIPPED else logging.DEBUG
|
|
800
830
|
log.log(level, "%s → %s (%s)", slug, decision.action.value, decision.reason)
|
|
801
831
|
results.append((slug, decision))
|
|
832
|
+
|
|
833
|
+
# Persist one poll_snapshots row per pass, built entirely from the
|
|
834
|
+
# Decision list already in hand plus the reap count -- no new GitHub
|
|
835
|
+
# calls. Written in dry-run too: a snapshot OBSERVES the pass, it is
|
|
836
|
+
# not a side effect the daemon takes, so a future console sees dry-run
|
|
837
|
+
# passes as well as live ones.
|
|
838
|
+
self._write_snapshot(
|
|
839
|
+
results, reaped, duration_ms=int((time.monotonic() - started) * 1000)
|
|
840
|
+
)
|
|
802
841
|
return results
|
|
803
842
|
|
|
843
|
+
def _stage_record(self, slug: str, decision: Decision) -> dict:
|
|
844
|
+
"""One per-item entry of a poll snapshot's compact JSON: the PR
|
|
845
|
+
reference (the slug and the number parsed from it), the current stage
|
|
846
|
+
(the decision's action, refined to name the stale-re-enqueue and
|
|
847
|
+
liveness-deferral buckets the bare action folds together), and the
|
|
848
|
+
round, session name, and origin task ref carried on the Decision.
|
|
849
|
+
`attempt` is carried as a fixed None for schema parity with the
|
|
850
|
+
devloop's per-item record -- the reviewloop is round-based and has no
|
|
851
|
+
attempt dimension -- so one console can read both loops' snapshots."""
|
|
852
|
+
_, _, tail = slug.partition("#")
|
|
853
|
+
stage = decision.action.value
|
|
854
|
+
if decision.reenqueued:
|
|
855
|
+
stage = "stale-re-enqueued"
|
|
856
|
+
elif decision.deferred:
|
|
857
|
+
stage = "deferred"
|
|
858
|
+
return {
|
|
859
|
+
"slug": slug,
|
|
860
|
+
"number": int(tail),
|
|
861
|
+
"round": decision.round,
|
|
862
|
+
"attempt": None,
|
|
863
|
+
"session": decision.session,
|
|
864
|
+
"stage": stage,
|
|
865
|
+
"reason": decision.reason,
|
|
866
|
+
"task_ref": decision.task_ref,
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
def _write_snapshot(
|
|
870
|
+
self,
|
|
871
|
+
results: list[tuple[str, Decision]],
|
|
872
|
+
reaped: int,
|
|
873
|
+
*,
|
|
874
|
+
duration_ms: int,
|
|
875
|
+
) -> None:
|
|
876
|
+
"""Persist one poll_snapshots row from the pass's Decision list and the
|
|
877
|
+
reaper count -- no new GitHub calls. The SPAWNED and IN_FLIGHT buckets
|
|
878
|
+
each split in two off observational Decision flags: a SPAWNED that
|
|
879
|
+
respawned a presumed-dead round is `stale_reenqueued`, and an
|
|
880
|
+
IN_FLIGHT that is a liveness deferral (not a freshly-enqueued round) is
|
|
881
|
+
`deferred`."""
|
|
882
|
+
stages = [self._stage_record(slug, d) for slug, d in results]
|
|
883
|
+
counts = Counter(d.action for _, d in results)
|
|
884
|
+
spawned = sum(
|
|
885
|
+
1 for _, d in results
|
|
886
|
+
if d.action is Action.SPAWNED and not d.reenqueued
|
|
887
|
+
)
|
|
888
|
+
stale_reenqueued = sum(
|
|
889
|
+
1 for _, d in results
|
|
890
|
+
if d.action is Action.SPAWNED and d.reenqueued
|
|
891
|
+
)
|
|
892
|
+
in_flight = sum(
|
|
893
|
+
1 for _, d in results
|
|
894
|
+
if d.action is Action.IN_FLIGHT and not d.deferred
|
|
895
|
+
)
|
|
896
|
+
deferred = sum(
|
|
897
|
+
1 for _, d in results
|
|
898
|
+
if d.action is Action.IN_FLIGHT and d.deferred
|
|
899
|
+
)
|
|
900
|
+
self.state.record_snapshot(
|
|
901
|
+
duration_ms=duration_ms,
|
|
902
|
+
candidates=len(results),
|
|
903
|
+
spawned=spawned,
|
|
904
|
+
stale_reenqueued=stale_reenqueued,
|
|
905
|
+
in_flight=in_flight,
|
|
906
|
+
deferred=deferred,
|
|
907
|
+
converged=counts[Action.CONVERGED],
|
|
908
|
+
capped=counts[Action.CAPPED],
|
|
909
|
+
escalated=counts[Action.ESCALATED],
|
|
910
|
+
skipped=counts[Action.SKIPPED],
|
|
911
|
+
reaped=reaped,
|
|
912
|
+
stages=stages,
|
|
913
|
+
)
|
|
914
|
+
|
|
804
915
|
def run_forever(self) -> None:
|
|
805
916
|
# preflight() is the caller's responsibility -- the CLI runs it once for
|
|
806
917
|
# every mode, so calling it here too would double every check.
|