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 sameskills/,commands/and templates (skills follow the Agent Skills open standard, so oneSKILL.mdserves 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 subclassingCommand,@register-ing it, and dropping the module underthe_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 perservice.enabled,/mcpperservice.mcp.enabled, the webhook receiver perwebhooks.ghWebhook.enabled, the poller perpolling.enabled), stops whatever runs, and reports per service.POST /api/v1/restartschedules 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-256HMAC (secret from an env var), exposesGET /health, and logs deliveries. Defaults come fromwebhooks.ghWebhookin the CLI config (cli-config.yaml— resolved via--config/env/cwd/home; seedocs/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).
- Verifies the GitHub
Webhook → session routing (
routing.enabled): a received event (PR/issue comment,workflow_runresult, …) 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 followrouting.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, defaultthe-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 explicitkeywordsoverride is unaffected) andthe-loop cleanup(issue-186). Withcontrol.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.pauseholds delivery without discarding the conversation;stopcloses the session through the normal close path.cleanupis 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 asthe-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).authorizedUsersis 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 typesthe-loop add-collaborator @login(or runsthe-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 readingauthorizedUsers. A grant covers one work item, is cleared when it closes, and is revoked withthe-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 aname, ascope.modeand a declaredscope.workItemslist; 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;addressedtakes only a start that names it;lockedtakes nothing new. Any control keyword can carry the address tokeninstance:<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 starton an instance is addressed to it; a spawned session findsTHE_LOOP_INSTANCE=<name>; the portable record'scontrol.instanceandGET /api/v1/instancesay who took what. Decision:docs/decisions/decision-110.md.Where the session takes its answers from (
routing.interaction.mode, issue-134):work-item(default) orcli. 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_directiveplaceholder; a custompromptTemplatethat omits the placeholder gets the directive appended, so a template edit cannot strip the rule, and an unrecognised mode resolves towork-itemwith a warning (never tocli). The behaviour it asks for — and the artifact-iteration invariant that rides along with it — isreference/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. Setrouting.workspace.rootto turn it on (leave empty to keep the legacy staticspawnWorkdir), then pick astrategy:worktree(default) — one shared clone per repo under<root>/<host>/<owner>/<repo>(host=github.comor an enterprise domain — the checkout's host from the payload orworkspace.defaultHost; which GitHub the-loop talks to isintegrations.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 isgit 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: trueto keep it for post-mortem; inworktreestrategy 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 ownclosingIssuesReferences(the Development panel), theissue-<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-permissionsincluded) 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 (honouringCLAUDE_CONFIG_DIR), and — only when yourharnessArgsalready 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 asworkspace.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.jsongrants (issue-136).harnessTrust.scopedecides only whether trust additionally widens toworkspace.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 withharnessTrust.enabled: falseif that is not what you want. Design:docs/specs/issue-90/design.mdanddocs/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
closedevent (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-taskscloses the ticket, andthe-loop sessions closeis 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.
Nis 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#NRegistration 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 (
issuesclosed), or the PR itself when the PR is the work item (pull_requestclosed) — the receiver auto-closes the session, so a finished work item never leaves a danglingactivesession.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, theissue-<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, andloop/<id>-…is not theissue-<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
PostToolUsehook (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 fromTHE_LOOP_WORK_ITEMor the session registry. You will see athe-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'scli-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 asksthe-loop checkwhether the current node is complete), and a PostToolUse recorder (the-loop-link-pr.py, which runssessions link-prfor 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 rulerules/the-loop.mdccarries the SessionStart reminder and.cursor/hooks.jsonthe 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:
- 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/). - 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.
- 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. - 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.