alissa-tools-github-reviewloop 0.1.1__tar.gz → 0.3.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.1.1/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.3.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/setup.py +1 -0
  3. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/alissa.py +80 -0
  4. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +31 -2
  5. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/loop.py +40 -9
  6. alissa_tools_github_reviewloop-0.3.0/src/main/alissa/tools/github/reviewloop/prreview.py +228 -0
  7. alissa_tools_github_reviewloop-0.3.0/src/main/alissa/tools/github/reviewloop/version +1 -0
  8. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
  9. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +1 -0
  10. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +1 -0
  11. alissa_tools_github_reviewloop-0.1.1/src/main/alissa/tools/github/reviewloop/version +0 -1
  12. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/MANIFEST.in +0 -0
  13. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/README.md +0 -0
  14. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/requirements.txt +0 -0
  15. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/setup.cfg +0 -0
  16. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  17. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  18. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  19. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  20. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/state.py +0 -0
  21. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  22. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
  23. {alissa_tools_github_reviewloop-0.1.1 → alissa_tools_github_reviewloop-0.3.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.1.1
3
+ Version: 0.3.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,
@@ -15,6 +15,23 @@ log = logging.getLogger(__name__)
15
15
  # A review task is "open" while it can still receive a verdict.
16
16
  OPEN_STATUSES = {"committed", "in_progress", "pending_validation", "todo"}
17
17
 
18
+ # CR6 verdict envelope outcomes.
19
+ VERDICT_APPROVE = "approve"
20
+ VERDICT_REQUEST_CHANGES = "request_changes"
21
+
22
+ # Envelope titles and bodies both read:
23
+ # Review verdict: <org>/<repo>#<n> — request_changes (round 3, ...)
24
+ # # Review verdict: <org>/<repo>#<n> — approve
25
+ # The separator is an em-dash in practice; en-dash and hyphen are accepted too
26
+ # so a hand-written envelope does not silently fail to parse.
27
+ # Kept to a single line on purpose: the verdict word sits on the same line as
28
+ # the "Review verdict:" lead-in, so matching across newlines could only pick up
29
+ # a later round's wording out of order.
30
+ _VERDICT_RE = re.compile(
31
+ r"Review\s+verdict\s*:[^\n]*?[—–-]\s*(approve|request_changes)\b",
32
+ re.IGNORECASE,
33
+ )
34
+
18
35
 
19
36
  @dataclass(frozen=True)
20
37
  class Task:
@@ -74,6 +91,69 @@ class Alissa:
74
91
  )
75
92
  return matches[0]
76
93
 
94
+ def latest_verdict(self, task_ref: str) -> str | None:
95
+ """The newest CR6 verdict envelope on a review task, or None.
96
+
97
+ Returns VERDICT_APPROVE / VERDICT_REQUEST_CHANGES. This is the verdict
98
+ of record: reviewers post comment-mode reviews, so the GitHub review
99
+ state is always COMMENTED and cannot express approval at all.
100
+
101
+ Never raises. The daemon polls forever and this runs inside every pass,
102
+ so absent, empty or malformed evidence degrades to "no verdict" rather
103
+ than taking the loop down.
104
+ """
105
+ try:
106
+ data = run_json(["alissa", "task", "get", task_ref, "--json"], timeout=90)
107
+ except CommandError as exc:
108
+ log.warning("could not read verdict evidence for %s: %s", task_ref, exc)
109
+ return None
110
+ except Exception: # pragma: no cover - defence in depth
111
+ log.exception("unexpected failure reading verdict evidence for %s", task_ref)
112
+ return None
113
+
114
+ try:
115
+ return self._newest_verdict(data)
116
+ except Exception: # pragma: no cover - defence in depth
117
+ log.exception("could not parse verdict evidence for %s", task_ref)
118
+ return None
119
+
120
+ @staticmethod
121
+ def _newest_verdict(payload: object) -> str | None:
122
+ """Pick the newest parseable verdict out of a task's evidence array.
123
+
124
+ Every layer is optional by design -- the payload shape is whatever the
125
+ CLI printed, and a task with no evidence is the normal round-1 case.
126
+ """
127
+ if not isinstance(payload, dict):
128
+ return None
129
+ evidence = payload.get("evidence")
130
+ if not isinstance(evidence, list):
131
+ return None
132
+
133
+ found: list[tuple[str, str]] = []
134
+ for item in evidence:
135
+ if not isinstance(item, dict):
136
+ continue
137
+ title = item.get("title")
138
+ content = item.get("markdownContent")
139
+ for blob in (title, content):
140
+ if not isinstance(blob, str):
141
+ continue
142
+ match = _VERDICT_RE.search(blob)
143
+ if match:
144
+ created = item.get("createdAt")
145
+ found.append(
146
+ (created if isinstance(created, str) else "", match.group(1).lower())
147
+ )
148
+ break
149
+
150
+ if not found:
151
+ return None
152
+ # ISO-8601 timestamps sort lexicographically. Undated evidence sorts
153
+ # first (empty string), so a dated envelope always wins over one that
154
+ # lost its timestamp.
155
+ return max(found, key=lambda pair: pair[0])[1]
156
+
77
157
  def enqueue_reviewer(
78
158
  self,
79
159
  *,
@@ -45,6 +45,17 @@ class Review:
45
45
  commit_id: str
46
46
  submitted_at: str
47
47
  url: str
48
+ body: str = ""
49
+
50
+ @property
51
+ def is_substantive(self) -> bool:
52
+ """A real review round, not a side effect of an inline comment.
53
+
54
+ Posting a standalone inline comment on a PR creates its own review
55
+ record with an empty body, so review records outnumber rounds. The
56
+ round-closing review always carries the verdict write-up in its body.
57
+ """
58
+ return bool(self.body.strip())
48
59
 
49
60
 
50
61
  class RateLimited(RuntimeError):
@@ -159,16 +170,34 @@ class GitHub:
159
170
  commit_id=r.get("commit_id") or "",
160
171
  submitted_at=r.get("submitted_at") or "",
161
172
  url=r.get("html_url", ""),
173
+ body=r.get("body") or "",
162
174
  )
163
175
  for r in data
164
176
  ]
165
177
 
166
178
  def my_reviews(self, owner: str, repo: str, number: int) -> list[Review]:
167
- """My submitted reviews, oldest first -- one per completed round."""
179
+ """My substantive submitted reviews, oldest first -- one per round.
180
+
181
+ Empty-bodied records are dropped: a standalone inline comment creates
182
+ its own zero-body review record, so counting raw records overcounts
183
+ rounds badly. On fahera-mx/studio.alissa.app#210 three real rounds
184
+ produced six records (round 1 plus three inline-comment artifacts, then
185
+ rounds 2 and 3), and round 3's reviewer was told it was "round 6 of
186
+ cap 10".
187
+
188
+ Do NOT dedupe by `commit_id` instead -- it looks like the natural
189
+ grouping key but it UNDERCOUNTS. A round reviews whatever head is
190
+ current, and consecutive rounds routinely land on the same commit when
191
+ the implementer triages findings without pushing: on #210 rounds 2 and
192
+ 3 both carry head 805398a and would collapse into one. Body presence
193
+ tracks "a reviewer wrote a verdict"; commit identity does not.
194
+ """
168
195
  mine = [
169
196
  r
170
197
  for r in self.reviews(owner, repo, number)
171
- if r.author == self.login and r.state in SUBMITTED_STATES
198
+ if r.author == self.login
199
+ and r.state in SUBMITTED_STATES
200
+ and r.is_substantive
172
201
  ]
173
202
  return sorted(mine, key=lambda r: r.submitted_at)
174
203
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  One pass = poll GitHub for pending review requests, decide per PR whether a
4
4
  fresh reviewer round is owed, and enqueue it. Rounds are derived from GitHub
5
- (one submitted review per round), not from local bookkeeping.
5
+ (one *substantive* submitted review per round -- empty-bodied records are
6
+ inline-comment artifacts, not rounds), not from local bookkeeping. Convergence
7
+ comes from either the GitHub review state or the CR6 verdict envelope on the
8
+ Alissa review task.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
@@ -14,9 +17,9 @@ from dataclasses import dataclass
14
17
  from enum import Enum
15
18
  from pathlib import Path
16
19
 
17
- from .alissa import Alissa
20
+ from .alissa import VERDICT_APPROVE, Alissa, Task
18
21
  from .config import HUB_ADD, ON_MISSING_SKIP, Config
19
- from .ghclient import GitHub, PullRequest, RateLimited
22
+ from .ghclient import GitHub, PullRequest, RateLimited, Review
20
23
  from .proc import CommandError
21
24
  from .state import State
22
25
 
@@ -114,8 +117,13 @@ class ReviewWatcher:
114
117
  my_reviews = self.github.my_reviews(owner, repo, number)
115
118
  completed = len(my_reviews)
116
119
 
117
- if my_reviews and my_reviews[-1].state == "APPROVED":
118
- return Decision(Action.CONVERGED, "last verdict is approve", completed)
120
+ # Looked up here rather than inside _spawn: convergence needs the ref
121
+ # too. _spawn still handles `task is None` exactly as before.
122
+ task = self.alissa.find_review_task(owner, repo, number)
123
+
124
+ converged = self._convergence_reason(my_reviews, task)
125
+ if converged is not None:
126
+ return Decision(Action.CONVERGED, converged, completed)
119
127
 
120
128
  # CR9: never queue round cap+1.
121
129
  if completed >= self.config.round_cap:
@@ -138,13 +146,36 @@ class ReviewWatcher:
138
146
  age / 60,
139
147
  )
140
148
 
141
- return self._spawn(pr, round_)
149
+ return self._spawn(pr, round_, task)
142
150
 
143
- # -- actions -----------------------------------------------------------
151
+ def _convergence_reason(self, my_reviews: list[Review], task: Task | None) -> str | None:
152
+ """Why the loop is done, or None if it is not.
153
+
154
+ Two independent signals, because neither alone is sufficient:
155
+
156
+ * The GitHub review state. Authoritative when it says APPROVED, but
157
+ reviewers work in comment mode, which can only ever produce
158
+ COMMENTED -- #210 has zero APPROVED records across its whole history.
159
+ On its own this made convergence unreachable: every PR, however
160
+ clean, ran to the round cap and escalated.
161
+ * The CR6 verdict envelope on the Alissa review task. The review skill
162
+ declares this the verdict of record, and unlike the GitHub state it
163
+ can actually express approval, so it is the signal that closes the
164
+ loop in practice.
165
+ """
166
+ if my_reviews and my_reviews[-1].state == "APPROVED":
167
+ return "last GitHub review state is APPROVED"
168
+
169
+ # Only checkable once a review task exists; before that there is
170
+ # nowhere for a verdict to have been recorded.
171
+ if task is not None and self.alissa.latest_verdict(task.ref) == VERDICT_APPROVE:
172
+ return f"newest verdict envelope on {task.ref} reads approve"
144
173
 
145
- def _spawn(self, pr: PullRequest, round_: int) -> Decision:
146
- task = self.alissa.find_review_task(pr.owner, pr.repo, pr.number)
174
+ return None
175
+
176
+ # -- actions -----------------------------------------------------------
147
177
 
178
+ def _spawn(self, pr: PullRequest, round_: int, task: Task | None) -> Decision:
148
179
  if task is None:
149
180
  if self.config.on_missing_review_task == ON_MISSING_SKIP:
150
181
  return Decision(
@@ -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.1.1
3
+ Version: 0.3.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