sessions
The work-item ↔ harness-session registry, the five execution-control commands, and the reset that forgets a work item entirely.
# registry
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|paused|closed] [--format table|json]
the-loop sessions attach --work-item github:OWNER/REPO#N [--read-only]
the-loop sessions close --work-item github:OWNER/REPO#N [--keep-tmux|--kill-tmux]
the-loop sessions reset --work-item github:OWNER/REPO#N [--work-item …] [--dry-run]
the-loop sessions reset --all [--dry-run]
# execution control — the same five commands as the comment keywords
the-loop sessions start --work-item github:OWNER/REPO#N [--no-comment]
the-loop sessions pause --work-item github:OWNER/REPO#N [--no-comment]
the-loop sessions resume --work-item github:OWNER/REPO#N [--no-comment]
the-loop sessions stop --work-item github:OWNER/REPO#N [--no-comment]
the-loop sessions cleanup --work-item github:OWNER/REPO#N [--no-comment]Every subcommand accepts --registry-dir, defaulting to routing.registryDir.
--registry-dir / --portable-dir and the service
Since issue-161 these commands run through the control-plane service, which reads its own configured state directories. The two path flags therefore only take effect on the in-process path the test suite uses; to point real invocations at other state, point the service at it — the-loop --config other.yaml start, or set routing.registryDir / state.root in the config the service runs on. Arbitrary filesystem paths are deliberately not accepted over HTTP.
The registry
One human-inspectable JSON file per session under <registryDir> (default <state.root>/local/, git-ignored). Writes are atomic, so concurrent sessions on one machine are safe.
The invariant is 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.
A missing harness binary only warns
register succeeds even when the harness CLI is not on PATH, telling you events cannot be dispatched until it is installed. Registration is bookkeeping; dispatch is what needs the binary.
register
| Flag | Required | Meaning |
|---|---|---|
--work-item | yes | Work-item ref, e.g. github:OWNER/REPO#15. |
--harness | yes | claude or cursor. |
--harness-session-id | yes | Claude session id, or Cursor chat id. |
--cwd | no (.) | Directory the session runs in — resume is scoped to it. |
--force | no | Replace an existing active registration for this work item. |
list
| Flag | Default | Meaning |
|---|---|---|
--status | all | active, paused or closed. |
--format | table | table or json. |
Shows each session's status — paused included — and its last control command. Retained tmux sessions accumulate after their work items close; --status closed is how you find them.
attach
| Flag | Default | Meaning |
|---|---|---|
--work-item | required | Which session to attach to. |
--read-only | off | Observe without a keyboard (tmux attach -r). |
Works after the work item is closed too — and is then always read-only, because a finished session takes no input. Equivalent to tmux attach -t loop-<slug>.
Attaching read-only is safe to leave running: the daemon delivers events by pasting into the pane, which consults no tmux client, so an observer cannot block the session's input. That was not true before 10.3.0 — the submit was a tmux send-keys, which resolves its target client from the session's current client, and tmux ≥ 3.7 refused it with client is read-only while anyone was attached this way, failing every delivery (issue-240).
A session registered by hand (sessions register) has no tmux session until its first dispatched event spawns one; until then attach errors, telling you no tmux session is recorded yet.
close
| Flag | Default | Meaning |
|---|---|---|
--work-item | required | Which session to close. |
--keep-tmux / --kill-tmux | routing.tmux.keepSessionOnClose | Whether the tmux session survives. |
Mutually exclusive. --keep-tmux keeps the transcript readable; the harness inside it is still ended unless routing.tmux.killHarnessOnClose is false. --kill-tmux ends it for good.
Sessions usually close themselves
When the work item ends — the issue closed, or, when the PR is the work item, that PR merged or closed — the session is auto-closed. No manual close needed.
A PR merely linked to the work item closing leaves the session running: one item is often delivered by several PRs, so only the item's own close ends it. Both ingress paths do it — the receiver on the closed event of the registered item, and the poller by noticing the item has left the open listing and confirming upstream that it really ended.
A tmux-hosted session is kept so you can read back what happened, but the harness inside it is ended so nothing can be typed into finished work.
reset
Forget everything this machine remembers about a work item, so it starts over on the code you have just fixed. The command #137 asked for: after fixing a bug in the-loop itself and releasing, an in-progress item is still holding a conversation the old CLI started, a poll ledger saying every comment is handled, and a control record saying it is armed.
| Flag | Default | Meaning |
|---|---|---|
--work-item | — | Which work item to reset. Repeatable. |
--all | off | Every work item this machine holds state for. Mutually exclusive with --work-item. |
--dry-run | off | Report what would go; change nothing. |
$ the-loop sessions reset --work-item github:octo/repo#15
github:octo/repo#15: ended a live session
github:octo/repo#15: removed the workspace checkout — uncommitted work in it is gone
github:octo/repo#15: reset — removed session, control, poll
reset 1 work itemWhat it removes
| What goes | Why | |
|---|---|---|
| the live session | ended through the normal close path | no harness is left running against records that have gone |
<state.root>/local/<slug>.json | deleted, not closed | a closed record still lists, and is still attach-able — that is the "still remembered" a reset ends |
portable/<slug>.json control | cleared | the item is disarmed: it waits for an explicit start rather than resuming itself |
portable/<slug>.json poll | cleared | the thread is first-sight again, so a fresh session re-reads it instead of finding everything already seen |
| the workspace checkout | per workspace.keepCheckoutOnClose | reset reuses the close path rather than inventing a second policy |
What it does not
The event log is appended to, never rewritten — the reset itself lands in it as session.reset, so "someone reset this" is a visible cause rather than an unexplained gap. And nothing in your repository is touched: docs/specs/<id>/graph-state.json, the spec artifacts and the phase label are checked in on the work item's branch, and the process graph re-derives the current node from the artifacts anyway.
Two things it will tell you about
A reset run while gh-webhook is up can be partly undone — the daemon holds poll state in memory and may write it back. Stop it first for a clean slate.
And when requireStartCommand is false, clearing the poll section makes the item first-sight again, so the next poll cycle may re-spawn it rather than wait for a start. The command warns in both cases and still does the work — the judgement is yours.
Selection is deliberate: a bare reset is a usage error rather than "reset everything", and one bad ref in a list resets none of them. Nothing is posted to the ticket — there is no reset keyword (a comment must not be able to delete local state), and posting stop-execution would record intent the reset has just cleared (decision-050).
Execution control
start / pause / resume / stop / cleanup apply exactly what the corresponding comment keyword applies, from the machine running the-loop:
startspawns through the same dispatcher the daemon uses — workspace checkout, harness trust, tmux hosting, announcement — or resumes a paused session, and prints the tmux target it spawned into.pauseholds events; the session keeps its conversation.resumedelivers events again.stoptakes the normal close path.cleanupreleases the work item's local resources — the end of the life cycle, and the only one of the five that destroys anything.
| Flag | Default | Meaning |
|---|---|---|
--work-item | required | Which work item to act on. |
--comment / --no-comment | on | Post the equivalent keyword comment on the work item. |
Each invocation records the command in that work item's portable record (<state.root>/portable/<slug>.json, control section) and posts the same keyword back to the work item, so the ticket stays the full record of who asked for what. That comment carries the loop-prevention marker, so the daemon never reads its own action back and re-applies it.
Posting is best-effort: --no-comment skips it, and a missing or failing gh only warns — it never undoes the local action.
cleanup
Release the local resources a finished work item accumulated on this machine, and nothing else. The counterpart to start: where that verb creates a checkout and a tmux session, this one reclaims them.
$ the-loop sessions cleanup --work-item github:octo/repo#15
ended github:octo/repo#16, github:octo/repo#15 — their tmux sessions and transcripts are gone
removed the workspace checkout under /srv/the-loop/ws — uncommitted work in it is gone
removed the machine-local session record
cleaned up github:octo/repo#15 — its portable record (control, poll, graph) is kept, and nothing remote was touched
commented 'the-loop cleanup' on github:octo/repo#15Destructive, and deliberately unconditional
It ignores tmux.keepSessionOnClose and workspace.keepCheckoutOnClose. Those settings answer "what should survive the end of the work"; cleanup is you saying you are done with all of it, and a retention default that silently made this a no-op would be a verb that lies. Uncommitted work in the checkout is gone.
| What goes | What stays | |
|---|---|---|
| tmux | every endpoint's session — the work item's own and one per PR delivering it, harness ended first | — |
| workspace | the work item's worktree, or its whole folder under the clone strategy | the shared per-repository clone |
| registry | <state.root>/local/<slug>.json | — |
| portable | — | control, poll and the frozen graph: persistence and tracking outlive the machine |
| remote | — | everything: no branch, pull request, issue or label is touched |
The work item's graph pointer moves to the terminal cleanup node first — so the ticket carries a loop:cleanup label and the transition is on the record — and the item is durably disarmed, like stop, so nothing re-spawns afterwards.
It works with or without a live session, and with or without a record: a checkout left behind by a crash is located from the work-item ref alone. "Nothing to clean up" is a normal outcome, not an error.
cleanup vs reset
Both remove things; they answer different questions.
cleanup | reset | |
|---|---|---|
| Asks | "this work is over — reclaim the machine's resources" | "start this work item over on the code I have just fixed" |
| Portable record | kept | cleared (control + poll) |
| Retention settings | ignored | honoured |
| Trigger | a comment keyword, the CLI, or an authorized closure | the CLI only |
| Posts to the ticket | yes — it is a control verb | no, deliberately (decision-050) |
Label-gated auto-execution
With spawnOnUnmatched: labeled: give an issue or PR the configurable autoExecuteLabel, have an authorized user comment the-loop start (or run sessions start), and the ingress spawns a session and starts /the-loop:work-on on it — then routes that item's later activity (comments, reviews, CI, and every PR linked to it) to the same session, and auto-closes when the item itself closes. Label presence is read straight from the webhook payload, with no extra API call. A new issue without the label is received and ignored.
PRs are work items too
The label applies to PRs directly: 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 cannot 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 that PR's merge or close ends it, exactly like a GitHub-ticketed item.
See also
- Routing options — registry location, tmux lifetime.
- Concepts — the invariant and how sessions end.
- interactive sessions — the capability doc.