Skip to main content

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.

This distribution ships only that module. Everything above it — alissa, alissa.tools, alissa.tools.github — is a PEP 420 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

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.

The discovery round (issue #164)

A discovery issue is a no-code lane in the fleet's plans repository (design alissa-build-discovery.md §3.2, lane DS-7a). Studio's feed_plan renders it with the trailers Alissa-Kind: discovery, Alissa-Reads: <owner>/<repo>@<40-hex sha> (the code repository it reads, at a pinned SHA) and Autonomous-Scope: knowledge-base/<YYYY>/<MM>/<DD>/TASK-<n>/**. The issue edge spawns the ordinary develop session — same name, same caps, same liveness rules — with DISCOVERY_DIRECTIVE (in loop.py, byte-pinned in test_discovery.py) instead of DEV_DIRECTIVE. Every other issue gets today's directive byte for byte; a resume of a dead discovery session keeps RESUME_DIRECTIVE.

The directive has the worker:

  1. create its TASK-<n>-<DESC> branch in the plans repository, the branch grammar unchanged;
  2. read the code at the pinned SHA, read-only — a git -C <hub> worktree add --detach <scratch> <sha> when the workspace has a hub of the read repository (no branch, removed at the end), otherwise gh api repos/<repo>/contents/<path>?ref=<sha> and gh search code; never a clone, never a commit or push there. The read is bounded by the SHA, not a file count; the output by the findings schema's limits;
  3. write exactly one directory, knowledge-base/<YYYY>/<MM>/<DD>/TASK-<n>/, holding only findings.md (findings schema v1, schema/findings.v1.json in the plans repository);
  4. run node lint/plan-lint.ts --kb <dir> locally with its own ALISSA_API_TOKEN (K6/K7) and never push findings it refuses;
  5. open a ready pull request with Closes #<n>, both task refs and the marker <!-- alissa-discovery v1 --> on a line of its own, attach a Findings evidence item, move its task to pending_validation, request no reviewer (revloop skips such PRs; orcloop merges on the lint of record) and end its session.

The push guard. Before it enqueues a discovery session (fresh or resumed), the daemon writes ${ALISSA_DISCOVERY_DIR:-/workspace/.discovery}/ ali-<session>.json naming the plans repository; a marker that cannot be written stands the spawn down for the pass. The container's shell guard (docker/claude/hooks/guard-shell.py) finds the marker through $ALISSA_TMUX_SESSION and refuses, with a PreToolUse deny and a reason, any git push from that session whose destination is not the plans repository — resolved from the remote name or URL in the directory the push runs in (-C, cd), and refused outright when it cannot be resolved. Sessions with no marker are untouched. ALISSA_SHELL_GUARD=off disables this guard with the rest.

Seat parking: what counts as managed (issue #166)

The idle block on GET /api/state (parking.idle_block, issue #153) is what orcloop's D2 invariant reads before it drains and parks the seat: idle only when live == managed == queued == owed == 0 with no timer and a fresh pass. live and managed come from alissa tmux ls --json:

  • a row is live unless it says otherwise (its live flag, else status != "gone");
  • a row is managed when it is live, or when it is dead but its session still backs a lane the seat holds — the session named by a stage of the latest poll snapshot that is owed or in flight (spawned / in-flight), any session the spawn ledgers record on such a stage's lane (an earlier attempt the lane may still resume), or a session the alissa queue holds an undispatched item for;
  • a dead row backing none of these is an orphan: it is not managed, and is reported in the block's gone field as {count, sessions} so the console and orcloop's logs still show it.

Unknown is never idle: a failed listing leaves live, managed and gone null, and before the first completed pass (no snapshot to read lanes from) every dead row stays managed. The console only reports an orphan — it never kills or reaps one. The fleet-vitals push applies the same rule to sessions.managed and idle.managed (Studio's park guard reads both) and leaves gone out, since Studio's idle schema is strict.

Wedges: alive is not working

A stale issue-edge session the tmux roster lists ALIVE is not trusted on that alone. The daemon kills the row's own session (alissa tmux kill <name>, never a sweep) and moves the lane on when the session is wedged:

  • wedged:claude-exited (issue #169) — the pane's #{pane_current_command} is a login shell (alissa.SHELL_COMMANDS): the claude the session was spawned for exited and the tmux session stayed. Checked on both stale branches (with and without a linked open PR), no wedge_minutes needed; the session reads DEAD in the same pass, so the pre-PR lane re-enqueues attempt k+1 and a draft-PR lane resumes its PR as attempt k+1, counting toward attempt_cap. Ledger kind claude-exited:a<k>.
  • wedged:first-run-dialog (issue #123) — the pane sits on Claude Code's trust / bypass-permissions gate: killed, hub pre-trusted, re-enqueued in the same pass.
  • footprint wedges (issues #110, #127; opt-in wedge_minutes) — no activity footprint inside the window: killed, and the next pass resumes or retries.

A pane tmux cannot read keeps the defer, a usage-limited session is never killed (issue #144), and dry-run only logs. The root README's Claude exited, the session stayed section has the details. Processes a dead claude started that outlived it (re-parented to init) are not reaped — a known follow-up.

License

Licensed under the Apache License, Version 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.

Metadata

Release files for alissa-tools-github-devloop 0.8.45

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for alissa-tools-github-devloop 0.8.45
File Size Uploaded
alissa_tools_github_devloop-0.8.45.tar.gz 382.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for alissa-tools-github-devloop 0.8.45
File Interpreter ABI Platform
alissa_tools_github_devloop-0.8.45-py3-none-any.whl Python 3 none any Details

Total release size: 774.8 kB

Release files / alissa_tools_github_devloop-0.8.45.tar.gz

Download URL alissa_tools_github_devloop-0.8.45.tar.gz
Size 382.2 kB
Tags Source
SHA-256 checksum
How to use checksums
be25e97c730655dd38102c66827d7f750ea365f7f4e091d918598a89dad7c2f9
BLAKE2b-256 checksum
How to use checksums
448b9a00fe0f3a2d0d9e6c93eca84c9f91c9fe7e2d837ade2ee6d9f7c1caa0e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / alissa_tools_github_devloop-0.8.45-py3-none-any.whl

Download URL alissa_tools_github_devloop-0.8.45-py3-none-any.whl
Size 392.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
988692629216f6d2f7833c41fdb92ad833e0c9e78dc13df5059f036e334ea82f
BLAKE2b-256 checksum
How to use checksums
3dd9f2524be4e4331999331756b36d2593b9b34a81559fcf90061e67298edb5b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

0.8.45 This release

2 release files

0.8.44

2 release files

0.8.43

2 release files

0.8.42

2 release files

0.8.41

2 release files

0.8.40

2 release files

0.8.35

2 release files

0.8.34

2 release files

0.8.33

2 release files

0.8.32

2 release files

0.8.31

2 release files

0.8.30

2 release files

0.8.29

2 release files

0.8.28

2 release files

0.8.27

2 release files

0.8.26

2 release files

0.8.25

2 release files

0.8.24

2 release files

0.8.23

2 release files

0.8.22

2 release files

0.8.21

2 release files

0.8.20

2 release files

0.8.19

2 release files

0.8.18

2 release files

0.8.12

2 release files

0.8.11

2 release files

0.8.10

2 release files

0.8.9

2 release files

0.8.8

2 release files

0.8.7

2 release files

0.8.6

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page