alissa-tools-github-devloop 0.8.18__tar.gz → 0.8.19__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.
- {alissa_tools_github_devloop-0.8.18/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.19}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/loop.py +269 -102
- alissa_tools_github_devloop-0.8.19/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.8.18/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/README.md +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/setup.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/bows.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/config.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/state.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.18 → alissa_tools_github_devloop-0.8.19}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -27,11 +27,15 @@ and the merge, a finished session's PR sits open for the whole review loop.
|
|
|
27
27
|
DEAD alone would therefore fire on the modal happy path. So the resume takes
|
|
28
28
|
a FOURTH signal from the PR the session left behind: the handoff publishes
|
|
29
29
|
itself (`gh pr ready` flips the draft off, then the re-request puts a
|
|
30
|
-
reviewer on it), which makes a PR that is STILL A DRAFT
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
reviewer on it), which makes a PR that is STILL A DRAFT the one state that
|
|
31
|
+
says "this session never handed off" -- whatever reviewer is on it, because
|
|
32
|
+
a review request on a draft publishes nothing (the reviewer daemon only
|
|
33
|
+
reviews non-draft PRs and the fix edge refuses drafts, so nothing downstream
|
|
34
|
+
can ever move a draft; studio.alissa.app#1204 sat 80 minutes behind exactly
|
|
35
|
+
that shape). That test also keeps the edges disjoint -- the moment a PR
|
|
36
|
+
leaves draft it belongs to the review lifecycle and the fix edge owns it
|
|
37
|
+
exclusively, so no `develop-*` resume can land on a branch a `fix-*` session
|
|
38
|
+
is already working.
|
|
35
39
|
|
|
36
40
|
The signals so far all describe the PR's STATE, and a sibling task's young
|
|
37
41
|
draft satisfies every one of them; a cross-reference is raised by ANY mention
|
|
@@ -91,8 +95,8 @@ above takes over as attempt k+1, counting toward attempt_cap like any
|
|
|
91
95
|
resume. Nothing about the kill is new machinery on the resume side: it
|
|
92
96
|
manufactures the one signal the matrix already acts on. The invariants are
|
|
93
97
|
the matrix's own -- never on INDETERMINATE (absence of evidence keeps the
|
|
94
|
-
defer), never on a PR that left draft
|
|
95
|
-
|
|
98
|
+
defer), never on a PR that left draft (the wedge sits strictly behind
|
|
99
|
+
`_resumable_pr`), never with `resume_enabled` false (a
|
|
96
100
|
kill whose resume cannot fire would only orphan the lane), and never a
|
|
97
101
|
roster sweep.
|
|
98
102
|
|
|
@@ -173,7 +177,9 @@ self-heal needs an unanswered review, and the issue-edge resume keys off an
|
|
|
173
177
|
OPEN labeled issue, which a draft replayed after its issue merged does not
|
|
174
178
|
have (fahera-mx/studio.alissa.app#279, 2026-07-28: an operator found it by
|
|
175
179
|
accident). So the PR search is widened to drafts and every draft is asked the
|
|
176
|
-
handoff question instead of being skipped: self-authored
|
|
180
|
+
handoff question instead of being skipped: self-authored (a reviewer already
|
|
181
|
+
requested on it is NOT a reason to skip -- a request on a draft arms nothing,
|
|
182
|
+
so the sweep readies it and requests only the reviewers still missing),
|
|
177
183
|
a daemon-shaped head ref naming an implementation task (a `TASK-<n>-<DESC>`
|
|
178
184
|
branch or a task ref in the body -- the provenance test, `closes_issue`'s
|
|
179
185
|
role here), no live worker session for the PR (the SAME liveness rule --
|
|
@@ -741,8 +747,9 @@ RESUME_DIRECTIVE = (
|
|
|
741
747
|
"previous developer session on GitHub issue {issue_url} died AFTER "
|
|
742
748
|
"opening its pull request but BEFORE handing it off for review — the "
|
|
743
749
|
"daemon confirmed its session `{dead_session}` is gone from the machine, "
|
|
744
|
-
"and its DRAFT pull request {pr_url} is still open
|
|
745
|
-
"
|
|
750
|
+
"and its DRAFT pull request {pr_url} is still open (a reviewer may "
|
|
751
|
+
"already be requested on it — that is not a handoff; the ready flip is). "
|
|
752
|
+
"This is attempt {attempt} of {cap}, and your job is to FINISH THAT "
|
|
746
753
|
"PULL REQUEST, not to start the work over. "
|
|
747
754
|
"{task_instruction} "
|
|
748
755
|
"FIRST hydrate the context you did not live through: read the issue (its "
|
|
@@ -2629,11 +2636,16 @@ class DevWatcher:
|
|
|
2629
2636
|
# the session behind the open PR is not running -- but NOT that it was
|
|
2630
2637
|
# abandoned, because a healthy handoff ends in `alissa tmux kill`. So
|
|
2631
2638
|
# DEAD only opens the question, and `_resumable_pr` answers it: a
|
|
2632
|
-
# still-draft
|
|
2639
|
+
# still-draft PR that CLOSES this issue (a reviewer on the draft
|
|
2640
|
+
# changes nothing -- a draft is never handed off) is one of this
|
|
2633
2641
|
# issue's own sessions that was never handed off, and it
|
|
2634
2642
|
# is the one case the deferral was always wrong about -- resumed as
|
|
2635
2643
|
# attempt k+1 with RESUME_DIRECTIVE instead of parking the lane behind
|
|
2636
|
-
# an operator ping.
|
|
2644
|
+
# an operator ping. (When the orphan sweep is on it runs EARLIER in
|
|
2645
|
+
# the same pass and readies such a draft mechanically, so the fetch
|
|
2646
|
+
# here sees a non-draft PR and defers -- the sweep's handoff wins
|
|
2647
|
+
# over a resume without any coordination between the edges.)
|
|
2648
|
+
# Everything else -- ALIVE, INDETERMINATE, and DEAD
|
|
2637
2649
|
# behind a PR already in the review lifecycle -- keeps the floored
|
|
2638
2650
|
# defer: past STALLED_DEFER_MULTIPLE stale windows it posts a
|
|
2639
2651
|
# "stalled" operator comment (this deferral sits above the cap check,
|
|
@@ -2708,9 +2720,9 @@ class DevWatcher:
|
|
|
2708
2720
|
# an operator reading the log must not have to guess.
|
|
2709
2721
|
why = (
|
|
2710
2722
|
"no linked PR is resumable (a handed-off PR — out of "
|
|
2711
|
-
"draft
|
|
2712
|
-
"
|
|
2713
|
-
"
|
|
2723
|
+
"draft — belongs to the review lifecycle, not to a "
|
|
2724
|
+
"resume; a PR that only MENTIONS this issue is "
|
|
2725
|
+
"another task's work)"
|
|
2714
2726
|
if liveness is Liveness.DEAD
|
|
2715
2727
|
and self.config.resume_enabled
|
|
2716
2728
|
else "work may be alive (the review loop can outlast "
|
|
@@ -3080,12 +3092,23 @@ class DevWatcher:
|
|
|
3080
3092
|
|
|
3081
3093
|
What separates the two is the PR the session left behind, because the
|
|
3082
3094
|
handoff PUBLISHES itself: `gh pr ready` flips the draft off, then the
|
|
3083
|
-
re-request puts a reviewer on it. So a PR that is STILL A DRAFT
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3095
|
+
re-request puts a reviewer on it. So a PR that is STILL A DRAFT is one
|
|
3096
|
+
its session never handed off -- the only state a resume is owed --
|
|
3097
|
+
and a reviewer requested on that draft does not change the reading: a
|
|
3098
|
+
request on a draft arms nothing (the reviewer daemon reviews only
|
|
3099
|
+
non-draft PRs, the fix edge refuses drafts), so no automation
|
|
3100
|
+
downstream can ever move it. The two half-states a session can die in
|
|
3101
|
+
are therefore read differently: (a) READY + NO reviewer -- the ready
|
|
3102
|
+
flip landed, the request did not -- reads handed-off here and keeps
|
|
3103
|
+
the legacy floored defer, conservative on purpose since that is also
|
|
3104
|
+
exactly what a live review loop looks like from this edge; where this
|
|
3105
|
+
daemon's own sweep made that flip, the `orphan-ready@<head>` row lets
|
|
3106
|
+
the PR edge's `_finish_orphan_handoff` complete it. (b) DRAFT +
|
|
3107
|
+
reviewer -- the request landed, the flip did not (studio#1204: 80
|
|
3108
|
+
minutes behind a draft with `alissa-app` on it) -- is resumable when
|
|
3109
|
+
the session is DEAD, exactly like a review-less draft, and deferred
|
|
3110
|
+
while ALIVE; with the orphan sweep on, that sweep readies it earlier
|
|
3111
|
+
in the same pass, so the resume yields to the mechanical handoff.
|
|
3089
3112
|
|
|
3090
3113
|
That same test settles the cross-edge race: the moment a PR leaves
|
|
3091
3114
|
draft it belongs to the review lifecycle, and the fix edge -- which
|
|
@@ -3162,16 +3185,24 @@ class DevWatcher:
|
|
|
3162
3185
|
# still a PR whose base can strand it, and the guard's own author
|
|
3163
3186
|
# test is what keeps it off PRs that are not ours.
|
|
3164
3187
|
self._check_stacked_base(pr)
|
|
3165
|
-
if not pr.draft
|
|
3188
|
+
if not pr.draft:
|
|
3166
3189
|
log.info(
|
|
3167
|
-
"%s: %s has been handed off (draft
|
|
3168
|
-
"not a resume candidate; the review lifecycle owns it",
|
|
3190
|
+
"%s: %s has been handed off (out of draft, reviewers=%s) "
|
|
3191
|
+
"— not a resume candidate; the review lifecycle owns it",
|
|
3169
3192
|
issue.issue_slug,
|
|
3170
3193
|
url,
|
|
3171
|
-
pr.draft,
|
|
3172
3194
|
", ".join(pr.requested_reviewers) or "none",
|
|
3173
3195
|
)
|
|
3174
3196
|
continue
|
|
3197
|
+
if pr.requested_reviewers:
|
|
3198
|
+
log.info(
|
|
3199
|
+
"%s: %s is still a DRAFT with %s requested — a review "
|
|
3200
|
+
"request on a draft is not a handoff (the ready flip is), "
|
|
3201
|
+
"so this PR is a resume candidate",
|
|
3202
|
+
issue.issue_slug,
|
|
3203
|
+
url,
|
|
3204
|
+
", ".join(pr.requested_reviewers),
|
|
3205
|
+
)
|
|
3175
3206
|
if pr.author != self.github.login:
|
|
3176
3207
|
log.warning(
|
|
3177
3208
|
"%s: %s is authored by %r, not the developer identity — "
|
|
@@ -3258,12 +3289,13 @@ class DevWatcher:
|
|
|
3258
3289
|
|
|
3259
3290
|
Two gates, in order. First `_resumable_pr`, verbatim -- the wedge
|
|
3260
3291
|
path inherits every admission test the resume has (still a draft,
|
|
3261
|
-
|
|
3262
|
-
|
|
3292
|
+
self-authored, daemon-shaped head, a body that CLOSES this issue,
|
|
3293
|
+
exactly one candidate; a reviewer requested on the draft is not a
|
|
3294
|
+
handoff and does not exclude it), because the kill it licenses is
|
|
3263
3295
|
only safe where the resume that follows it is: a PR that has left
|
|
3264
|
-
draft
|
|
3265
|
-
|
|
3266
|
-
|
|
3296
|
+
draft belongs to the review lifecycle, and a live review loop behind
|
|
3297
|
+
an ALIVE session is the healthy shape this daemon was built around.
|
|
3298
|
+
Then the footprint (`_footprint_age`):
|
|
3267
3299
|
None means unreadable, and unreadable keeps the defer -- the same
|
|
3268
3300
|
fail-safe every other presumed-dead decision here makes.
|
|
3269
3301
|
|
|
@@ -4459,17 +4491,18 @@ class DevWatcher:
|
|
|
4459
4491
|
f"identity — not this daemon's to hand off",
|
|
4460
4492
|
)
|
|
4461
4493
|
|
|
4462
|
-
# A draft already carrying a review request is
|
|
4463
|
-
#
|
|
4464
|
-
#
|
|
4465
|
-
#
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4494
|
+
# A draft already carrying a review request is NOT excluded (issue
|
|
4495
|
+
# #113). The sweep used to read a reviewer on a draft as someone's
|
|
4496
|
+
# deliberate work in progress, but a request on a draft arms nothing
|
|
4497
|
+
# -- the reviewer daemon reviews only non-draft PRs and the fix edge
|
|
4498
|
+
# refuses drafts -- and the issue edge read the same shape as "handed
|
|
4499
|
+
# off", so a session that died between its `--add-reviewer` and its
|
|
4500
|
+
# `gh pr ready` (studio.alissa.app#1204: 80 minutes of silence after
|
|
4501
|
+
# a 10-hour attempt) left a draft NO edge would ever move. The
|
|
4502
|
+
# liveness gate below is what protects a deliberately-parked draft:
|
|
4503
|
+
# a live session behind it is never swept, and for a DEAD one the
|
|
4504
|
+
# sweep does the FIRST half only -- the ready flip -- and requests
|
|
4505
|
+
# just the configured reviewers that are not already on the PR.
|
|
4473
4506
|
|
|
4474
4507
|
# head_ref reaches nothing but log lines here (the sweep spawns no
|
|
4475
4508
|
# session, so it never becomes a worktree path or a shell token), but
|
|
@@ -4804,36 +4837,64 @@ class DevWatcher:
|
|
|
4804
4837
|
not retried on this head (escalate-don't-retry, `_escalate_rejection`
|
|
4805
4838
|
verbatim).
|
|
4806
4839
|
|
|
4840
|
+
A draft that ALREADY carries a review request (issue #113) takes the
|
|
4841
|
+
same walk minus the request: the session died between its
|
|
4842
|
+
`--add-reviewer` and its `gh pr ready`, so the flip is the only act
|
|
4843
|
+
still owed. `request_reviews` is skipped entirely when every
|
|
4844
|
+
configured reviewer is already on the PR -- re-requesting an
|
|
4845
|
+
already-requested reviewer is a no-op on GitHub, but it must not be
|
|
4846
|
+
recorded as a fresh request -- and requests only the missing ones
|
|
4847
|
+
otherwise. Both ledger rows still land, so the once-per-(PR, head)
|
|
4848
|
+
guarantee and `_owes_orphan_handoff` keep holding on this shape.
|
|
4849
|
+
|
|
4807
4850
|
The activity line comes LAST and stays best-effort: it is telemetry
|
|
4808
4851
|
about acts that have already happened, and it is written as a NON-SPAWN
|
|
4809
4852
|
context because no session exists for it to claim a lane on behalf of.
|
|
4810
4853
|
"""
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4854
|
+
already, missing = self._orphan_reviewers_split(pr)
|
|
4855
|
+
if already:
|
|
4856
|
+
log.warning(
|
|
4857
|
+
"ORPHAN DRAFT %s (%s): head %s has been public %.0f min, %s "
|
|
4858
|
+
"already requested on it, and no worker session for this PR "
|
|
4859
|
+
"is listed — the session that opened it died after its "
|
|
4860
|
+
"review request but before flipping it ready (a request on a "
|
|
4861
|
+
"draft arms nothing); marking it ready mechanically%s (once "
|
|
4862
|
+
"per PR/head)",
|
|
4863
|
+
pr.pr_slug,
|
|
4864
|
+
task_ref,
|
|
4865
|
+
pr.head_sha[:8],
|
|
4866
|
+
waited / 60,
|
|
4867
|
+
", ".join(already),
|
|
4868
|
+
f" and requesting {', '.join(missing)}" if missing else "",
|
|
4869
|
+
)
|
|
4870
|
+
else:
|
|
4871
|
+
log.warning(
|
|
4872
|
+
"ORPHAN DRAFT %s (%s): head %s has been public %.0f min, no "
|
|
4873
|
+
"reviewer is requested, and no worker session for this PR is "
|
|
4874
|
+
"listed — the session that opened it died before handing it "
|
|
4875
|
+
"off; marking it ready and requesting %s mechanically (once "
|
|
4876
|
+
"per PR/head)",
|
|
4877
|
+
pr.pr_slug,
|
|
4878
|
+
task_ref,
|
|
4879
|
+
pr.head_sha[:8],
|
|
4880
|
+
waited / 60,
|
|
4881
|
+
", ".join(missing),
|
|
4882
|
+
)
|
|
4824
4883
|
|
|
4825
4884
|
if self.config.dry_run:
|
|
4885
|
+
request = (
|
|
4886
|
+
f"request {', '.join(missing)}"
|
|
4887
|
+
if missing
|
|
4888
|
+
else f"request nobody ({', '.join(already)} already requested)"
|
|
4889
|
+
)
|
|
4826
4890
|
log.info(
|
|
4827
|
-
"[dry-run] would mark %s ready and
|
|
4828
|
-
pr.pr_slug,
|
|
4829
|
-
", ".join(reviewers),
|
|
4891
|
+
"[dry-run] would mark %s ready and %s", pr.pr_slug, request
|
|
4830
4892
|
)
|
|
4831
4893
|
return Decision(
|
|
4832
4894
|
Action.IN_FLIGHT,
|
|
4833
4895
|
f"[dry-run] would mechanically hand off draft head "
|
|
4834
4896
|
f"{pr.head_sha[:8]} (public {int(waited / 60)} min, its "
|
|
4835
|
-
f"session gone): mark ready + request
|
|
4836
|
-
f"{', '.join(reviewers)}",
|
|
4897
|
+
f"session gone): mark ready + {request}",
|
|
4837
4898
|
task_ref=task_ref,
|
|
4838
4899
|
)
|
|
4839
4900
|
|
|
@@ -4863,6 +4924,28 @@ class DevWatcher:
|
|
|
4863
4924
|
|
|
4864
4925
|
return self._request_orphan_reviewers(pr, task_ref, waited)
|
|
4865
4926
|
|
|
4927
|
+
def _orphan_reviewers_split(
|
|
4928
|
+
self, pr: PullRequest
|
|
4929
|
+
) -> "tuple[tuple[str, ...], tuple[str, ...]]":
|
|
4930
|
+
"""The configured reviewers, split by what the PR already carries:
|
|
4931
|
+
(already requested, still missing), each in config order.
|
|
4932
|
+
|
|
4933
|
+
Casefolded like `_reviewer_reviews`: the configured set is
|
|
4934
|
+
operator-typed and GitHub logins are case-insensitive, so `Alissa-App`
|
|
4935
|
+
in the config and `alissa-app` on the PR are one reviewer, not a
|
|
4936
|
+
missing one. Stated once because the sweep and the completion path
|
|
4937
|
+
both need exactly this answer -- the request must never name a
|
|
4938
|
+
reviewer who is already on the PR (a no-op on GitHub that would still
|
|
4939
|
+
be recorded, and logged, as a fresh request)."""
|
|
4940
|
+
on_pr = {r.casefold() for r in pr.requested_reviewers}
|
|
4941
|
+
already = tuple(
|
|
4942
|
+
r for r in self.config.reviewers if r.casefold() in on_pr
|
|
4943
|
+
)
|
|
4944
|
+
missing = tuple(
|
|
4945
|
+
r for r in self.config.reviewers if r.casefold() not in on_pr
|
|
4946
|
+
)
|
|
4947
|
+
return already, missing
|
|
4948
|
+
|
|
4866
4949
|
def _owes_orphan_handoff(self, pr: PullRequest) -> bool:
|
|
4867
4950
|
"""Did THIS daemon ready this exact head and still owe it a reviewer?
|
|
4868
4951
|
|
|
@@ -4904,6 +4987,14 @@ class DevWatcher:
|
|
|
4904
4987
|
entitled to finish its own handoff). Not re-run is the grace window --
|
|
4905
4988
|
it was satisfied when the flip fired, and the head has not moved since
|
|
4906
4989
|
(a moved head would be a different key with no ready row at all).
|
|
4990
|
+
|
|
4991
|
+
Reached only for a PR with NO pending request (evaluate_pr's own
|
|
4992
|
+
guard sits above this). So a draft that already carried a reviewer
|
|
4993
|
+
can never arrive here: it left draft carrying that reviewer, and the
|
|
4994
|
+
pending-request guard reads it as in review. That is why
|
|
4995
|
+
`_request_orphan_reviewers` never leaves that shape half-recorded --
|
|
4996
|
+
a failed request for the MISSING reviewers there is given up on and
|
|
4997
|
+
the handoff completed on the spot (round-1 review fix, PR #114).
|
|
4907
4998
|
"""
|
|
4908
4999
|
if not self._owes_orphan_handoff(pr) or not self.config.reviewers:
|
|
4909
5000
|
return None
|
|
@@ -4952,43 +5043,73 @@ class DevWatcher:
|
|
|
4952
5043
|
this is the completion path (where that number is a poll or more old
|
|
4953
5044
|
and would be reported as fact).
|
|
4954
5045
|
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5046
|
+
Only the configured reviewers NOT already on the PR are requested
|
|
5047
|
+
(`_orphan_reviewers_split`, issue #113). When every one of them is
|
|
5048
|
+
already requested -- the draft the session put a reviewer on before
|
|
5049
|
+
dying, or a completion-path PR someone requested by hand between
|
|
5050
|
+
polls -- no call is made at all: the DONE marker lands, the activity
|
|
5051
|
+
line says who was already there, and the reviewer daemon already sees
|
|
5052
|
+
the PR.
|
|
5053
|
+
|
|
5054
|
+
Failure handling on the review-less shape mirrors
|
|
5055
|
+
`_rerequest_review`'s exactly: a ReviewRequestRejected is permanent
|
|
5056
|
+
and operator-fixable (escalate, don't retry, and never record the
|
|
5057
|
+
DONE marker); a CommandError is transient (record nothing, retry next
|
|
5058
|
+
poll). The READY row stays in both cases -- the flip really did
|
|
5059
|
+
happen, and re-flipping an already-ready PR is not what the retry
|
|
5060
|
+
needs to do.
|
|
5061
|
+
|
|
5062
|
+
On the PARTIAL shape -- some configured reviewers already on the PR,
|
|
5063
|
+
the call carrying only the missing rest -- neither doctrine applies
|
|
5064
|
+
(round-1 review fix, PR #114). `request_reviews` raises only when
|
|
5065
|
+
nothing in THAT call landed, which here does not mean nobody is on
|
|
5066
|
+
the PR: the flip landed and `already` is requested, so the PR is in
|
|
5067
|
+
review and the reviewer daemon sees it. The refusal page would be
|
|
5068
|
+
false ("never marked ready or sent to a reviewer"), and a retry is
|
|
5069
|
+
structurally impossible: next poll the pending-request guard in
|
|
5070
|
+
evaluate_pr reads the PR as in review before `_finish_orphan_handoff`
|
|
5071
|
+
is reached, so a ready row left down would stick on that head
|
|
5072
|
+
forever. So a failed request for the missing subset, permanent or
|
|
5073
|
+
transient, is logged as given up on -- naming who IS on the PR --
|
|
5074
|
+
and the handoff is completed: DONE row, activity line."""
|
|
5075
|
+
already, missing = self._orphan_reviewers_split(pr)
|
|
5076
|
+
landed: tuple[str, ...] = ()
|
|
5077
|
+
if missing:
|
|
5078
|
+
try:
|
|
5079
|
+
landed = self.github.request_reviews(
|
|
5080
|
+
pr.owner, pr.repo, pr.number, missing
|
|
5081
|
+
)
|
|
5082
|
+
except ReviewRequestRejected as exc:
|
|
5083
|
+
if not already:
|
|
5084
|
+
return self._escalate_orphan_rejected(pr, task_ref, exc)
|
|
5085
|
+
self._log_orphan_reviewers_given_up(pr, missing, already, exc)
|
|
5086
|
+
except CommandError as exc:
|
|
5087
|
+
if not already:
|
|
5088
|
+
log.error(
|
|
5089
|
+
"could not request review from %s on %s: %s — NOT "
|
|
5090
|
+
"recording the handoff; the sweep retries next poll",
|
|
5091
|
+
", ".join(missing),
|
|
5092
|
+
pr.pr_slug,
|
|
5093
|
+
exc,
|
|
5094
|
+
)
|
|
5095
|
+
return Decision(
|
|
5096
|
+
Action.SKIPPED,
|
|
5097
|
+
f"the PR was marked ready but the review request "
|
|
5098
|
+
f"did not land ({exc}) — transient; not recorded, "
|
|
5099
|
+
f"retrying next poll",
|
|
5100
|
+
task_ref=task_ref,
|
|
5101
|
+
)
|
|
5102
|
+
self._log_orphan_reviewers_given_up(pr, missing, already, exc)
|
|
4982
5103
|
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5104
|
+
dropped = tuple(r for r in missing if r not in landed)
|
|
5105
|
+
if dropped and landed:
|
|
5106
|
+
log.warning(
|
|
5107
|
+
"%s: GitHub silently dropped reviewer(s) %s from the "
|
|
5108
|
+
"mechanical handoff; %s landed, so the PR IS in review",
|
|
5109
|
+
pr.pr_slug,
|
|
5110
|
+
", ".join(dropped),
|
|
5111
|
+
", ".join(landed + already),
|
|
5112
|
+
)
|
|
4992
5113
|
|
|
4993
5114
|
self.state.record_escalation(
|
|
4994
5115
|
pr.full_name, pr.number, orphan_kind(pr.head_sha)
|
|
@@ -4998,6 +5119,23 @@ class DevWatcher:
|
|
|
4998
5119
|
if waited is not None
|
|
4999
5120
|
else ""
|
|
5000
5121
|
)
|
|
5122
|
+
if landed and already:
|
|
5123
|
+
acted = (
|
|
5124
|
+
f"marked ready and requested review from {', '.join(landed)} "
|
|
5125
|
+
f"({', '.join(already)} already requested)"
|
|
5126
|
+
)
|
|
5127
|
+
died = "before handing it off"
|
|
5128
|
+
elif landed:
|
|
5129
|
+
acted = f"marked ready and requested review from {', '.join(landed)}"
|
|
5130
|
+
died = "before handing it off"
|
|
5131
|
+
else:
|
|
5132
|
+
noun = "reviewer" if len(already) == 1 else "reviewers"
|
|
5133
|
+
verb = "was" if len(already) == 1 else "were"
|
|
5134
|
+
acted = (
|
|
5135
|
+
f"marked ready ({noun} {', '.join(already)} {verb} already "
|
|
5136
|
+
f"requested)"
|
|
5137
|
+
)
|
|
5138
|
+
died = "before flipping it"
|
|
5001
5139
|
self._append_activity(
|
|
5002
5140
|
pr.owner,
|
|
5003
5141
|
pr.repo,
|
|
@@ -5005,22 +5143,51 @@ class DevWatcher:
|
|
|
5005
5143
|
self._activity_line(
|
|
5006
5144
|
orphan_activity_lane(pr),
|
|
5007
5145
|
"orphan sweep",
|
|
5008
|
-
f"{ACTIVITY_HANDED_OFF} —
|
|
5009
|
-
f"
|
|
5010
|
-
f"
|
|
5011
|
-
f"protocol (no session spawned, once per PR/head)",
|
|
5146
|
+
f"{ACTIVITY_HANDED_OFF} — {acted}; the session that opened "
|
|
5147
|
+
f"this draft died {died}, so the daemon completed its "
|
|
5148
|
+
f"protocol{age} (no session spawned, once per PR/head)",
|
|
5012
5149
|
),
|
|
5013
5150
|
pr.pr_slug,
|
|
5014
5151
|
)
|
|
5015
5152
|
|
|
5153
|
+
if landed:
|
|
5154
|
+
requested = f"and review requested from {', '.join(landed)}"
|
|
5155
|
+
else:
|
|
5156
|
+
requested = (
|
|
5157
|
+
f"(review already requested from {', '.join(already)}, no "
|
|
5158
|
+
f"new request made)"
|
|
5159
|
+
)
|
|
5016
5160
|
return Decision(
|
|
5017
5161
|
Action.IN_FLIGHT,
|
|
5018
|
-
f"orphaned draft handed off — marked ready
|
|
5019
|
-
f"
|
|
5020
|
-
f"in the reviewer's court",
|
|
5162
|
+
f"orphaned draft handed off — marked ready {requested} for head "
|
|
5163
|
+
f"{pr.head_sha[:8]}; the ball is in the reviewer's court",
|
|
5021
5164
|
task_ref=task_ref,
|
|
5022
5165
|
)
|
|
5023
5166
|
|
|
5167
|
+
@staticmethod
|
|
5168
|
+
def _log_orphan_reviewers_given_up(
|
|
5169
|
+
pr: PullRequest,
|
|
5170
|
+
missing: "tuple[str, ...]",
|
|
5171
|
+
already: "tuple[str, ...]",
|
|
5172
|
+
exc: Exception,
|
|
5173
|
+
) -> None:
|
|
5174
|
+
"""The partial shape's one failure path (see
|
|
5175
|
+
`_request_orphan_reviewers`): the request for the configured
|
|
5176
|
+
reviewers NOT yet on the PR failed, `already` IS on it, and no retry
|
|
5177
|
+
can ever reach this PR again -- so the missing reviewers are given up
|
|
5178
|
+
on, out loud, and the handoff completes with who is there."""
|
|
5179
|
+
log.warning(
|
|
5180
|
+
"%s: could not request review from %s on the mechanical handoff "
|
|
5181
|
+
"(%s) — giving up on them: %s already requested and the PR is out "
|
|
5182
|
+
"of draft, so it IS in review; the daemon will not retry (the "
|
|
5183
|
+
"pending request reads as in review from here on), request them "
|
|
5184
|
+
"by hand if wanted",
|
|
5185
|
+
pr.pr_slug,
|
|
5186
|
+
", ".join(missing),
|
|
5187
|
+
exc,
|
|
5188
|
+
", ".join(already),
|
|
5189
|
+
)
|
|
5190
|
+
|
|
5024
5191
|
def _escalate_orphan_rejected(
|
|
5025
5192
|
self, pr: PullRequest, task_ref: "str | None", exc: Exception
|
|
5026
5193
|
) -> Decision:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.8.19
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.8.18
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|