alissa-tools-github-devloop 0.8.14__tar.gz → 0.8.15__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.14/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.15}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.8.15/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/page.py +40 -17
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/sources.py +114 -7
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.8.14/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/README.md +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/setup.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/bows.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/config.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/loop.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/state.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.14 → alissa_tools_github_devloop-0.8.15}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.8.15
|
|
@@ -231,6 +231,11 @@ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
|
231
231
|
.inbox-item { display: flex; align-items: baseline; justify-content: space-between;
|
|
232
232
|
gap: 0.75rem; padding: 0.55rem 0; border-top: 1px solid var(--surface-border); }
|
|
233
233
|
.inbox-kind { font-family: var(--mono); font-size: 0.75rem; color: var(--status-blocked); }
|
|
234
|
+
.inbox-settled { border-top: 1px solid var(--surface-border); margin-top: 0.55rem; }
|
|
235
|
+
.inbox-settled > summary { cursor: pointer; padding: 0.55rem 0; font-family: var(--mono);
|
|
236
|
+
font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
|
|
237
|
+
.inbox-settled .inbox-item { opacity: 0.55; }
|
|
238
|
+
.inbox-settled .inbox-item .inbox-kind { color: var(--text-muted); }
|
|
234
239
|
.task-title { flex: 1; margin: 0 0.6rem; overflow: hidden;
|
|
235
240
|
text-overflow: ellipsis; white-space: nowrap; }
|
|
236
241
|
.log {
|
|
@@ -450,22 +455,40 @@ _JS = r"""
|
|
|
450
455
|
el('pipeline').innerHTML = out;
|
|
451
456
|
}
|
|
452
457
|
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
458
|
+
function inboxRow(it) {
|
|
459
|
+
// A row with no url has no GitHub artifact behind it (a foreign-session
|
|
460
|
+
// page: the subject is a tmux session, and its repo_slug/number are
|
|
461
|
+
// placeholders). Rendering the anchor anyway would link to
|
|
462
|
+
// github.com/-/issues/0, so the kind — which already names the session —
|
|
463
|
+
// stands alone.
|
|
464
|
+
var target = it.url
|
|
465
|
+
? '<a href="' + esc(it.url) + '" target="_blank" rel="noopener">' +
|
|
466
|
+
esc(it.repo_slug) + (it.is_pr ? '#pr' : '#') + esc(it.number) + '</a>'
|
|
467
|
+
: '<span class="muted">no linked artifact</span>';
|
|
468
|
+
return '<div class="inbox-item"><span>' +
|
|
469
|
+
'<span class="inbox-kind">' + esc(it.kind) + '</span> ' + target +
|
|
470
|
+
'</span><span class="muted num">' + dur(it.age_seconds) + '</span></div>';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function renderInbox(items, settled, settledCount) {
|
|
474
|
+
// Live rows are the inbox; "Inbox clear." is their empty state alone.
|
|
475
|
+
// Settled rows (the artifact left the poll's candidate set -- exhaust,
|
|
476
|
+
// not backlog) sit behind a collapsed footer so nothing is lost, only
|
|
477
|
+
// demoted. The footer stays open across the 10s re-render once opened.
|
|
478
|
+
var wasOpen = false;
|
|
479
|
+
var prior = el('inbox').querySelector('details.inbox-settled');
|
|
480
|
+
if (prior && prior.open) { wasOpen = true; }
|
|
481
|
+
var out = items.length ? items.map(inboxRow).join('') : '<div class="empty">Inbox clear.</div>';
|
|
482
|
+
if (settledCount) {
|
|
483
|
+
out += '<details class="inbox-settled"' + (wasOpen ? ' open' : '') + '>' +
|
|
484
|
+
'<summary>' + esc(settledCount) + ' settled — show</summary>' +
|
|
485
|
+
settled.map(inboxRow).join('') +
|
|
486
|
+
(settledCount > settled.length
|
|
487
|
+
? '<div class="empty">' + esc(settledCount - settled.length) + ' older settled pages not listed</div>'
|
|
488
|
+
: '') +
|
|
489
|
+
'</details>';
|
|
490
|
+
}
|
|
491
|
+
el('inbox').innerHTML = out;
|
|
469
492
|
}
|
|
470
493
|
|
|
471
494
|
function renderHoldouts(items) {
|
|
@@ -557,7 +580,7 @@ _JS = r"""
|
|
|
557
580
|
sparkline(el('spark-duration'), d.sparklines.poll_duration_ms);
|
|
558
581
|
sparkline(el('spark-active'), d.sparklines.active_sessions);
|
|
559
582
|
renderPipeline(d.pipeline);
|
|
560
|
-
renderInbox(d.inbox);
|
|
583
|
+
renderInbox(d.inbox, d.inbox_settled || [], d.inbox_settled_count || 0);
|
|
561
584
|
renderHoldouts(d.holdouts || []);
|
|
562
585
|
renderTasks(d.tasks);
|
|
563
586
|
renderSessions(d.sessions);
|
|
@@ -61,6 +61,39 @@ ARCHIVE_FAILED_KIND = "janitor-archive-failed"
|
|
|
61
61
|
|
|
62
62
|
# How many recent snapshots feed the sparklines / pipeline board.
|
|
63
63
|
SPARK_POINTS = 60
|
|
64
|
+
|
|
65
|
+
# The operator inbox's settled-page demotion (issue #103). The `escalations`
|
|
66
|
+
# table is a dedupe key store -- each row is what stops a second page for the
|
|
67
|
+
# same episode -- so it is never pruned, and the console filters at READ time
|
|
68
|
+
# instead: an escalation row whose (repo_slug, number, is_pr) has left the
|
|
69
|
+
# latest poll snapshot's candidate set is SETTLED -- the trigger cleared (the
|
|
70
|
+
# issue closed, the PR merged or closed, the label came off) and the page is
|
|
71
|
+
# no longer actionable from this console. Settled rows are demoted, never
|
|
72
|
+
# dropped: the payload carries the newest INBOX_SETTLED_CAP of them plus the
|
|
73
|
+
# full count. No GitHub call is spent on the verdict -- the snapshot is the
|
|
74
|
+
# daemon's own exhaust.
|
|
75
|
+
INBOX_SETTLED_CAP = 50
|
|
76
|
+
# A page raised BETWEEN two polls is not in the latest snapshot yet, so a row
|
|
77
|
+
# escalated less than this many poll intervals ago stays live regardless of
|
|
78
|
+
# the candidate set -- it must not flicker out before the pass that lists its
|
|
79
|
+
# artifact lands.
|
|
80
|
+
INBOX_GRACE_POLLS = 2
|
|
81
|
+
# Kind prefixes whose rows store a PULL REQUEST number in the `issue` column.
|
|
82
|
+
# Every issue-edge kind (`cap`, `assignment`, `stalled:a<k>`,
|
|
83
|
+
# `live-stalled:a<k>`) stores an issue number; the PR edge's kinds (`fix-cap`,
|
|
84
|
+
# `fix-stalled`, `rerequest`, `rerequest-rejected`, the orphan sweep's three,
|
|
85
|
+
# `stacked-base`), the maintain edge's, and the janitor's archive page all
|
|
86
|
+
# store the PR's. The split drives both the link an operator follows and the
|
|
87
|
+
# settled verdict: a PR-keyed row is matched against the PR edges only, so a
|
|
88
|
+
# settled PR #905 is never revived by an open issue #905, nor the reverse.
|
|
89
|
+
# Agreement with `loop.ESCALATION_*` is pinned by a test.
|
|
90
|
+
PR_KINDS = ("fix-", "maintain", "orphan", "rerequest", "stacked-", ARCHIVE_FAILED_KIND)
|
|
91
|
+
# Kinds whose SUBJECT is not an issue or PR the poll evaluates: a foreign
|
|
92
|
+
# session (no artifact at all) and a janitor holdout that could not be
|
|
93
|
+
# archived (its anchor PR is terminal by construction, so it is never a
|
|
94
|
+
# candidate). The snapshot carries no evidence either way about these, and
|
|
95
|
+
# the rule is never to hide on missing evidence, so they are always live.
|
|
96
|
+
UNEVIDENCED_KINDS = (FOREIGN_SESSION_KIND, ARCHIVE_FAILED_KIND)
|
|
64
97
|
# Cache lifetimes for the remote / shared-token checks (seconds).
|
|
65
98
|
RATE_CACHE_TTL = 60.0
|
|
66
99
|
VERSION_CACHE_TTL = 600.0
|
|
@@ -91,6 +124,36 @@ _EDGE_PREFIXES = (
|
|
|
91
124
|
)
|
|
92
125
|
|
|
93
126
|
|
|
127
|
+
def is_pr_kind(kind: str) -> bool:
|
|
128
|
+
"""Whether an escalation kind stores a PR number in the `issue` column
|
|
129
|
+
(see PR_KINDS). A foreign-session row is neither: it has no artifact."""
|
|
130
|
+
return kind.startswith(PR_KINDS)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def snapshot_candidates(snapshot: "dict | None") -> "set[tuple] | None":
|
|
134
|
+
"""The poll snapshot's candidate set as `(repo_slug, number, is_pr)` keys,
|
|
135
|
+
or None when there is no snapshot to consult (a fresh boot, an empty
|
|
136
|
+
`poll_snapshots`) -- the caller then treats every inbox row as live:
|
|
137
|
+
never hide on missing evidence, the same rule the reaper applies to a
|
|
138
|
+
failed session listing.
|
|
139
|
+
|
|
140
|
+
The `issue` edge carries issue numbers; every other edge (fix, maintain,
|
|
141
|
+
and any PR-shaped edge a later daemon adds) carries PR numbers -- the
|
|
142
|
+
daemon's own vocabulary (`_reap_records`: issue or pr), and exactly the
|
|
143
|
+
split the inbox's `is_pr` flag encodes. A stage row missing its slug or
|
|
144
|
+
number contributes nothing rather than a degenerate key."""
|
|
145
|
+
if snapshot is None:
|
|
146
|
+
return None
|
|
147
|
+
keys: "set[tuple]" = set()
|
|
148
|
+
for stage in snapshot.get("stages") or []:
|
|
149
|
+
slug = (stage.get("slug") or "").split("#", 1)[0]
|
|
150
|
+
number = stage.get("number")
|
|
151
|
+
if not slug or number is None:
|
|
152
|
+
continue
|
|
153
|
+
keys.add((slug, int(number), stage.get("edge") != "issue"))
|
|
154
|
+
return keys
|
|
155
|
+
|
|
156
|
+
|
|
94
157
|
def edge_of(name: "str | None") -> "str | None":
|
|
95
158
|
if not name:
|
|
96
159
|
return None
|
|
@@ -504,7 +567,9 @@ class Sources:
|
|
|
504
567
|
else:
|
|
505
568
|
sess["retry"] = None
|
|
506
569
|
|
|
507
|
-
|
|
570
|
+
live, settled = self._partition_inbox(ledgers["escalations"], latest)
|
|
571
|
+
inbox = self._inbox(live)
|
|
572
|
+
inbox_settled = self._inbox(settled[:INBOX_SETTLED_CAP])
|
|
508
573
|
candidates = 0
|
|
509
574
|
if latest:
|
|
510
575
|
candidates = (latest["issue_candidates"] + latest["pr_candidates"]
|
|
@@ -537,24 +602,66 @@ class Sources:
|
|
|
537
602
|
"edges": pipeline,
|
|
538
603
|
},
|
|
539
604
|
"inbox": inbox,
|
|
605
|
+
"inbox_settled": inbox_settled,
|
|
606
|
+
"inbox_settled_count": len(settled),
|
|
540
607
|
"holdouts": holdouts,
|
|
541
608
|
"sessions": sessions,
|
|
542
609
|
"tasks": self.worker_tasks(),
|
|
543
610
|
"log": self.log_tail(),
|
|
544
611
|
}
|
|
545
612
|
|
|
613
|
+
def _partition_inbox(
|
|
614
|
+
self, escalations: "list[dict]", snapshot: "dict | None"
|
|
615
|
+
) -> "tuple[list[dict], list[dict]]":
|
|
616
|
+
"""Split the escalation ledger's rows into (live, settled) against the
|
|
617
|
+
latest poll snapshot (issue #103). Live rows keep the ledger's order;
|
|
618
|
+
settled rows come back newest-first. A row is live when any of:
|
|
619
|
+
|
|
620
|
+
- there is no snapshot to consult (fresh boot): every row is live;
|
|
621
|
+
- it was escalated less than INBOX_GRACE_POLLS poll intervals ago, or
|
|
622
|
+
after the snapshot was taken -- the pass that would list its
|
|
623
|
+
artifact has not landed yet (a long, rate-limited pass can outlast
|
|
624
|
+
the grace window, so the snapshot's own clock is checked too);
|
|
625
|
+
- its kind has no candidate to match (UNEVIDENCED_KINDS);
|
|
626
|
+
- its (repo_slug, number, is_pr) key is in the snapshot's candidate
|
|
627
|
+
set -- a PR-keyed row matches PR edges only, an issue-keyed row the
|
|
628
|
+
issue edge only.
|
|
629
|
+
|
|
630
|
+
Everything else is settled: the artifact left the poll's candidate
|
|
631
|
+
set, so the page is exhaust, not backlog. Pure ledger + snapshot
|
|
632
|
+
arithmetic -- no GitHub call, no write."""
|
|
633
|
+
candidates = snapshot_candidates(snapshot)
|
|
634
|
+
if candidates is None:
|
|
635
|
+
return list(escalations), []
|
|
636
|
+
horizon = int(self._wall()) - INBOX_GRACE_POLLS * self.config.poll_interval
|
|
637
|
+
snapshot_ts = int(snapshot.get("ts") or 0) if snapshot else 0
|
|
638
|
+
live: "list[dict]" = []
|
|
639
|
+
settled: "list[dict]" = []
|
|
640
|
+
for row in escalations:
|
|
641
|
+
kind = row["kind"]
|
|
642
|
+
escalated_at = int(row["escalated_at"])
|
|
643
|
+
key = (row["repo_slug"], int(row["issue"]), is_pr_kind(kind))
|
|
644
|
+
if (
|
|
645
|
+
escalated_at >= horizon
|
|
646
|
+
or escalated_at >= snapshot_ts
|
|
647
|
+
or kind.startswith(UNEVIDENCED_KINDS)
|
|
648
|
+
or key in candidates
|
|
649
|
+
):
|
|
650
|
+
live.append(row)
|
|
651
|
+
else:
|
|
652
|
+
settled.append(row)
|
|
653
|
+
settled.sort(key=lambda r: int(r["escalated_at"]), reverse=True)
|
|
654
|
+
return live, settled
|
|
655
|
+
|
|
546
656
|
def _inbox(self, escalations: "list[dict]") -> "list[dict]":
|
|
547
657
|
now = int(self._wall())
|
|
548
658
|
out = []
|
|
549
659
|
for row in escalations:
|
|
550
660
|
kind = row["kind"]
|
|
551
661
|
number = row["issue"]
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
is_pr = kind.startswith(
|
|
556
|
-
("fix-cap", "maintain", "orphan", ARCHIVE_FAILED_KIND)
|
|
557
|
-
)
|
|
662
|
+
# PR-side kinds (PR_KINDS) store a PR number in the `issue`
|
|
663
|
+
# column; everything else is an issue number.
|
|
664
|
+
is_pr = is_pr_kind(kind)
|
|
558
665
|
leaf = "pull" if is_pr else "issues"
|
|
559
666
|
# foreign-session rows have NO GitHub artifact behind them (issue
|
|
560
667
|
# #95): the subject is a tmux session, and their repo_slug/issue
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.8.14
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|