alissa-tools-github-revloop 0.29.0__tar.gz → 0.30.1__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 (36) hide show
  1. {alissa_tools_github_revloop-0.29.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.30.1}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/__main__.py +29 -0
  3. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/alissa_client.py +25 -2
  4. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/config.py +86 -13
  5. alissa_tools_github_revloop-0.30.1/src/main/alissa/tools/github/revloop/fleet_vitals.py +504 -0
  6. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/ghclient.py +82 -0
  7. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/loop.py +301 -3
  8. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/state.py +71 -0
  9. alissa_tools_github_revloop-0.30.1/src/main/alissa/tools/github/revloop/version +1 -0
  10. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/sources.py +58 -9
  11. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +19 -0
  12. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  13. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +1 -0
  14. alissa_tools_github_revloop-0.29.0/src/main/alissa/tools/github/revloop/version +0 -1
  15. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/LICENSE +0 -0
  16. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/MANIFEST.in +0 -0
  17. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/NOTICE +0 -0
  18. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/README.md +0 -0
  19. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/requirements.txt +0 -0
  20. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/setup.cfg +0 -0
  21. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/setup.py +0 -0
  22. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  23. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  24. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  25. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
  26. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  27. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  28. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/version.py +0 -0
  29. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
  30. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
  31. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
  32. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
  33. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
  34. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  35. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
  36. {alissa_tools_github_revloop-0.29.0 → alissa_tools_github_revloop-0.30.1}/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.29.0
3
+ Version: 0.30.1
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -13,6 +13,7 @@ from .alissa_client import AlissaClient, AlissaError
13
13
  from .config import (
14
14
  BOW_OWNERS_ENV,
15
15
  BOWS_REFRESH_POLLS_ENV,
16
+ FLEET_VITALS_ENV,
16
17
  HUB_ADD,
17
18
  HUB_SKIP,
18
19
  LOOP_EVENTS_ENV,
@@ -200,6 +201,15 @@ def build_parser() -> argparse.ArgumentParser:
200
201
  "evidence; 0 queues immediately and relies on the directive alone",
201
202
  )
202
203
 
204
+ over.add_argument(
205
+ "--verdict-cooldown-s",
206
+ type=int,
207
+ metavar="SECONDS",
208
+ help="how long after a verdict lands on a head no round may be queued "
209
+ "on that same head, whatever the PR's requested_reviewers snapshot "
210
+ "says; 0 leaves the review_requested timeline check to decide alone",
211
+ )
212
+
203
213
  over.add_argument(
204
214
  "--review-task-miss-ttl-polls",
205
215
  type=int,
@@ -253,6 +263,23 @@ def build_parser() -> argparse.ArgumentParser:
253
263
  action="store_false",
254
264
  help="do not push loop telemetry even if the config enables it",
255
265
  )
266
+ vitals = over.add_mutually_exclusive_group()
267
+ vitals.add_argument(
268
+ "--fleet-vitals",
269
+ dest="fleet_vitals_enabled",
270
+ action="store_true",
271
+ default=None,
272
+ help="push one fleet-vitals snapshot (heartbeat, sessions, inbox, "
273
+ "rate, memory) to Studio's POST /v1/loop/fleet-vitals at the end of "
274
+ "every completed poll pass — best-effort, never fatal. "
275
+ f"Overridden by ${FLEET_VITALS_ENV}",
276
+ )
277
+ vitals.add_argument(
278
+ "--no-fleet-vitals",
279
+ dest="fleet_vitals_enabled",
280
+ action="store_false",
281
+ help="do not push fleet vitals even if the config enables it",
282
+ )
256
283
  over.add_argument(
257
284
  "--alissa-endpoint",
258
285
  dest="alissa_endpoint",
@@ -303,10 +330,12 @@ def overrides_from(args: argparse.Namespace) -> dict:
303
330
  "max_concurrent_sessions": args.max_concurrent_sessions,
304
331
  "checks_wait_seconds": args.checks_wait_seconds,
305
332
  "checks_spawn_wait_seconds": args.checks_spawn_wait_seconds,
333
+ "verdict_cooldown_s": args.verdict_cooldown_s,
306
334
  "review_task_miss_ttl_polls": args.review_task_miss_ttl_polls,
307
335
  "task_list_self_scope": args.task_list_self_scope,
308
336
  "task_list_bow_id": args.task_list_bow_id,
309
337
  "loop_events_enabled": args.loop_events_enabled,
338
+ "fleet_vitals_enabled": args.fleet_vitals_enabled,
310
339
  "alissa_endpoint": args.alissa_endpoint,
311
340
  "dry_run": args.dry_run,
312
341
  }
