the-loop CLI
A lightweight, extensible command-line companion to the the-loop plugin, written in Python (stdlib-only core — zero runtime dependencies). Python is intentional: it leaves room to add self-learning / ML capabilities later (mostly exposed as Python SDKs).
Install
From PyPI (published as the-loopy-one — the base name the-loop was taken; the
import package and CLI keep the natural the_loop/the-loop):
pip install the-loopy-one # or: uv pip install the-loopy-one
pip install "the-loopy-one[config]" # + PyYAML, for reading .the-loop/config.yaml defaults
the-loop --help
For local development the-loop uses uv (its declared Python package manager). From the repo root:
uv sync # installs the workspace (this CLI + dev tooling) from uv.lock
uv run the-loop --help # run the CLI
Or install this package on its own with any PEP 517 installer:
uv pip install -e . # or: pip install -e .
uv pip install -e ".[config]" # PyYAML, for reading .the-loop/config.yaml defaults
uv pip install -e ".[dev]" # pytest + commitizen
This exposes the primary CLI: the-loop. Releases are automatic: on merge to main,
.github/workflows/release.yml runs cz bump to derive the next version from the
Conventional Commits / PR titles since the last tag (feat → minor, fix → patch,
BREAKING CHANGE → major), tags it, and publishes to PyPI via Trusted Publishing (OIDC —
no stored token). Merges with no feat/fix/breaking change publish nothing. See
docs/decisions/decision-019.md.
Commands
gh-webhook — GitHub webhook receiver
the-loop gh-webhook start [--host 127.0.0.1] [--port 8787] [--path /gh-webhook] \
[--pidfile .the-loop/gh-webhook.pid] \
[--secret-env THE_LOOP_GH_WEBHOOK_SECRET] \
[--route | --no-route]
the-loop gh-webhook stop [--pidfile .the-loop/gh-webhook.pid]
- Verifies the GitHub
X-Hub-Signature-256HMAC when the secret env var is set (exportTHE_LOOP_GH_WEBHOOK_SECRET=...). The secret is read from the environment, never a flag, so it doesn't leak into process listings. GET /healthreturns200 ok.- Defaults can come from
.the-loop/config.yaml(webhooks.ghWebhook) when PyYAML is installed; flags always override. --route(default fromwebhooks.ghWebhook.routing.enabled) routes each verified event to the registered harness session working that item: the router extracts the work item(s) from the payload (issue/PR number, PR head-branchissue-<n>convention, closing keywords,workflow_run/check_*PRs), deduplicates onX-GitHub-Delivery, and the dispatcher resumes the matched session via its official CLI (claude -p … --resume <session-id>/cursor-agent -p … --resume <chat-id>), one event at a time per session, in parallel across sessions. Unmatched events followrouting.spawnOnUnmatched(neverdrops;alwaysspawns + registers a session). Design:docs/specs/issue-15/design.md,docs/decisions/decision-016.md.- Config hot-reload: while the receiver runs, edits to
webhooks.ghWebhook.routing/eventsare picked up on the next received event — no restart. The soft policy (events filter, label, spawn policy, harness/runner, per-harness args, prompt templates) swaps live; the dedup cache, per-session queues and registry are preserved. Infrastructural settings (host/port/path,secretEnv,maxConcurrentDispatches,dedupCacheSize,registryDir,webTerminal) still need a restart. An invalid edit is logged and the previous config kept. - Authorized-actor guard (prompt-injection remediation): the receiver acts only on
actions by logins in
routing.authorizedUsers— comments/reviews and issue/PR labels/opens from anyone else are dropped before dispatch (CI/system events, which carry no human instructions, still pass; a PR-close still auto-closes the session). Empty ⇒ falls back toticketing.github.owner, else fails closed with a warning. Each operator runs their own instance for their own login(s). Seedocs/decisions/decision-023.md. - Structured event log: every receive/reject/route/dispatch/spawn/close decision is
appended to
.the-loop/logs/events.jsonl— query it withthe-loop events(below).
sessions — link work items to harness sessions (webhook routing)
the-loop sessions register --work-item github:OWNER/REPO#N --harness claude \
--harness-session-id "$CLAUDE_SESSION_ID" [--cwd .] [--force]
the-loop sessions list [--status active|closed] [--format table|json]
the-loop sessions close --work-item github:OWNER/REPO#N
- The registry lives in
webhooks.ghWebhook.routing.registryDir(default.the-loop/sessions/, git-ignored) as one human-inspectable JSON file per session; writes are atomic, so concurrent sessions on the same machine are safe. - One work item ↔ one active session;
--forcereplaces a stale registration. - Claude Code sessions register with
$CLAUDE_SESSION_ID; Cursor sessions register with the chat id they were launched with (non-interactivecursor-agent lsis unreliable for id discovery, so the id is captured at registration time). - When a work item's PR is merged or closed, the receiver auto-closes the session
(on the
pull_requestclosedevent) — no manualsessions closeneeded.
Label-gated auto-execution (spawnOnUnmatched: labeled): give an issue/PR the
configurable routing.autoExecuteLabel (default the-loop: auto-execute) and the
receiver spawns a session and starts /the-loop:work-on on it — then routes that item's
later activity (comments, reviews, CI, its linked PR) to the same session, and
auto-closes on PR merge. Label presence is read straight from the webhook payload (no
extra API call). A new issue without the label is received and ignored.
poll — pull ingress (provider-agnostic) when a webhook can't reach you
the-loop poll start [--interval 60] [--once] \
[--state-file .the-loop/poll-state.json] \
[--pidfile .the-loop/poll.pid]
the-loop poll stop [--pidfile .the-loop/poll.pid]
A pull-based alternative to gh-webhook for hosts a webhook cannot reach (behind
NAT/a firewall, a laptop, unreachable infra). Every --interval seconds it asks each
configured provider for the label-gated work items in its scope and drives them
through the same routing/dispatch/session stack the webhook receiver uses — so
spawning, one-session-per-work-item, the tmux runner, harness adapters and prompt
templates are all reused unchanged.
-
Provider-agnostic: the poller core and CLI carry no GitHub knobs. Which systems are polled is defined purely by
polling.sourcesin.the-loop/config.yaml— each entry names aprovider(GitHub ships; the seam admits others). GitHub is reached only through a configured source:polling: intervalSeconds: 60 sources: - provider: github repos: [octo/repo] # empty = fall back to ticketing.github monitor: { issues: true, pullRequests: true } label: "" # empty = reuse routing.autoExecuteLabel
-
Label-gated: only items carrying the configured label are polled. A source's
labeldefaults towebhooks.ghWebhook.routing.autoExecuteLabel, so one label drives both ingresses. -
No duplicate sessions: a session is spawned for a labelled item only when the registry has none; a live session is never doubled (the registry is the source of truth), so a work item maps to exactly one session — the same one on later polls.
-
Spawns tmux sessions when
webhooks.ghWebhook.routing.runner: tmux— attach withthe-loop sessions attach --work-item github:OWNER/REPO#N(issue-32). -
New comments are forwarded to the item's session exactly once, deduped across polls and restarts via
--state-file(git-ignored runtime state). The pre-existing thread is baselined on first sight, not replayed. -
Config: ingress defaults come from
pollingin.the-loop/config.yaml(when PyYAML is installed); dispatch behaviour is reused fromwebhooks.ghWebhook.routing. Flags cover only the run loop. -
Hot reload: edit
polling.sources/intervalSecondswhile it runs and the change is picked up on the next cycle — no restart. An invalid edit is logged and the previous config kept. (The shared dispatch config still needs a restart.) -
Authorized-actor guard (prompt-injection remediation): the poller spawns only for items authored by a login in
routing.authorizedUsers, and forwards only comments from authorized authors — everything else is ignored. Empty ⇒ falls back toticketing.github.owner, else fails closed with a warning. Seedocs/decisions/decision-023.md. -
--onceruns a single cycle and exits (for a cron/systemd timer); otherwise it loops untilpoll stop(or SIGINT/SIGTERM), writing a pidfile like the receiver. Design:docs/specs/issue-34/design.md,docs/decisions/decision-022.md. -
Structured event log: cycle summaries, spawns, forwarded comments and provider/item errors are appended to the same event log as the receiver — query with
the-loop events --source poll.
events — query the structured event log (end-to-end o11y)
the-loop events [--file .the-loop/logs/events.jsonl] [--type PATTERN ...] \
[--work-item github:OWNER/REPO#N] [--delivery-id ID] \
[--source gh-webhook|poll|sessions] [--level warning] \
[--since 2h|2026-07-22T10:00:00Z] [--limit 50] \
[--format table|json|jsonl] [--follow]
the-loop events --types # the documented catalog of event types
The receiver, the poller and the sessions CLI append every decision they make —
webhook accepted/rejected (and why), event routed/dropped (with a machine-readable
reason like unauthorized-actor or duplicate-delivery), session spawned/resumed
(naming the triggering event and delivery id), dispatch failed (with the error and
whether redelivery/the next poll cycle retries it), session closed/auto-closed — as one
JSON object per line to observability.eventLog.path (default
.the-loop/logs/events.jsonl, git-ignored). This command is the query surface:
--work-itemshows one item's full history ("which events triggered this session?");--delivery-idfollows a single GitHub delivery end to end.--typetakes fnmatch patterns (repeatable):--type 'dispatch.*' --level erroranswers "what failed?".--sinceaccepts ISO-8601 UTC or relative (30s/15m/2h/1d);--followtails the log live;--limitkeeps the last N (default 50,0= all).--format json|jsonlis machine-readable (for agents and dashboards); the file itself is plain JSONL, sogrep/jq/tail -fwork directly on it.
Every record carries ts/source/event/level/pid plus documented per-type
fields; the catalog (the-loop events --types) is enforced against the emitted types
by a unit test. Writes are append-only and multi-process safe, a broken log never
breaks ingress, and observability.eventLog.enabled: false turns emission off.
Schema + agent guidance: skills/the-loop/reference/observability.md; storage decision
(JSONL, not SQLite): docs/decisions/decision-025.md.
scenarios — query the Gherkin scenarios integration tests cover
the-loop scenarios [--root .] [--glob PATTERN ...] [--format table|markdown|json]
- Scans integration-test files for the Gherkin-syntax docstrings the-loop requires
(
Feature:/Scenario:/ Given-When-Then, plus an optionalRequirement:link to arequirements.md) and presents them as a table — so a coding-agent harness can answer "what scenarios are tested?" without running anything. - Language-agnostic: Python docstrings, JS/TS block comments and Go comments all work.
- Globs come from
--glob(repeatable), elsetesting.integrationTestGlobsin.the-loop/config.yaml(when PyYAML is installed), else built-in defaults covering common layouts. --format markdownemits a GitHub-flavoured table (for PR briefings);--format jsonis machine-readable (includes each scenario's steps andfile:line).
Adding a command (extensibility)
- Create
the_loop/commands/<your_command>.py. - Subclass
Command, setname/help, implementadd_argumentsandrun, and decorate the class with@register. - Import the module in
the_loop/commands/__init__.py.
The CLI discovers registered commands automatically.
Test
pytest # from this directory
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file the_loopy_one-0.9.0.tar.gz.
File metadata
- Download URL: the_loopy_one-0.9.0.tar.gz
- Upload date:
- Size: 83.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a36a247e4facb1fe2441ba99df54bd1f988d3c88ee73211a42e5a30b8056d69
|
|
| MD5 |
7e41229fc92fafa17efd5d1225212ebe
|
|
| BLAKE2b-256 |
a78b1f8377a82c66f7608c5ac5e531f6fd0f731b2176d7bc2e24bc898797816b
|
Provenance
The following attestation bundles were made for the_loopy_one-0.9.0.tar.gz:
Publisher:
release.yml on MadaraUchiha-314/the-loop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
the_loopy_one-0.9.0.tar.gz -
Subject digest:
5a36a247e4facb1fe2441ba99df54bd1f988d3c88ee73211a42e5a30b8056d69 - Sigstore transparency entry: 2222378828
- Sigstore integration time:
-
Permalink:
MadaraUchiha-314/the-loop@926b93428bb8f0903163b8356960f7b7d886c64d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MadaraUchiha-314
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@926b93428bb8f0903163b8356960f7b7d886c64d -
Trigger Event:
push
-
Statement type:
File details
Details for the file the_loopy_one-0.9.0-py3-none-any.whl.
File metadata
- Download URL: the_loopy_one-0.9.0-py3-none-any.whl
- Upload date:
- Size: 71.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be808cb57953f2ac05511e0293da9c8ee63abfdd92fcf572c0ca3c560efa9ec3
|
|
| MD5 |
47dcf973002dfd36963699c444b53f77
|
|
| BLAKE2b-256 |
45432bf42625dfa9c637558d401441d62a0b1c11ea69ab42634b0095a37734a4
|
Provenance
The following attestation bundles were made for the_loopy_one-0.9.0-py3-none-any.whl:
Publisher:
release.yml on MadaraUchiha-314/the-loop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
the_loopy_one-0.9.0-py3-none-any.whl -
Subject digest:
be808cb57953f2ac05511e0293da9c8ee63abfdd92fcf572c0ca3c560efa9ec3 - Sigstore transparency entry: 2222379435
- Sigstore integration time:
-
Permalink:
MadaraUchiha-314/the-loop@926b93428bb8f0903163b8356960f7b7d886c64d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MadaraUchiha-314
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@926b93428bb8f0903163b8356960f7b7d886c64d -
Trigger Event:
push
-
Statement type: