Metadata-Version: 2.4
Name: alissa-tools-github-devloop
Version: 0.8.31
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).

**Usage limits** (issue #142). `usage_limit` admits the window wordings
(`You've hit your weekly limit`, `Usage limit reached`, …) 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 for 1 h from
  the newest limit (it outlives the killed session and a restart); when it
  expires the retry edges respawn as they would have, and a fresh pane still
  showing the notice re-arms it;
- **paged** — once per 6 h window, through the account-kind page;
- **killed** — the session is idle and useless;
- **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.

The same notice after real worker output (a tool call, the worker's own
prose, a quoted tool row) stays worker output. Each observation is one
`usage.limited` loop event per session: `data: { kind: "usage_limit", source:
"pane" | "first_turn", matched: "model_limit" | "usage_limit", model }` — 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 (… attempt refunded)".

## 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.
