alissa-tools-github-reviewloop 0.3.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.3.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.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/alissa.py +21 -0
  3. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/loop.py +62 -4
  4. {alissa_tools_github_reviewloop-0.3.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.3.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.3.0/src/main/alissa/tools/github/reviewloop/version +0 -1
  8. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/MANIFEST.in +0 -0
  9. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/README.md +0 -0
  10. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/requirements.txt +0 -0
  11. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/setup.cfg +0 -0
  12. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/setup.py +0 -0
  13. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  14. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  15. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  16. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
  17. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  18. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
  19. {alissa_tools_github_reviewloop-0.3.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  20. {alissa_tools_github_reviewloop-0.3.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.3.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.3.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.3.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.3.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:
@@ -29,14 +29,40 @@ log = logging.getLogger(__name__)
29
29
  # (skill failure mode: "reviewer session stalls"). The round is re-enqueued.
30
30
  STALE_ROUND_SECONDS = 90 * 60
31
31
 
32
+ # The closing contract is spelled out in both directives (not just the skill)
33
+ # because it is the reviewer's most-skipped step: on re-review, sessions produce
34
+ # findings but never register the review on the PR, or stop without a verdict.
35
+ _CLOSE_THE_ROUND = (
36
+ "CLOSE THE ROUND — both are mandatory or the round does not count: "
37
+ "(1) SUBMIT your review so it lands as one registered review record ON the "
38
+ "PR (gh pr review / the reviews API) and confirm it with "
39
+ "`gh api repos/<org>/<repo>/pulls/<n>/reviews` — findings left only in your "
40
+ "session do not exist; (2) end with a decisive verdict — approve OR "
41
+ "request_changes, never neither, never comment-only. You are read-only: "
42
+ "never commit or fix, even a one-character typo — a needed fix IS "
43
+ "request_changes. "
44
+ )
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
+
32
56
  ROUND_1_DIRECTIVE = (
33
57
  "You are a PR REVIEWER, not an implementer. {assignment} "
34
58
  "Load the alissa-code-review skill and follow procedures/review-a-pr.md: "
35
59
  "hydrate the task and the PR it names, review per the rubric, post "
36
60
  "severity-tagged comments via gh pr review, record the verdict evidence, "
37
61
  "move the task to pending_validation. "
62
+ + _CLOSE_THE_ROUND +
38
63
  "NEVER push commits, merge, or change PR state. "
39
- "Do NOT create further ali-* sessions."
64
+ "Do NOT create further ali-* sessions. "
65
+ + _RELEASE_SLOT
40
66
  )
41
67
 
42
68
  ROUND_K_DIRECTIVE = (
@@ -46,8 +72,10 @@ ROUND_K_DIRECTIVE = (
46
72
  "including its round-k section: verify the triage of every prior finding, "
47
73
  "verify the fixes, sweep the new diff with the full rubric, record a "
48
74
  "round-{round} verdict envelope, move the task to pending_validation. "
75
+ + _CLOSE_THE_ROUND +
49
76
  "NEVER push commits, merge, or change PR state. "
50
- "Do NOT create further ali-* sessions."
77
+ "Do NOT create further ali-* sessions. "
78
+ + _RELEASE_SLOT
51
79
  )
52
80
 
53
81
  ESCALATION_COMMENT = (
@@ -117,6 +145,11 @@ class ReviewWatcher:
117
145
  my_reviews = self.github.my_reviews(owner, repo, number)
118
146
  completed = len(my_reviews)
119
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
+
120
153
  # Looked up here rather than inside _spawn: convergence needs the ref
121
154
  # too. _spawn still handles `task is None` exactly as before.
122
155
  task = self.alissa.find_review_task(owner, repo, number)
@@ -173,6 +206,31 @@ class ReviewWatcher:
173
206
 
174
207
  return None
175
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
+
176
234
  # -- actions -----------------------------------------------------------
177
235
 
178
236
  def _spawn(self, pr: PullRequest, round_: int, task: Task | None) -> Decision:
@@ -194,16 +252,16 @@ class ReviewWatcher:
194
252
  else:
195
253
  assignment = f"You've been assigned Alissa review task {task.ref}."
196
254
 
255
+ name = session_name(pr, round_)
197
256
  template = ROUND_1_DIRECTIVE if round_ == 1 else ROUND_K_DIRECTIVE
198
257
  directive = template.format(
199
- assignment=assignment, round=round_, cap=self.config.round_cap
258
+ assignment=assignment, round=round_, cap=self.config.round_cap, session=name
200
259
  )
201
260
 
202
261
  hub, problem = self._ensure_hub(pr)
203
262
  if problem is not None:
204
263
  return Decision(Action.SKIPPED, problem, round_)
205
264
 
206
- name = session_name(pr, round_)
207
265
  self.alissa.enqueue_reviewer(
208
266
  session=name,
209
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.3.0
3
+ Version: 0.5.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera