alissa-tools-github-reviewloop 0.2.0__tar.gz → 0.4.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.2.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.4.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/setup.py +1 -0
  3. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/loop.py +16 -0
  4. alissa_tools_github_reviewloop-0.4.0/src/main/alissa/tools/github/reviewloop/prreview.py +228 -0
  5. alissa_tools_github_reviewloop-0.4.0/src/main/alissa/tools/github/reviewloop/version +1 -0
  6. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
  7. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +1 -0
  8. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +1 -0
  9. alissa_tools_github_reviewloop-0.2.0/src/main/alissa/tools/github/reviewloop/version +0 -1
  10. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/MANIFEST.in +0 -0
  11. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/README.md +0 -0
  12. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/requirements.txt +0 -0
  13. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/setup.cfg +0 -0
  14. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  15. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  16. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/alissa.py +0 -0
  17. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  18. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
  19. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  20. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/state.py +0 -0
  21. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  22. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
  23. {alissa_tools_github_reviewloop-0.2.0 → alissa_tools_github_reviewloop-0.4.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.2.0
3
+ Version: 0.4.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -49,6 +49,7 @@ setup(
49
49
  entry_points={
50
50
  "console_scripts": [
51
51
  "alissa-reviewloop=alissa.tools.github.reviewloop.__main__:main",
52
+ "alissa-pr-review=alissa.tools.github.reviewloop.prreview:main",
52
53
  ]
53
54
  },
54
55
  install_requires=requirements,
@@ -29,12 +29,27 @@ 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
+
32
46
  ROUND_1_DIRECTIVE = (
33
47
  "You are a PR REVIEWER, not an implementer. {assignment} "
34
48
  "Load the alissa-code-review skill and follow procedures/review-a-pr.md: "
35
49
  "hydrate the task and the PR it names, review per the rubric, post "
36
50
  "severity-tagged comments via gh pr review, record the verdict evidence, "
37
51
  "move the task to pending_validation. "
52
+ + _CLOSE_THE_ROUND +
38
53
  "NEVER push commits, merge, or change PR state. "
39
54
  "Do NOT create further ali-* sessions."
40
55
  )
@@ -46,6 +61,7 @@ ROUND_K_DIRECTIVE = (
46
61
  "including its round-k section: verify the triage of every prior finding, "
47
62
  "verify the fixes, sweep the new diff with the full rubric, record a "
48
63
  "round-{round} verdict envelope, move the task to pending_validation. "
64
+ + _CLOSE_THE_ROUND +
49
65
  "NEVER push commits, merge, or change PR state. "
50
66
  "Do NOT create further ali-* sessions."
51
67
  )
@@ -0,0 +1,228 @@
1
+ """alissa-pr-review: the implementer-side driver for ONE review round.
2
+
3
+ The reviewer daemon (`alissa-reviewloop`) is autonomous but only the *reviewer*
4
+ half: a review request in, a review out. This command is the *implementer* half,
5
+ run by the dev session that just finished the work — it fires the trigger and
6
+ blocks on the verdict, so the loop closes without a second always-on daemon.
7
+
8
+ One invocation = one round:
9
+
10
+ 1. resolve the PR from the branch
11
+ 2. flip it ready-for-review (from draft)
12
+ 3. request the reviewer ← this is the daemon's edge-trigger
13
+ 4. block until a NEW review round lands, or the timeout
14
+
15
+ It reads the verdict from the **review task envelope**, never from GitHub's
16
+ review state: reviewers work in comment mode, so the GitHub state is always
17
+ COMMENTED and cannot express approval. The round-completion signal is the
18
+ reviewer's substantive-review count going up; the verdict word then comes from
19
+ `Alissa.latest_verdict` — the exact logic the daemon uses, so the two halves
20
+ cannot disagree.
21
+
22
+ The triage -> fix -> re-enter loop and the round cap live in the caller (see the
23
+ `run-the-review-loop` how-to): on `request_changes` the dev triages, fixes, and
24
+ runs this again for round k+1.
25
+
26
+ Exit codes: 0 approve · 1 request_changes · 2 timeout/no verdict · 3 usage/setup
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import logging
33
+ import re
34
+ import sys
35
+ import time
36
+
37
+ from .alissa import Alissa, VERDICT_APPROVE, VERDICT_REQUEST_CHANGES
38
+ from .ghclient import GitHub
39
+ from .proc import CommandError, run, run_json
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+ # After a new review lands, the verdict envelope is written on the task around
44
+ # the same moment; give it a short grace window to appear before giving up.
45
+ _ENVELOPE_GRACE_SECONDS = 120
46
+
47
+
48
+ def resolve_pr(branch: str | None) -> tuple[str, str, int, str, bool]:
49
+ """(owner, repo, number, url, is_draft) for `branch` (or the current branch).
50
+
51
+ Runs in the repo working tree, so `gh` infers the repo; the head branch
52
+ selects the PR. owner/repo come from the PR URL, which is the base repo.
53
+ """
54
+ argv = ["gh", "pr", "view"]
55
+ if branch:
56
+ argv.append(branch)
57
+ argv += ["--json", "url,number,isDraft,state"]
58
+ data = run_json(argv)
59
+ if not data:
60
+ raise ValueError(
61
+ f"no pull request found for {'branch ' + branch if branch else 'the current branch'}"
62
+ )
63
+ url = data.get("url", "")
64
+ match = re.search(r"github\.com/([\w.-]+)/([\w.-]+)/pull/(\d+)", url)
65
+ if not match:
66
+ raise ValueError(f"could not parse owner/repo from PR url {url!r}")
67
+ if data.get("state") not in (None, "OPEN"):
68
+ raise ValueError(f"PR {url} is {data.get('state')}, not open — nothing to review")
69
+ return match.group(1), match.group(2), int(match.group(3)), url, bool(data.get("isDraft"))
70
+
71
+
72
+ def _review_task_ref(alissa: Alissa, owner: str, repo: str, number: int) -> str | None:
73
+ """The CR2 review task for this PR, by title, regardless of status.
74
+
75
+ Deliberately not `Alissa.find_review_task` (which filters to open tasks): we
76
+ still want the ref just after the task is validated, to read its verdict.
77
+ """
78
+ pattern = re.compile(
79
+ rf"^Review PR\s+{re.escape(owner)}/{re.escape(repo)}#{number}\b", re.IGNORECASE
80
+ )
81
+ matches = [t for t in alissa.list_tasks() if pattern.match(t.title)]
82
+ return matches[0].ref if matches else None
83
+
84
+
85
+ def _reviewer_review_count(gh: GitHub, owner: str, repo: str, number: int, reviewer: str) -> int:
86
+ """Substantive reviews the reviewer has submitted — one per completed round.
87
+
88
+ Same 'substantive' rule the daemon counts by (empty-bodied inline-comment
89
+ artifacts don't close a round), so a fresh round is a clean +1 edge.
90
+ """
91
+ return sum(
92
+ 1
93
+ for r in gh.reviews(owner, repo, number)
94
+ if r.author == reviewer and r.is_substantive
95
+ )
96
+
97
+
98
+ def build_parser() -> argparse.ArgumentParser:
99
+ p = argparse.ArgumentParser(
100
+ prog="alissa-pr-review",
101
+ description="Implementer-side: flip a PR ready, request a reviewer, and "
102
+ "block until the review verdict lands (one round of the adversarial loop).",
103
+ )
104
+ p.add_argument(
105
+ "--reviewer",
106
+ required=True,
107
+ metavar="LOGIN",
108
+ help="GitHub login to request review from (e.g. alissa-app)",
109
+ )
110
+ p.add_argument(
111
+ "--branch",
112
+ metavar="NAME",
113
+ help="head branch of the PR (default: the current branch)",
114
+ )
115
+ p.add_argument(
116
+ "--timeout",
117
+ type=int,
118
+ default=2700,
119
+ metavar="SECONDS",
120
+ help="give up waiting after this long (default: 2700 = 45 min)",
121
+ )
122
+ p.add_argument(
123
+ "--poll-interval",
124
+ type=int,
125
+ default=30,
126
+ metavar="SECONDS",
127
+ help="seconds between checks (default: 30)",
128
+ )
129
+ p.add_argument("-v", "--verbose", action="store_true")
130
+ return p
131
+
132
+
133
+ def main(argv: list[str] | None = None) -> int:
134
+ args = build_parser().parse_args(argv)
135
+ logging.basicConfig(
136
+ level=logging.DEBUG if args.verbose else logging.INFO,
137
+ format="%(asctime)s %(levelname)-7s %(message)s",
138
+ datefmt="%H:%M:%S",
139
+ )
140
+
141
+ gh = GitHub()
142
+ alissa = Alissa()
143
+
144
+ try:
145
+ # Identity guard: GitHub forbids requesting review from the PR author, so
146
+ # the dev's gh token must not be the reviewer.
147
+ dev = gh.token_login()
148
+ if dev == args.reviewer:
149
+ print(
150
+ f"identity error: the gh token belongs to {dev!r}, the same as "
151
+ f"--reviewer. GitHub forbids requesting review from the PR author; "
152
+ f"run this as a different account than the reviewer.",
153
+ file=sys.stderr,
154
+ )
155
+ return 3
156
+
157
+ owner, repo, number, url, is_draft = resolve_pr(args.branch)
158
+ slug = f"{owner}/{repo}#{number}"
159
+ log.info("PR %s (%s), reviewing as reviewer=%s, dev=%s", slug, url, args.reviewer, dev)
160
+
161
+ if is_draft:
162
+ log.info("flipping %s ready-for-review", slug)
163
+ run(["gh", "pr", "ready", str(number)])
164
+
165
+ before = _reviewer_review_count(gh, owner, repo, number, args.reviewer)
166
+ log.info("requesting review from %s (round %d)", args.reviewer, before + 1)
167
+ run(["gh", "pr", "edit", str(number), "--add-reviewer", args.reviewer])
168
+
169
+ deadline = time.time() + args.timeout
170
+ ref: str | None = None
171
+ round_landed_at: float | None = None
172
+
173
+ while time.time() < deadline:
174
+ time.sleep(args.poll_interval)
175
+
176
+ if round_landed_at is None:
177
+ now = _reviewer_review_count(gh, owner, repo, number, args.reviewer)
178
+ if now > before:
179
+ round_landed_at = time.time()
180
+ log.info("%s: a new review landed — reading the verdict", slug)
181
+ else:
182
+ log.debug("%s: still waiting (reviews=%d)", slug, now)
183
+ continue
184
+
185
+ ref = ref or _review_task_ref(alissa, owner, repo, number)
186
+ verdict = alissa.latest_verdict(ref) if ref else None
187
+ if verdict == VERDICT_APPROVE:
188
+ print(f"\n✔ APPROVE — {slug} converged.\n PR: {url}\n task: {ref}")
189
+ return 0
190
+ if verdict == VERDICT_REQUEST_CHANGES:
191
+ print(
192
+ f"\n✗ REQUEST_CHANGES — {slug}.\n PR: {url}\n task: {ref}\n"
193
+ f" Triage each finding on its PR thread, fix, then run this again "
194
+ f"for the next round."
195
+ )
196
+ return 1
197
+
198
+ # Review landed but the envelope isn't readable yet — brief grace.
199
+ if round_landed_at and time.time() - round_landed_at > _ENVELOPE_GRACE_SECONDS:
200
+ print(
201
+ f"\n⚠ a review landed on {slug} but no verdict envelope was found "
202
+ f"on the review task {ref or '(not found)'} within "
203
+ f"{_ENVELOPE_GRACE_SECONDS}s.\n PR: {url}\n Read the PR review "
204
+ f"and the review task directly.",
205
+ file=sys.stderr,
206
+ )
207
+ return 2
208
+
209
+ print(
210
+ f"\n⏱ timed out after {args.timeout}s waiting for a review on {slug} — no "
211
+ f"verdict yet.\n PR: {url}\n The reviewer daemon re-spawns a stalled "
212
+ f"reviewer at ~90 min; re-run this, or check the worker.",
213
+ file=sys.stderr,
214
+ )
215
+ return 2
216
+
217
+ except CommandError as exc:
218
+ print(f"error: {exc}", file=sys.stderr)
219
+ return 3
220
+ except ValueError as exc:
221
+ print(f"error: {exc}", file=sys.stderr)
222
+ return 3
223
+ except KeyboardInterrupt:
224
+ return 130
225
+
226
+
227
+ if __name__ == "__main__":
228
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -9,6 +9,7 @@ src/main/alissa/tools/github/reviewloop/config.py
9
9
  src/main/alissa/tools/github/reviewloop/ghclient.py
10
10
  src/main/alissa/tools/github/reviewloop/loop.py
11
11
  src/main/alissa/tools/github/reviewloop/proc.py
12
+ src/main/alissa/tools/github/reviewloop/prreview.py
12
13
  src/main/alissa/tools/github/reviewloop/state.py
13
14
  src/main/alissa/tools/github/reviewloop/version
14
15
  src/main/alissa/tools/github/reviewloop/version.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ alissa-pr-review = alissa.tools.github.reviewloop.prreview:main
2
3
  alissa-reviewloop = alissa.tools.github.reviewloop.__main__:main