alissa-tools-github-devloop 0.5.1__tar.gz → 0.6.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.
- {alissa_tools_github_devloop-0.5.1/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.6.0}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/__main__.py +21 -1
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/config.py +22 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/ghclient.py +47 -13
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/loop.py +853 -127
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/state.py +36 -0
- alissa_tools_github_devloop-0.6.0/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/sources.py +1 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.5.1/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/README.md +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/setup.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -147,6 +147,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
147
147
|
"sessions, even if the config enables it",
|
|
148
148
|
)
|
|
149
149
|
|
|
150
|
+
resume = over.add_mutually_exclusive_group()
|
|
151
|
+
resume.add_argument(
|
|
152
|
+
"--resume",
|
|
153
|
+
dest="resume_enabled",
|
|
154
|
+
action="store_true",
|
|
155
|
+
default=None,
|
|
156
|
+
help="auto-resume a stale attempt whose session is provably dead "
|
|
157
|
+
"(absent from a successful `alissa tmux ls`) while its PR stays open, "
|
|
158
|
+
"instead of deferring behind an operator ping (the default)",
|
|
159
|
+
)
|
|
160
|
+
resume.add_argument(
|
|
161
|
+
"--no-resume",
|
|
162
|
+
dest="resume_enabled",
|
|
163
|
+
action="store_false",
|
|
164
|
+
help="legacy two-signal staleness: never read session liveness, "
|
|
165
|
+
"never auto-resume — a dead-with-PR lane waits for an operator",
|
|
166
|
+
)
|
|
167
|
+
|
|
150
168
|
dry = over.add_mutually_exclusive_group()
|
|
151
169
|
dry.add_argument(
|
|
152
170
|
"--dry-run",
|
|
@@ -184,6 +202,7 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
184
202
|
"fix_rounds_enabled": args.fix_rounds_enabled,
|
|
185
203
|
"maintain_label": args.maintain_label,
|
|
186
204
|
"maintain_enabled": args.maintain_enabled,
|
|
205
|
+
"resume_enabled": args.resume_enabled,
|
|
187
206
|
"dry_run": args.dry_run,
|
|
188
207
|
}
|
|
189
208
|
|
|
@@ -215,12 +234,13 @@ def log_effective_config(config: Config, login: str) -> None:
|
|
|
215
234
|
log.info("reviewers: %s", ", ".join(config.reviewers) or "none")
|
|
216
235
|
log.info(
|
|
217
236
|
"poll every %ss; dry_run=%s; attempt_cap=%s; stale after %s min; "
|
|
218
|
-
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r)",
|
|
237
|
+
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r); resume=%s",
|
|
219
238
|
config.poll_interval, config.dry_run, config.attempt_cap,
|
|
220
239
|
config.stale_minutes,
|
|
221
240
|
config.max_sessions or "unlimited",
|
|
222
241
|
config.fix_rounds_enabled,
|
|
223
242
|
config.maintain_enabled, config.maintain_label,
|
|
243
|
+
config.resume_enabled,
|
|
224
244
|
)
|
|
225
245
|
log.debug("hub_template: %s", config.hub_template)
|
|
226
246
|
log.debug("agent_profile: %s", config.agent_profile)
|
|
@@ -62,6 +62,7 @@ CONFIG_KEYS = (
|
|
|
62
62
|
"fix_rounds_enabled",
|
|
63
63
|
"maintain_label",
|
|
64
64
|
"maintain_enabled",
|
|
65
|
+
"resume_enabled",
|
|
65
66
|
"dry_run",
|
|
66
67
|
)
|
|
67
68
|
|
|
@@ -161,6 +162,24 @@ class Config:
|
|
|
161
162
|
maintain_label: str = "alissa:maintain"
|
|
162
163
|
maintain_enabled: bool = True
|
|
163
164
|
|
|
165
|
+
# The third staleness signal (session liveness, read from the reaper's own
|
|
166
|
+
# tmux listing) and the auto-resume it unlocks: a stale attempt whose
|
|
167
|
+
# ledger session is provably gone is respawned with RESUME_DIRECTIVE /
|
|
168
|
+
# FIX_RESUME_DIRECTIVE instead of parking the lane behind an operator
|
|
169
|
+
# ping. Provably gone is necessary, not sufficient: a healthy handoff ends
|
|
170
|
+
# in `alissa tmux kill` too, so on the ISSUE edge the resume additionally
|
|
171
|
+
# requires the linked PR to show the handoff never happened AND to be this
|
|
172
|
+
# issue's own work -- exactly one linked open PR, still a DRAFT, no
|
|
173
|
+
# reviewer requested, authored by the token identity, daemon-shaped head
|
|
174
|
+
# ref, and a body that CLOSES this issue (a cross-reference is raised by
|
|
175
|
+
# any mention, so a sibling task's PR would otherwise qualify). Default ON
|
|
176
|
+
# -- it closes the loop's one
|
|
177
|
+
# deliberate self-healing gap, and every side effect still honors dry_run.
|
|
178
|
+
# false restores the legacy two-signal matrix WHOLESALE on both edges (the
|
|
179
|
+
# floored defer, no liveness lookup, no resume): the escape hatch for an
|
|
180
|
+
# operator who wants a dead-with-PR lane to wait for a human.
|
|
181
|
+
resume_enabled: bool = True
|
|
182
|
+
|
|
164
183
|
dry_run: bool = False
|
|
165
184
|
|
|
166
185
|
# Derived at build time: `repos` casefolded for matching. GitHub names are
|
|
@@ -316,6 +335,9 @@ class Config:
|
|
|
316
335
|
maintain_enabled=bool(
|
|
317
336
|
raw.get("maintain_enabled", cls.maintain_enabled)
|
|
318
337
|
),
|
|
338
|
+
resume_enabled=bool(
|
|
339
|
+
raw.get("resume_enabled", cls.resume_enabled)
|
|
340
|
+
),
|
|
319
341
|
dry_run=bool(raw.get("dry_run", cls.dry_run)),
|
|
320
342
|
)
|
|
321
343
|
|
|
@@ -107,6 +107,13 @@ class PullRequest:
|
|
|
107
107
|
head_sha: str
|
|
108
108
|
draft: bool
|
|
109
109
|
url: str
|
|
110
|
+
# The PR description. Carried for PROVENANCE: a cross-reference proves
|
|
111
|
+
# only that a PR mentioned an issue, while a closing keyword ("Closes
|
|
112
|
+
# #<n>") is the author's claim that this PR is the issue's work -- the
|
|
113
|
+
# one the developer directives mandate, and the only thing that tells
|
|
114
|
+
# THIS issue's PR from a sibling task's PR that merely said "related to
|
|
115
|
+
# #<n>". Empty when the PR has no body.
|
|
116
|
+
body: str
|
|
110
117
|
# Logins/slugs whose review is CURRENTLY requested (users and teams
|
|
111
118
|
# merged): non-empty means the ball is in a reviewer's court, so the
|
|
112
119
|
# review-response edge is closed. GitHub clears an entry when that
|
|
@@ -536,9 +543,11 @@ class GitHub:
|
|
|
536
543
|
return newest
|
|
537
544
|
|
|
538
545
|
def pull_request(self, owner: str, repo: str, number: int) -> PullRequest:
|
|
539
|
-
"""The evaluate_pr() re-fetch: draft state, head, author, and
|
|
540
|
-
pending review requests -- the consistency guard over search
|
|
541
|
-
staleness, like issue() is for the issue edge.
|
|
546
|
+
"""The evaluate_pr() re-fetch: draft state, head, author, body, and
|
|
547
|
+
the pending review requests -- the consistency guard over search
|
|
548
|
+
staleness, like issue() is for the issue edge. (The body is what the
|
|
549
|
+
issue edge's resume probe reads for provenance; it rides along on the
|
|
550
|
+
same GET rather than costing a call of its own.)"""
|
|
542
551
|
data = self._api(f"repos/{owner}/{repo}/pulls/{number}") or {}
|
|
543
552
|
head = data.get("head") or {}
|
|
544
553
|
# Users carry `login`, teams `slug`; both hold the edge closed -- a
|
|
@@ -564,6 +573,8 @@ class GitHub:
|
|
|
564
573
|
head_sha=head.get("sha") or "",
|
|
565
574
|
draft=bool(data.get("draft")),
|
|
566
575
|
url=data.get("html_url", ""),
|
|
576
|
+
# `or ""`: GitHub sends null for an empty description.
|
|
577
|
+
body=data.get("body") or "",
|
|
567
578
|
requested_reviewers=requested,
|
|
568
579
|
)
|
|
569
580
|
|
|
@@ -619,7 +630,14 @@ class GitHub:
|
|
|
619
630
|
),
|
|
620
631
|
)
|
|
621
632
|
|
|
622
|
-
def linked_open_prs(
|
|
633
|
+
def linked_open_prs(
|
|
634
|
+
self,
|
|
635
|
+
owner: str,
|
|
636
|
+
repo: str,
|
|
637
|
+
number: int,
|
|
638
|
+
*,
|
|
639
|
+
exhaustive: bool = False,
|
|
640
|
+
) -> list[str]:
|
|
623
641
|
"""URLs of OPEN same-repo pull requests linked to the issue -- the
|
|
624
642
|
external liveness signal behind the stale-retry decision.
|
|
625
643
|
|
|
@@ -647,10 +665,9 @@ class GitHub:
|
|
|
647
665
|
comments, labeled/assigned/mentioned events, bot noise -- ahead of
|
|
648
666
|
the cross-reference, so a long pre-label discussion can push it past
|
|
649
667
|
the first page, and a first-page-only probe would mis-read live work
|
|
650
|
-
as dead (and respawn over it). The walk is bounded:
|
|
651
|
-
as a page has yielded any open same-repo PR (
|
|
652
|
-
|
|
653
|
-
comment's URL list), it stops at the
|
|
668
|
+
as dead (and respawn over it). The walk is bounded: by default it
|
|
669
|
+
stops as soon as a page has yielded any open same-repo PR (see
|
|
670
|
+
`exhaustive` below), it stops at the
|
|
654
671
|
first short page (the timeline is exhausted) and hard-caps at
|
|
655
672
|
TIMELINE_MAX_PAGES pages (TIMELINE_MAX_PAGES * TIMELINE_PAGE_SIZE
|
|
656
673
|
events) -- a cross-reference past that ceiling is invisible to the
|
|
@@ -660,6 +677,21 @@ class GitHub:
|
|
|
660
677
|
explicit `page=` loop is the safe shape for this client. Malformed
|
|
661
678
|
events are skipped, never fatal -- one odd timeline entry must not
|
|
662
679
|
wedge the staleness decision.
|
|
680
|
+
|
|
681
|
+
`exhaustive` picks WHICH question the walk answers. Default False is
|
|
682
|
+
the boolean one -- is ANY open same-repo PR linked? -- and stops at
|
|
683
|
+
the first page that yields a URL, because for that question later
|
|
684
|
+
pages can only lengthen a log message. True keeps walking to the
|
|
685
|
+
short page (or the TIMELINE_MAX_PAGES ceiling) and returns the
|
|
686
|
+
COMPLETE set within it: the issue edge's resume probe does not ask
|
|
687
|
+
whether a PR is linked but WHICH one, and it deliberately refuses to
|
|
688
|
+
act on more than one candidate -- against a truncated list that
|
|
689
|
+
refusal is unreachable, because "exactly one survivor" can just mean
|
|
690
|
+
the second was on the next page, and the daemon would push to the one
|
|
691
|
+
it happened to page in. The ceiling applies to both modes: a
|
|
692
|
+
cross-reference past TIMELINE_MAX_PAGES * TIMELINE_PAGE_SIZE events
|
|
693
|
+
is invisible either way, so `exhaustive` means complete UP TO the
|
|
694
|
+
documented ceiling, not unbounded.
|
|
663
695
|
"""
|
|
664
696
|
same_repo_pull = f"https://github.com/{owner}/{repo}/pull/"
|
|
665
697
|
urls: list[str] = []
|
|
@@ -696,11 +728,13 @@ class GitHub:
|
|
|
696
728
|
if not url.startswith(same_repo_pull):
|
|
697
729
|
continue # a mention from another repo says nothing here
|
|
698
730
|
urls.append(url)
|
|
699
|
-
if urls:
|
|
700
|
-
# The caller's question is boolean -- is ANY open
|
|
701
|
-
# PR linked? -- so once a page yields one, later
|
|
702
|
-
# only add more URLs to a message, never change the
|
|
703
|
-
# decision: stop paying for them.
|
|
731
|
+
if urls and not exhaustive:
|
|
732
|
+
# The DEFAULT caller's question is boolean -- is ANY open
|
|
733
|
+
# same-repo PR linked? -- so once a page yields one, later
|
|
734
|
+
# pages can only add more URLs to a message, never change the
|
|
735
|
+
# decision: stop paying for them. An `exhaustive` caller is
|
|
736
|
+
# asking which PR, not whether, and for THAT question the
|
|
737
|
+
# later pages are the whole point -- so it keeps walking.
|
|
704
738
|
break
|
|
705
739
|
if len(events) < TIMELINE_PAGE_SIZE:
|
|
706
740
|
break # short page: the timeline is exhausted
|