alissa-tools-github-orcloop 0.18.2__tar.gz → 0.18.3__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.2/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.18.3}/PKG-INFO +1 -1
  2. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/alissa_client.py +13 -3
  3. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/merge.py +122 -30
  4. alissa_tools_github_orcloop-0.18.3/src/main/alissa/tools/github/orcloop/version +1 -0
  5. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_orcloop-0.18.2/src/main/alissa/tools/github/orcloop/version +0 -1
  7. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/LICENSE +0 -0
  8. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/MANIFEST.in +0 -0
  9. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/NOTICE +0 -0
  10. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/README.md +0 -0
  11. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/requirements.txt +0 -0
  12. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/setup.cfg +0 -0
  13. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/setup.py +0 -0
  14. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
  15. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
  16. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/alissa_cli.py +0 -0
  17. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/config.py +0 -0
  18. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/console.py +0 -0
  19. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/fleet_vitals.py +0 -0
  20. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
  21. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/lever.py +0 -0
  22. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/loop.py +0 -0
  23. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/loop_events.py +0 -0
  24. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
  25. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
  26. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/seats.py +0 -0
  27. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/sentinel.py +0 -0
  28. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/state.py +0 -0
  29. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/version.py +0 -0
  30. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/watchdog.py +0 -0
  31. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/workplan.py +0 -0
  32. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/workplan_html.py +0 -0
  33. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa/tools/github/orcloop/workplan_state.py +0 -0
  34. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
  35. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
  36. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
  37. {alissa_tools_github_orcloop-0.18.2 → alissa_tools_github_orcloop-0.18.3}/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.2
3
+ Version: 0.18.3
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -1413,13 +1413,23 @@ class AlissaClient:
1413
1413
  and return the new task's opaque id -- all the create answers
1414
1414
  (`{"taskId": …}`; the number is a `get_task` away). The merge policy's
1415
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.
1416
+ merge" task on the origin task, OWNED BY THE CALLER -- the fleet
1417
+ actor, the API's own default when `assign_to_actor_id` is left empty.
1418
+
1419
+ `assign_to_actor_id` seats another actor as owner and is subject to
1420
+ the API's owner-assign gate: a caller that is not an admin of a team
1421
+ the target belongs to (and does not pass `teamSlug`) is refused with
1422
+ `HTTP 403 FORBIDDEN: {'error': 'FORBIDDEN', 'message': 'Only a team
1423
+ admin can seat another member as owner; pass teamSlug'}`. The fleet
1424
+ actor is a member, not an admin, so the floor never passes it (issue
1425
+ #120); the parameter stays for a caller that can.
1417
1426
 
1418
1427
  `criteria` is the API's shape verbatim (`{id, type, description}`
1419
1428
  objects -- plain strings are refused with a 400). Permission is the
1420
1429
  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)."""
1430
+ a refusal comes back as an `AlissaError` subclass and the caller
1431
+ decides (the floor skips the merge -- and escalates once when the
1432
+ refusal is permanent)."""
1423
1433
  body: dict = {
1424
1434
  "title": title,
1425
1435
  "definitionOfDone": definition_of_done,
@@ -84,14 +84,24 @@ messages). The gate, in order:
84
84
  10. `dry_run` -> SKIPPED "would merge …", no writes -- the floor included.
85
85
  9b. The FLOOR (issue #116, L3), only for a merge the policy downgraded from
86
86
  an operator hold: BEFORE the merge call, once per (pr, head) (ledger
87
- `merge:floor:<pr>:<head7>`), a comment on the origin Alissa task naming
88
- the class and the reason, and a committed `Validate after merge`
89
- downstream task owned by the origin's reporter. Either write failing ->
90
- SKIPPED "floor not posted", retried next poll. Never a merge without
91
- the page. Three rows keep the retry from repeating a write that landed:
92
- the comment's, the task's (`merge:floor-task:<pr>:<head7>:<taskId>`,
93
- written the moment the create returns, so a failed read-back or status
94
- set never creates a second task), and the floor's.
87
+ `merge:floor:<pr>:<head7>`), a comment on the origin Alissa task saying
88
+ the policy IS MERGING past the gate (class and reason), and a committed
89
+ `Validate after merge` downstream task OWNED BY THE CALLING ACTOR -- the
90
+ fleet actor, the identity that owns every fed task (issue #120: the API's
91
+ owner-assign gate refuses the fleet actor seating the origin's reporter
92
+ as owner, so the reporter is reached through the origin task, which is
93
+ the parent of the floor task and carries the floor comment). A write
94
+ failing TRANSIENTLY (5xx, 429, transport) -> SKIPPED "floor not posted",
95
+ retried next poll. A write failing PERMANENTLY (a 4xx that is not
96
+ transient: 403, 404, 422) -> one escalation comment on the origin task
97
+ (`merge:floor-failed:<pr>:<head7>`) saying the PR needs a human merge
98
+ until the floor can be written, one WARNING (L4), and every later poll
99
+ on this head is a quiet SKIP that writes nothing; a new head is a new
100
+ episode. Never a merge without the page. Three rows keep the retry from
101
+ repeating a write that landed: the comment's, the task's
102
+ (`merge:floor-task:<pr>:<head7>:<taskId>`, written the moment the create
103
+ returns, so a failed read-back or status set never creates a second
104
+ task), and the floor's.
95
105
  11. Merge: `PUT /pulls/{n}/merge` with the configured method and
96
106
  `sha=head_sha` -- the guard that makes GitHub refuse (405/409) when the
97
107
  head moved between our reads and the merge; a refusal is SKIPPED and
@@ -133,7 +143,7 @@ from functools import lru_cache
133
143
  from typing import Callable, Sequence
134
144
 
135
145
  from . import loop_events
136
- from .alissa_client import AlissaClient, AlissaError
146
+ from .alissa_client import AlissaClient, AlissaError, AlissaTransient
137
147
  from .config import MERGE_POLICY_YOLO, MERGE_READINESS_CLASSES, Config
138
148
  from .ghclient import (
139
149
  CheckSummary,
@@ -249,6 +259,18 @@ def policy_verdict(config: Config, klass: str | None) -> tuple[bool, str]:
249
259
  return (False, f"policy yolo holds class {klass} (in neither class list)")
250
260
 
251
261
 
262
+ def floor_failure_is_permanent(exc: AlissaError) -> bool:
263
+ """Is this floor write refused for GOOD (issue #120) -- a 4xx that is not a
264
+ retry-later signal? 403 (the owner-assign gate, a missing role), 404 (the
265
+ origin task is gone or invisible), 422 (a payload the API refuses) do not
266
+ heal between polls; retrying them costs a WARNING and a refused write per
267
+ poll for as long as the head is approved. `AlissaTransient` (408/429/5xx
268
+ and transport failures, status 0) is excluded by CLASS, so the client's
269
+ taxonomy and this predicate cannot drift; a status-0 base error (the
270
+ create answered no `taskId`) is a malformed answer, retried too."""
271
+ return not isinstance(exc, AlissaTransient) and 400 <= exc.status < 500
272
+
273
+
252
274
  # --- hard-stop path globs ----------------------------------------------------
253
275
  #
254
276
  # `fnmatch` has no `**`: `*` there crosses `/`, so `**/migrations/**` and
@@ -358,11 +380,16 @@ HELD_KIND_PREFIX = MERGE_KIND_PREFIX + "held:" # merge:held:<why>:<pr
358
380
  # a failed read-back or status set finishes THAT task instead of creating a
359
381
  # second (PR #117 round 1); the floor row is the one the merge reads. A
360
382
  # fourth, `floor-unmerged`, is the once-per-head correction the origin task
361
- # gets when the merge call it was paged about is then refused.
383
+ # gets when the merge call it was paged about is then refused. A fifth,
384
+ # `floor-failed` (issue #120), is the once-per-head escalation a floor write
385
+ # that failed PERMANENTLY (a non-transient 4xx) leaves: its row is what keeps
386
+ # every later poll on that head a quiet SKIP instead of a WARNING and a
387
+ # refused write per poll.
362
388
  FLOOR_KIND_PREFIX = MERGE_KIND_PREFIX + "floor:" # merge:floor:<pr>:<head7>
363
389
  FLOOR_COMMENT_KIND_PREFIX = MERGE_KIND_PREFIX + "floor-comment:" # merge:floor-comment:<pr>:<head7>
364
390
  FLOOR_TASK_KIND_PREFIX = MERGE_KIND_PREFIX + "floor-task:" # merge:floor-task:<pr>:<head7>:<taskId>
365
391
  FLOOR_UNMERGED_KIND_PREFIX = MERGE_KIND_PREFIX + "floor-unmerged:" # merge:floor-unmerged:<pr>:<head7>
392
+ FLOOR_FAILED_KIND_PREFIX = MERGE_KIND_PREFIX + "floor-failed:" # merge:floor-failed:<pr>:<head7>
366
393
 
367
394
  # The hold reasons, as the short slugs the ledger kind and the event key carry.
368
395
  HOLD_OPERATOR = "operator"
@@ -422,10 +449,14 @@ MERGED_BY_POLICY_COMMENT = (
422
449
  "on {task_ref} as a comment and a `Validate after merge` task before the merge."
423
450
  )
424
451
  # The floor (issue #116, L3): what the origin task reads BEFORE the merge call
425
- # when the policy downgrades an operator hold. The wording is the issue's.
452
+ # when the policy downgrades an operator hold. The wording is issue #120's: it
453
+ # is posted before the merge call, so it says the merge IS HAPPENING -- the
454
+ # earlier "merged" read false at posting time and stayed false whenever the
455
+ # floor's second write then failed.
426
456
  FLOOR_TASK_COMMENT = (
427
- "**Merge policy {policy} merged PR #{pr} with an open operator gate** — "
428
- "{klass}: {reason}. Validate this before validating the task. ({url})"
457
+ "**Merge policy `{policy}` is merging PR #{pr} with an open operator gate** — "
458
+ "{klass}: {reason}. A `Validate after merge` task follows this comment; "
459
+ "validate that before validating this task. ({url})"
429
460
  )
430
461
  FLOOR_TASK_TITLE = "Validate after merge: {reason}"
431
462
  FLOOR_TASK_DOD = (
@@ -433,14 +464,31 @@ FLOOR_TASK_DOD = (
433
464
  "has been validated by a human after the merge, and the outcome is recorded "
434
465
  "on this task."
435
466
  )
467
+ # Owner: the calling actor (issue #120). The API defaults the owner of a
468
+ # downstream task to the caller, and the caller is the fleet actor -- the one
469
+ # identity that owns every task the loop feeds. The origin's reporter is not
470
+ # seated (the owner-assign gate refuses it) and does not need to be: the human
471
+ # validates the ORIGIN task, which is this task's parent and carries the floor
472
+ # comment; this row exists so the item cannot vanish.
436
473
  FLOOR_TASK_DESCRIPTION = (
437
- "The orchestrator's merge edge merged {pr_url} under merge policy `{policy}` "
474
+ "The orchestrator's merge edge is merging {pr_url} under merge policy `{policy}` "
438
475
  "although the reviewer's approve read `Merge-Readiness: operator — {klass}: "
439
- "{reason}`. The class is in `merge_yolo_merge_classes`, so the merge did not "
476
+ "{reason}`. The class is in `merge_yolo_merge_classes`, so the merge does not "
440
477
  "wait -- but the gate is still open: validate it before validating the origin "
441
- "task.\n\nAlissa-Task: {task_ref}"
478
+ "task. Owner: the fleet actor; the human validates the ORIGIN task — this row "
479
+ "exists so the item cannot vanish.\n\nAlissa-Task: {task_ref}"
442
480
  )
443
481
  FLOOR_TASK_CRITERION = "The open operator gate ({klass}) has been validated by a human after the merge."
482
+ # The escalation (issue #120): a floor write refused for good -- a 4xx that is
483
+ # not transient, the owner-assign gate's 403 being the one that was seen in
484
+ # production -- is not a next-poll retry. Once per (pr, head), on the origin
485
+ # task, naming the HTTP error; the merge edge then leaves this head alone.
486
+ FLOOR_FAILED_TASK_COMMENT = (
487
+ "**Merge policy `{policy}` cannot write the validation floor for PR #{pr} on "
488
+ "`{head7}`** — {detail}. This PR needs a human merge until the floor can be "
489
+ "written; the merge edge skips this head without retrying (a new head is a "
490
+ "new attempt). ({url})"
491
+ )
444
492
  # The correction (PR #117 round 1): the floor is worded as the issue words it
445
493
  # and posted BEFORE the merge call, so a refused merge leaves the origin task
446
494
  # paged about a merge that did not happen. Once per (pr, head), best-effort.
@@ -941,10 +989,21 @@ class MergeEdge:
941
989
  ) -> "MergeDecision | None":
942
990
  """Page the origin task about the gate the policy is about to merge
943
991
  past: one comment, then one committed `Validate after merge`
944
- downstream task owned by the origin's reporter -- BEFORE the merge
945
- call, once per (pr, head). None when the floor stands (posted now or
946
- on an earlier poll for this head); a SKIPPED decision when any write
947
- failed, so this poll merges nothing and the next retries the floor.
992
+ downstream task owned by the calling actor (issue #120) -- BEFORE the
993
+ merge call, once per (pr, head). None when the floor stands (posted
994
+ now or on an earlier poll for this head); a SKIPPED decision when any
995
+ write failed: a TRANSIENT failure (5xx, 429, transport) is retried
996
+ next poll; a PERMANENT one (a 4xx that is not transient -- 403, 404,
997
+ 422) is escalated once on the origin task and every later poll on
998
+ this head is a quiet SKIP (`_floor_failed`).
999
+
1000
+ The task's owner is whoever the API defaults to -- the caller, i.e.
1001
+ the fleet actor. `assign_to_actor_id` is deliberately NOT passed: the
1002
+ owner-assign gate refuses the fleet actor seating another actor as
1003
+ owner ("Only a team admin can seat another member as owner; pass
1004
+ teamSlug"), which is what kept every yolo merge skipped forever
1005
+ (issue #120). The reporter still sees the floor through the origin
1006
+ task, this task's parent, which carries the floor comment.
948
1007
 
949
1008
  Three ledger rows: the comment's lands after the comment so a failed
950
1009
  task create does not re-post it; the task's -- carrying the created
@@ -957,6 +1016,14 @@ class MergeEdge:
957
1016
  floor_kind = f"{FLOOR_KIND_PREFIX}{pr.number}:{pr.head7}"
958
1017
  if self.state.escalated(candidate.task_number, floor_kind):
959
1018
  return None
1019
+ failed_kind = f"{FLOOR_FAILED_KIND_PREFIX}{pr.number}:{pr.head7}"
1020
+ if self.state.escalated(candidate.task_number, failed_kind):
1021
+ # Escalated on an earlier poll: quiet, no write, no WARNING (L4).
1022
+ return self._skipped(
1023
+ candidate,
1024
+ f"{pr.pr_slug}: floor failed for good on {pr.head7} (escalated on {ref}) — "
1025
+ f"needs a human merge; a new head retries",
1026
+ )
960
1027
  if self.alissa is None:
961
1028
  return self._skipped(
962
1029
  candidate,
@@ -973,15 +1040,7 @@ class MergeEdge:
973
1040
  self.state.record_escalation(candidate.task_number, comment_kind, at=now)
974
1041
  task_id = self._floor_task_id(candidate, task_prefix)
975
1042
  if task_id is None:
976
- # The reporter owns the validation: read it off the origin
977
- # task. A payload without one falls back to the calling actor
978
- # (the API's own default) and says so in the log.
979
- origin = self.alissa.get_task(ref)
980
- if not origin.reporter_id:
981
- log.warning(
982
- "merge: %s carries no reporterId — the validation task defaults to the "
983
- "daemon's own actor", ref,
984
- )
1043
+ # No owner override (issue #120): the API seats the caller.
985
1044
  task_id = self.alissa.create_downstream_task(
986
1045
  ref,
987
1046
  title=FLOOR_TASK_TITLE.format(reason=reason or klass)[:200],
@@ -995,7 +1054,6 @@ class MergeEdge:
995
1054
  "id": "vc-1", "type": "manual",
996
1055
  "description": FLOOR_TASK_CRITERION.format(klass=klass),
997
1056
  }],
998
- assign_to_actor_id=origin.reporter_id,
999
1057
  )
1000
1058
  # The task exists from here on: its row lands BEFORE the
1001
1059
  # read-back and the status set, so a failure in either
@@ -1010,6 +1068,8 @@ class MergeEdge:
1010
1068
  if created.status != "committed":
1011
1069
  self.alissa.set_status(task_id, "committed")
1012
1070
  except AlissaError as exc:
1071
+ if floor_failure_is_permanent(exc):
1072
+ return self._floor_failed(candidate, now, exc=exc, kind=failed_kind)
1013
1073
  log.warning(
1014
1074
  "merge: floor on %s for %s not posted (%s) — merge skipped this poll",
1015
1075
  ref, pr.pr_slug, exc,
@@ -1037,6 +1097,38 @@ class MergeEdge:
1037
1097
  task_id = str(rows[0]["kind"])[len(task_prefix):]
1038
1098
  return task_id or None
1039
1099
 
1100
+ def _floor_failed(
1101
+ self, candidate: MergeCandidate, now: int, *, exc: AlissaError, kind: str,
1102
+ ) -> MergeDecision:
1103
+ """A floor write refused for good (issue #120): one escalation comment
1104
+ on the origin task naming the HTTP error, one WARNING, the
1105
+ `merge:floor-failed:<pr>:<head7>` row -- and SKIPPED. The row is what
1106
+ makes every later poll on this head a quiet SKIP before any write
1107
+ (`_floor`'s second check). The comment is best-effort in the same
1108
+ sense as the correction: one that did not post leaves no row, so the
1109
+ next poll escalates again (and re-tries the floor, which fails the
1110
+ same way) until the page lands -- an escalation that never posted is
1111
+ not an escalation."""
1112
+ pr = candidate.pr
1113
+ ref = candidate.ref
1114
+ log.warning(
1115
+ "merge: floor on %s for %s failed for good (%s) — escalated once, this head "
1116
+ "needs a human merge; skipped quietly from here on",
1117
+ ref, pr.pr_slug, exc,
1118
+ )
1119
+ posted = self._task_comment(candidate, FLOOR_FAILED_TASK_COMMENT.format(
1120
+ policy=self.config.merge_policy, pr=pr.number, head7=pr.head7, detail=exc,
1121
+ url=pr.url or pr.pr_slug,
1122
+ ))
1123
+ if posted:
1124
+ self.state.record_escalation(candidate.task_number, kind, at=now)
1125
+ return self._skipped(
1126
+ candidate,
1127
+ f"{pr.pr_slug}: floor failed for good on {ref} ({exc}) — escalated"
1128
+ + ("" if posted else " (escalation comment pending — retries next poll)")
1129
+ + ", needs a human merge; a new head retries",
1130
+ )
1131
+
1040
1132
  def _floor_unmerged(self, candidate: MergeCandidate, now: int, *, detail: str) -> None:
1041
1133
  """The correction the origin task gets when the merge call fails on a
1042
1134
  head it was paged about: once per (pr, head), best-effort (a comment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-orcloop
3
- Version: 0.18.2
3
+ Version: 0.18.3
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera