alissa-tools-github-revloop 0.31.9__tar.gz → 0.31.11__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_revloop-0.31.9/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.11}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/alissa.py +42 -1
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/fleet_vitals.py +64 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/loop.py +456 -6
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/loop_events.py +177 -22
- alissa_tools_github_revloop-0.31.11/src/main/alissa/tools/github/revloop/parking.py +222 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/state.py +136 -1
- alissa_tools_github_revloop-0.31.11/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/__main__.py +20 -1
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/server.py +114 -2
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/sources.py +77 -3
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +1 -0
- alissa_tools_github_revloop-0.31.9/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -527,6 +527,32 @@ def parse_commit_readiness(blob: object) -> "tuple[str | None, str, str | None]"
|
|
|
527
527
|
return _commit_classed(match.group(1), match.group(2))
|
|
528
528
|
|
|
529
529
|
|
|
530
|
+
# The contract version the reviewer judged against (issue #155; Studio
|
|
531
|
+
# design `loop-operator-surface.md` §6.4, lanes L10/L11): the reviewer reads
|
|
532
|
+
# the task at round start and records `**Contract:** v<n>` under the reviewed
|
|
533
|
+
# head. The same markdown tolerance as the readiness lines -- an optional
|
|
534
|
+
# bullet, optional bold around the label with the colon inside or outside --
|
|
535
|
+
# but the `v` is REQUIRED: L11 has not shipped the line yet, and without it any
|
|
536
|
+
# prose that opens with `Contract` and a number (`Contract 3 criteria
|
|
537
|
+
# re-checked`) would set the version (PR #156 round 1). Absent (every
|
|
538
|
+
# envelope written before L11 ships) reads as None, never as v1: the event's
|
|
539
|
+
# key is then null, the design's "absent" case, rather than a version nobody
|
|
540
|
+
# recorded.
|
|
541
|
+
_CONTRACT_RE = re.compile(
|
|
542
|
+
r"^[ \t]*(?:[-*+][ \t]+)?(?:\*\*)?[ \t]*Contract[ \t]*:?[ \t]*(?:\*\*)?"
|
|
543
|
+
r"[ \t]*:?[ \t]*(?:\*\*)?[ \t]*v(\d{1,9})\b",
|
|
544
|
+
re.MULTILINE,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def parse_contract_version(blob: object) -> "int | None":
|
|
549
|
+
"""The `Contract: v<n>` an envelope records, as an int, or None."""
|
|
550
|
+
if not isinstance(blob, str):
|
|
551
|
+
return None
|
|
552
|
+
match = _CONTRACT_RE.search(blob)
|
|
553
|
+
return None if match is None else int(match.group(1))
|
|
554
|
+
|
|
555
|
+
|
|
530
556
|
@dataclass(frozen=True)
|
|
531
557
|
class Task:
|
|
532
558
|
ref: str # TASK-<taskNumber>
|
|
@@ -829,6 +855,9 @@ class VerdictEnvelope:
|
|
|
829
855
|
envelope's `Commit-Readiness` line (issue #148), read off the same
|
|
830
856
|
evidence item; only a PLAN PR's native post reads them, and a code PR's
|
|
831
857
|
envelope simply has none.
|
|
858
|
+
|
|
859
|
+
`contract_version` is the envelope's `Contract: v<n>` (issue #155), or
|
|
860
|
+
None when it records none -- read for the `round.verdict` event only.
|
|
832
861
|
"""
|
|
833
862
|
|
|
834
863
|
verdict: str
|
|
@@ -836,6 +865,7 @@ class VerdictEnvelope:
|
|
|
836
865
|
readiness_reason: str = ""
|
|
837
866
|
commit_readiness: "str | None" = None
|
|
838
867
|
commit_readiness_reason: str = ""
|
|
868
|
+
contract_version: "int | None" = None
|
|
839
869
|
|
|
840
870
|
|
|
841
871
|
@dataclass(frozen=True)
|
|
@@ -854,11 +884,16 @@ class TaskDetail:
|
|
|
854
884
|
`verdict` is None when no envelope on the task parses -- the normal round-1
|
|
855
885
|
case, indistinguishable here from "no verdict of record yet", which is what
|
|
856
886
|
the caller wants it to mean anyway.
|
|
887
|
+
|
|
888
|
+
`envelope` is that same newest envelope whole (issue #155): the
|
|
889
|
+
`round.verdict` event of a session-closed round reads its readiness and
|
|
890
|
+
contract version off the read that already knew the verdict word.
|
|
857
891
|
"""
|
|
858
892
|
|
|
859
893
|
task: Task
|
|
860
894
|
verdicts: int
|
|
861
895
|
verdict: "str | None"
|
|
896
|
+
envelope: "VerdictEnvelope | None" = None
|
|
862
897
|
|
|
863
898
|
|
|
864
899
|
class Alissa:
|
|
@@ -1182,10 +1217,12 @@ class Alissa:
|
|
|
1182
1217
|
task = _task_from_row(data)
|
|
1183
1218
|
if task is None:
|
|
1184
1219
|
return None
|
|
1220
|
+
envelope = self._newest_envelope(data)
|
|
1185
1221
|
return TaskDetail(
|
|
1186
1222
|
task=task,
|
|
1187
1223
|
verdicts=self._count_verdicts(data),
|
|
1188
|
-
verdict=
|
|
1224
|
+
verdict=None if envelope is None else envelope.verdict,
|
|
1225
|
+
envelope=envelope,
|
|
1189
1226
|
)
|
|
1190
1227
|
except Exception: # pragma: no cover - defence in depth
|
|
1191
1228
|
log.exception("could not parse task payload for %s", ref)
|
|
@@ -1348,6 +1385,9 @@ class Alissa:
|
|
|
1348
1385
|
commit, commit_reason, _ = parse_commit_readiness(content)
|
|
1349
1386
|
if commit is None:
|
|
1350
1387
|
commit, commit_reason, _ = parse_commit_readiness(title)
|
|
1388
|
+
contract = parse_contract_version(content)
|
|
1389
|
+
if contract is None:
|
|
1390
|
+
contract = parse_contract_version(title)
|
|
1351
1391
|
found.append(
|
|
1352
1392
|
(Alissa._created_key(item.get("createdAt")),
|
|
1353
1393
|
index,
|
|
@@ -1357,6 +1397,7 @@ class Alissa:
|
|
|
1357
1397
|
readiness_reason=reason,
|
|
1358
1398
|
commit_readiness=commit,
|
|
1359
1399
|
commit_readiness_reason=commit_reason,
|
|
1400
|
+
contract_version=contract,
|
|
1360
1401
|
))
|
|
1361
1402
|
)
|
|
1362
1403
|
break
|
|
@@ -41,6 +41,15 @@ Design rules, all load-bearing:
|
|
|
41
41
|
user has not activated the loop app) warns ONCE per boot with the activate
|
|
42
42
|
URL and logs at DEBUG after, because it is a fixed condition the operator
|
|
43
43
|
resolves out of band, and the next pass after activation lands on its own.
|
|
44
|
+
|
|
45
|
+
Seat parking (issue #157, studio.alissa.app seat-parking design §2.2/§2.4)
|
|
46
|
+
adds the `idle` block -- the seat's own idle verdict (`parking.idle_block`),
|
|
47
|
+
with `drainedAt` while a drain holds -- so Studio can re-check a park against
|
|
48
|
+
the seat's OWN snapshot. The console's drain pushes one snapshot before it
|
|
49
|
+
answers (`FleetVitalsPusher.push_now`). Until Studio's schema carries the
|
|
50
|
+
block (the design's lane L3), a strict 400 naming `idle` makes the pusher
|
|
51
|
+
resend without it and leave it out for IDLE_REPROBE_S: the vitals the Factory
|
|
52
|
+
already renders never go dark over a field nobody reads yet.
|
|
44
53
|
"""
|
|
45
54
|
|
|
46
55
|
from __future__ import annotations
|
|
@@ -51,6 +60,7 @@ import time
|
|
|
51
60
|
from datetime import datetime, timezone
|
|
52
61
|
from typing import TYPE_CHECKING, Any, Callable
|
|
53
62
|
|
|
63
|
+
from . import parking
|
|
54
64
|
from .alissa import parse_session_name, session_repo_slug
|
|
55
65
|
from .alissa_client import (
|
|
56
66
|
NOT_ACTIVATED,
|
|
@@ -87,6 +97,12 @@ VITALS_PUSHED = "pushed"
|
|
|
87
97
|
VITALS_SKIPPED = "skipped"
|
|
88
98
|
VITALS_FAILED = "failed"
|
|
89
99
|
|
|
100
|
+
# How long a snapshot leaves the `idle` block out after Studio refused it as
|
|
101
|
+
# an unknown key (a schema that predates seat parking), before it offers the
|
|
102
|
+
# block again. An hour: one extra POST an hour against an old Studio, and a
|
|
103
|
+
# newly deployed one starts receiving the block within the hour.
|
|
104
|
+
IDLE_REPROBE_S = 3600.0
|
|
105
|
+
|
|
90
106
|
# The operator's lever per inbox kind, in the Factory's `lever` slot. The
|
|
91
107
|
# cap-out and the stability hold are both lifted by the same re-entry ack;
|
|
92
108
|
# a stalled episode is the console's retry-now (or a reap).
|
|
@@ -252,6 +268,11 @@ def build_snapshot(
|
|
|
252
268
|
optional field degrades to null on its own -- a broken tmux does not
|
|
253
269
|
blank the memory split, and vice versa -- and `kpis` is always null (an
|
|
254
270
|
orcloop-only section).
|
|
271
|
+
|
|
272
|
+
`idle` (seat parking, issue #157) is the seat's idle block: owed and
|
|
273
|
+
timers from the newest poll snapshot, live / managed from the SAME roster
|
|
274
|
+
read as `sessions`, queued from the alissa queue, and `drainedAt` while
|
|
275
|
+
a drain holds.
|
|
255
276
|
"""
|
|
256
277
|
stamp = time.time() if now is None else now
|
|
257
278
|
snaps = sources.snapshots(DURATION_POINTS)
|
|
@@ -277,6 +298,12 @@ def build_snapshot(
|
|
|
277
298
|
)
|
|
278
299
|
session_list = [_session_row(r, repos) for r in ordered[:LIST_CAP]]
|
|
279
300
|
inbox = [_inbox_row(item) for item in sources.inbox()["live"][:LIST_CAP]]
|
|
301
|
+
live, managed = parking.roster_counts(rows)
|
|
302
|
+
idle = parking.idle_block(
|
|
303
|
+
snaps[0] if snaps else None,
|
|
304
|
+
live=live, managed=managed, queued=sources.queued(),
|
|
305
|
+
drain=sources.drain(), now=stamp,
|
|
306
|
+
)
|
|
280
307
|
return {
|
|
281
308
|
"schemaVersion": SCHEMA_VERSION,
|
|
282
309
|
"seat": SEAT,
|
|
@@ -297,6 +324,7 @@ def build_snapshot(
|
|
|
297
324
|
"queueDepth": _int_or_none(queue_depth),
|
|
298
325
|
"kpis": None,
|
|
299
326
|
"inbox": inbox,
|
|
327
|
+
"idle": idle,
|
|
300
328
|
}
|
|
301
329
|
|
|
302
330
|
|
|
@@ -366,6 +394,9 @@ class FleetVitalsPusher:
|
|
|
366
394
|
# every later refusal is a DEBUG line. The push itself is still
|
|
367
395
|
# attempted every pass -- the pass after activation lands on its own.
|
|
368
396
|
self._not_activated_warned = False
|
|
397
|
+
# Until when the `idle` block is left out (Studio refused it as an
|
|
398
|
+
# unknown key); 0 = offer it.
|
|
399
|
+
self._idle_refused_until = 0.0
|
|
369
400
|
|
|
370
401
|
def build(self, *, heartbeat_at: "int | float", queue_depth: "int | None") -> dict:
|
|
371
402
|
"""This pass's contract body, fitted to the API's caps."""
|
|
@@ -400,9 +431,31 @@ class FleetVitalsPusher:
|
|
|
400
431
|
type(exc).__name__, exc,
|
|
401
432
|
)
|
|
402
433
|
return VITALS_FAILED
|
|
434
|
+
if self._clock() < self._idle_refused_until:
|
|
435
|
+
snapshot.pop("idle", None)
|
|
403
436
|
if self.config.dry_run:
|
|
404
437
|
log.info("[dry-run] would push fleet vitals (%s)", describe(snapshot))
|
|
405
438
|
return VITALS_SKIPPED
|
|
439
|
+
return self._post(snapshot)
|
|
440
|
+
|
|
441
|
+
def push_now(self) -> str:
|
|
442
|
+
"""The console's out-of-pass push (the drain handshake, issue #157):
|
|
443
|
+
the same snapshot, its `heartbeatAt` the daemon's LAST completed pass
|
|
444
|
+
-- the console completes none of its own, and stamping now would read
|
|
445
|
+
as a heartbeat the loop never gave -- and `asOf` now. `queueDepth` is
|
|
446
|
+
null: the slot queue lives in the daemon's memory, and the console
|
|
447
|
+
cannot say. One of VITALS_PUSHED, VITALS_SKIPPED or VITALS_FAILED."""
|
|
448
|
+
snaps = self._sources.snapshots(1)
|
|
449
|
+
heartbeat = snaps[0]["ts"] if snaps else self._clock()
|
|
450
|
+
return self.push_once(heartbeat_at=heartbeat, queue_depth=None)
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def _refuses_idle(exc: AlissaError) -> bool:
|
|
454
|
+
"""A 400 that names the `idle` key: a Studio whose strict schema
|
|
455
|
+
predates seat parking (the design's lane L3 adds the field)."""
|
|
456
|
+
return exc.status == 400 and "idle" in str(exc.detail)
|
|
457
|
+
|
|
458
|
+
def _post(self, snapshot: dict) -> str:
|
|
406
459
|
try:
|
|
407
460
|
result = self._client.post_fleet_vitals(snapshot)
|
|
408
461
|
except AlissaAuthError as exc:
|
|
@@ -412,6 +465,17 @@ class FleetVitalsPusher:
|
|
|
412
465
|
self._warn_failed(exc)
|
|
413
466
|
return VITALS_FAILED
|
|
414
467
|
except AlissaError as exc:
|
|
468
|
+
if "idle" in snapshot and self._refuses_idle(exc):
|
|
469
|
+
self._idle_refused_until = self._clock() + IDLE_REPROBE_S
|
|
470
|
+
log.warning(
|
|
471
|
+
"fleet-vitals: Studio refused the seat-parking `idle` "
|
|
472
|
+
"block (status %s: %s) — its schema predates it; "
|
|
473
|
+
"resending without it, and leaving it out for %d min",
|
|
474
|
+
exc.status, exc.detail, int(IDLE_REPROBE_S // 60),
|
|
475
|
+
)
|
|
476
|
+
trimmed = dict(snapshot)
|
|
477
|
+
trimmed.pop("idle")
|
|
478
|
+
return self._post(trimmed)
|
|
415
479
|
self._warn_failed(exc)
|
|
416
480
|
return VITALS_FAILED
|
|
417
481
|
log.info(
|