alissa-tools-github-devloop 0.8.58__py3-none-any.whl → 0.8.60__py3-none-any.whl

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.
@@ -547,11 +547,16 @@ _CRITERIA_RULE = (
547
547
  )
548
548
 
549
549
  # The adopt half: a resumed or fix-round session inherits a task some other
550
- # session typed, possibly before this rule existed.
550
+ # session typed, possibly before this rule existed. No worker surface can
551
+ # edit, retype or add a criterion on an existing task (the REST API only
552
+ # creates tasks with criteria and satisfies them; the CLI has neither), so
553
+ # the clause asks for the one thing a worker can do: put each breach in front
554
+ # of the validator, who reads the delivery note.
551
555
  _CRITERIA_ADOPT = (
552
- " On a task you ADOPT, retype any criterion that breaks this rule (a fact "
553
- "typed `human` becomes `evidence`), and type any criterion you add the "
554
- "same way."
556
+ " On a task you ADOPT, leave its criteria as they are — no tool you have "
557
+ "can edit, retype or add one — and list every criterion that breaks this "
558
+ "rule, with the type it should carry (a fact typed `human` should be "
559
+ "`evidence`), under the delivery note's Judgment calls."
555
560
  )
556
561
 
557
562
  # The origin-task instruction is split in two because the downstream rule is
@@ -1889,6 +1894,16 @@ LIVE_STALLED_COMMENT = (
1889
1894
  "`{classified}`."
1890
1895
  )
1891
1896
 
1897
+ ASSIGNED_OUTSIDE_COMMENT = (
1898
+ "**Dev loop skipping this issue** — it has been assigned to `{login}` for "
1899
+ "at least {minutes} min, but this daemon has no spawn for it on its "
1900
+ "ledger and no spawn line of its own on the activity comment, so the "
1901
+ "assignment was made outside the loop (by hand, or under another "
1902
+ "workspace's ledger). The daemon will not spawn over it. Operator "
1903
+ "options: unassign `{login}` to let the loop take the issue, or leave it "
1904
+ "if someone is working it by hand. (One-time notice.)"
1905
+ )
1906
+
1892
1907
  # The operator page for an account-level prompt kind (issue #127): an
1893
1908
  # expired login, a hit usage limit, an empty credit balance. No keystroke
1894
1909
  # answers these, so the page is the act -- one per kind per
@@ -2253,6 +2268,16 @@ ESCALATION_FIX_HANDOVER = "fix-handover"
2253
2268
  # neither ping may silence the other.
2254
2269
  ESCALATION_LIVE_STALLED = "live-stalled"
2255
2270
 
2271
+ # The assigned-outside skip's once-per-issue operator page (issue #200): the
2272
+ # issue is assigned to this login, the ledger has no row and its activity
2273
+ # comment no spawn line of ours, so nothing the daemon can see explains the
2274
+ # assignment. Once-ever like ESCALATION_ASSIGNMENT -- the remedy (unassign,
2275
+ # or leave it to whoever is working it by hand) does not change per poll.
2276
+ # ASSIGNED_OUTSIDE_SEEN is the episode's durable first-seen clock, kept in the
2277
+ # activity_log ledger so a daemon restart does not restart the wait.
2278
+ ESCALATION_ASSIGNED_OUTSIDE = "assigned-outside"
2279
+ ASSIGNED_OUTSIDE_SEEN = "assigned-outside-seen"
2280
+
2256
2281
  # The wedge kill's once-per-episode marker (see wedge_kind). Not an operator
2257
2282
  # page -- the daemon acted instead of asking -- but, like ESCALATION_REREQUEST,
2258
2283
  # a once-per-key act the `escalations` table is the ledger of: it dedupes the
@@ -4832,14 +4857,21 @@ class DevWatcher:
4832
4857
  return self._claim_deferral(claim, attempts)
4833
4858
 
4834
4859
  if login in issue.assignees and age is None:
4835
- # Self-assigned but the ledger never spawned: assigned outside this
4836
- # daemon (by hand, or under another workspace's ledger). Not a
4837
- # fresh self-spawn, so not ours to double-book.
4838
- return Decision(
4839
- Action.SKIPPED,
4840
- f"assigned to {login} outside this daemon (no spawn on the "
4841
- f"ledger) — unassign the issue to let the loop take it",
4842
- )
4860
+ # Self-assigned but the ledger never spawned. Issue #200: when the
4861
+ # issue's own activity comment carries an EXPIRED spawn line by
4862
+ # this login on this lane (the claim above already let it age
4863
+ # out), the assignment is a dead attempt of ours whose ledger row
4864
+ # was lost (an overlapping deploy wrote it) -- adopt it into the
4865
+ # ledger and let the ordinary stale matrix below decide, exactly
4866
+ # as if the row had been there all along.
4867
+ orphans = self._orphaned_spawns(issue, session_name(issue, 1))
4868
+ if orphans:
4869
+ attempts, age = self._adopt_orphaned_spawns(issue, orphans)
4870
+ else:
4871
+ # No spawn line at all: assigned outside this daemon (by
4872
+ # hand, or under another workspace's ledger). Not ours to
4873
+ # double-book -- but not silent forever either.
4874
+ return self._assigned_outside(issue, login)
4843
4875
 
4844
4876
  # Staleness needs THREE signals, not one: the ledger timer says the
4845
4877
  # newest spawn is old, but elapsed time alone cannot tell a dead
@@ -10177,6 +10209,148 @@ class DevWatcher:
10177
10209
  defer_token=loop_events_mod.DEFER_CLAIM,
10178
10210
  )
10179
10211
 
10212
+ def _orphaned_spawns(
10213
+ self, issue: Issue, session: str
10214
+ ) -> "list[ActivityClaim]":
10215
+ """The EXPIRED spawn lines this login wrote on the issue's activity
10216
+ comment for `session`'s lane, oldest first and one per session name
10217
+ (issue #200). Called only when the issue is self-assigned and the
10218
+ ledger has no row for it, i.e. after `external_claim` has already
10219
+ turned down every line as not fresh: what is left is a dead attempt
10220
+ of ours whose ledger row was lost (written by an overlapping
10221
+ deployment, or before a ledger loss).
10222
+
10223
+ Own authorship comes from `marked_comment`, the lane from
10224
+ `activity_lane`. Same switch and fail-open rule as `external_claim`:
10225
+ with claims disabled, or on any read failure, the answer is "no
10226
+ lines", which keeps today's skip."""
10227
+ if not self.config.activity_claims_enabled:
10228
+ return []
10229
+ try:
10230
+ comment = self.github.marked_comment(
10231
+ issue.owner, issue.repo, issue.number, ACTIVITY_MARKER
10232
+ )
10233
+ except Exception as exc:
10234
+ log.warning(
10235
+ "could not read the activity comment on %s (%s) — no "
10236
+ "orphaned attempt to adopt this poll",
10237
+ issue.issue_slug,
10238
+ exc,
10239
+ )
10240
+ return []
10241
+ if comment is None:
10242
+ return []
10243
+ lane = activity_lane(session)
10244
+ window = self.config.stale_minutes * 60
10245
+ oldest: "dict[str, ActivityClaim]" = {}
10246
+ for claim in activity_claims(comment.body):
10247
+ if activity_lane(claim.session) != lane or claim.age < window:
10248
+ continue
10249
+ known = oldest.get(claim.session)
10250
+ if known is None or claim.at < known.at:
10251
+ oldest[claim.session] = claim
10252
+ return sorted(oldest.values(), key=lambda claim: claim.at)
10253
+
10254
+ def _adopt_orphaned_spawns(
10255
+ self, issue: Issue, orphans: "list[ActivityClaim]"
10256
+ ) -> "tuple[int, float]":
10257
+ """Record each orphaned spawn line as a ledger row stamped with the
10258
+ line's own time, so attempt_cap counts it and the ordinary stale
10259
+ matrix (linked-PR probe, tmux liveness, resume) judges it exactly as
10260
+ a row that was never lost. Returns the (attempts, age) the caller
10261
+ continues with.
10262
+
10263
+ Rows are numbered 1..k in line order (the ledger has none for this
10264
+ issue), keeping the session name the line carries; numbering by the
10265
+ name's own suffix could REPLACE a row when a line is missing. Not
10266
+ written on a dry run (no ledger write is), but the returned values
10267
+ let the dry pass reach the same decision."""
10268
+ for attempt, orphan in enumerate(orphans, start=1):
10269
+ log.warning(
10270
+ "%s: adopted orphaned attempt %d from activity log (`%s`, "
10271
+ "spawned %d min ago by this login, no local ledger row) — "
10272
+ "taking the stale path",
10273
+ issue.issue_slug,
10274
+ attempt,
10275
+ orphan.session,
10276
+ int(orphan.age / 60),
10277
+ )
10278
+ if self.config.dry_run:
10279
+ continue
10280
+ self.state.record_spawn(
10281
+ repo_slug=issue.full_name,
10282
+ issue=issue.number,
10283
+ attempt=attempt,
10284
+ session=orphan.session,
10285
+ )
10286
+ # The row just written is the newest; move its clock back to the
10287
+ # line's, so its age says what the line says.
10288
+ self.state.age_out_spawn(issue.full_name, issue.number, orphan.at)
10289
+ return len(orphans), orphans[-1].age
10290
+
10291
+ def _assigned_outside(self, issue: Issue, login: str) -> Decision:
10292
+ """The skip for an issue assigned to this login with nothing on the
10293
+ ledger or the activity comment to explain it -- assigned by hand.
10294
+ Not silent forever (issue #200): once the skip has persisted for
10295
+ STALLED_DEFER_MULTIPLE stale windows (clock: the episode's durable
10296
+ first-seen row) it posts ONE operator comment, recorded only after
10297
+ it lands so a failed post retries next poll."""
10298
+ full_name, number = issue.full_name, issue.number
10299
+ reason = (
10300
+ f"assigned to {login} outside this daemon (no spawn on the "
10301
+ f"ledger) — unassign the issue to let the loop take it"
10302
+ )
10303
+ if self.state.escalated(full_name, number, ESCALATION_ASSIGNED_OUTSIDE):
10304
+ return Decision(Action.SKIPPED, reason)
10305
+ if self.config.dry_run:
10306
+ return Decision(Action.SKIPPED, reason)
10307
+ first_seen = self.state.activity_logged_at(
10308
+ full_name, number, ASSIGNED_OUTSIDE_SEEN
10309
+ )
10310
+ if first_seen is None:
10311
+ self.state.record_activity(full_name, number, ASSIGNED_OUTSIDE_SEEN)
10312
+ first_seen = int(time.time())
10313
+ stalled_after = STALLED_DEFER_MULTIPLE * self.config.stale_minutes * 60
10314
+ waited = time.time() - first_seen
10315
+ if waited < stalled_after:
10316
+ # The page's clock is the seat's: no parking across it, or it
10317
+ # would fire on the first pass after the wake (issue #153).
10318
+ return Decision(
10319
+ Action.SKIPPED,
10320
+ reason,
10321
+ owed=True,
10322
+ timer=(
10323
+ parking_mod.TIMER_STALE_WINDOW, first_seen + stalled_after
10324
+ ),
10325
+ )
10326
+ body = ASSIGNED_OUTSIDE_COMMENT.format(
10327
+ login=login, minutes=int(waited / 60)
10328
+ )
10329
+ log.warning(
10330
+ "%s has been assigned to %s outside this daemon for %.0f min — "
10331
+ "escalating to operator (once)",
10332
+ issue.issue_slug,
10333
+ login,
10334
+ waited / 60,
10335
+ )
10336
+ try:
10337
+ self.github.comment(issue.owner, issue.repo, number, body)
10338
+ except CommandError as exc:
10339
+ log.error(
10340
+ "could not post the assigned-outside comment on %s: %s — "
10341
+ "the page retries next poll",
10342
+ issue.issue_slug,
10343
+ exc,
10344
+ )
10345
+ return Decision(Action.SKIPPED, reason, owed=True)
10346
+ self.state.record_escalation(
10347
+ full_name, number, ESCALATION_ASSIGNED_OUTSIDE
10348
+ )
10349
+ return Decision(
10350
+ Action.ESCALATED,
10351
+ f"{reason}; operator paged after {int(waited / 60)} min",
10352
+ )
10353
+
10180
10354
  def _log_deferral(
10181
10355
  self,
10182
10356
  owner: str,
@@ -189,7 +189,7 @@ _LIMIT_LIFTED_RE = re.compile(r"^usage-limit-lifted:(?P<session>[^@]+)@(?P<since
189
189
  # points the operator at an unrelated PR or at nothing.
190
190
  _ISSUE_ANCHORED = (
191
191
  "cap", "assignment", "stalled", "live-stalled", "wedged", "first-run-dialog",
192
- "claude-exited",
192
+ "claude-exited", "assigned-outside",
193
193
  )
194
194
 
195
195
 
@@ -1245,6 +1245,23 @@ class State:
1245
1245
  ).fetchone()
1246
1246
  return row is not None
1247
1247
 
1248
+ def activity_logged_at(
1249
+ self, repo_slug: str, number: int, kind: str
1250
+ ) -> "int | None":
1251
+ """When this activity KIND was first recorded (unix seconds), or None.
1252
+ The durable first-seen clock of an episode that must outlive a daemon
1253
+ restart (issue #200: the assigned-outside skip escalates once it has
1254
+ persisted long enough). A row held in memory by a failed write answers
1255
+ with the timestamp it would have carried."""
1256
+ pending = self._pending_gates.get(("activity_log", repo_slug, number, kind))
1257
+ if pending is not None:
1258
+ return int(pending)
1259
+ row = self._db.execute(
1260
+ "SELECT logged_at FROM activity_log WHERE repo_slug=? AND number=? AND kind=?",
1261
+ (repo_slug, number, kind),
1262
+ ).fetchone()
1263
+ return None if row is None else int(row["logged_at"])
1264
+
1248
1265
  def record_activity(self, repo_slug: str, number: int, kind: str) -> None:
1249
1266
  """Idempotent per kind: OR IGNORE keeps the first line's timestamp.
1250
1267
  Recorded by the caller only AFTER the append lands, so a transient
@@ -1 +1 @@
1
- 0.8.58
1
+ 0.8.60
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.58
3
+ Version: 0.8.60
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -10,13 +10,13 @@ alissa/tools/github/devloop/hubs.py,sha256=yjnsnSHLG7GNNP8X2hILeAMV6P5n4JGc_IaAx
10
10
  alissa/tools/github/devloop/hungtests.py,sha256=TozIDydUe_Kn1m5EtGeGtYt_BrLqGznrNjikLYhxH-Q,24621
11
11
  alissa/tools/github/devloop/integration_check.py,sha256=JkFwz3JqOQBIvzszmiql-IMVGSH3lDers6mJUHh9zMM,17928
12
12
  alissa/tools/github/devloop/janitor.py,sha256=R8REoHBeY2cfW7fYJVhKr-F5z9Mjlc-vmv-z4KtvBZc,27360
13
- alissa/tools/github/devloop/loop.py,sha256=zseujnScvwQaQxAQaWZJ7FipwnFddCjwi7sk2MFW5UU,695978
14
- alissa/tools/github/devloop/loop_events.py,sha256=9b7G2ak69Zm-oc_3IK8PplgOEdBydfER_ZFjKlCJJ5A,38093
13
+ alissa/tools/github/devloop/loop.py,sha256=RA1gBliGUGsmcUZONWg17clzN70szohgD7dSPqumZQs,704494
14
+ alissa/tools/github/devloop/loop_events.py,sha256=B8wq4_MMXbf1-od3pk9OHVo6F6W2lzzUZmU6GsKcCkM,38113
15
15
  alissa/tools/github/devloop/parking.py,sha256=7h3Gg2XQ0eqlvn0M6K6NeEg9xbqnlUPrZNs9gRofzxc,12410
16
16
  alissa/tools/github/devloop/proc.py,sha256=nzfsxJSHFWRQcrXmfv588KazFysv8dVQrhMPH1q5eRk,3134
17
17
  alissa/tools/github/devloop/prompts.py,sha256=VzN6ec5eF2wwg7QSCRg52WLcxo-tGbGRgQbc4HSahUc,55169
18
- alissa/tools/github/devloop/state.py,sha256=uVwhbkogUTjWXXFq-COKzUeYrpuGbHzi05CjfEzAtPA,88668
19
- alissa/tools/github/devloop/version,sha256=jbJJYfy1NcvGPpw31pt2VRtpDpZ8FX7UhBwTKUK-jFo,7
18
+ alissa/tools/github/devloop/state.py,sha256=MIlE3siJjUs5YEv1-Xm-Gmur6B-Okq3M_Lu6d_zxtmA,89534
19
+ alissa/tools/github/devloop/version,sha256=MQj5gu3ulgK9CxgShUYHkZ5gnQUvytZl9LkGg_h0KPs,7
20
20
  alissa/tools/github/devloop/version.py,sha256=znuuOaitXGrBpbskNy78_rlVpFdtmKmk_3k1ACxZrl0,1418
21
21
  alissa/tools/github/devloop/webui/__init__.py,sha256=vdXYoyGYzD7BrJ5lV6XdH8Bpw_pej8-RK0hjfxop7-8,1513
22
22
  alissa/tools/github/devloop/webui/__main__.py,sha256=wORyW5AkgsniK0TZcrOrvm_P7Q4Mr-fH-4S6qj9XcgA,6137
@@ -25,10 +25,10 @@ alissa/tools/github/devloop/webui/page.py,sha256=4OD3e01ByvVM4Wo04AZyFuyOYTFEMT2
25
25
  alissa/tools/github/devloop/webui/server.py,sha256=3aGYGesjUSrPrgcpfy-oYDefBNKnAj3lGfRzwsRkv_I,27031
26
26
  alissa/tools/github/devloop/webui/sources.py,sha256=Uw_2B5QGSmW6f66WfVbQb6yLLYmSv6uuF7HNJ5OSr_I,39314
27
27
  alissa/tools/github/devloop/webui/sysinfo.py,sha256=ptlogH9alFv0vZ-pQfyUaA3w-K7LQS3tQHGv0d8Tdlk,9649
28
- alissa_tools_github_devloop-0.8.58.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
29
- alissa_tools_github_devloop-0.8.58.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
30
- alissa_tools_github_devloop-0.8.58.dist-info/METADATA,sha256=VSQAsLRKCHN13_i7eLfVUkYfCy70pxnEIzJc7b2r8yc,15973
31
- alissa_tools_github_devloop-0.8.58.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
- alissa_tools_github_devloop-0.8.58.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
33
- alissa_tools_github_devloop-0.8.58.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
34
- alissa_tools_github_devloop-0.8.58.dist-info/RECORD,,
28
+ alissa_tools_github_devloop-0.8.60.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
29
+ alissa_tools_github_devloop-0.8.60.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
30
+ alissa_tools_github_devloop-0.8.60.dist-info/METADATA,sha256=sXX8hAM08-DQtojqLRYh46EqoEPiPnsvfMzoGc6tVfA,15973
31
+ alissa_tools_github_devloop-0.8.60.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
+ alissa_tools_github_devloop-0.8.60.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
33
+ alissa_tools_github_devloop-0.8.60.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
34
+ alissa_tools_github_devloop-0.8.60.dist-info/RECORD,,