alissa-tools-github-reviewloop 0.9.0__tar.gz → 0.10.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.
Files changed (23) hide show
  1. {alissa_tools_github_reviewloop-0.9.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.10.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +40 -0
  3. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/loop.py +92 -3
  4. alissa_tools_github_reviewloop-0.10.0/src/main/alissa/tools/github/reviewloop/version +1 -0
  5. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_reviewloop-0.9.0/src/main/alissa/tools/github/reviewloop/version +0 -1
  7. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/MANIFEST.in +0 -0
  8. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/README.md +0 -0
  9. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/requirements.txt +0 -0
  10. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/setup.cfg +0 -0
  11. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/setup.py +0 -0
  12. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  13. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  14. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/alissa.py +0 -0
  15. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  16. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  17. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
  18. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/state.py +0 -0
  19. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  20. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +0 -0
  21. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
  22. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +0 -0
  23. {alissa_tools_github_reviewloop-0.9.0 → alissa_tools_github_reviewloop-0.10.0}/src/main/alissa_tools_github_reviewloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -46,6 +46,17 @@ class PullRequest:
46
46
  return self.merged or self.state != "open"
47
47
 
48
48
 
49
+ @dataclass(frozen=True)
50
+ class IssueComment:
51
+ """An issue comment on a PR (a PR is an issue). Distinct from Review:
52
+ issue comments live on the issues endpoints and never create review
53
+ records, so posting one can never disturb round counting."""
54
+
55
+ id: int
56
+ author: str
57
+ body: str
58
+
59
+
49
60
  @dataclass(frozen=True)
50
61
  class Review:
51
62
  author: str
@@ -221,3 +232,32 @@ class GitHub:
221
232
  f"body={body}",
222
233
  ]
223
234
  )
235
+
236
+ def issue_comments(self, owner: str, repo: str, number: int) -> list[IssueComment]:
237
+ data = (
238
+ self._api(
239
+ "-X",
240
+ "GET",
241
+ f"repos/{owner}/{repo}/issues/{number}/comments",
242
+ "-f",
243
+ "per_page=100",
244
+ )
245
+ or []
246
+ )
247
+ return [
248
+ IssueComment(
249
+ id=int(c.get("id") or 0),
250
+ author=(c.get("user") or {}).get("login", ""),
251
+ body=c.get("body") or "",
252
+ )
253
+ for c in data
254
+ ]
255
+
256
+ def update_comment(self, owner: str, repo: str, comment_id: int, body: str) -> None:
257
+ self._api(
258
+ "-X",
259
+ "PATCH",
260
+ f"repos/{owner}/{repo}/issues/comments/{comment_id}",
261
+ "-f",
262
+ f"body={body}",
263
+ )
@@ -123,12 +123,38 @@ STALLED_COMMENT = (
123
123
  "finish the round by hand."
124
124
  )
125
125
 
126
+ # The hidden marker that identifies THE activity comment on a PR. Find-or-create
127
+ # keys on it (plus own authorship -- anyone can paste the marker into their own
128
+ # comment, and a spoofed marker must never be PATCHed), so every append lands in
129
+ # the same single comment however many rounds run.
130
+ ACTIVITY_MARKER = "<!-- alissa-reviewloop:activity -->"
131
+
132
+ ACTIVITY_HEADER = (
133
+ ACTIVITY_MARKER + "\n"
134
+ "**Review-loop activity** — mechanical spawn/round log; the daemon appends "
135
+ "a line each time it queues (or defers) a reviewer round on this PR."
136
+ )
137
+
126
138
  # The ping-ledger kind prefix for the stalled-deferral operator ping. Unlike
127
139
  # the cap-out escalation (terminal per head), a stall can recur, so the kind
128
140
  # is narrowed per episode -- see stalled_kind.
129
141
  ESCALATION_STALLED = "stalled"
130
142
 
131
143
 
144
+ def deferral_activity_kind(session: str) -> str:
145
+ """The ping-ledger kind that dedupes ONE deferral episode's activity line.
146
+
147
+ A deferral is re-decided every poll, so appending per decision would grow
148
+ the activity comment by one identical line a minute for as long as the
149
+ session holds out. One line per episode carries the same information --
150
+ "the daemon is deferring behind this session" -- and the session name is
151
+ the episode identity (nonce-unique per spawn), exactly as stalled_kind
152
+ reasons for the operator ping. Recorded only after the append lands, so a
153
+ transient failure retries next poll.
154
+ """
155
+ return f"activity-deferred:{session}"
156
+
157
+
132
158
  def stalled_kind(session: str) -> str:
133
159
  """The ping-ledger kind that dedupes ONE deferral episode's operator ping.
134
160
 
@@ -252,7 +278,7 @@ class ReviewWatcher:
252
278
  age / 60,
253
279
  )
254
280
 
255
- return self._spawn(pr, round_, task)
281
+ return self._spawn(pr, round_, task, reenqueued=age is not None)
256
282
 
257
283
  def _defer_stale_round(self, pr: PullRequest, round_: int, age: float) -> Decision | None:
258
284
  """The liveness signal under the stale timer: a deferral, or None to
@@ -310,10 +336,24 @@ class ReviewWatcher:
310
336
  and not self.state.pinged(pr.full_name, pr.number, stalled_kind(session))
311
337
  ):
312
338
  self._escalate_stalled(pr, round_, session, age)
339
+
340
+ life = "active" if ses.is_idle else "busy"
341
+ if not self.state.pinged(pr.full_name, pr.number, deferral_activity_kind(session)):
342
+ appended = self._append_activity(
343
+ pr,
344
+ self._activity_line(
345
+ session, round_, f"deferred — session `{session}` still {life}"
346
+ ),
347
+ )
348
+ if appended:
349
+ self.state.record_ping(
350
+ pr.full_name, pr.number, deferral_activity_kind(session)
351
+ )
352
+
313
353
  return Decision(
314
354
  Action.IN_FLIGHT,
315
355
  f"round {round_} is stale ({int(age / 60)} min) but session "
316
- f"{session} is still {'active' if ses.is_idle else 'busy'} — not "
356
+ f"{session} is still {life} — not "
317
357
  f"respawning over a live reviewer",
318
358
  round_,
319
359
  )
@@ -495,7 +535,9 @@ class ReviewWatcher:
495
535
 
496
536
  # -- actions -----------------------------------------------------------
497
537
 
498
- def _spawn(self, pr: PullRequest, round_: int, task: Task | None) -> Decision:
538
+ def _spawn(
539
+ self, pr: PullRequest, round_: int, task: Task | None, *, reenqueued: bool = False
540
+ ) -> Decision:
499
541
  if task is None:
500
542
  if self.config.on_missing_review_task == ON_MISSING_SKIP:
501
543
  return Decision(
@@ -543,6 +585,13 @@ class ReviewWatcher:
543
585
  task_ref=task.ref if task else None,
544
586
  )
545
587
 
588
+ # AFTER the enqueue on purpose: the activity comment is telemetry and
589
+ # must never gate the spawn it reports on.
590
+ context = (
591
+ "re-enqueued — previous session presumed dead" if reenqueued else "spawned"
592
+ )
593
+ self._append_activity(pr, self._activity_line(name, round_, context))
594
+
546
595
  return Decision(
547
596
  Action.SPAWNED,
548
597
  f"session {name} → {task.ref if task else 'no task'}",
@@ -621,6 +670,46 @@ class ReviewWatcher:
621
670
 
622
671
  return warnings
623
672
 
673
+ def _activity_line(self, session: str, round_: int, context: str) -> str:
674
+ ts = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
675
+ return f"- {ts} — `{session}` — round {round_} of {self.config.round_cap} — {context}"
676
+
677
+ def _append_activity(self, pr: PullRequest, line: str) -> bool:
678
+ """Append one line to THE activity comment on the PR; True if it landed.
679
+
680
+ Find-or-create: the PR's issue comments are filtered to OWN authorship
681
+ AND the hidden marker, and the line is PATCH-appended to the first
682
+ match; with no match, one marker-carrying comment is created. A marker
683
+ pasted by anyone else fails the author filter and is never touched.
684
+
685
+ Best-effort by contract: this is telemetry about a spawn that has
686
+ already happened, so no failure here may surface to the caller. The
687
+ except is deliberately broad -- _api turns rate-limit errors into
688
+ RateLimited (not CommandError), and letting that fly out of evaluate()
689
+ would fail the whole poll pass over a log line.
690
+ """
691
+ if self.config.dry_run:
692
+ log.info("[dry-run] would append activity line on %s: %s", pr.slug, line)
693
+ return False
694
+ try:
695
+ mine = [
696
+ c
697
+ for c in self.github.issue_comments(pr.owner, pr.repo, pr.number)
698
+ if c.author == self.github.login and ACTIVITY_MARKER in c.body
699
+ ]
700
+ if mine:
701
+ self.github.update_comment(
702
+ pr.owner, pr.repo, mine[0].id, mine[0].body + "\n" + line
703
+ )
704
+ else:
705
+ self.github.comment(
706
+ pr.owner, pr.repo, pr.number, ACTIVITY_HEADER + "\n" + line
707
+ )
708
+ except Exception as exc:
709
+ log.warning("could not append activity line on %s: %s", pr.slug, exc)
710
+ return False
711
+ return True
712
+
624
713
  def _escalate_stalled(
625
714
  self, pr: PullRequest, round_: int, session: str, age: float
626
715
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera