Metadata-Version: 2.4
Name: alissa-tools-github-devloop
Version: 0.8.34
Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
Home-page: https://alissa.app
Author: Fahera
Author-email: support@alissa.app
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# alissa-tools-github-devloop

The `alissa.tools.github.devloop` module: a GitHub watcher that turns
`alissa:develop`-labeled issues into fresh developer sessions — the
developer-side counterpart of
[`alissa-tools-github-reviewloop`](https://github.com/fahera-mx/alissa-github-review-daemon).

This distribution ships **only** that module. Everything above it —
`alissa`, `alissa.tools`, `alissa.tools.github` — is a
[PEP 420](https://peps.python.org/pep-0420/) namespace package with no
`__init__.py`, so other distributions in other repositories can contribute
their own packages under the same namespace:

```
alissa/                                  ← namespace (no __init__.py)
└── tools/                               ← namespace
    └── github/                          ← namespace
        ├── devloop/      __init__.py    ← THIS distribution
        └── reviewloop/   __init__.py    ← ships from alissa-github-review-daemon
```

The rule: a distribution owns a leaf package and declares only that subtree
(`find_namespace_packages(include=[PACKAGE, f"{PACKAGE}.*"])`). Adding an
`__init__.py` at any namespace level would claim it for one distribution and
shadow the others.

## Install

```sh
pip install -e ./alissa-tools-github-devloop
```

## Console scripts

| Command | Entry point |
| --- | --- |
| `alissa-devloop` | `alissa.tools.github.devloop.__main__:main` |

## Layout

`src/main` holds the package tree, `src/test` mirrors it as `test_*`. The
distribution version lives in the plain-text `version` file next to the module
it versions (`src/main/alissa/tools/github/devloop/version`), read by both
`setup.py` and `version.py`.

## Account-level conditions

The prompt responder (`prompts.py` classifies, `loop.py` acts) treats four
pane conditions as the ACCOUNT's, not the worker's — nothing a keystroke can
answer: `login_expired`, `auth_rejected` (a 401), `usage_limit` and
`out_of_credits`. Each pages the operator once per kind per 6 h on the lane's
issue/PR and holds every new spawn while it stands (at most 1 h on one pane,
then kill-and-retry re-confirms) — except `usage_limit`, whose session is
never killed and whose hold is the ledger's (below).

**Usage limits** (issues #142, #144). `usage_limit` admits the window wordings
(`You've hit your weekly limit`, `Usage limit reached`, …), the "limit
reached … resets …" family (`Claude usage limit reached. Your limit will
reset at 5pm (America/New_York).`, `5-hour limit reached ∙ resets 3pm`,
`Opus weekly limit reached ∙ resets Oct 6, 1pm`) and the
MODEL-named one Claude Code prints now — `You've reached your Fable limit.
Run /usage-credits to continue or switch models with /model.` (also `Opus`,
`Sonnet`, …): one capitalised model token, a known family name or any token
corroborated by that trailing hint on the same line. An over-limit account
answers the directive with that notice as the worker's ONLY assistant turn
and leaves the session idle at `❯`, which the banner rule (never a `●` row)
cannot see, so the **first-turn limit check** reads the responder's capture of
any young worker (`develop-*`, `fix-*`, `maintain-*`, `revert-*`, spawned by
this ledger inside the last `stale_minutes`): the notice, above an idle
prompt, with no other `●` turn since the directive. A match
(`source: "first_turn"`, `matched: "model_limit"`, the model) is:

- **held** — the daemon-wide spawn hold, clocked on the ledger from the
  newest limit (it survives a restart and a listing failure) and lifted only
  when a spawn made AFTER the limit gets through its first turn: after 1 h
  ONE probe spawn is admitted at a time (none while a probe is in flight,
  none while another account notice holds; under `max_sessions` the probe
  is judged without the kept limited sessions, which would otherwise fill the
  cap for good); a probe whose first turn is the notice again is refunded,
  re-arms the clock and is **killed** — it did no work, and keeping it would
  add one session per hour of the limit (past `max_sessions` too), so the
  live sessions stay at the ones the limit found at work plus at most the one
  probe in flight; the first probe that does
  real work lifts the hold (`usage-limit-lifted:` on the escalation ledger,
  `escalation.usage_limit_lifted` on the wire) and each kept limited
  session's lane gets a line saying to resume it;
- **paged** — once per 6 h window, through the account-kind page;
- **never killed** (issue #144; a failed probe, above, is the one exception) —
  a retry would hit the same wall, and the idle session is the lane's own work for the operator to resume once the
  account is back (or `alissa tmux kill`, to let the retry edge respawn);
  the wedge kills skip a limited session too;
- **refunded** — the spawn row is stamped `limited_at`, and every
  `attempt_cap` count leaves limited rows out (numbering stays monotonic, so
  session names never repeat). An account over its limit never caps a lane.

A usage-limit BANNER on a working session (`source: "pane"`) is held the same
way: its row is stamped (refunded), the hold is the ledger's, and the session
is never killed. The same notice after real worker output (a tool call, the
worker's own prose, a quoted tool row) stays worker output. Each limited
session is one `usage.limited` loop event: `data: { kind: "usage_limit",
source: "pane" | "first_turn", matched: "model_limit" | "usage_limit", model,
resetsAt? }` — `resetsAt` only when the notice states a reset, as an ISO-8601
UTC instant when that needs no guess (the legacy `|<epoch>` form, or a clock
time with an explicit zone) and as the stated phrase otherwise; no pane text,
no credential material.

**The fix edge stops presuming.** A stale fix round with no triage reply from
the author used to be re-enqueued on the timer alone ("presumed dead"). It now
reads the tmux roster first: ALIVE is in flight (the responder owns a live
idle pane; a once-per-episode "Fix loop stalled?" ping floors the wait),
INDETERMINATE defers one pass, and only DEAD respawns — "CONFIRMED dead", or,
after a refunded limit, "previous session limited (usage limit; attempt
refunded)". A kept limited session reads ALIVE, so no edge respawns over it.

## The re-enqueue guard (issue #144)

The issue edge's respawn rule reads the linked-PR signal over **every** PR
state: `GitHub.linked_pull_requests` walks the timeline once and reports each
same-repo cross-referenced PR as open, merged or closed. When a stale
attempt's session is CONFIRMED dead with no open PR, a merged PR of the
daemon's own that closes the issue is `SKIPPED` "finished (PR #n merged)" and
a closed-unmerged one is `SKIPPED` "closed unmerged — orchestrator's page" —
never a respawn (provenance: token author and a closing keyword, fetched; an
unreadable candidate stands the respawn down). And `_spawn` re-reads the
issue immediately before every enqueue: a closed issue is `SKIPPED` "issue
closed between search and spawn", nothing is claimed, and the reserved slot
is handed back. The respawn-with-resume on an open draft PR is unchanged.

## License

Licensed under the
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) — the
`LICENSE` and `NOTICE` files shipped with this distribution carry the full text
and the attribution and trademark terms. Both ship inside the sdist and the
wheel's `.dist-info`.

"Alissa" and "Fahera" are trademarks of CORE FAHERA ENTERPRISE HOLDINGS
S. DE R.L. and are not covered by the license — the grant covers the code, not
the names.
