Skip to content

Automation & distribution reference

How the-loop is delivered and the automation capabilities it provides in your project. This describes what the-loop does today, in your project.

Distribution

  • the-loop ships as a plugin for Claude Code and Cursor. Personas across the PDLC (PM, design, architect, dev, QA) all work through an agent harness, so the-loop meets them there.
  • It is hosted on GitHub and installed via each harness's marketplace construct, directly from GitHub — no bespoke marketplace publishing. The Claude Code manifest lives in .claude-plugin/, the Cursor manifest in .cursor-plugin/; both share the same skills/, commands/ and templates (skills follow the Agent Skills open standard, so one SKILL.md serves both harnesses).

Footprint tracking

  • Every file the-loop creates/maintains/tracks in your repo is listed in .the-loop/manifest.yaml. Meta files the-loop uses live under .the-loop/.

CLI companion (the-loop, Python)

the-loop is primarily a plugin, but it ALSO ships a lightweight, extensible Python CLI (cli/, package the_loop) for quality-of-life commands the plugin itself can use. Python is deliberate — future self-learning/ML capabilities are mostly exposed as Python SDKs. The core has exactly one runtime dependency — PyYAML, because the CLI's whole configuration is YAML (decision-038) — and is stdlib otherwise.

  • Primary CLI: the-loop. Add a command by subclassing Command, @register-ing it, and dropping the module under the_loop/commands/.

  • Lifecycle: the-loop start | stop | status | restart [--with-upgrade] (issue-228) — one surface that starts, detached, every service the CLI config enables (the control-plane service per service.enabled, /mcp per service.mcp.enabled, the webhook receiver per webhooks.ghWebhook.enabled, the poller per polling.enabled), stops whatever runs, and reports per service. POST /api/v1/restart schedules the same restart over the API. python -m the_loop.daemon_entry <poller|gh-webhook> [--once] is the foreground/cron form of either daemon.

  • GitHub webhook receiver:

    • Verifies the GitHub X-Hub-Signature-256 HMAC (secret from an env var), exposes GET /health, and logs deliveries. Defaults come from webhooks.ghWebhook in the CLI config (cli-config.yaml — resolved via --config/env/cwd/home; see docs/config/cli/) — not this project's .the-loop/harness-config.yaml; the daemon is not tied to any one repo (decision-032), and the CLI never reads this repository's harness config at all (issue-352, decision-123): the spec directory, the critic roster and the operator's graph hooks are CLI-config keys (routing.graph.specDir, critics[], routing.graph.hooks).
  • Webhook → session routing (routing.enabled): a received event (PR/issue comment, workflow_run result, …) is matched to the registered session working that item and delivered by resuming that session through its official CLI (claude -p … --resume / cursor-agent -p … --resume) with a prompt that embeds the payload as untrusted data. Per-session FIFO, parallel across sessions; duplicates (X-GitHub-Delivery) processed at most once; unmatched events follow routing.spawnOnUnmatched. Design: docs/specs/issue-15/design.md, decision: docs/decisions/decision-016.md.

  • Label-gated auto-execution (spawnOnUnmatched: labeled): a configurable label (routing.autoExecuteLabel, default the-loop: auto-execute) arms a work item for autonomous execution. The item's later activity (and that of every PR linked to it) resumes its session; the item being closed auto-closes it. An unlabelled new issue is received and ignored. Label presence is read from the webhook payload (no extra API call).

  • Execution control — the label is necessary, not sufficient (routing.control, issue-106). Declared keywords, usable by an authorized user (routing.authorizedUsers) in a comment on the work item or its PR, are interpreted by the-loop instead of being forwarded to the agent: the-loop start, the-loop stop, the-loop pause, the-loop resume (issue-135 default; an operator's own explicit keywords override is unaffected) and the-loop cleanup (issue-186). With control.requireStartCommand (default on) an armed work item spawns only once someone has started it — the request is durable across restarts, and a stop/pause disarms it again. pause holds delivery without discarding the conversation; stop closes the session through the normal close path. cleanup is the other end of the life cycle: it releases the work item's local resources — every endpoint's tmux session, the workspace checkout (uncommitted work in it is gone) and the machine-local session record — while keeping the portable record and touching nothing remote. Closing the work item does the same, but only when the close event names an authorized actor; otherwise it is deferred, and this keyword is the remedy. The same five are available to an operator with shell access as the-loop sessions start|pause|resume|stop|cleanup, which post the same keyword back to the ticket (marked as the-loop's own, so the daemon never reads its own action back). A comment carrying two different keywords executes nothing and forwards nothing. Decision: docs/decisions/decision-040.md.

  • Work-item collaborators (routing.control.keywords.add-collaborator, issue-307). authorizedUsers is global, so the person who knows one answer on one issue had no place at all: their comment was dropped before anything read it. An authorized user now types the-loop add-collaborator @login (or runs the-loop add-collaborator @login --work-item <ref>) and that login's comments on that one work item — and on the pull requests routed to its session — reach the session as agent input. Nothing else: a collaborator cannot issue a control command (these two included, so a grant is never transitive), cannot spawn or arm a session, and cannot satisfy a human gate, all of which keep reading authorizedUsers. A grant covers one work item, is cleared when it closes, and is revoked with the-loop remove-collaborator @login. Decision: docs/decisions/decision-102.md.

  • Several instances, one of them addressed (instance, issue-322). One CLI config is one instance of the-loop, and several can watch one repository from separate environments (the environments are the operator's to provide). An instance has a name, a scope.mode and a declared scope.workItems list; a work item it already manages — declared, or with a session or control record on it — is handled in every mode as before, and the mode decides only what happens to a new one: open (default) takes any armed, authorized start; addressed takes only a start that names it; locked takes nothing new. Any control keyword can carry the address token instance:<name> (the-loop start instance:laptop-b), a whole-word token whose name must fit the standing-session grammar; an address to another instance is authoritative, two different addresses refuse the comment, and a refused instance leaves nothing on the thread — one event-log line with the reason. the-loop sessions start on an instance is addressed to it; a spawned session finds THE_LOOP_INSTANCE=<name>; the portable record's control.instance and GET /api/v1/instance say who took what. Decision: docs/decisions/decision-110.md.

  • Where the session takes its answers from (routing.interaction.mode, issue-134): work-item (default) or cli. Until this existed the prompt never said, so the agent guessed — and a session guessing "the terminal" asks into a tmux pane nobody may be attached to. The resolved mode is rendered into every event/spawn prompt through the $interaction_directive placeholder; a custom promptTemplate that omits the placeholder gets the directive appended, so a template edit cannot strip the rule, and an unrecognised mode resolves to work-item with a warning (never to cli). The behaviour it asks for — and the artifact-iteration invariant that rides along with it — is reference/collaboration.md § Where questions go. Decision: docs/decisions/decision-052.md.

  • Per-work-item checkout workspace (routing.workspace, issue-76): the CLI daemon runs independent of any repo, so a spawned session needs a checkout of the repo an event concerns. Set routing.workspace.root to turn it on (leave empty to keep the legacy static spawnWorkdir), then pick a strategy:

    • worktree (default) — one shared clone per repo under <root>/<host>/<owner>/<repo> (host = github.com or an enterprise domain — the checkout's host from the payload or workspace.defaultHost; which GitHub the-loop talks to is integrations.github.host, issue-311), plus a git worktree per work item (quarantined under <root>/.worktrees/…), so N concurrent work items on one repo share objects instead of paying for N full clones. The shared clone stays on the default branch and is git fetched to stay fresh; a fresh issue gets a detached worktree (the harness makes its own branch), a PR event seeds the worktree from its head branch.
    • clone — one folder per work item under <root>/.work-items/<slug>/, with a full clone of each repo it touches. Self-contained and simpler to reason about/clean up when a work item spans multiple repos, at the cost of a full clone per work item.

    When the work item ends the checkout is removed (set keepCheckoutOnClose: true to keep it for post-mortem; in worktree strategy the shared clone is always kept). One of the item's PRs merging does not remove it — the next PR in the series is written from the same checkout (issue-101). Auth is your own git credentials (e.g. gh auth setup-git). Design: docs/specs/issue-76/design.md, decision: docs/decisions/decision-034.md.

  • An event on a PR resolves the PR's linked issue first. A PR is the vehicle for a work item, not the work item itself, so any event on it (comment — including a PR conversation comment, which GitHub delivers as issue_comment — review, CI result) is routed to the issue(s) the PR is linked to before the PR's own number. The linkage is read from GitHub's own closingIssuesReferences (the Development panel), the issue-<n> head-branch convention, and closing keywords in the PR body (Closes #N, Fixes: #N, Closes OWNER/REPO#N, GH-N, an issue URL; a reference to another repository is ignored). So the PR's activity reuses the existing session for the issue — the same tmux session, keeping continuity — and an unmatched event spawns against the issue's ref, never a second session for the same work. Decision: docs/decisions/decision-036.md (issue-93).

  • Pre-spawn harness preparation (routing.harnessTrust, issue-90): a brand-new checkout is a directory the harness has never seen, and Claude Code's workspace-trust dialog — plus the one-time bypass-permissions disclaimer — are not permission rules, so no CLI flag (--dangerously-skip-permissions included) silences them. An unattended session therefore used to sit on a modal forever. Before every spawn/respawn the dispatcher marks that exact directory trusted in the harness's own user config (honouring CLAUDE_CONFIG_DIR), and — only when your harnessArgs already ask for bypass mode — records the disclaimer acceptance too. Writes are narrow and non-destructive (those keys only, merged, atomic, skipped when already set, never applied to a file that does not parse), audited as workspace.trusted, and best-effort: a failure warns and still spawns. Both keys always land on the exact spawn directory — the harness reads each of them from the exact project key on at least one path, so an ancestor entry alone leaves the dialog up for a repo shipping .claude/settings.json grants (issue-136). harnessTrust.scope decides only whether trust additionally widens to workspace.root (workspace-root, the default) or stops at the checkout (directory). Trusting a checkout is what lets that repo's own settings pre-approve tool permissions, so opt out with harnessTrust.enabled: false if that is not what you want. Design: docs/specs/issue-90/design.md and docs/specs/issue-136/design.md, decisions: docs/decisions/decision-037.md, docs/decisions/decision-052.md.

  • The label works on PRs directly — the ticketing system need not be GitHub. A PR carrying the auto-execute label is routed as its own work item (github:OWNER/REPO#<pr-number>) when it is linked to no GitHub issue. This is the supported path when work items live in Jira or another provider: the ticket itself can't be routed, but the PR delivering it is still monitorable by the-loop's CLI. /the-loop:work-on <jira-id> applies this automatically — once the PR is opened it adds the label to the PR and registers the session against the PR's ref, so PR comments/reviews/CI resume the session and that PR's merge/close auto-closes it, identical to the GitHub-ticketed flow.

  • One work item, many PRs. A work item is frequently delivered by more than one PR (a spec PR then an implementation PR, a stacked series, a follow-up after review, one PR per repository). Every one of them is labelled and routes back to the same session, and — because a PR is a delivery vehicle, not the work item — a PR closing or merging ends only the session registered against that PR itself. A session registered against the issue the PR is linked to is left running; the issue's own closed event (or the poller's closure reconciliation) is what ends it. The operational consequence: a PR that merges without closing its ticket leaves the session active until the ticket closes — /the-loop:finish-tasks closes the ticket, and the-loop sessions close is the manual escape hatch. Decision: docs/decisions/decision-039.md (issue-101).

  • Session registration is a workflow step. When the harness starts executing a work item (execute-tasks / work-on), it registers itself so events can find it — and closes the registration in finish-tasks. N is the GitHub issue number — or, for a non-GitHub-ticketed item (Jira, …), the PR number once the PR exists (the ref names the routable GitHub object, not the ticket):

    bash
    # Claude Code (session id is exposed to hooks/commands as $CLAUDE_SESSION_ID)
    the-loop sessions register --work-item github:OWNER/REPO#N \
        --harness claude --harness-session-id "$CLAUDE_SESSION_ID"
    # Cursor (use the chat id this agent was launched with)
    the-loop sessions register --work-item github:OWNER/REPO#N \
        --harness cursor --harness-session-id "<chat-id>"
    # on completion
    the-loop sessions close --work-item github:OWNER/REPO#N
    # execution control (issue-106) — the CLI half of the comment keywords
    the-loop sessions start|pause|resume|stop|cleanup --work-item github:OWNER/REPO#N

    Registration is best-effort: if it fails, routing degrades to log-and-drop (or spawn), never blocking the session's own work. When the registered work item is closed — the issue (issues closed), or the PR itself when the PR is the work item (pull_request closed) — the receiver auto-closes the session, so a finished work item never leaves a dangling active session.

  • RULE: record every PR you open, in the same step as opening it (issue-274). Opening a pull request and labelling it is not enough for the PR's own activity to reach this session. The router answers "which work item does this PR deliver?" from GitHub's closingIssuesReferences, the issue-<n> head-branch convention, or a closing keyword in the body — and a pull request the-loop authors carries none of them: a spec PR must not close its ticket, and loop/<id>-… is not the issue-<n> convention. The recorded binding is also the only thing that puts the pull request in the work item's own tracking: since issue-370 nothing infers that list.

    In Claude Code this is automatic. The plugin's PostToolUse hook (hooks/the-loop-link-pr.py) runs the command for you when a tool call creates a pull request, reading the number from what the tool returned and the work item from THE_LOOP_WORK_ITEM or the session registry. You will see a the-loop: recorded … against … line in the transcript. Nothing to do.

    Everywhere else — Cursor, a bare session, hooks disabled — run it yourself:

    bash
    # right after `gh pr create`, for EVERY PR you open for the work item
    the-loop sessions link-pr --work-item github:OWNER/REPO#N --pull-request <pr-number>
    # a PR in ANOTHER repository (the multi-repo shape) is named by its full ref
    the-loop sessions link-pr --work-item github:OWNER/REPO#N \
        --pull-request github:OTHER_OWNER/OTHER_REPO#<pr-number>

    Best-effort like registration: a failure is reported and the work carries on, and re-running it is safe (a PR already recorded is a no-op) — so running it after the hook already did costs nothing. Skipping it where no hook runs is what makes a review comment on a spec PR a dead letter — the comment resolves to the PR as a work item nobody armed, is refused as unstarted, and is never re-evaluated — and leaves the pull request out of the work item's pullRequests[], which is what a work-item review's scope is built from.

  • A repository with no .the-loop/ is worked on the skill's defaults, and nothing writes one for you (issue-352, decision-123; this replaces issue-193's adoption). the-loop is routinely pointed at a repository that never ran /the-loop:init. The harness config is the agent's file: read it when it is there, fall back to the schema's defaults when it is not, and say which in the PR briefing. The CLI neither reads nor writes it — it takes the spec directory, the critics and the graph hooks from the operator's cli-config.yaml — so a spawned session finds exactly what the repository committed, never a file the daemon planted. A contribution or a review (guest loops) never installs the-loop in the repository it was invited into, as before.

Predictability & execution guarantees

The PDLC is largely fixed; the harness should not re-derive it each run. Steps are made predictable via:

  • Harness hooks — force steps to run at lifecycle points. In Claude Code: hooks/hooks.json — a SessionStart reminder, a Stop gate (the-loop-gate.py, which asks the-loop check whether the current node is complete), and a PostToolUse recorder (the-loop-link-pr.py, which runs sessions link-pr for a pull request the session just created). A hook is how a step stops depending on the model remembering a rule; prefer one whenever the step is mechanical and its inputs are in the payload. In Cursor: the always-applied rule rules/the-loop.mdc carries the SessionStart reminder and .cursor/hooks.json the gate (Cursor has no SessionStart or PostToolUse equivalent), so there the prose rules above are what runs.
  • Custom code/scripts (the CLI is a natural home) where hooks are insufficient.

Self-improvement (learnings lifecycle)

the-loop is not expected to be perfect from the start; it captures learnings in your repo so it measurably stops repeating mistakes — without letting the index grow unbounded and blow the context budget. Learnings live in docs/learnings/learnings.md (index) + docs/learnings/learning-<nnn>.md (detail), with overflow in docs/learnings/topics/<category>.md — the loop's fixed convention, beside the spec and capability trees; a repository that publishes its docs/ tree publishes its learnings with it. The lifecycle itself is fixed: the index stays under 200 lines, and a learning is written once the same finding has occurred three times. Sources: user feedback (requirements/design/tasks iteration, PR reviews) and system feedback (repeated failures or insights). The skill implements this today; the Python CLI can harden it later. Four stages:

  1. Capture. At logical checkpoints the loop emits learning candidates from the pass/fail signals it already produces (a red check, a rejected review, a repeated reviewer comment) into a git-ignored pending queue (.the-loop/learnings-pending/).
  2. Write-gate. A candidate becomes a durable, injected learning only once it recurs (rule-of-three: the third occurrence) — or immediately for a clearly high-severity one-off. This keeps one-off noise out of the index.
  3. Consolidate. At the end of a run, classify each candidate against the existing index as ADD / UPDATE / DELETE (on contradiction) / NOOP, then prune to the size cap (200 lines) by evicting the least-important/least-recent entries into docs/learnings/topics/<category>.md.
  4. Inject. Load the capped index (under 200 lines) into each agent's prompt at the start of a run; overflow detail is read on demand from the topic files.

Everything durable is checked in so you can review it and give feedback.

Reporting problems with the-loop

Feedback and bug reports about the-loop itself are filed as GitHub issues on the the-loop repository.

Released under the MIT License.