alissa-tools-github-devloop 0.8.31__py3-none-any.whl → 0.8.33__py3-none-any.whl
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/ghclient.py +61 -9
- alissa/tools/github/devloop/loop.py +564 -86
- alissa/tools/github/devloop/loop_events.py +47 -16
- alissa/tools/github/devloop/prompts.py +140 -5
- alissa/tools/github/devloop/state.py +56 -0
- alissa/tools/github/devloop/version +1 -1
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/METADATA +53 -16
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/RECORD +13 -13
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/WHEEL +0 -0
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/licenses/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/licenses/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.31.dist-info → alissa_tools_github_devloop-0.8.33.dist-info}/top_level.txt +0 -0
|
@@ -53,6 +53,13 @@ FIRST_PARENT_MAX_PAGES = 3
|
|
|
53
53
|
SUBMITTED_STATES = {"APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED"}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
# The states `GitHub.linked_pull_requests` reports a cross-referenced pull request in
|
|
57
|
+
# (issue #144): still open, merged, or closed without a merge.
|
|
58
|
+
PR_OPEN = "open"
|
|
59
|
+
PR_MERGED = "merged"
|
|
60
|
+
PR_CLOSED = "closed"
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def parse_github_timestamp(ts: "str | None") -> "int | None":
|
|
57
64
|
"""A GitHub API timestamp (`2026-07-23T02:13:56Z`) as unix seconds, or
|
|
58
65
|
None when absent/unparseable. GitHub emits UTC `Z` timestamps
|
|
@@ -1325,8 +1332,45 @@ class GitHub:
|
|
|
1325
1332
|
is invisible either way, so `exhaustive` means complete UP TO the
|
|
1326
1333
|
documented ceiling, not unbounded.
|
|
1327
1334
|
"""
|
|
1335
|
+
return [
|
|
1336
|
+
url
|
|
1337
|
+
for url, state in self.linked_pull_requests(
|
|
1338
|
+
owner, repo, number, exhaustive=exhaustive
|
|
1339
|
+
)
|
|
1340
|
+
if state == PR_OPEN
|
|
1341
|
+
]
|
|
1342
|
+
|
|
1343
|
+
def linked_pull_requests(
|
|
1344
|
+
self,
|
|
1345
|
+
owner: str,
|
|
1346
|
+
repo: str,
|
|
1347
|
+
number: int,
|
|
1348
|
+
*,
|
|
1349
|
+
exhaustive: bool = False,
|
|
1350
|
+
) -> "list[tuple[str, str]]":
|
|
1351
|
+
"""Every same-repo pull request the issue's timeline cross-references,
|
|
1352
|
+
in ANY state, as `(url, state)` pairs -- `state` is `PR_OPEN`,
|
|
1353
|
+
`PR_MERGED` or `PR_CLOSED` (closed without a merge). The one walk
|
|
1354
|
+
behind `linked_open_prs` (which filters it to the open ones) and the
|
|
1355
|
+
issue edge's respawn rule (issue #144, design studio.alissa.app
|
|
1356
|
+
docs/design/loop-operator-surface.md §8.4): a DEAD session's lane
|
|
1357
|
+
with no open PR is only an implement-phase death when no MERGED or
|
|
1358
|
+
closed PR claims the issue either -- 2026-09-28, an operator's hand
|
|
1359
|
+
merge closed issue #1620 two seconds before the daemon read "no PR
|
|
1360
|
+
opened" off the open-only probe and spawned a duplicate lane.
|
|
1361
|
+
|
|
1362
|
+
Merged is read off the cross-reference's own payload
|
|
1363
|
+
(`source.issue.pull_request.merged_at`), so the walk costs nothing
|
|
1364
|
+
more than it did; the caller still fetches a candidate before acting
|
|
1365
|
+
on it, because a cross-reference proves only a MENTION (provenance is
|
|
1366
|
+
the fetched body's closing keyword and author). The walk, its bounds,
|
|
1367
|
+
its same-repo filter and the `exhaustive` switch are exactly
|
|
1368
|
+
`linked_open_prs`'s: the default stops at the first page that yields
|
|
1369
|
+
an OPEN PR (closed ones never stop it), so a lane with no open PR
|
|
1370
|
+
walks the whole timeline -- up to TIMELINE_MAX_PAGES -- and sees
|
|
1371
|
+
every closed one."""
|
|
1328
1372
|
same_repo_pull = f"https://github.com/{owner}/{repo}/pull/"
|
|
1329
|
-
|
|
1373
|
+
found: "dict[str, str]" = {}
|
|
1330
1374
|
for page in range(1, TIMELINE_MAX_PAGES + 1):
|
|
1331
1375
|
events = (
|
|
1332
1376
|
self._api(
|
|
@@ -1352,15 +1396,25 @@ class GitHub:
|
|
|
1352
1396
|
src = source.get("issue") if isinstance(source, dict) else None
|
|
1353
1397
|
if not isinstance(src, dict):
|
|
1354
1398
|
continue
|
|
1355
|
-
|
|
1399
|
+
pull = src.get("pull_request")
|
|
1400
|
+
if not pull:
|
|
1356
1401
|
continue # the cross-reference came from a plain issue
|
|
1357
|
-
if src.get("state") != "open":
|
|
1358
|
-
continue
|
|
1359
1402
|
url = src.get("html_url") or ""
|
|
1360
1403
|
if not url.startswith(same_repo_pull):
|
|
1361
1404
|
continue # a mention from another repo says nothing here
|
|
1362
|
-
|
|
1363
|
-
|
|
1405
|
+
if src.get("state") == "open":
|
|
1406
|
+
state = PR_OPEN
|
|
1407
|
+
elif isinstance(pull, dict) and pull.get("merged_at"):
|
|
1408
|
+
state = PR_MERGED
|
|
1409
|
+
elif src.get("state") == "closed":
|
|
1410
|
+
state = PR_CLOSED
|
|
1411
|
+
else:
|
|
1412
|
+
continue # no readable state: never guess
|
|
1413
|
+
# One PR can cross-reference the issue more than once (body
|
|
1414
|
+
# edit, comment mention); report it once, in its first
|
|
1415
|
+
# position -- the state is the PR's, not the event's.
|
|
1416
|
+
found.setdefault(url, state)
|
|
1417
|
+
if not exhaustive and PR_OPEN in found.values():
|
|
1364
1418
|
# The DEFAULT caller's question is boolean -- is ANY open
|
|
1365
1419
|
# same-repo PR linked? -- so once a page yields one, later
|
|
1366
1420
|
# pages can only add more URLs to a message, never change the
|
|
@@ -1370,9 +1424,7 @@ class GitHub:
|
|
|
1370
1424
|
break
|
|
1371
1425
|
if len(events) < TIMELINE_PAGE_SIZE:
|
|
1372
1426
|
break # short page: the timeline is exhausted
|
|
1373
|
-
|
|
1374
|
-
# comment mention); report it once.
|
|
1375
|
-
return list(dict.fromkeys(urls))
|
|
1427
|
+
return list(found.items())
|
|
1376
1428
|
|
|
1377
1429
|
def assign_self(self, owner: str, repo: str, number: int) -> None:
|
|
1378
1430
|
"""Assign the token identity -- the server-side in-flight marker.
|