alissa-tools-github-orcloop 0.3.10__tar.gz → 0.3.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 (23) hide show
  1. {alissa_tools_github_orcloop-0.3.10/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.11}/PKG-INFO +1 -1
  2. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/loop.py +293 -28
  3. alissa_tools_github_orcloop-0.3.11/src/main/alissa/tools/github/orcloop/version +1 -0
  4. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
  5. alissa_tools_github_orcloop-0.3.10/src/main/alissa/tools/github/orcloop/version +0 -1
  6. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/MANIFEST.in +0 -0
  7. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/README.md +0 -0
  8. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/requirements.txt +0 -0
  9. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/setup.cfg +0 -0
  10. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/setup.py +0 -0
  11. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
  12. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
  13. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/alissa_client.py +0 -0
  14. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/config.py +0 -0
  15. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
  16. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
  17. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
  18. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/state.py +0 -0
  19. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/version.py +0 -0
  20. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
  21. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
  22. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
  23. {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/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.3.10
3
+ Version: 0.3.11
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -428,6 +428,27 @@ ESCALATION_CLOSELOOP_FAILED = "closeloop-failed"
428
428
  ESCALATION_ALREADY_MERGED = "already-merged"
429
429
  ESCALATION_DEDUPE_UNREADABLE = "dedupe-unreadable"
430
430
 
431
+ # The close edge could not write its evidence AT ALL -- not the enriched
432
+ # delivery record, not the bare confirmation line -- for longer than the
433
+ # close-loop retry floor, so the hand-off completed WITHOUT evidence (O17,
434
+ # issue #44). Terminal by construction: it is raised on the pass that also
435
+ # records `CLOSELOOP_EVIDENCE_DONE`, so there is never a second attempt to page
436
+ # about. That is the whole point -- see `_close_evidence_unwritable`.
437
+ ESCALATION_CLOSE_EVIDENCE_FAILED = "close-evidence-failed"
438
+
439
+ # NOT an operator page: the timestamp of the FIRST permanent evidence rejection
440
+ # on this task, so the floor above it measures how long THE FAILURE has lasted.
441
+ # Same shape and same reasoning as `DEDUPE_UNREADABLE_SINCE` (including the
442
+ # load-bearing `marker:` namespace, which keeps it out of every prefix read of
443
+ # the page kind above), with one difference: this clock is never CLEARED. The
444
+ # dedupe/context clocks time a condition that recurs on every poll of a task
445
+ # still waiting to be released, so a blip that clears must not leave a stale
446
+ # start time behind. The close-evidence step settles exactly once per task
447
+ # (`CLOSELOOP_EVIDENCE_DONE` closes it for good, on every path), so this clock
448
+ # can never be re-read after it settles -- leaving the row is free, and it is
449
+ # the audit record of when the failure began.
450
+ CLOSE_EVIDENCE_FAILED_SINCE = "marker:close-evidence-failed"
451
+
431
452
  # NOT an operator page: the timestamp of the FIRST dedupe check that could not
432
453
  # answer for this task, so the floor above it measures how long the CONDITION
433
454
  # has lasted rather than how old the task is. Task age is a fair proxy for a
@@ -471,13 +492,22 @@ MAX_CLOSED_TRAILER_CHECKS = 3
471
492
  # O2 design) purely for its per-(task, kind) dedupe; no comment is ever posted.
472
493
  CLOSELOOP_DONE = "closeloop-done"
473
494
 
474
- # NOT an operator page: an idempotency marker recording that the close-loop
475
- # merged-PR evidence has been attached to the origin task. Gating the evidence
476
- # write on THIS (rather than on the task still being `committed`) survives a
477
- # partial-failure retry that died AFTER the `committed → in_progress` step but
478
- # BEFORE the evidence write -- otherwise the retry re-enters at `in_progress`,
479
- # reads `first_entry` as False, and skips the evidence permanently. Recorded
480
- # only after `add_evidence` succeeds, so a failed write retries next poll.
495
+ # NOT an operator page: an idempotency marker recording that the close-loop's
496
+ # evidence step is SETTLED on the origin task. Gating the evidence write on THIS
497
+ # (rather than on the task still being `committed`) survives a partial-failure
498
+ # retry that died AFTER the `committed → in_progress` step but BEFORE the
499
+ # evidence write -- otherwise the retry re-enters at `in_progress`, reads
500
+ # `first_entry` as False, and skips the evidence permanently.
501
+ #
502
+ # "Settled", not "attached", since O17 (issue #44): the marker is recorded when
503
+ # the enriched record lands, when the DEGRADED bare confirmation lands in its
504
+ # place, and when both have been rejected permanently past the floor and the
505
+ # hand-off gave up on evidence entirely. All three are ends of the same step --
506
+ # there is nothing left for a later poll to attempt -- and one marker for the
507
+ # step is what makes "no double write, no re-page" true on every path. It is
508
+ # never recorded while a retry is still owed: a transient failure, or a
509
+ # permanent one inside the floor, raises instead, leaving the marker absent so
510
+ # the next poll re-enters exactly as before.
481
511
  CLOSELOOP_EVIDENCE_DONE = "closeloop-evidence-done"
482
512
 
483
513
  # NOT an operator page: an idempotency marker recording that the daemon closed a
@@ -634,6 +664,54 @@ CLOSELOOP_FAILED_ISSUE_COMMENT = (
634
664
  "Retrying; operator help may be needed."
635
665
  )
636
666
 
667
+ # The TERMINAL page of the close edge (O17, issue #44). Everything a page has to
668
+ # say when the daemon stops: what it could not do, what it is doing anyway, that
669
+ # it will not try that again, and what is left for a human. The last part
670
+ # matters most -- the operator's soft validator is what reads evidence, and this
671
+ # task now reaches it without any, so the comment has to say where the record
672
+ # went.
673
+ #
674
+ # EVERY CLAIM IS SCOPED TO WHAT IS TRUE AT POST TIME, and that is a constraint
675
+ # the wording had to be rewritten for (review round 1). The page is posted from
676
+ # `_close_evidence_unwritable`, which runs BEFORE `_mutate_close`'s
677
+ # `set_status(pending_validation)` -- it has to, because the page landing is
678
+ # what licenses the walk to continue. So it must not report the hand-off in the
679
+ # past tense (a hand-off write can fail, transiently or for good, and then the
680
+ # task sits at `in_progress` under a comment claiming otherwise), and it must
681
+ # not promise silence on behalf of the whole loop (a failed hand-off pages
682
+ # `closeloop-failed:a<j>` on the very next line, retracting the retraction).
683
+ #
684
+ # What IS terminal here is exactly the step this page is about: the evidence
685
+ # will never be attempted again -- `CLOSELOOP_EVIDENCE_DONE` is recorded on this
686
+ # same pass, on every path out of it. So the terminality is stated about the
687
+ # evidence, and the close-loop's own retries are named as the separate condition
688
+ # they are. Same standard `_autoclose_heal`'s comment holds for a mere WARNING
689
+ # ("AFTER the close, never before it"), applied to something that outranks it.
690
+ CLOSE_EVIDENCE_FAILED_TASK_COMMENT = (
691
+ "**Orchestrator: close evidence could not be attached** — the released "
692
+ "issue {slug} merged, but attaching the merged-pull-request evidence to "
693
+ "this task was rejected permanently (not a rate limit or an outage) for "
694
+ "over {floor_hours}h, with the full delivery record AND a bare merge "
695
+ "confirmation both refused: {error}. **The orchestrator has given up on "
696
+ "the evidence — it will not be attempted again — and is completing the "
697
+ "close hand-off without it now**, moving this task to `pending_validation`: "
698
+ "the merge is a fact, and holding it at `in_progress` over a missing "
699
+ "write-up is strictly worse than thin evidence. The delivery record is on "
700
+ "the merged pull request {pr_url}; attach it by hand if validating this "
701
+ "task needs it. Usual causes: the orchestrator's token lost its role on "
702
+ "this task, or the evidence endpoint is rejecting the request outright. "
703
+ "(If that hand-off write fails too it is a separate condition and pages "
704
+ "separately as `close-loop failed`; those retries are about the status "
705
+ "transition, never about this evidence.)"
706
+ )
707
+ CLOSE_EVIDENCE_FAILED_ISSUE_COMMENT = (
708
+ "**Orchestrator: close evidence could not be attached** — this issue's "
709
+ "merge is being handed off to its origin Alissa task WITHOUT the "
710
+ "merged-pull-request evidence: writing it was refused permanently and the "
711
+ "orchestrator has stopped trying. Nothing is needed here — the record is "
712
+ "on the merged pull request, and any remaining problem is on the task."
713
+ )
714
+
637
715
  # Posted on the issue the daemon closes on GitHub's behalf (O16, issue #40). It
638
716
  # says what was true, what the daemon did, and what it did NOT do -- a reader
639
717
  # arriving at a mechanically-closed issue must be able to tell this apart from
@@ -733,14 +811,19 @@ DELIVERY_NOTES_HEADING = "## Delivery notes"
733
811
  # compatibility commitment.
734
812
  #
735
813
  # THE HEADROOM IS MEASURED, not assumed, because the size of this write is the
736
- # one way this feature could newly BLOCK a close: `add_evidence` is not wrapped
737
- # in `_mutate_close`, `_close_loop` routes its `AlissaError` to
738
- # `_closeloop_failed`, and `set_status(pending_validation)` comes after it -- so
739
- # a rejection that is deterministic rather than transient would strand the
740
- # origin task at `in_progress` on every future poll. `AddEvidenceRequest`
741
- # declares `markdownContent` with a `minLength` and NO `maxLength`, so the
742
- # ceiling was probed directly against the live endpoint (2026-07-28, disposable
743
- # task, since cancelled):
814
+ # one way this feature could newly BLOCK a close -- and O17 (issue #44) did NOT
815
+ # retire that argument, it narrowed it. A permanent rejection now degrades to
816
+ # the bare confirmation line and, past the floor, gives up on evidence rather
817
+ # than stranding the task (`_attach_close_evidence`), but that ladder is driven
818
+ # by the client's status taxonomy: `AlissaTransient` is 408/429/5xx, and the
819
+ # oversize failure below answers **HTTP 500**. An over-cap body is therefore
820
+ # deterministic AND classified transient -- the one shape that retries forever
821
+ # with the identical payload and never reaches the degrade. Nothing here is
822
+ # wrong: statuses are the contract and codes are advisory (see `_classify`), so
823
+ # the guard against that shape is THIS CAP, not the ladder.
824
+ # `AddEvidenceRequest` declares `markdownContent` with a `minLength` and NO
825
+ # `maxLength`, so the ceiling was probed directly against the live endpoint
826
+ # (2026-07-28, disposable task, since cancelled):
744
827
  #
745
828
  # 32,300 -> 201 131,072 -> 201 524,288 -> 201
746
829
  # 65,536 -> 201 262,144 -> 201 1,048,576 -> 500
@@ -956,20 +1039,31 @@ class Orchestrator:
956
1039
  task_comment: str,
957
1040
  issue: Issue | None = None,
958
1041
  issue_comment: str | None = None,
959
- ) -> None:
960
- """Raise an operator escalation once per (task, kind).
1042
+ ) -> bool:
1043
+ """Raise an operator escalation once per (task, kind). Returns whether
1044
+ the page HAS BEEN DELIVERED -- True when a dedupe row exists for it
1045
+ after this call, which (see below) is exactly "the primary comment
1046
+ posted, now or on an earlier pass".
961
1047
 
962
1048
  Primary channel is a comment on the Alissa task; the GitHub issue gets
963
1049
  a best-effort comment too when one exists. All dry-run gated. The
964
1050
  dedupe row is recorded only AFTER the primary comment posts, so a
965
1051
  transient primary failure retries next poll (exactly-once primary
966
- delivery) rather than latching a page that never landed."""
1052
+ delivery) rather than latching a page that never landed.
1053
+
1054
+ That ordering is what makes the return value meaningful rather than
1055
+ decorative: a caller whose next step is IRREVERSIBLE if nobody was told
1056
+ (`_close_evidence_unwritable` settles the evidence step, which stops
1057
+ every future attempt AND every future page) can gate on the receipt
1058
+ instead of re-reading the ledger and hoping the two stay in step. Every
1059
+ pre-existing caller ignores it, which is unchanged behaviour: a page
1060
+ that could not be posted was already retried next poll."""
967
1061
  if self.state.escalated(task_number, kind):
968
- return
1062
+ return True
969
1063
  ref = f"TASK-{task_number}"
970
1064
  if self.config.dry_run:
971
1065
  log.info("[dry-run] would escalate %s on %s:\n%s", kind, ref, task_comment)
972
- return
1066
+ return False
973
1067
 
974
1068
  try:
975
1069
  self.alissa.add_comment(ref, task_comment)
@@ -978,7 +1072,7 @@ class Orchestrator:
978
1072
  "could not post %s escalation on %s: %s — not recording; "
979
1073
  "retries next poll", kind, ref, exc,
980
1074
  )
981
- return
1075
+ return False
982
1076
 
983
1077
  if issue is not None and issue_comment is not None:
984
1078
  try:
@@ -989,6 +1083,7 @@ class Orchestrator:
989
1083
  kind, issue.issue_slug, exc,
990
1084
  )
991
1085
  self.state.record_escalation(task_number, kind)
1086
+ return True
992
1087
 
993
1088
  # ======================================================================
994
1089
  # RELEASE EDGE
@@ -3003,7 +3098,7 @@ class Orchestrator:
3003
3098
  ))
3004
3099
 
3005
3100
  try:
3006
- self._mutate_close(task_number, ref, task, slug, pr)
3101
+ self._mutate_close(task_number, ref, task, slug, issue, pr)
3007
3102
  except AlissaError as exc:
3008
3103
  return self._closeloop_failed(task_number, slug, issue, exc, prior)
3009
3104
 
@@ -3015,7 +3110,13 @@ class Orchestrator:
3015
3110
  ))
3016
3111
 
3017
3112
  def _mutate_close(
3018
- self, task_number: int, ref: str, task: Task, slug: str, pr: PullRequest
3113
+ self,
3114
+ task_number: int,
3115
+ ref: str,
3116
+ task: Task,
3117
+ slug: str,
3118
+ issue: Issue,
3119
+ pr: PullRequest,
3019
3120
  ) -> None:
