Skip to main content

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-256 HMAC when the secret env var is set (export THE_LOOP_GH_WEBHOOK_SECRET=...). The secret is read from the environment, never a flag, so it doesn't leak into process listings.
  • GET /health returns 200 ok.
  • Defaults can come from .the-loop/config.yaml (webhooks.ghWebhook) when PyYAML is installed; flags always override.
  • --route (default from webhooks.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-branch issue-<n> convention, closing keywords, workflow_run/check_* PRs), deduplicates on X-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 follow routing.spawnOnUnmatched (never drops; always spawns + 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 / events are 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 to ticketing.github.owner, else fails closed with a warning. Each operator runs their own instance for their own login(s). See docs/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 with the-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; --force replaces a stale registration.
  • Claude Code sessions register with $CLAUDE_SESSION_ID; Cursor sessions register with the chat id they were launched with (non-interactive cursor-agent ls is 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_request closed event) — no manual sessions close needed.

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.sources in .the-loop/config.yaml — each entry names a provider (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 label defaults to webhooks.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 with the-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 polling in .the-loop/config.yaml (when PyYAML is installed); dispatch behaviour is reused from webhooks.ghWebhook.routing. Flags cover only the run loop.

  • Hot reload: edit polling.sources / intervalSeconds while 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 to ticketing.github.owner, else fails closed with a warning. See docs/decisions/decision-023.md.

  • --once runs a single cycle and exits (for a cron/systemd timer); otherwise it loops until poll 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-item shows one item's full history ("which events triggered this session?"); --delivery-id follows a single GitHub delivery end to end.
  • --type takes fnmatch patterns (repeatable): --type 'dispatch.*' --level error answers "what failed?".
  • --since accepts ISO-8601 UTC or relative (30s/15m/2h/1d); --follow tails the log live; --limit keeps the last N (default 50, 0 = all).
  • --format json|jsonl is machine-readable (for agents and dashboards); the file itself is plain JSONL, so grep/jq/tail -f work 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 optional Requirement: link to a requirements.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), else testing.integrationTestGlobs in .the-loop/config.yaml (when PyYAML is installed), else built-in defaults covering common layouts.
  • --format markdown emits a GitHub-flavoured table (for PR briefings); --format json is machine-readable (includes each scenario's steps and file:line).

Adding a command (extensibility)

  1. Create the_loop/commands/<your_command>.py.
  2. Subclass Command, set name/help, implement add_arguments and run, and decorate the class with @register.
  3. 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

the_loopy_one-0.7.0.tar.gz (82.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

the_loopy_one-0.7.0-py3-none-any.whl (69.8 kB view details)

Uploaded Python 3

File details

Details for the file the_loopy_one-0.7.0.tar.gz.

File metadata

  • Download URL: the_loopy_one-0.7.0.tar.gz
  • Upload date:
  • Size: 82.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for the_loopy_one-0.7.0.tar.gz
Algorithm Hash digest
SHA256 32fb15dc4d9cba9db1d3bbe4c822e9767cf5ed0c58808bb33bdb34377ec3cb5f
MD5 ab84ffad21520d79b50a5ac6e6358969
BLAKE2b-256 002f16f506b72382db2f561fb51702cc0781c59a8f1e7be24c21eeed4f5903ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for the_loopy_one-0.7.0.tar.gz:

Publisher: release.yml on MadaraUchiha-314/the-loop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file the_loopy_one-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: the_loopy_one-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 69.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for the_loopy_one-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13bd68e93615c4b2d7b00647bf2a632906853e5b27bc5638b5820ea0a053be03
MD5 09f812b1ee03a1c10d8559323ce292ea
BLAKE2b-256 2c3cf8638e5891fe5d38b8b8fdd5c2ab6daf1620c83b2495dd191e03d8066b0d

See more details on using hashes here.

Provenance

The following attestation bundles were made for the_loopy_one-0.7.0-py3-none-any.whl:

Publisher: release.yml on MadaraUchiha-314/the-loop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

13.6.0

2 files

13.5.0

2 files

13.4.0

2 files

13.3.1

2 files

13.3.0

2 files

13.2.0

2 files

13.1.1

2 files

13.1.0

2 files

13.0.1

2 files

13.0.0

2 files

12.1.0

2 files

12.0.0

2 files

11.6.1

2 files

11.6.0

2 files

11.5.0

2 files

11.4.0

2 files

11.3.1

2 files

11.3.0

2 files

11.2.0

2 files

11.1.3

2 files

11.1.2

2 files

11.1.1

2 files

11.1.0

2 files

11.0.1

2 files

11.0.0

2 files

10.6.0

2 files

10.5.0

2 files

10.4.1

2 files

10.4.0

2 files

10.3.1

2 files

10.3.0

2 files

10.2.5

2 files

10.2.4

2 files

10.2.3

2 files

10.2.2

2 files

10.2.1

2 files

10.2.0

2 files

10.1.0

2 files

10.0.0

2 files

9.15.0

2 files

9.14.0

2 files

9.13.0

2 files

9.12.0

2 files

9.11.0

2 files

9.10.0

2 files

9.9.0

2 files

9.8.0

2 files

9.7.1

2 files

9.7.0

2 files

9.6.3

2 files

9.6.2

2 files

9.6.1

2 files

9.6.0

2 files

9.5.1

2 files

9.5.0

2 files

9.4.0

2 files

9.3.0

2 files

9.2.0

2 files

9.1.0

2 files

9.0.0

2 files

8.1.0

2 files

8.0.0

2 files

7.4.1

2 files

7.4.0

2 files

7.3.0

2 files

7.2.0

2 files

7.1.1

2 files

7.1.0

2 files

7.0.0

2 files

6.2.1

2 files

6.2.0

2 files

6.1.1

2 files

6.1.0

2 files

6.0.0

2 files

5.2.1

2 files

5.2.0

2 files

5.1.0

2 files

5.0.0

2 files

4.2.0

2 files

4.1.0

2 files

4.0.0

2 files

3.0.3

2 files

3.0.2

2 files

3.0.1

2 files

3.0.0

2 files

2.1.0

2 files

2.0.1

2 files

2.0.0

2 files

1.0.0

2 files

0.22.1

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.19.0

2 files

0.18.1

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.0

2 files

0.14.2

2 files

0.14.1

2 files

0.14.0

2 files

0.13.0

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 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