@@ -1,5 +1,6 @@
1
- """Alissa REST access — loop telemetry's one write (issue #112) and the two
2
- reads `repos_source: bows` needs (issue #119).
1
+ """Alissa REST access — loop telemetry's write (issue #112), the fleet-vitals
2
+ write beside it (issue #126) and the two reads `repos_source: bows` needs
3
+ (issue #119).
3
4
 
4
5
  This is the SECOND Alissa adapter in the package, and the split is deliberate.
5
6
  `alissa.py` shells out to the `alissa` CLI, which is the daemon's established
@@ -62,6 +63,12 @@ ENV_TOKEN = "ALISSA_API_TOKEN"
62
63
  # rather than silently posting a request the API will 400.
63
64
  MAX_EVENTS_PER_POST = 200
64
65
 
66
+ # The `error` code a 403 carries when the token's user has not activated the
67
+ # stacked app that owns the loop ingest. Advisory (classification keys on the
68
+ # status), but the vitals pusher reads it to warn ONCE with the activate URL
69
+ # instead of once per pass.
70
+ NOT_ACTIVATED = "not_activated"
71
+
65
72
 
66
73
  class _RefuseRedirects(urllib.request.HTTPRedirectHandler):
67
74
  """Refuse every redirect instead of following it (PR #113 round 1).
@@ -246,6 +253,22 @@ class AlissaClient:
246
253
  payload = self._request("/v1/loop-events", {"events": events})
247
254
  return payload if isinstance(payload, dict) else {}
248
255
 
256
+ def post_fleet_vitals(self, snapshot: dict) -> dict:
257
+ """Replace this seat's fleet-vitals snapshot (`POST
258
+ /v1/loop/fleet-vitals`, issue #126).
259
+
260
+ One body, one seat, replaced on every call — there is no batching
261
+ and no dedupe key, because the API keeps only the LATEST snapshot per
262
+ user×seat (loop events are the history). Returns the API's
263
+ `{"seat", "receivedAt", "replaced"}` payload (empty dict when the
264
+ body was empty), for the caller's debug line. The API is strict —
265
+ unknown keys 400, lists cap at 50, the body at 64 KB — and the
266
+ BUILDER (`fleet_vitals`) owns fitting the snapshot to that, so this
267
+ method sends exactly what it is handed.
268
+ """
269
+ payload = self._request("/v1/loop/fleet-vitals", snapshot)
270
+ return payload if isinstance(payload, dict) else {}
271
+
249
272
  def ping(self) -> Identity:
250
273
  """The identity this token acts as (`GET /v1/ping`).
251
274
 
@@ -15,6 +15,9 @@ site. `loop_events_enabled` (`ALISSA_REV_LOOP_EVENTS_ENABLED`, see
15
15
  `env_loop_events_enabled`) has it because a container deployment toggles
16
16
  telemetry with one variable and no config-file edit (issue #112); the env wins
17
17
  over both other layers so the two env-backed keys share one precedence story.
18
+ `fleet_vitals_enabled` (`ALISSA_REV_FLEET_VITALS_ENABLED`, see
19
+ `env_fleet_vitals_enabled`) is the second Studio push and rides the same rail
20
+ through the same boolean reader (issue #126).
18
21
  The three `repos_source: bows` keys (issue #119) ride the same rail for the
19
22
  same container reason: `repos_source` (`ALISSA_REVIEW_REPOS_SOURCE`),
20
23
  `bows_refresh_polls` (`ALISSA_REVIEW_BOWS_REFRESH_POLLS`) and `bow_owners`
@@ -187,10 +190,12 @@ CONFIG_KEYS = (
187
190
  "max_concurrent_sessions",
188
191
  "checks_wait_seconds",
189
192
  "checks_spawn_wait_seconds",
193
+ "verdict_cooldown_s",
190
194
  "review_task_miss_ttl_polls",
191
195
  "task_list_self_scope",
192
196
  "task_list_bow_id",
193
197
  "loop_events_enabled",
198
+ "fleet_vitals_enabled",
194
199
  "alissa_endpoint",
195
200
  "dry_run",
196
201
  )
@@ -206,6 +211,11 @@ TASK_LIST_BOW_ENV = "ALISSA_REVIEW_TASK_BOW"
206
211
  # module docstring for the shared precedence story.
207
212
  LOOP_EVENTS_ENV = "ALISSA_REV_LOOP_EVENTS_ENABLED"
208
213
 
214
+ # The environment variable toggling the fleet-vitals push (issue #126): one
215
+ # snapshot of this daemon's live state per completed pass, to Studio's
216
+ # `POST /v1/loop/fleet-vitals`. Same rail and same reader as LOOP_EVENTS_ENV.
217
+ FLEET_VITALS_ENV = "ALISSA_REV_FLEET_VITALS_ENABLED"
218
+
209
219
  # The three `repos_source: bows` rails (issue #119). Same precedence as the two
210
220
  # above -- env > file > flag -- and the same blank-falls-through rule, because
211
221
  # the container bakes every knob as an ENV that renders `""` when unset.
@@ -258,18 +268,21 @@ def _validate_alissa_endpoint(endpoint: str) -> str:
258
268
  )
259
269
 
260
270
 
261
- def env_loop_events_enabled(
262
- environ: "Mapping[str, str] | None" = None,
271
+ def _env_bool(
272
+ name: str, environ: "Mapping[str, str] | None" = None,
263
273
  ) -> "bool | None":
264
- """The loop-events toggle from the environment, or None when unset.
265
-
266
- None and empty are the SAME answer an exported-but-empty variable is how
267
- a container renders "unset" (the Dockerfile bakes empty ENV defaults), and
268
- it must fall through to the file/CLI layers rather than read as false. A
269
- non-boolean spelling raises: the startup phase turns a ValueError into
270
- `config error` + exit 2, which is where a typo belongs.
274
+ """One boolean env rail: True/False, or None when unset.
275
+
276
+ Shared by every boolean the environment can set (`LOOP_EVENTS_ENV`,
277
+ `FLEET_VITALS_ENV`) so the accepted spellings and the refusal are one
278
+ rule, not two copies that drift (issue #126). None and empty are the SAME
279
+ answer an exported-but-empty variable is how a container renders
280
+ "unset" (the Dockerfile bakes empty ENV defaults), and it must fall
281
+ through to the file/CLI layers rather than read as false. A non-boolean
282
+ spelling raises, naming the variable: the startup phase turns a
283
+ ValueError into `config error` + exit 2, which is where a typo belongs.
271
284
  """
272
- raw = (os.environ if environ is None else environ).get(LOOP_EVENTS_ENV)
285
+ raw = (os.environ if environ is None else environ).get(name)
273
286
  value = (raw or "").strip().lower()
274
287
  if not value:
275
288
  return None
@@ -278,11 +291,27 @@ def env_loop_events_enabled(
278
291
  if value in _ENV_FALSE:
279
292
  return False
280
293
  raise ValueError(
281
- f"{LOOP_EVENTS_ENV} must be a boolean "
294
+ f"{name} must be a boolean "
282
295
  f"(1/0, true/false, yes/no, on/off), got {raw!r}"
283
296
  )
284
297
 
285
298
 
299
+ def env_loop_events_enabled(
300
+ environ: "Mapping[str, str] | None" = None,
301
+ ) -> "bool | None":
302
+ """The loop-events toggle from the environment, or None when unset —
303
+ `_env_bool` over LOOP_EVENTS_ENV."""
304
+ return _env_bool(LOOP_EVENTS_ENV, environ)
305
+
306
+
307
+ def env_fleet_vitals_enabled(
308
+ environ: "Mapping[str, str] | None" = None,
309
+ ) -> "bool | None":
310
+ """The fleet-vitals toggle from the environment, or None when unset —
311
+ `_env_bool` over FLEET_VITALS_ENV (issue #126)."""
312
+ return _env_bool(FLEET_VITALS_ENV, environ)
313
+
314
+
286
315
  def env_task_list_bow_id(environ: "Mapping[str, str] | None" = None) -> "str | None":
287
316
  """The review BOW id from the environment, or None when it is not set.
288
317
 
@@ -511,6 +540,24 @@ DEFAULT_CHECKS_WAIT_SECONDS = 30 * 60
511
540
  # still-running rollup, which is the directive-only posture.
512
541
  DEFAULT_CHECKS_SPAWN_WAIT_SECONDS = 15 * 60
513
542
 
543
+ # How long, after a verdict lands on a head, NO round may be queued on that same
544
+ # head -- whatever the PR's `requested_reviewers` snapshot says (issue #128).
545
+ #
546
+ # GitHub consumes a review request the moment the requested identity submits a
547
+ # review, but not atomically with the daemon's reads: on studio #1243 a
548
+ # `request_changes` verdict landed at 14:35:32 and the poll ten seconds later
549
+ # still saw the reviewer in `requested_reviewers`, queued round 2 on the SAME
550
+ # head, and that phantom round bounced on the CR8 triage gate -- which made the
551
+ # devloop spawn a second fix session on one branch. The cooldown is the blunt
552
+ # guard against that propagation lag: two polls' worth at the default cadence,
553
+ # long enough for the request the verdict consumed to be gone from every read.
554
+ # The precise guard is the timeline check in loop._admit_round, which admits a
555
+ # same-head round only on a `review_requested` event NEWER than the verdict;
556
+ # the cooldown spares that check a GitHub call in exactly the window where the
557
+ # stale snapshot is most likely. 0 disables the cooldown and leaves the timeline
558
+ # check to decide alone.
559
+ DEFAULT_VERDICT_COOLDOWN_SECONDS = 120
560
+
514
561
  # How many polls a PR with NO review task may be taken on trust before the
515
562
  # daemon searches the task corpus for one again (issue #87).
516
563
  #
@@ -750,6 +797,12 @@ class Config:
750
797
  # timer to configure.
751
798
  checks_spawn_wait_seconds: int = DEFAULT_CHECKS_SPAWN_WAIT_SECONDS
752
799
 
800
+ # How long after a verdict lands on a head no round may be queued on that
801
+ # same head, regardless of the PR's `requested_reviewers` snapshot; see
802
+ # DEFAULT_VERDICT_COOLDOWN_SECONDS. 0 is legal and means "no cooldown": the
803
+ # timeline check alone decides whether a same-head round is admitted.
804
+ verdict_cooldown_s: int = DEFAULT_VERDICT_COOLDOWN_SECONDS
805
+
753
806
  # How many polls a PR with no review task is taken on trust before the
754
807
  # corpus is searched again; see DEFAULT_REVIEW_TASK_MISS_TTL_POLLS. Floor 1
755
808
  # -- there is no value that turns the negative cache off.
@@ -786,6 +839,15 @@ class Config:
786
839
  # when on: a failed push is one WARN and the pass completes.
787
840
  loop_events_enabled: bool = False
788
841
 
842
+ # Whether the loop pushes one FLEET-VITALS snapshot — heartbeat, poll
843
+ # durations, the reviewer-session roster, rate, memory, queue depth, the
844
+ # operator inbox — to Studio's `POST /v1/loop/fleet-vitals` at the end
845
+ # of every completed pass (issue #126), so a Factory with no console URL
846
+ # for this seat can still render its card. OFF by default for the same
847
+ # reason `loop_events_enabled` is; toggled by FLEET_VITALS_ENV above the
848
+ # file and the CLI; best-effort when on (one WARN per failed pass).
849
+ fleet_vitals_enabled: bool = False
850
+
789
851
  # The Alissa/Studio API base the loop-events client posts to. One knob so
790
852
  # a staging deployment can point telemetry somewhere else; everything else
791
853
  # about the client (its bearer token) comes from the environment.
@@ -884,8 +946,8 @@ class Config:
884
946
  "not specified on the CLI" and fall through to the file / defaults.
885
947
 
886
948
  `environ` is the fourth layer and applies to the env-backed keys
887
- (`task_list_bow_id`, `loop_events_enabled`, `repos_source`,
888
- `bows_refresh_polls`, `bow_owners`); it wins over both of the others
949
+ (`task_list_bow_id`, `loop_events_enabled`, `fleet_vitals_enabled`,
950
+ `repos_source`, `bows_refresh_polls`, `bow_owners`); it wins over both of the others
889
951
  (see the module docstring). Defaults to the real environment, so
890
952
  callers that do not care pass nothing.
891
953
  """
@@ -941,6 +1003,11 @@ class Config:
941
1003
  env_events = env_loop_events_enabled(environ)
942
1004
  if env_events is not None:
943
1005
  raw["loop_events_enabled"] = env_events
1006
+ # ...and the fleet-vitals toggle (issue #126), the same rail through
1007
+ # the same reader.
1008
+ env_vitals = env_fleet_vitals_enabled(environ)
1009
+ if env_vitals is not None:
1010
+ raw["fleet_vitals_enabled"] = env_vitals
944
1011
 
945
1012
  # The three bows rails (issue #119), same layer and same reason: the
946
1013
  # container hands the daemon its mode with one variable. Each is
@@ -1103,6 +1170,10 @@ class Config:
1103
1170
  f"checks_spawn_wait_seconds must be >= 0, got {spawn_wait}"
1104
1171
  )
1105
1172
 
1173
+ cooldown = int(raw.get("verdict_cooldown_s", cls.verdict_cooldown_s))
1174
+ if cooldown < 0:
1175
+ raise ValueError(f"verdict_cooldown_s must be >= 0, got {cooldown}")
1176
+
1106
1177
  miss_ttl = int(
1107
1178
  raw.get("review_task_miss_ttl_polls", cls.review_task_miss_ttl_polls)
1108
1179
  )
@@ -1171,10 +1242,12 @@ class Config:
1171
1242
  max_concurrent_sessions=max_sessions,
1172
1243
  checks_wait_seconds=checks_wait,
1173
1244
  checks_spawn_wait_seconds=spawn_wait,
1245
+ verdict_cooldown_s=cooldown,
1174
1246
  review_task_miss_ttl_polls=miss_ttl,
1175
1247
  task_list_self_scope=bool(raw.get("task_list_self_scope", False)),
1176
1248
  task_list_bow_id=bow_id,
1177
1249
  loop_events_enabled=bool(raw.get("loop_events_enabled", False)),
1250
+ fleet_vitals_enabled=bool(raw.get("fleet_vitals_enabled", False)),
1178
1251
  alissa_endpoint=endpoint,
1179
1252
  dry_run=bool(raw.get("dry_run", False)),
1180
1253
  )