3020
3121
  """The close-loop mutation, ordered for idempotency on partial-failure
3021
3122
  retry. Two independent idempotency gates, each keyed on what it actually
@@ -3025,8 +3126,8 @@ class Orchestrator:
3025
3126
  `committed`, so a retry that re-enters at `in_progress` never re-issues
3026
3127
  the (illegal) `in_progress → in_progress` transition;
3027
3128
  * the merged-PR evidence write is gated on the `CLOSELOOP_EVIDENCE_DONE`
3028
- marker, recorded only AFTER the write lands. A retry that died between
3029
- the `in_progress` step and the evidence write re-enters at
3129
+ marker, recorded only once that step is SETTLED. A retry that died
3130
+ between the `in_progress` step and the evidence write re-enters at
3030
3131
  `in_progress` with the marker absent, so it still attaches the evidence
3031
3132
  -- where a shared `first_entry` flag would have skipped it forever
3032
3133
  (the task would reach `pending_validation` with no merged-PR audit
@@ -3039,6 +3140,11 @@ class Orchestrator:
3039
3140
  failure mode to the walk: a PR whose write-up cannot be read still
3040
3141
  reaches `pending_validation`, carrying an explicit line saying so.
3041
3142
 
3143
+ And since O17 (issue #44), a rejected write cannot stop the walk either:
3144
+ `_attach_close_evidence` owns the whole ladder (degrade to the bare
3145
+ line, then give up past the floor), so THIS method's last line is
3146
+ reached on every path that is not still owed a retry.
3147
+
3042
3148
  Criteria satisfaction is naturally idempotent (already-satisfied ones are
3043
3149
  skipped), so it needs no gate."""
3044
3150
  if task.status == TASK_TODO_STATE:
@@ -3046,14 +3152,173 @@ class Orchestrator:
3046
3152
  for criterion in task.criteria:
3047
3153
  if not criterion.satisfied:
3048
3154
  self.alissa.satisfy_criterion(ref, criterion.id)
3049
- if not self.state.escalated(task_number, CLOSELOOP_EVIDENCE_DONE):
3155
+ self._attach_close_evidence(task_number, ref, slug, issue, pr)
3156
+ self.alissa.set_status(ref, "pending_validation")
3157
+
3158
+ def _attach_close_evidence(
3159
+ self, task_number: int, ref: str, slug: str, issue: Issue, pr: PullRequest
3160
+ ) -> None:
3161
+ """Attach the merged-PR evidence, or settle the step without it. The
3162
+ one rule: never RAISE unless a retry is genuinely owed, because the
3163
+ caller's next line is the `pending_validation` hand-off and an
3164
+ exception here strands the task at `in_progress` (issue #44 -- the
3165
+ strand this method exists to remove).
3166
+
3167
+ The ladder, in the order a failure walks it:
3168
+
3169
+ 1. **Transient** (`AlissaTransient`: 408/429/5xx or a transport
3170
+ failure) -- re-raised untouched, at every rung. This is the ONLY
3171
+ path that keeps today's behavior byte-for-byte: `_close_loop` routes
3172
+ it to `_closeloop_failed`, which pages `closeloop-failed:a<j>` and
3173
+ floors the retry. An outage must not spend the degrade.
3174
+ 2. **Permanent rejection of the ENRICHED body** -- retried ONCE,
3175
+ immediately, with the bare `CLOSE_EVIDENCE_BODY`. The enriched record
3176
+ is the only *variable* input to this write (a PR body someone else
3177
+ authored, up to `CLOSE_EVIDENCE_RECORD_MAX_CHARS` of it), so it is
3178
+ the first suspect for a deterministic refusal, and dropping it is
3179
+ the same "degrade loudly, never block" trade `render_close_evidence`
3180
+ already makes for an unreadable body. Nothing is added to the bare
3181
+ payload to explain the degradation: the payload is the suspect, and
3182
+ the log line is where the explanation belongs.
3183
+ 3. **Permanent rejection of the BARE line too** -- the payload was
3184
+ never the problem (a lost role, an endpoint refusing outright), so
3185
+ `_close_evidence_unwritable` decides between one more retry and
3186
+ giving up.
3187
+
3188
+ Why the split at transient/permanent is `AlissaTransient` and not a
3189
+ status test here: the client's taxonomy already IS that split (it keys
3190
+ on status, which is the contract), and `_dedupe_unreadable`'s lesson is
3191
+ the reason it matters -- a self-clearing rate limit must never spend a
3192
+ one-shot remedy that a real, permanent failure will need."""
3193
+ if self.state.escalated(task_number, CLOSELOOP_EVIDENCE_DONE):
3194
+ return
3195
+ try:
3050
3196
  self.alissa.add_evidence(
3051
3197
  ref,
3052
3198
  CLOSE_EVIDENCE_TITLE,
3053
3199
  render_close_evidence(slug, pr),
3054
3200
  )
3055
- self.state.record_escalation(task_number, CLOSELOOP_EVIDENCE_DONE)
3056
- self.alissa.set_status(ref, "pending_validation")
3201
+ except AlissaTransient:
3202
+ raise
3203
+ except AlissaError as rich_exc:
3204
+ log.warning(
3205
+ "%s: the close evidence was REJECTED permanently on %s (%s) — "
3206
+ "retrying once with the bare merge confirmation, without the "
3207
+ "merged pull request's delivery record",
3208
+ slug, ref, rich_exc,
3209
+ )
3210
+ try:
3211
+ self.alissa.add_evidence(
3212
+ ref,
3213
+ CLOSE_EVIDENCE_TITLE,
3214
+ CLOSE_EVIDENCE_BODY.format(slug=slug, pr_url=pr.url),
3215
+ )
3216
+ except AlissaTransient:
3217
+ raise
3218
+ except AlissaError as bare_exc:
3219
+ self._close_evidence_unwritable(
3220
+ task_number, ref, slug, issue, pr, bare_exc,
3221
+ )
3222
+ else:
3223
+ log.warning(
3224
+ "%s: DEGRADED close evidence attached to %s — the merge "
3225
+ "confirmation only; the delivery record was rejected (%s) "
3226
+ "and is on the pull request %s",
3227
+ slug, ref, rich_exc, pr.url or pr.pr_slug,
3228
+ )
3229
+ self.state.record_escalation(task_number, CLOSELOOP_EVIDENCE_DONE)
3230
+
3231
+ def _close_evidence_unwritable(
3232
+ self,
3233
+ task_number: int,
3234
+ ref: str,
3235
+ slug: str,
3236
+ issue: Issue,
3237
+ pr: PullRequest,
3238
+ exc: AlissaError,
3239
+ ) -> None:
3240
+ """Neither the enriched record nor the bare line could be written, and
3241
+ the refusal was permanent. Either raise (a retry is still owed) or
3242
+ return, having paged terminally -- the caller then settles the step and
3243
+ the hand-off finishes WITHOUT evidence.
3244
+
3245
+ Why give up at all: a permanent refusal does not self-heal, so the
3246
+ alternative is re-attempting the identical write every poll forever
3247
+ while the origin task sits at `in_progress` and the operator is paged
3248
+ on a schedule. The merge already happened; thin evidence on a task that
3249
+ reached `pending_validation` is strictly better than a full record on a
3250
+ task nobody can validate. That is the same trade the close edge already
3251
+ makes for an unreadable PR body, taken one rung further down.
3252
+
3253
+ Why not give up on the FIRST permanent refusal, since a deterministic
3254
+ failure is deterministic by definition: because "permanent" here is a
3255
+ CLASSIFICATION, not an observation. A 403 is permanent as a rule and
3256
+ transient in fact when a role is being re-granted, and this remedy is
3257
+ one-shot -- the evidence is dropped for good. So the first refusal
3258
+ records this clock and re-raises into today's unchanged path (page,
3259
+ floor, retry), and only a refusal that is STILL permanent
3260
+ `CLOSELOOP_RETRY_FLOOR_SECONDS` later spends the remedy. The floor
3261
+ measures THE FAILURE and not the task's age, for the reason
3262
+ `_dedupe_unreadable` gives at length.
3263
+
3264
+ Why the page must have LANDED before the step settles: settling is what
3265
+ stops every future attempt, so a give-up whose page never posted would
3266
+ be silent forever -- the exact failure mode issue #44 is about, moved
3267
+ one layer up. `_escalate` records its dedupe row only after the primary
3268
+ comment posts, so it can say whether the page was delivered and this
3269
+ gates on that; without it, this raises instead and the give-up is
3270
+ re-attempted next poll. (When the page cannot post because the token
3271
+ lost its role on the task, the `pending_validation` write below was
3272
+ going to fail anyway -- so nothing is stranded that was not already.)
3273
+
3274
+ What this page may NOT claim: the hand-off in the past tense, or
3275
+ silence from the close loop. Both were false in round 1 -- see
3276
+ `CLOSE_EVIDENCE_FAILED_TASK_COMMENT`, which now scopes its terminality
3277
+ to the evidence step, the one thing this method really does end."""
3278
+ rows = self.state.escalations_matching(task_number, CLOSE_EVIDENCE_FAILED_SINCE)
3279
+ now = int(self._now())
3280
+ if not rows:
3281
+ self.state.record_escalation(task_number, CLOSE_EVIDENCE_FAILED_SINCE, at=now)
3282
+ log.error(
3283
+ "%s: close evidence rejected permanently on %s, bare line too "
3284
+ "(%s) — first refusal, retrying behind the close-loop floor",
3285
+ slug, ref, exc,
3286
+ )
3287
+ raise exc
3288
+ failing_for = now - int(rows[0]["escalated_at"])
3289
+ if failing_for < CLOSELOOP_RETRY_FLOOR_SECONDS:
3290
+ log.error(
3291
+ "%s: close evidence rejected permanently on %s, bare line too "
3292
+ "(%s) — refused for %ds, within the %ds floor; retrying",
3293
+ slug, ref, exc, failing_for, CLOSELOOP_RETRY_FLOOR_SECONDS,
3294
+ )
3295
+ raise exc
3296
+
3297
+ delivered = self._escalate(
3298
+ task_number,
3299
+ ESCALATION_CLOSE_EVIDENCE_FAILED,
3300
+ CLOSE_EVIDENCE_FAILED_TASK_COMMENT.format(
3301
+ slug=slug,
3302
+ floor_hours=CLOSELOOP_RETRY_FLOOR_SECONDS // 3600,
3303
+ error=exc,
3304
+ pr_url=pr.url or pr.pr_slug,
3305
+ ),
3306
+ issue,
3307
+ CLOSE_EVIDENCE_FAILED_ISSUE_COMMENT,
3308
+ )
3309
+ if not delivered:
3310
+ log.error(
3311
+ "%s: giving up on close evidence for %s, but the terminal page "
3312
+ "could not be delivered — NOT settling; retries next poll",
3313
+ slug, ref,
3314
+ )
3315
+ raise exc
3316
+ log.error(
3317
+ "%s: GAVE UP on close evidence for %s after %ds of permanent "
3318
+ "refusals (%s) — completing the hand-off without it; operator "
3319
+ "paged once, no further attempts",
3320
+ slug, ref, failing_for, exc,
3321
+ )
3057
3322
 
3058
3323
  def _closeloop_failed(
3059
3324
  self, task_number: int, slug: str, issue: Issue, exc: Exception, prior: list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-orcloop
3
- Version: 0.3.10
3
+ Version: 0.3.11
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera