alissa-tools-github-reviewloop 0.4.0__tar.gz → 0.5.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.4.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.5.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/alissa.py +21 -0
  3. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/loop.py +46 -4
  4. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/state.py +18 -0
  5. alissa_tools_github_reviewloop-0.5.0/src/main/alissa/tools/github/reviewloop/version +1 -0
  6. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
  7. alissa_tools_github_reviewloop-0.4.0/src/main/alissa/tools/github/reviewloop/version +0 -1
  8. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/MANIFEST.in +0 -0
  9. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/README.md +0 -0
  10. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/requirements.txt +0 -0
  11. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/setup.cfg +0 -0
  12. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/setup.py +0 -0
  13. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  14. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  15. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  16. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
  17. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  18. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
  19. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  20. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +0 -0
  21. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
  22. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +0 -0
  23. {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.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.4.0
3
+ Version: 0.5.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -185,6 +185,27 @@ class Alissa:
185
185
 
186
186
  run(argv, timeout=60)
187
187
 
188
+ # Reviewers are one-shot per round (CR3): once the session finishes and is
189
+ # reaped, it must never be respawned. Make that explicit so a self-kill or
190
+ # a daemon reap can't trigger a respawn loop. Best-effort — an older CLI
191
+ # without `queue set` should not fail the enqueue.
192
+ try:
193
+ run(["alissa", "tmux", "queue", "set", session, "respawn", "off"],
194
+ timeout=30, check=False)
195
+ except CommandError: # pragma: no cover - defence in depth
196
+ log.warning("could not set respawn off for %s", session)
197
+
198
+ def kill_session(self, session: str, *, dry_run: bool = False) -> None:
199
+ """Kill a finished reviewer's managed session to free its worker slot.
200
+
201
+ Best-effort and idempotent-friendly: the session may already be gone (the
202
+ reviewer self-killed), so a non-zero exit is not an error here.
203
+ """
204
+ if dry_run:
205
+ log.info("[dry-run] would kill session %s", session)
206
+ return
207
+ run(["alissa", "tmux", "kill", session], timeout=30, check=False)
208
+
188
209
  def add_repo_to_workspace(
189
210
  self, owner: str, repo: str, workspace_root: Path, *, dry_run: bool = False
190
211
  ) -> None:
@@ -43,6 +43,16 @@ _CLOSE_THE_ROUND = (
43
43
  "request_changes. "
44
44
  )
45
45
 
46
+ # Reviewers are one-shot per round (CR3), so a finished session should not linger
47
+ # holding a worker slot. The daemon reaps it as a backstop, but the fast path is
48
+ # the reviewer releasing its own slot as its very last action. {session} is the
49
+ # reviewer's own managed session name, injected at spawn.
50
+ _RELEASE_SLOT = (
51
+ "FINALLY, and only once the round is fully closed above (review registered "
52
+ "AND verdict recorded), release your worker slot as your last action: run "
53
+ "`alissa tmux kill {session}`. Do nothing after it."
54
+ )
55
+
46
56
  ROUND_1_DIRECTIVE = (
47
57
  "You are a PR REVIEWER, not an implementer. {assignment} "
48
58
  "Load the alissa-code-review skill and follow procedures/review-a-pr.md: "
@@ -51,7 +61,8 @@ ROUND_1_DIRECTIVE = (
51
61
  "move the task to pending_validation. "
52
62
  + _CLOSE_THE_ROUND +
53
63
  "NEVER push commits, merge, or change PR state. "
54
- "Do NOT create further ali-* sessions."
64
+ "Do NOT create further ali-* sessions. "
65
+ + _RELEASE_SLOT
55
66
  )
56
67
 
57
68
  ROUND_K_DIRECTIVE = (
@@ -63,7 +74,8 @@ ROUND_K_DIRECTIVE = (
63
74
  "round-{round} verdict envelope, move the task to pending_validation. "
64
75
  + _CLOSE_THE_ROUND +
65
76
  "NEVER push commits, merge, or change PR state. "
66
- "Do NOT create further ali-* sessions."
77
+ "Do NOT create further ali-* sessions. "
78
+ + _RELEASE_SLOT
67
79
  )
68
80
 
69
81
  ESCALATION_COMMENT = (
@@ -133,6 +145,11 @@ class ReviewWatcher:
133
145
  my_reviews = self.github.my_reviews(owner, repo, number)
134
146
  completed = len(my_reviews)
135
147
 
148
+ # Backstop: reap the sessions of rounds that have already landed a review
149
+ # (the fast path is the reviewer self-killing, but it may forget). A round
150
+ # <= completed is done; its session, if we still have it, can be killed.
151
+ self._reap_finished(pr.full_name, number, completed)
152
+
136
153
  # Looked up here rather than inside _spawn: convergence needs the ref
137
154
  # too. _spawn still handles `task is None` exactly as before.
138
155
  task = self.alissa.find_review_task(owner, repo, number)
@@ -189,6 +206,31 @@ class ReviewWatcher:
189
206
 
190
207
  return None
191
208
 
209
+ def _reap_finished(self, repo: str, number: int, completed: int) -> None:
210
+ """Kill the managed session of every round whose review has landed.
211
+
212
+ A round `<= completed` is done (its review was submitted), so its session
213
+ is finished and should not hold a worker slot. The reviewer self-kills as
214
+ the fast path; this is the backstop for when it forgets. Idempotent — each
215
+ session is killed at most once (recorded in the ledger) — never runs in
216
+ dry-run, and best-effort: a reap failure (or an already-gone session) must
217
+ not take the poll down.
218
+ """
219
+ if self.config.dry_run or completed < 1:
220
+ return
221
+ for round_ in range(1, completed + 1):
222
+ row = self.state.get_spawn(repo, number, round_)
223
+ if row is None or self.state.is_reaped(row["session"]):
224
+ continue
225
+ session = row["session"]
226
+ try:
227
+ self.alissa.kill_session(session)
228
+ except Exception: # pragma: no cover - defence in depth
229
+ log.exception("failed to reap session %s", session)
230
+ continue
231
+ self.state.record_reap(session)
232
+ log.info("reaped finished reviewer session %s (round %d done)", session, round_)
233
+
192
234
  # -- actions -----------------------------------------------------------
193
235
 
194
236
  def _spawn(self, pr: PullRequest, round_: int, task: Task | None) -> Decision:
@@ -210,16 +252,16 @@ class ReviewWatcher:
210
252
  else:
211
253
  assignment = f"You've been assigned Alissa review task {task.ref}."
212
254
 
255
+ name = session_name(pr, round_)
213
256
  template = ROUND_1_DIRECTIVE if round_ == 1 else ROUND_K_DIRECTIVE
214
257
  directive = template.format(
215
- assignment=assignment, round=round_, cap=self.config.round_cap
258
+ assignment=assignment, round=round_, cap=self.config.round_cap, session=name
216
259
  )
217
260
 
218
261
  hub, problem = self._ensure_hub(pr)
219
262
  if problem is not None:
220
263
  return Decision(Action.SKIPPED, problem, round_)
221
264
 
222
- name = session_name(pr, round_)
223
265
  self.alissa.enqueue_reviewer(
224
266
  session=name,
225
267
  directive=directive,
@@ -31,6 +31,11 @@ CREATE TABLE IF NOT EXISTS escalations (
31
31
  escalated_at INTEGER NOT NULL,
32
32
  PRIMARY KEY (repo, number, head_sha)
33
33
  );
34
+
35
+ CREATE TABLE IF NOT EXISTS reaps (
36
+ session TEXT NOT NULL PRIMARY KEY,
37
+ reaped_at INTEGER NOT NULL
38
+ );
34
39
  """
35
40
 
36
41
 
@@ -81,6 +86,19 @@ class State:
81
86
  )
82
87
  self._db.commit()
83
88
 
89
+ def is_reaped(self, session: str) -> bool:
90
+ row = self._db.execute(
91
+ "SELECT 1 FROM reaps WHERE session=?", (session,)
92
+ ).fetchone()
93
+ return row is not None
94
+
95
+ def record_reap(self, session: str) -> None:
96
+ self._db.execute(
97
+ "INSERT OR REPLACE INTO reaps (session, reaped_at) VALUES (?,?)",
98
+ (session, int(time.time())),
99
+ )
100
+ self._db.commit()
101
+
84
102
  def escalated(self, repo: str, number: int, head_sha: str) -> bool:
85
103
  row = self._db.execute(
86
104
  "SELECT 1 FROM escalations WHERE repo=? AND number=? AND head_sha=?",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera