alissa-tools-github-devloop 0.5.1__tar.gz → 0.6.1__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.1}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/__main__.py +42 -1
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/config.py +40 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/ghclient.py +70 -13
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/loop.py +1225 -165
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/state.py +61 -0
- alissa_tools_github_devloop-0.6.1/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/sources.py +2 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1/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.1}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/README.md +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/setup.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.1}/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.1}/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.1}/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.1}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -147,6 +147,42 @@ 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
|
+
|
|
168
|
+
claims = over.add_mutually_exclusive_group()
|
|
169
|
+
claims.add_argument(
|
|
170
|
+
"--activity-claims",
|
|
171
|
+
dest="activity_claims_enabled",
|
|
172
|
+
action="store_true",
|
|
173
|
+
default=None,
|
|
174
|
+
help="read the trigger artifact's activity comment for another "
|
|
175
|
+
"instance's fresh spawn line and defer on it, so spawn dedupe "
|
|
176
|
+
"survives a ledger the local instance never wrote (the default)",
|
|
177
|
+
)
|
|
178
|
+
claims.add_argument(
|
|
179
|
+
"--no-activity-claims",
|
|
180
|
+
dest="activity_claims_enabled",
|
|
181
|
+
action="store_false",
|
|
182
|
+
help="ledger-only spawn dedupe: never read external claim evidence "
|
|
183
|
+
"(two overlapping instances can then both spawn on one item)",
|
|
184
|
+
)
|
|
185
|
+
|
|
150
186
|
dry = over.add_mutually_exclusive_group()
|
|
151
187
|
dry.add_argument(
|
|
152
188
|
"--dry-run",
|
|
@@ -184,6 +220,8 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
184
220
|
"fix_rounds_enabled": args.fix_rounds_enabled,
|
|
185
221
|
"maintain_label": args.maintain_label,
|
|
186
222
|
"maintain_enabled": args.maintain_enabled,
|
|
223
|
+
"resume_enabled": args.resume_enabled,
|
|
224
|
+
"activity_claims_enabled": args.activity_claims_enabled,
|
|
187
225
|
"dry_run": args.dry_run,
|
|
188
226
|
}
|
|
189
227
|
|
|
@@ -215,12 +253,15 @@ def log_effective_config(config: Config, login: str) -> None:
|
|
|
215
253
|
log.info("reviewers: %s", ", ".join(config.reviewers) or "none")
|
|
216
254
|
log.info(
|
|
217
255
|
"poll every %ss; dry_run=%s; attempt_cap=%s; stale after %s min; "
|
|
218
|
-
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r)"
|
|
256
|
+
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r); resume=%s; "
|
|
257
|
+
"activity_claims=%s",
|
|
219
258
|
config.poll_interval, config.dry_run, config.attempt_cap,
|
|
220
259
|
config.stale_minutes,
|
|
221
260
|
config.max_sessions or "unlimited",
|
|
222
261
|
config.fix_rounds_enabled,
|
|
223
262
|
config.maintain_enabled, config.maintain_label,
|
|
263
|
+
config.resume_enabled,
|
|
264
|
+
config.activity_claims_enabled,
|
|
224
265
|
)
|
|
225
266
|
log.debug("hub_template: %s", config.hub_template)
|
|
226
267
|
log.debug("agent_profile: %s", config.agent_profile)
|
|
@@ -62,6 +62,8 @@ CONFIG_KEYS = (
|
|
|
62
62
|
"fix_rounds_enabled",
|
|
63
63
|
"maintain_label",
|
|
64
64
|
"maintain_enabled",
|
|
65
|
+
"resume_enabled",
|
|
66
|
+
"activity_claims_enabled",
|
|
65
67
|
"dry_run",
|
|
66
68
|
)
|
|
67
69
|
|
|
@@ -161,6 +163,38 @@ class Config:
|
|
|
161
163
|
maintain_label: str = "alissa:maintain"
|
|
162
164
|
maintain_enabled: bool = True
|
|
163
165
|
|
|
166
|
+
# The third staleness signal (session liveness, read from the reaper's own
|
|
167
|
+
# tmux listing) and the auto-resume it unlocks: a stale attempt whose
|
|
168
|
+
# ledger session is provably gone is respawned with RESUME_DIRECTIVE /
|
|
169
|
+
# FIX_RESUME_DIRECTIVE instead of parking the lane behind an operator
|
|
170
|
+
# ping. Provably gone is necessary, not sufficient: a healthy handoff ends
|
|
171
|
+
# in `alissa tmux kill` too, so on the ISSUE edge the resume additionally
|
|
172
|
+
# requires the linked PR to show the handoff never happened AND to be this
|
|
173
|
+
# issue's own work -- exactly one linked open PR, still a DRAFT, no
|
|
174
|
+
# reviewer requested, authored by the token identity, daemon-shaped head
|
|
175
|
+
# ref, and a body that CLOSES this issue (a cross-reference is raised by
|
|
176
|
+
# any mention, so a sibling task's PR would otherwise qualify). Default ON
|
|
177
|
+
# -- it closes the loop's one
|
|
178
|
+
# deliberate self-healing gap, and every side effect still honors dry_run.
|
|
179
|
+
# false restores the legacy two-signal matrix WHOLESALE on both edges (the
|
|
180
|
+
# floored defer, no liveness lookup, no resume): the escape hatch for an
|
|
181
|
+
# operator who wants a dead-with-PR lane to wait for a human.
|
|
182
|
+
resume_enabled: bool = True
|
|
183
|
+
|
|
184
|
+
# Cross-instance spawn dedupe (issue #53): before spawning on any edge,
|
|
185
|
+
# read the trigger artifact's mechanical activity comment and treat a
|
|
186
|
+
# spawn line younger than stale_minutes as an in-flight claim EVEN when
|
|
187
|
+
# the local ledger has no row. The ledger is instance-local and its volume
|
|
188
|
+
# follows the active deployment, so during a deploy overlap or a
|
|
189
|
+
# crashloop two daemons run with one GitHub identity and two ledgers and
|
|
190
|
+
# both spawn; the activity comment is the one claim record either can
|
|
191
|
+
# read. Default ON -- it closes a thrice-observed duplicate-PR incident,
|
|
192
|
+
# it can only ever DEFER a spawn, and a claim expires with the same
|
|
193
|
+
# stale_minutes window a ledger row does. false restores the
|
|
194
|
+
# ledger-only dedupe: the escape hatch for a single-instance deployment
|
|
195
|
+
# that would rather not pay the extra comments fetch.
|
|
196
|
+
activity_claims_enabled: bool = True
|
|
197
|
+
|
|
164
198
|
dry_run: bool = False
|
|
165
199
|
|
|
166
200
|
# Derived at build time: `repos` casefolded for matching. GitHub names are
|
|
@@ -316,6 +350,12 @@ class Config:
|
|
|
316
350
|
maintain_enabled=bool(
|
|
317
351
|
raw.get("maintain_enabled", cls.maintain_enabled)
|
|
318
352
|
),
|
|
353
|
+
resume_enabled=bool(
|
|
354
|
+
raw.get("resume_enabled", cls.resume_enabled)
|
|
355
|
+
),
|
|
356
|
+
activity_claims_enabled=bool(
|
|
357
|
+
raw.get("activity_claims_enabled", cls.activity_claims_enabled)
|
|
358
|
+
),
|
|
319
359
|
dry_run=bool(raw.get("dry_run", cls.dry_run)),
|
|
320
360
|
)
|
|
321
361
|
|
|
@@ -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
|
|
@@ -787,6 +821,29 @@ class GitHub:
|
|
|
787
821
|
if isinstance(c, dict)
|
|
788
822
|
]
|
|
789
823
|
|
|
824
|
+
def marked_comment(
|
|
825
|
+
self, owner: str, repo: str, number: int, marker: str
|
|
826
|
+
) -> "IssueComment | None":
|
|
827
|
+
"""THE own-authored comment carrying `marker` on this issue/PR, or None.
|
|
828
|
+
|
|
829
|
+
The find half of the activity comment's find-or-create, lifted out of
|
|
830
|
+
the writer so the READER shares one definition of "the daemon's
|
|
831
|
+
activity comment" with it -- a marker/author filter that drifted
|
|
832
|
+
between the two would let a spawn append to one comment while the
|
|
833
|
+
cross-instance claim check read another.
|
|
834
|
+
|
|
835
|
+
Both halves of the filter are load-bearing. The marker names the
|
|
836
|
+
mechanical log (an operator page on the same artifact must never be
|
|
837
|
+
mistaken for it), and OWN AUTHORSHIP is what keeps a marker anyone can
|
|
838
|
+
paste from being PATCHed by the writer -- or, for the reader, from
|
|
839
|
+
being able to park a lane by spoofing a claim line. First match wins,
|
|
840
|
+
matching the writer's `mine[0]`.
|
|
841
|
+
"""
|
|
842
|
+
for comment in self.issue_comments(owner, repo, number):
|
|
843
|
+
if comment.author == self.login and marker in comment.body:
|
|
844
|
+
return comment
|
|
845
|
+
return None
|
|
846
|
+
|
|
790
847
|
def update_comment(
|
|
791
848
|
self, owner: str, repo: str, comment_id: int, body: str
|
|
792
849
|
) -> None:
|