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.
Files changed (29) hide show
  1. {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
  2. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/__main__.py +21 -1
  3. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/config.py +22 -0
  4. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/ghclient.py +47 -13
  5. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/loop.py +853 -127
  6. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/state.py +36 -0
  7. alissa_tools_github_devloop-0.6.0/src/main/alissa/tools/github/devloop/version +1 -0
  8. {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
  9. {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
  10. alissa_tools_github_devloop-0.5.1/src/main/alissa/tools/github/devloop/version +0 -1
  11. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/MANIFEST.in +0 -0
  12. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/README.md +0 -0
  13. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/requirements.txt +0 -0
  14. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/setup.cfg +0 -0
  15. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/setup.py +0 -0
  16. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
  17. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
  18. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/proc.py +0 -0
  19. {alissa_tools_github_devloop-0.5.1 → alissa_tools_github_devloop-0.6.0}/src/main/alissa/tools/github/devloop/version.py +0 -0
  20. {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
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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 the
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(self, owner: str, repo: str, number: int) -> list[str]:
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: it stops as soon
651
- as a page has yielded any open same-repo PR (every caller's question
652
- is effectively boolean -- further pages could only lengthen a
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 same-repo
701
- # PR linked? -- so once a page yields one, later pages can
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