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.
Files changed (39) hide show
  1. {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
  2. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/alissa.py +42 -1
  3. {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
  4. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/loop.py +456 -6
  5. {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
  6. alissa_tools_github_revloop-0.31.11/src/main/alissa/tools/github/revloop/parking.py +222 -0
  7. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/state.py +136 -1
  8. alissa_tools_github_revloop-0.31.11/src/main/alissa/tools/github/revloop/version +1 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. alissa_tools_github_revloop-0.31.9/src/main/alissa/tools/github/revloop/version +0 -1
  15. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/LICENSE +0 -0
  16. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/MANIFEST.in +0 -0
  17. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/NOTICE +0 -0
  18. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/README.md +0 -0
  19. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/requirements.txt +0 -0
  20. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/setup.cfg +0 -0
  21. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/setup.py +0 -0
  22. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  23. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  24. {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
  25. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  26. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/config.py +0 -0
  27. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  28. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  29. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
  30. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  31. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/trust.py +0 -0
  32. {alissa_tools_github_revloop-0.31.9 → alissa_tools_github_revloop-0.31.11}/src/main/alissa/tools/github/revloop/version.py +0 -0
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.9
3
+ Version: 0.31.11
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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=self._newest_verdict(data),
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(