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 config-file 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 config-file 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.

Two independent config files (decision-032)

The-loop's config is split into two files that never overlap keys:

  • Plugin config.the-loop/config.yaml, installed per repo (Claude/Cursor plugin), read by /the-loop:* commands and the skill: ticketing, workflow, tooling, reviews, autonomy, security, personas, … Validated against .the-loop/config.schema.json.

  • CLI config (cli-config.yaml) — read only by this CLI's daemon commands below (gh-webhook, poll, sessions, events): webhooks, polling, eventLog. The CLI is expected to work across multiple repos, so it isn't required to live in any one of them — resolved in priority order:

    1. --config/-c — an explicit flag, e.g. the-loop --config path/to/cli-config.yaml gh-webhook start (must precede the subcommand).
    2. $THE_LOOP_CLI_CONFIG — an explicit env var, same priority as --config (handy for containers/systemd units where a flag is less convenient).
    3. ./.the-loop/cli-config.yaml (repo-relative) — an operator can choose to track their CLI config in a specific repo (e.g. a "dev box" repo, checked in and versioned) instead of their home directory; picked up automatically when the-loop <command> runs from that checkout.
    4. ~/.the-loop/cli-config.yaml — the always-available fallback, not tied to any repo.

    Validated against .the-loop/cli-config.schema.json; a commented starting point ships at skills/the-loop/templates/cli-config.yaml.

Each command's defaults below note which file they come from. The CLI daemon reads only the CLI config — it never reads a repo's plugin config for anything, including routing.authorizedUsers (who may trigger it) or a GitHub poll source's repos (what it watches): both are CLI-config-only settings with no plugin-config fallback, set them explicitly.

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 CLI config (webhooks.ghWebhook, see "Two independent config files" above for the --config/env/cwd/home resolution order) 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 (CLI config — REQUIRED, no fallback to any repo's plugin config) — 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 fails closed with a warning. Each operator runs their own instance for their own login(s). See docs/decisions/decision-023.md.
  • Self-reply guard (loop prevention): the harness posts its own replies under the operator's own credentials, so authorship alone can't tell them apart from a human comment. Every comment/review/reply the-loop posts carries an embedded marker (the_loop.authz.SELF_COMMENT_MARKER); the receiver drops a marker-carrying event before dispatch, regardless of actor, so the-loop's own reply never resumes the session that wrote it. See docs/decisions/decision-031.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.

The label applies to PRs directly too — a labelled PR with no linked issue is routed as its own work item (github:OWNER/REPO#<pr-number>). That makes PRs monitorable even when the ticketing system is Jira or another provider: the ticket can't be routed, but the PR delivering it can. /the-loop:work-on <jira-id> adds the label to the PR it opens and registers its session against the PR's ref automatically, so PR activity resumes the session and merge/close ends it — same as a GitHub-ticketed item.

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 CLI config — 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]         # REQUIRED — no fallback to any repo's plugin config
          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 CLI config (when PyYAML is installed); dispatch behaviour is reused from webhooks.ghWebhook.routing (same file). 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 (CLI config — REQUIRED, no fallback to any repo's plugin config), and forwards only comments from authorized authors — everything else is ignored. Empty fails closed with a warning. See docs/decisions/decision-023.md.

  • Self-reply guard (loop prevention): same marker check as the receiver — a comment the-loop itself posted is excluded from "new comments" (and can't retrigger a spawn), even though it was posted under an authorized login. See docs/decisions/decision-031.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 eventLog.path in the CLI config (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 eventLog.enabled: false (CLI config) 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.13.0.tar.gz (92.6 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.13.0-py3-none-any.whl (76.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: the_loopy_one-0.13.0.tar.gz
  • Upload date:
  • Size: 92.6 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.13.0.tar.gz
Algorithm Hash digest
SHA256 0608ea574b00542f6d36376a0c73a9ae6456b4179a1a911e7918fe39e47e3db3
MD5 556121d4cff65662f27e94761651ace1
BLAKE2b-256 37c11d419fda9be82408b061d795953e4a0114350ab59acebba243bf48639080

See more details on using hashes here.

Provenance

The following attestation bundles were made for the_loopy_one-0.13.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.13.0-py3-none-any.whl.

File metadata

  • Download URL: the_loopy_one-0.13.0-py3-none-any.whl
  • Upload date:
  • Size: 76.2 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.13.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb494595a98f00479b4b9669068007a0f4306e9c0ca1b9130a620bf2305b365e
MD5 32a8234909bb08945dbbabcd565ae33e
BLAKE2b-256 21d1c9ff6b5351838026b52339a652829199a33947ad8718b92d205825bfea1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for the_loopy_one-0.13.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

This release

0.13.0 This release

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

0.7.0

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