alissa-tools-github-devloop 0.8.31__tar.gz → 0.8.32__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.31/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.32}/PKG-INFO +53 -16
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/README.md +52 -15
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/ghclient.py +61 -9
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/loop.py +564 -86
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/loop_events.py +47 -16
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/prompts.py +140 -5
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/state.py +56 -0
- alissa_tools_github_devloop-0.8.32/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +53 -16
- alissa_tools_github_devloop-0.8.31/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/setup.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/bows.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/config.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/fleet_vitals.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/hubs.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.31 → alissa_tools_github_devloop-0.8.32}/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.8.
|
|
3
|
+
Version: 0.8.32
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -72,10 +72,14 @@ pane conditions as the ACCOUNT's, not the worker's — nothing a keystroke can
|
|
|
72
72
|
answer: `login_expired`, `auth_rejected` (a 401), `usage_limit` and
|
|
73
73
|
`out_of_credits`. Each pages the operator once per kind per 6 h on the lane's
|
|
74
74
|
issue/PR and holds every new spawn while it stands (at most 1 h on one pane,
|
|
75
|
-
then kill-and-retry re-confirms)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
then kill-and-retry re-confirms) — except `usage_limit`, whose session is
|
|
76
|
+
never killed and whose hold is the ledger's (below).
|
|
77
|
+
|
|
78
|
+
**Usage limits** (issues #142, #144). `usage_limit` admits the window wordings
|
|
79
|
+
(`You've hit your weekly limit`, `Usage limit reached`, …), the "limit
|
|
80
|
+
reached … resets …" family (`Claude usage limit reached. Your limit will
|
|
81
|
+
reset at 5pm (America/New_York).`, `5-hour limit reached ∙ resets 3pm`,
|
|
82
|
+
`Opus weekly limit reached ∙ resets Oct 6, 1pm`) and the
|
|
79
83
|
MODEL-named one Claude Code prints now — `You've reached your Fable limit.
|
|
80
84
|
Run /usage-credits to continue or switch models with /model.` (also `Opus`,
|
|
81
85
|
`Sonnet`, …): one capitalised model token, a known family name or any token
|
|
@@ -88,28 +92,61 @@ this ledger inside the last `stale_minutes`): the notice, above an idle
|
|
|
88
92
|
prompt, with no other `●` turn since the directive. A match
|
|
89
93
|
(`source: "first_turn"`, `matched: "model_limit"`, the model) is:
|
|
90
94
|
|
|
91
|
-
- **held** — the daemon-wide spawn hold, clocked on the ledger
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
- **held** — the daemon-wide spawn hold, clocked on the ledger from the
|
|
96
|
+
newest limit (it survives a restart and a listing failure) and lifted only
|
|
97
|
+
when a spawn made AFTER the limit gets through its first turn: after 1 h
|
|
98
|
+
ONE probe spawn is admitted at a time (none while a probe is in flight,
|
|
99
|
+
none while another account notice holds; under `max_sessions` the probe
|
|
100
|
+
is judged without the kept limited sessions, which would otherwise fill the
|
|
101
|
+
cap for good); a probe whose first turn is the notice again is refunded,
|
|
102
|
+
re-arms the clock and is **killed** — it did no work, and keeping it would
|
|
103
|
+
add one session per hour of the limit (past `max_sessions` too), so the
|
|
104
|
+
live sessions stay at the ones the limit found at work plus at most the one
|
|
105
|
+
probe in flight; the first probe that does
|
|
106
|
+
real work lifts the hold (`usage-limit-lifted:` on the escalation ledger,
|
|
107
|
+
`escalation.usage_limit_lifted` on the wire) and each kept limited
|
|
108
|
+
session's lane gets a line saying to resume it;
|
|
95
109
|
- **paged** — once per 6 h window, through the account-kind page;
|
|
96
|
-
- **killed**
|
|
110
|
+
- **never killed** (issue #144; a failed probe, above, is the one exception) —
|
|
111
|
+
a retry would hit the same wall, and the idle session is the lane's own work for the operator to resume once the
|
|
112
|
+
account is back (or `alissa tmux kill`, to let the retry edge respawn);
|
|
113
|
+
the wedge kills skip a limited session too;
|
|
97
114
|
- **refunded** — the spawn row is stamped `limited_at`, and every
|
|
98
115
|
`attempt_cap` count leaves limited rows out (numbering stays monotonic, so
|
|
99
116
|
session names never repeat). An account over its limit never caps a lane.
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
A usage-limit BANNER on a working session (`source: "pane"`) is held the same
|
|
119
|
+
way: its row is stamped (refunded), the hold is the ledger's, and the session
|
|
120
|
+
is never killed. The same notice after real worker output (a tool call, the
|
|
121
|
+
worker's own prose, a quoted tool row) stays worker output. Each limited
|
|
122
|
+
session is one `usage.limited` loop event: `data: { kind: "usage_limit",
|
|
123
|
+
source: "pane" | "first_turn", matched: "model_limit" | "usage_limit", model,
|
|
124
|
+
resetsAt? }` — `resetsAt` only when the notice states a reset, as an ISO-8601
|
|
125
|
+
UTC instant when that needs no guess (the legacy `|<epoch>` form, or a clock
|
|
126
|
+
time with an explicit zone) and as the stated phrase otherwise; no pane text,
|
|
127
|
+
no credential material.
|
|
106
128
|
|
|
107
129
|
**The fix edge stops presuming.** A stale fix round with no triage reply from
|
|
108
130
|
the author used to be re-enqueued on the timer alone ("presumed dead"). It now
|
|
109
131
|
reads the tmux roster first: ALIVE is in flight (the responder owns a live
|
|
110
132
|
idle pane; a once-per-episode "Fix loop stalled?" ping floors the wait),
|
|
111
133
|
INDETERMINATE defers one pass, and only DEAD respawns — "CONFIRMED dead", or,
|
|
112
|
-
after a refunded limit, "previous session limited (
|
|
134
|
+
after a refunded limit, "previous session limited (usage limit; attempt
|
|
135
|
+
refunded)". A kept limited session reads ALIVE, so no edge respawns over it.
|
|
136
|
+
|
|
137
|
+
## The re-enqueue guard (issue #144)
|
|
138
|
+
|
|
139
|
+
The issue edge's respawn rule reads the linked-PR signal over **every** PR
|
|
140
|
+
state: `GitHub.linked_pull_requests` walks the timeline once and reports each
|
|
141
|
+
same-repo cross-referenced PR as open, merged or closed. When a stale
|
|
142
|
+
attempt's session is CONFIRMED dead with no open PR, a merged PR of the
|
|
143
|
+
daemon's own that closes the issue is `SKIPPED` "finished (PR #n merged)" and
|
|
144
|
+
a closed-unmerged one is `SKIPPED` "closed unmerged — orchestrator's page" —
|
|
145
|
+
never a respawn (provenance: token author and a closing keyword, fetched; an
|
|
146
|
+
unreadable candidate stands the respawn down). And `_spawn` re-reads the
|
|
147
|
+
issue immediately before every enqueue: a closed issue is `SKIPPED` "issue
|
|
148
|
+
closed between search and spawn", nothing is claimed, and the reserved slot
|
|
149
|
+
is handed back. The respawn-with-resume on an open draft PR is unchanged.
|
|
113
150
|
|
|
114
151
|
## License
|
|
115
152
|
|
|
@@ -50,10 +50,14 @@ pane conditions as the ACCOUNT's, not the worker's — nothing a keystroke can
|
|
|
50
50
|
answer: `login_expired`, `auth_rejected` (a 401), `usage_limit` and
|
|
51
51
|
`out_of_credits`. Each pages the operator once per kind per 6 h on the lane's
|
|
52
52
|
issue/PR and holds every new spawn while it stands (at most 1 h on one pane,
|
|
53
|
-
then kill-and-retry re-confirms)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
then kill-and-retry re-confirms) — except `usage_limit`, whose session is
|
|
54
|
+
never killed and whose hold is the ledger's (below).
|
|
55
|
+
|
|
56
|
+
**Usage limits** (issues #142, #144). `usage_limit` admits the window wordings
|
|
57
|
+
(`You've hit your weekly limit`, `Usage limit reached`, …), the "limit
|
|
58
|
+
reached … resets …" family (`Claude usage limit reached. Your limit will
|
|
59
|
+
reset at 5pm (America/New_York).`, `5-hour limit reached ∙ resets 3pm`,
|
|
60
|
+
`Opus weekly limit reached ∙ resets Oct 6, 1pm`) and the
|
|
57
61
|
MODEL-named one Claude Code prints now — `You've reached your Fable limit.
|
|
58
62
|
Run /usage-credits to continue or switch models with /model.` (also `Opus`,
|
|
59
63
|
`Sonnet`, …): one capitalised model token, a known family name or any token
|
|
@@ -66,28 +70,61 @@ this ledger inside the last `stale_minutes`): the notice, above an idle
|
|
|
66
70
|
prompt, with no other `●` turn since the directive. A match
|
|
67
71
|
(`source: "first_turn"`, `matched: "model_limit"`, the model) is:
|
|
68
72
|
|
|
69
|
-
- **held** — the daemon-wide spawn hold, clocked on the ledger
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
- **held** — the daemon-wide spawn hold, clocked on the ledger from the
|
|
74
|
+
newest limit (it survives a restart and a listing failure) and lifted only
|
|
75
|
+
when a spawn made AFTER the limit gets through its first turn: after 1 h
|
|
76
|
+
ONE probe spawn is admitted at a time (none while a probe is in flight,
|
|
77
|
+
none while another account notice holds; under `max_sessions` the probe
|
|
78
|
+
is judged without the kept limited sessions, which would otherwise fill the
|
|
79
|
+
cap for good); a probe whose first turn is the notice again is refunded,
|
|
80
|
+
re-arms the clock and is **killed** — it did no work, and keeping it would
|
|
81
|
+
add one session per hour of the limit (past `max_sessions` too), so the
|
|
82
|
+
live sessions stay at the ones the limit found at work plus at most the one
|
|
83
|
+
probe in flight; the first probe that does
|
|
84
|
+
real work lifts the hold (`usage-limit-lifted:` on the escalation ledger,
|
|
85
|
+
`escalation.usage_limit_lifted` on the wire) and each kept limited
|
|
86
|
+
session's lane gets a line saying to resume it;
|
|
73
87
|
- **paged** — once per 6 h window, through the account-kind page;
|
|
74
|
-
- **killed**
|
|
88
|
+
- **never killed** (issue #144; a failed probe, above, is the one exception) —
|
|
89
|
+
a retry would hit the same wall, and the idle session is the lane's own work for the operator to resume once the
|
|
90
|
+
account is back (or `alissa tmux kill`, to let the retry edge respawn);
|
|
91
|
+
the wedge kills skip a limited session too;
|
|
75
92
|
- **refunded** — the spawn row is stamped `limited_at`, and every
|
|
76
93
|
`attempt_cap` count leaves limited rows out (numbering stays monotonic, so
|
|
77
94
|
session names never repeat). An account over its limit never caps a lane.
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
A usage-limit BANNER on a working session (`source: "pane"`) is held the same
|
|
97
|
+
way: its row is stamped (refunded), the hold is the ledger's, and the session
|
|
98
|
+
is never killed. The same notice after real worker output (a tool call, the
|
|
99
|
+
worker's own prose, a quoted tool row) stays worker output. Each limited
|
|
100
|
+
session is one `usage.limited` loop event: `data: { kind: "usage_limit",
|
|
101
|
+
source: "pane" | "first_turn", matched: "model_limit" | "usage_limit", model,
|
|
102
|
+
resetsAt? }` — `resetsAt` only when the notice states a reset, as an ISO-8601
|
|
103
|
+
UTC instant when that needs no guess (the legacy `|<epoch>` form, or a clock
|
|
104
|
+
time with an explicit zone) and as the stated phrase otherwise; no pane text,
|
|
105
|
+
no credential material.
|
|
84
106
|
|
|
85
107
|
**The fix edge stops presuming.** A stale fix round with no triage reply from
|
|
86
108
|
the author used to be re-enqueued on the timer alone ("presumed dead"). It now
|
|
87
109
|
reads the tmux roster first: ALIVE is in flight (the responder owns a live
|
|
88
110
|
idle pane; a once-per-episode "Fix loop stalled?" ping floors the wait),
|
|
89
111
|
INDETERMINATE defers one pass, and only DEAD respawns — "CONFIRMED dead", or,
|
|
90
|
-
after a refunded limit, "previous session limited (
|
|
112
|
+
after a refunded limit, "previous session limited (usage limit; attempt
|
|
113
|
+
refunded)". A kept limited session reads ALIVE, so no edge respawns over it.
|
|
114
|
+
|
|
115
|
+
## The re-enqueue guard (issue #144)
|
|
116
|
+
|
|
117
|
+
The issue edge's respawn rule reads the linked-PR signal over **every** PR
|
|
118
|
+
state: `GitHub.linked_pull_requests` walks the timeline once and reports each
|
|
119
|
+
same-repo cross-referenced PR as open, merged or closed. When a stale
|
|
120
|
+
attempt's session is CONFIRMED dead with no open PR, a merged PR of the
|
|
121
|
+
daemon's own that closes the issue is `SKIPPED` "finished (PR #n merged)" and
|
|
122
|
+
a closed-unmerged one is `SKIPPED` "closed unmerged — orchestrator's page" —
|
|
123
|
+
never a respawn (provenance: token author and a closing keyword, fetched; an
|
|
124
|
+
unreadable candidate stands the respawn down). And `_spawn` re-reads the
|
|
125
|
+
issue immediately before every enqueue: a closed issue is `SKIPPED` "issue
|
|
126
|
+
closed between search and spawn", nothing is claimed, and the reserved slot
|
|
127
|
+
is handed back. The respawn-with-resume on an open draft PR is unchanged.
|
|
91
128
|
|
|
92
129
|
## License
|
|
93
130
|
|
|
@@ -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.
|