alissa-tools-github-orcloop 0.18.0__tar.gz → 0.18.2__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 (37) hide show
  1. {alissa_tools_github_orcloop-0.18.0/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.18.2}/PKG-INFO +1 -1
  2. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/setup.py +4 -0
  3. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/__main__.py +181 -1
  4. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/alissa_client.py +39 -0
  5. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/config.py +419 -1
  6. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/ghclient.py +100 -0
  7. alissa_tools_github_orcloop-0.18.2/src/main/alissa/tools/github/orcloop/lever.py +198 -0
  8. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/loop.py +93 -1
  9. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/loop_events.py +25 -2
  10. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/merge.py +403 -37
  11. alissa_tools_github_orcloop-0.18.2/src/main/alissa/tools/github/orcloop/seats.py +614 -0
  12. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/sentinel.py +148 -6
  13. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/state.py +76 -0
  14. alissa_tools_github_orcloop-0.18.2/src/main/alissa/tools/github/orcloop/version +1 -0
  15. alissa_tools_github_orcloop-0.18.2/src/main/alissa/tools/github/orcloop/watchdog.py +543 -0
  16. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
  17. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +3 -0
  18. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +1 -0
  19. alissa_tools_github_orcloop-0.18.0/src/main/alissa/tools/github/orcloop/version +0 -1
  20. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/LICENSE +0 -0
  21. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/MANIFEST.in +0 -0
  22. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/NOTICE +0 -0
  23. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/README.md +0 -0
  24. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/requirements.txt +0 -0
  25. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/setup.cfg +0 -0
  26. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
  27. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/alissa_cli.py +0 -0
  28. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/console.py +0 -0
  29. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/fleet_vitals.py +0 -0
  30. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
  31. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
  32. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/version.py +0 -0
  33. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/workplan.py +0 -0
  34. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/workplan_html.py +0 -0
  35. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa/tools/github/orcloop/workplan_state.py +0 -0
  36. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
  37. {alissa_tools_github_orcloop-0.18.0 → alissa_tools_github_orcloop-0.18.2}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-orcloop
3
- Version: 0.18.0
3
+ Version: 0.18.2
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -56,6 +56,10 @@ setup(
56
56
  entry_points={
57
57
  "console_scripts": [
58
58
  "alissa-orcloop=alissa.tools.github.orcloop.__main__:main",
59
+ # The sentinel pass's bounded lever over the seats' consoles
60
+ # (W4, issue #112): reads the passcodes from the environment
61
+ # itself and never prints one.
62
+ "orcloop-lever=alissa.tools.github.orcloop.lever:main",
59
63
  ]
60
64
  },
61
65
  install_requires=requirements,
@@ -36,8 +36,10 @@ from .alissa_client import AlissaClient, AlissaError
36
36
  from .config import (
37
37
  FEED_BOW,
38
38
  MERGE_METHODS,
39
+ MERGE_POLICIES,
39
40
  RELEASE_FEEDS,
40
41
  REPOS_SOURCES,
42
+ WATCHDOG_MODES,
41
43
  Config,
42
44
  load_config_file,
43
45
  resolve_config_path,
@@ -446,6 +448,130 @@ def build_parser() -> argparse.ArgumentParser:
446
448
  help="merge only: hold a head that reports no checks, even if the config "
447
449
  "allows it",
448
450
  )
451
+ # --- the merge policy (issue #116) ------------------------------------
452
+ over.add_argument(
453
+ "--merge-policy",
454
+ choices=MERGE_POLICIES,
455
+ help="merge only: strict (auto only), standard (the default: auto only, "
456
+ "today's edge) or yolo (also merge an `operator — <class>: …` approve "
457
+ "whose class is in --merge-yolo-merge-class, after posting the validation "
458
+ "floor on the origin task; unclassed and hold-listed classes hold)",
459
+ )
460
+ over.add_argument(
461
+ "--merge-yolo-merge-class",
462
+ dest="merge_yolo_merge_classes",
463
+ action="append",
464
+ metavar="CLASS",
465
+ help="merge only (yolo): an operator class the policy MERGES; repeatable, "
466
+ "REPLACES the default set (unverified-ux, unverified-runtime)",
467
+ )
468
+ over.add_argument(
469
+ "--merge-yolo-hold-class",
470
+ dest="merge_yolo_hold_classes",
471
+ action="append",
472
+ metavar="CLASS",
473
+ help="merge only (yolo): an operator class the policy HOLDS; repeatable, "
474
+ "REPLACES the default set (schema-migration, data-backfill, secrets-env, "
475
+ "infra-deploy, billing, security, release-act). A class in both lists is "
476
+ "a fatal startup error",
477
+ )
478
+ over.add_argument(
479
+ "--merge-allow-no-checks-repo",
480
+ dest="merge_allow_no_checks_repos",
481
+ action="append",
482
+ metavar="OWNER/REPO",
483
+ help="merge only: a repository whose heads may merge with no check reported "
484
+ "(the per-repo scope of --merge-allow-no-checks); repeatable",
485
+ )
486
+ follow_ups = over.add_mutually_exclusive_group()
487
+ follow_ups.add_argument(
488
+ "--merge-follow-ups",
489
+ dest="merge_follow_ups",
490
+ action="store_true",
491
+ default=None,
492
+ help="merge only: enumerate follow-up pull requests (`Follow-up-To: #k`) as "
493
+ "candidates (default: on under yolo, off otherwise)",
494
+ )
495
+ follow_ups.add_argument(
496
+ "--no-merge-follow-ups",
497
+ dest="merge_follow_ups",
498
+ action="store_false",
499
+ help="merge only: never enumerate follow-up pull requests",
500
+ )
501
+ over.add_argument(
502
+ "--merge-followup-candidates-per-repo",
503
+ type=int,
504
+ metavar="N",
505
+ help="merge only: at most N follow-up candidates per repo per poll (default 5)",
506
+ )
507
+
508
+ # --- the flow watchdog and the seat consoles (W4, issue #112) --------
509
+ over.add_argument(
510
+ "--seat-consoles-json",
511
+ metavar="JSON",
512
+ help="the seats' consoles, as a JSON list: [{\"seat\": \"devloop\", \"url\": "
513
+ "\"http://dark-devloop-shared.railway.internal:8080\", \"passcodeEnv\": "
514
+ "\"ALISSA_ORC_DEVLOOP_PASSCODE\"}, …] — the passcode itself comes from the "
515
+ "named environment variable, never from here. Prefer "
516
+ "ALISSA_ORC_SEAT_CONSOLES_JSON",
517
+ )
518
+ over.add_argument(
519
+ "--watchdog",
520
+ choices=WATCHDOG_MODES,
521
+ metavar="{" + ",".join(WATCHDOG_MODES) + "}",
522
+ help="the flow watchdog: 'off' reads nothing; 'observe' (the default) reads "
523
+ "every seat console each poll and narrates what it would pull; 'on' climbs "
524
+ "decline → kill → retry, one rung per poll, on sessions a seat reports "
525
+ "waiting past --watchdog-grace-seconds, pages account-level conditions "
526
+ "and holds releases into that seat's repos",
527
+ )
528
+ over.add_argument(
529
+ "--watchdog-grace-seconds",
530
+ type=int,
531
+ metavar="SECONDS",
532
+ help="watchdog only: how long a waiting session is the seat's own responder's "
533
+ "to answer before the orchestrator climbs a rung (default 180; >= 0)",
534
+ )
535
+ over.add_argument(
536
+ "--watchdog-retry-seconds",
537
+ type=int,
538
+ metavar="SECONDS",
539
+ help="watchdog only: after a kill, how long the lane may show no respawn "
540
+ "before the seat's console is asked to retry it (default 600; >= 0)",
541
+ )
542
+ over.add_argument(
543
+ "--watchdog-idle-minutes",
544
+ type=int,
545
+ metavar="MINUTES",
546
+ help="watchdog only: a live, non-waiting session idle longer than this has "
547
+ "its pane read; unclassified idleness is narrated and paged once, never "
548
+ "killed (default 45; >= 1)",
549
+ )
550
+
551
+ levers = over.add_mutually_exclusive_group()
552
+ levers.add_argument(
553
+ "--sentinel-levers",
554
+ dest="sentinel_levers",
555
+ action="store_true",
556
+ default=None,
557
+ help="sentinel only: let the pass pull the bounded `orcloop-lever` verbs "
558
+ "(pane, answer decline/escape, kill, retry) after a pane capture, at most "
559
+ "--sentinel-max-levers per pass, every pull listed in its findings (off by "
560
+ "default — with it off the directive is the report-only one, unchanged)",
561
+ )
562
+ levers.add_argument(
563
+ "--no-sentinel-levers",
564
+ dest="sentinel_levers",
565
+ action="store_false",
566
+ help="sentinel only: keep the pass report-only, even if the config enables levers",
567
+ )
568
+ over.add_argument(
569
+ "--sentinel-max-levers",
570
+ type=int,
571
+ metavar="N",
572
+ help="sentinel only: how many lever ACTIONS (decline/escape/kill/retry) one pass "
573
+ "may make — pane captures are not counted (default 3; >= 0)",
574
+ )
449
575
 
450
576
  dry = over.add_mutually_exclusive_group()
451
577
  dry.add_argument(
@@ -508,7 +634,26 @@ def overrides_from(args: argparse.Namespace) -> dict:
508
634
  "merge_hold_label": args.merge_hold_label,
509
635
  "merge_maintain_label": args.merge_maintain_label,
510
636
  "merge_allow_no_checks": args.merge_allow_no_checks,
637
+ "merge_policy": args.merge_policy,
638
+ "merge_yolo_merge_classes": (
639
+ tuple(args.merge_yolo_merge_classes) if args.merge_yolo_merge_classes else None
640
+ ),
641
+ "merge_yolo_hold_classes": (
642
+ tuple(args.merge_yolo_hold_classes) if args.merge_yolo_hold_classes else None
643
+ ),
644
+ "merge_allow_no_checks_repos": (
645
+ tuple(args.merge_allow_no_checks_repos) if args.merge_allow_no_checks_repos else None
646
+ ),
647
+ "merge_follow_ups": args.merge_follow_ups,
648
+ "merge_followup_candidates_per_repo": args.merge_followup_candidates_per_repo,
511
649
  "fleet_vitals_enabled": args.fleet_vitals_enabled,
650
+ "seat_consoles_json": args.seat_consoles_json,
651
+ "watchdog": args.watchdog,
652
+ "watchdog_grace_seconds": args.watchdog_grace_seconds,
653
+ "watchdog_retry_seconds": args.watchdog_retry_seconds,
654
+ "watchdog_idle_minutes": args.watchdog_idle_minutes,
655
+ "sentinel_levers": args.sentinel_levers,
656
+ "sentinel_max_levers": args.sentinel_max_levers,
512
657
  }
513
658
 
514
659
 
@@ -647,6 +792,29 @@ def log_effective_config(config: Config) -> None:
647
792
  )
648
793
  else:
649
794
  log.debug("sentinel edge: disabled")
795
+ if config.sentinel_enabled and config.sentinel_levers:
796
+ log.info(
797
+ "sentinel levers: ON — a pass may pull at most %d bounded `orcloop-lever` "
798
+ "action(s) after a pane capture (captures not counted)", config.sentinel_max_levers,
799
+ )
800
+ if config.watchdog_active:
801
+ # Seat names, URLs and the passcode VARIABLE names -- never a value.
802
+ log.info(
803
+ "flow watchdog: %s — seats %s; grace %ds, retry after %ds, idle floor %dmin%s",
804
+ config.watchdog.upper(),
805
+ ", ".join(
806
+ f"{seat.seat} ({seat.url}, passcode from {seat.passcode_env}"
807
+ + (f", repos {', '.join(seat.repos)}" if seat.repos else ", every repo") + ")"
808
+ for seat in config.seat_consoles
809
+ ),
810
+ config.watchdog_grace_seconds, config.watchdog_retry_seconds,
811
+ config.watchdog_idle_minutes,
812
+ "" if config.watchdog_pulls else " (narrating only, pulling nothing)",
813
+ )
814
+ elif config.seat_consoles:
815
+ log.info("flow watchdog: OFF (%d seat console(s) declared, none read)", len(config.seat_consoles))
816
+ else:
817
+ log.debug("flow watchdog: no seat consoles declared")
650
818
  if config.merge_when_ready:
651
819
  log.info(
652
820
  "merge edge: ON — merging PRs by %s on an approve from %s carrying "
@@ -656,7 +824,19 @@ def log_effective_config(config: Config) -> None:
656
824
  ", ".join("@" + login for login in config.merge_reviewer_logins),
657
825
  config.merge_method, config.merge_hold_label, config.merge_maintain_label,
658
826
  ", ".join(config.merge_hold_paths) or "(none)",
659
- "merged" if config.merge_allow_no_checks else "held",
827
+ "merged" if config.merge_allow_no_checks else (
828
+ f"merged in {', '.join(config.merge_allow_no_checks_repos)}, held elsewhere"
829
+ if config.merge_allow_no_checks_repos else "held"
830
+ ),
831
+ )
832
+ log.info(
833
+ "merge policy: %s — yolo merges classes %s, holds classes %s (unclassed and "
834
+ "unlisted hold; hard-stop paths hold in every policy); follow-up PRs %s",
835
+ config.merge_policy,
836
+ ", ".join(config.merge_yolo_merge_classes) or "(none)",
837
+ ", ".join(config.merge_yolo_hold_classes) or "(none)",
838
+ f"enumerated (at most {config.merge_followup_candidates_per_repo} per repo per poll)"
839
+ if config.merge_follow_ups else "not enumerated",
660
840
  )
661
841
  else:
662
842
  log.debug("merge edge: disabled")
@@ -638,6 +638,11 @@ class Task:
638
638
  # Defaults to `full`: a `Task` built by hand carries whatever its author put
639
639
  # in it, and treating that as partial would hold releases on nothing.
640
640
  view: str = VIEW_FULL
641
+ # The task's `reporterId`, verbatim -- the actor the merge policy's
642
+ # validation floor (issue #116) assigns the "Validate after merge" task
643
+ # to. "" when the payload omitted it, in which case the downstream task
644
+ # defaults to the calling actor and the floor says so.
645
+ reporter_id: str = ""
641
646
 
642
647
 
643
648
  def _query(params: dict) -> str:
@@ -1311,6 +1316,7 @@ class AlissaClient:
1311
1316
  context_count=context_count,
1312
1317
  updated_at=updated_at,
1313
1318
  view=payload_view,
1319
+ reporter_id=str(task.get("reporterId") or ""),
1314
1320
  )
1315
1321
 
1316
1322
  def get_task_context(self, ref: str) -> list[ContextItem]:
@@ -1399,6 +1405,39 @@ class AlissaClient:
1399
1405
  {"content": body},
1400
1406
  )
1401
1407
 
1408
+ def create_downstream_task(
1409
+ self, ref: str, *, title: str, definition_of_done: str, description: str,
1410
+ criteria: "Sequence[dict]", assign_to_actor_id: str = "",
1411
+ ) -> str:
1412
+ """Create a task DOWNSTREAM of `ref` (`POST /v1/tasks/:id/downstream`)
1413
+ and return the new task's opaque id -- all the create answers
1414
+ (`{"taskId": …}`; the number is a `get_task` away). The merge policy's
1415
+ validation floor (issue #116) is the one caller: the "Validate after
1416
+ merge" task on the origin task, assigned to the origin's reporter.
1417
+
1418
+ `criteria` is the API's shape verbatim (`{id, type, description}`
1419
+ objects -- plain strings are refused with a 400). Permission is the
1420
+ parent's OWNER, which the daemon's token is on the tasks it released;
1421
+ a refusal comes back as the base `AlissaError` and the caller decides
1422
+ (the floor skips the merge)."""
1423
+ body: dict = {
1424
+ "title": title,
1425
+ "definitionOfDone": definition_of_done,
1426
+ "description": description,
1427
+ "validationCriteria": list(criteria),
1428
+ }
1429
+ if assign_to_actor_id:
1430
+ body["assignToActorId"] = assign_to_actor_id
1431
+ payload = self._request(
1432
+ "POST",
1433
+ f"/v1/tasks/{urllib.parse.quote(str(ref), safe='')}/downstream",
1434
+ body,
1435
+ )
1436
+ task_id = payload.get("taskId") if isinstance(payload, dict) else None
1437
+ if not isinstance(task_id, str) or not task_id:
1438
+ raise AlissaError(0, payload, "downstream create answered no taskId")
1439
+ return task_id
1440
+
1402
1441
  def post_loop_events(self, events: "Sequence[dict]") -> dict:
1403
1442
  """Push one batch of loop events (`POST /v1/loop-events`, O34).
1404
1443