Skip to content

Collaboration reference ​

How the-loop works with humans and other personas. The governing rule: every decision or opinion taken from a human MUST have a paper trail on the ticketing system or the PR.

Paper-trail rules ​

  • Planning questions/opinions/decisions → asked and recorded as comments in the ticketing system (GitHub issue / Jira), not resolved silently in files.
  • PR reviews → happen on the PR as comments and replies.
  • Self & critic reviews → also as PR/ticket comments.
  • Notifications/escalations when a human action is pending → published by the graph's notify hook on the event bus and delivered to a channel: the Slack bot under the CLI config's channels.slack, which posts every event its subscribe list names (issue-245, issue-309). No event → role filter lives in the harness config any more (issue-352); a role a node names rides along as detail and is not a delivery address — per-person routing is not built (issue-304). Notification only; the decision itself still lands as a comment.

Where questions go, and where artifacts are iterated (issue-134) ​

Two rules. The first is configurable, because only the operator knows where they are. The second is not, because it is a property of the artifact.

1. The interaction channel is declared, not guessed ​

A session driven by the CLI daemon is told where its answers come from, via routing.interaction.mode in the operator's cli-config.yaml (rendered into every event/spawn prompt as $interaction_directive — reference/automation.md):

  • work-item (default) — do not assume a human is watching this terminal. Every question, clarification, decision or approval is asked as a comment on the work item or its PR, by running the-loop ask --work-item <ref> --question '…' (issue-208): the verb posts the comment, stamps the loop-prevention marker centrally (see the rule below — no agent memory involved), and records the wait as a session.awaiting_input event, which is what lets the control plane surface the question (attention, the dashboard's card) and answer it straight into the session (POST /api/v1/sessions/reply). The session then stops and waits: the reply reaches it as the next event. Only if the CLI is unavailable, post the comment with gh (or a GitHub MCP server) and mark it yourself. Never block on an interactive prompt, and never read silence as consent — if genuinely blocked, log the conflict, escalate once, and move to the next available work. The daemon enforces the first half for Claude Code: a work-item-mode session is launched without AskUserQuestion (issue-426), because its menu waits on a keypress in a pane nobody watches.
  • cli — a human is attached to this session's terminal, so ask there. This does not waive the paper trail: the outcome of every human decision still lands on the work item as a comment.

When no daemon is involved (a human ran /the-loop:work-on themselves), the human is by definition at the terminal — that is cli behaviour, with the same paper-trail obligation.

Always give the-loop ask a --summary, and a --default when there is a safe one. A channel (Slack) leads its message with your --summary — your own 2-3 sentence account of what you decided and what you are least sure of — instead of an excerpt of the document, which is what a reviewer on a phone actually needs to act (issue-393 R8); the full question text is still recorded on the ticket. When you would proceed a certain way absent an objection, pass that as --default "…" (e.g. --default "Defaults are fine"): the room then offers a one-tap button that replies with exactly that text (R12.1), so the human can approve without typing. So the normal shape of a question is:

sh
the-loop ask --work-item <ref> \
  --question-file q.md \
  --summary "Requirements are ready: 7 requirements, the three checks plus make check, no new attack surface. Least sure of: whether to count binary files." \
  --default "Defaults are fine — proceed"

At an approval gate the graph posts its own request with an artifact excerpt; your own account still reaches the room if you post it — a the-loop ask --summary (or a marked comment that states, in 2-3 sentences, what the artifact decided and what you are least sure of) is delivered in your voice and wins over the runtime template for that node (the session-voice rule), so the room reads your summary rather than the raw excerpt. Omitting the summary/default is not a failure, but it wastes the channel rework — a bare ask falls back to the document excerpt and no button (the 2026-09-20 e2e run's finding).

Every channel is a peer on one event bus, and the work item is the ledger (issue-245, issue-309, decision-103). When the operator's CLI config declares channels (today: a Slack bot — channels.slack), everything the loop says is an event: the ask, the graph's notifications (an approval request carries the work item's link and an excerpt of the artifact; work-item-complete fires at the end), the loop's lifecycle (phase.started / phase.completed as the phase label changes, work-item.closed when the ticket closes — published by the runtime and the daemon, issue-378), and the comments the ledger's ingress sees (comment.agent — your own marked comments; comment.human — an authorized user's or collaborator's). A channel receives the ones its subscribe list names, renders them natively (Slack: Block Kit, a link button, Approve / Request changes buttons where a press can be received — and, under the control.command grant, an Execute button on the phase-selection checklist and Do / Start buttons on a kickoff's reply, each a press of the configured keyword whose outcome is written back onto the message, issue-337; a text longer than maxChars arrives as a structural digest — the ask first, choices numbered, code and traces as pointers, cut at a sentence, the link for the rest — never a mid-sentence cut, issue-338) into one thread per work item, rooted on the work item — opened the moment the work item starts (issue-317), root only, with every event a reply into it (issue-312) — and the ledger records every event that started elsewhere first — the ask's record is the question comment. What a message on a channel may become is the channel's publish grant: session input by default (work-item.reply: mirrored onto the work item as the-loop's own marked, defanged comment, then delivered into the waiting session); by grant, an answer to an open human gate (gate.feedback — read through the dispatcher's own graph coupling, and, when the pipeline cannot read the gate at all, deferred to the ledger under the same grant rather than mirrored where the gate never looks; issue-321) or a control keyword (control.command) — both recorded on the ledger as an unmarked comment under the operator's credential, carrying an envelope that names the person, so the ledger's own ingress classifies or executes it through the guards a typed comment goes through and the artifact's approvedBy names the person; or a new work item (work-item.create, a top-level message opening an issue in the repository its own first line names — a <repo>: prefix resolved against the declared set, kickoff.repo and the poll sources, refused rather than guessed when it resolves to none or several; issue-341). Who may speak on any channel is the one list routing.authorizedUsers, each entry a person with their id on every channel. What has no thread — a work item that has not started, a standing session, the instance itself — is reached by the /the-loop slash command (issue-334): its work-item verbs are control.command through the same ledger record, and status / restart / upgrade and the standing verbs call the core facade under the instance.command / standing.command grants. Whatever channel carried the conversation, the ticket carries the record — and a channel advances the loop through the ledger, never around it. The operator's map of every mode is docs/guide/slack.md.

A Slack conversation reaches the loop only when it addresses it (issue-389, decision-133). Every message meant for the-loop carries the @the-loop mention, delivered as Slack's app_mention event — in a room, in a thread the-loop opened, under its own question, in the central channel; a message without it is dropped as not-addressed with no record, no reaction and no reply. Two exceptions: a direct message with the bot (Slack sends no app_mention there, so message.im stays the input), and a room an authorized user declared with --listen all, which hears every message as before. After the mention, a fixed grammar and no model: record-context (this thread, snapshotted onto the ticket as a marked context.added record and into context.md), record-decision <text> (a marked decision.recorded record attributed to the person, which becomes docs/decisions/decision-<nnn>.md), add-collaborator @login or slack:U… (the issue-307 keyword, a collaborator now known by Slack id), any control keyword's last word (start, execute, … composed into the configured keyword), help (an ephemeral answer, nothing recorded) — and anything else is a work-item.reply delivered to you as today. Two message shortcuts (Add as the-loop context, Record the-loop decision) are exactly the typed mention. Two tiers of speaker: input — record-context, a reply, help — from routing.authorizedUsers and that work item's collaborators (who may be on the roster by Slack id alone); binding acts — record-decision, every keyword, --listen — from authorized users only. The fold-in is yours: a session that receives a context.added record appends one entry with provenance to docs/specs/<id>/context.md; a decision.recorded record becomes a decision record plus its index row; both are committed with the work item, and at the start of every phase the-loop channels records <ref> lists the records not yet folded — a record whose delivery found no session stands on the ledger and waits for you (reference/workflow.md § The artifact chain).

2. RULE: a generated artifact is iterated on a durable, reviewable surface ​

Once an artifact of the chain exists — brainstorm.md, requirements.md/bugfix.md, design.md, testing-plan.md, tasks.md, and the living context.md beside them — iteration on it happens only through comments on a surface that outlives the session: the pull request that carries it, or the ticket. In both interaction modes. What is never permitted is iterating on it interactively, where the reasoning vanishes with the scrollback — that is the invariant, and it is not configurable (decision-051 §5, amended by decision-069).

Which of the two durable surfaces applies is this work item's own choice, made by its author at phase-selection (issue-183) — and it applies to the outer loop only:

The outer-loop-on-pull-request boxOuter loop's artifacts are iteratedTypical work item
unticked — the defaultas comments on the work item, Jira-stylework whose code lands in other repositories, so a PR in the origin repo would only ever hold a discussion
tickedas review comments on the PR carrying them, in the repository the ticket was created insingle-repo work, where that PR also carries the code
  • Commit and push the artifact either way, and link the checked-in file from the ticket; never paste its contents into a comment. The choice is where it is discussed, not whether it is checked in — every gate in the process graph reads files.
  • It is deliberately not a setting in harness-config.yaml or cli-config.yaml: it is a property of the work item, and it is frozen by the same signed reply that freezes the phase selection.
  • A contribution is not offered the box at all (issue-199): pdlc-contribution-loop joins a work item somebody else is already running, so there is no outer loop to place — its one contribution.md is iterated on that thread, and the code arrives as an ordinary pull request on the repository it targets. A review (issue-279) and an ad-hoc task (issue-225) run no phase-selection gate at all, so neither ever meets the question: the thread is their whole surface.
  • A Claude artifact (issue-471, the claude-artifact box) is a reading surface beside these two, not a third place to decide: its comments are feedback folded into the files, and a gate is never answered there. See workflow.md § The spec chain as one Claude artifact.
  • This is the reference, don't duplicate rule (SKILL.md) reaching its conclusion: the artifact is a checked-in file, and the file, the discussion and the approval belong in one place.
  • The inner loop has no surface to choose. A pull request's pdlc-pr-loop is iterated on that pull request — review comments, review threads, its own approval.
  • Planning questions asked before any artifact exists still follow rule 1.

See docs/decisions/decision-051.md for why the mode is a two-value enum, and docs/decisions/decision-069.md for why the artifact rule now names two surfaces rather than one.

RULE: mark every comment/reply as the-loop's own (loop prevention) ​

The webhook/poller trigger paths react to their own repo's activity, but the harness posts comments/reviews/replies under your own credentials (no separate bot token — decision-023's operating model), so by author alone the-loop's own reply is indistinguishable from something you typed. Left unmarked, it would re-enter the loop as new input on the next poll/webhook cycle, resuming the session that just wrote it — which may reply again, forever.

GitHub (and every ticketing system the-loop targets) attaches no queryable custom metadata to a comment or review — the body text is the only channel available. So:

  • Every comment, PR review, and reply the-loop posts (issue comments, PR conversation comments, PR review comments/replies, review submissions — anywhere this session writes a reply, not just review findings) MUST end with two things, appended after a blank line:
    1. The exact, invisible marker <!-- the-loop:agent-comment --> — an HTML comment, so it never clutters the rendered thread. This exact string is what the_loop.authz.is_self_authored matches on; do not paraphrase or omit it, and do not use it verbatim in a comment that is not the-loop's own.
    2. A short, visible attribution line so a human reading the thread also knows — reuse the round's [<harness>/<model>] prefix (reviewing.md) where one already applies; a plain 🤖 _the-loop, autonomous reply_ otherwise.
  • This applies everywhere GitHub-style credentials post on your behalf — issues, PRs, and (once supported) Jira or any other ticketing system — not only GitHub.
  • It applies to both producers, not just this session. The CLI daemon posts comments of its own (the interactive-session announcement, the_loop.announce; the reply route's delivery report, issue-208) with the same credentials, so they carry the marker too — via the_loop.authz.mark_self_authored, the producer-side counterpart of is_self_authored. Any new daemon-side comment MUST go through that helper; an unmarked one is re-ingested on the next cycle and pasted into the session it was about (issue-104).
  • Questions go through the-loop ask, which stamps for you (issue-208): when the question travels through the verb, the marker and attribution are appended centrally and idempotently — the by-hand rule above is the fallback for when the CLI is unavailable, not the primary path.
  • Every other comment goes through the-loop comment, which stamps for you too (issue-447): spec-gate replies, the completion summary, the reviewer briefing, a decision record. The verb appends the marker and an event envelope centrally, and channels subscribed to the agent's comments mirror it once — the ingress sees the envelope and never re-publishes it.
  • Only ever mark text the-loop composed. mark_self_authored asserts authorship and the trigger paths silently drop whatever carries the marker — never apply it to payload-derived text or another author's words.
  • Do not rely on this for anything else. It identifies authorship for loop prevention; it is not an authorization mechanism and does not replace the authorized-actor guard (security.md).

See docs/decisions/decision-031.md and cli/the_loop/authz.py for the CLI-side enforcement (both the webhook router and the poller drop a marker-carrying event before dispatch, regardless of who technically posted it) — the same module now holds the marking helper, so what the-loop writes and what it recognises cannot drift apart.

Conflicts & assumptions (keep unattended runs moving) ​

docs/decisions/ captures deliberate decisions; unattended runs also hit ambiguities and conflicts mid-flight (a missing field, contested ownership, an unexpected tool failure, an assumption the agent had to make). Those must neither block the loop nor vanish. Rule:

  • Resolvable with a reasonable default → assume and continue. Record the assumption in the append-only conflict log and keep going; do not stall the whole run on one low-stakes ambiguity.
  • Genuinely blocked → log, escalate once, move on. Record the conflict, escalate once via the paper trail (ticket/PR comment + a conflict-escalated notification), and proceed to the next available work rather than spinning.

The log is docs/decisions/conflicts.md (append-only). Each entry is one line: timestamp · phase · one-liner · status (assumed / escalated / resolved). It gives the human a precise, reviewable trail of every judgment call the agent made on their behalf.

Personas, roles and groups ​

  • The full list of available collaborators is defined up-front in the repo, in .the-loop/collaborators.yaml — the SINGLE source of truth for who works on this project and in which roles, validated against the plugin's collaborators.schema.json (issue-82, decision-035; the former config.personas/config.messaging keys are retired). CODEOWNERS-like: these are the stewards of the repository.
  • A collaborator may be an individual or a group (e.g. a GitHub team @org/team). A single user may hold multiple roles.
  • Supported roles: product-manager, architect, designer, engineer, qa, reviewer, approver.
  • A collaborator declares no delivery of their own (issue-304). Roles are what every filter and every phase gate targets; the notification itself goes to the one Slack bot configured under the CLI config's channels.slack. A collaborators.yaml still carrying the retired per-person notifications block is refused by the schema, with channels.slack and the-loop migrate-config named in the message.
  • Human identity is declared in exactly one place, hand-maintained and not in this file: routing.authorizedUsers — one entry per person, carrying their GitHub login (who may arm and command a work item) and, since issue-309, their Slack member id (whose thread reply, button press or /the-loop command is acted on). The retired channels.slack.authorizedUsers is refused at load.
  • A "work-item collaborator" is a different thing entirely (issue-307). This file's collaborators are the project's stewards and their roles, read by the plugin. A work-item collaborator is runtime state the daemon reads: an authorized user typing the-loop add-collaborator @login on one work item grants that login the right to be input on it — their comments reach that item's session — and nothing else. It is neither hand-maintained nor declared here, it is not a role, and it grants no authority: a work-item collaborator cannot command the-loop, arm a session, or answer a phase gate. See reference/automation.md.
  • A work item can also name the room it is worked in (issue-375). Where a work-item collaborator says who else may be input, a collaboration channel says where the conversation happens: an authorized user typing the-loop add-channel slack@C… moves that work item's updates into that channel and makes every message there — not only a thread reply — a message on that work item. It is a place, not a person: it grants nobody anything, and it is declared and revoked independently of the roster, in either order. See reference/automation.md.
  • The CLI daemon never reads this file (decision-032): it watches many repositories and belongs to none of them — see reference/automation.md.

RULE: identify collaborators up-front ​

Every work item MUST clearly identify the collaborators it needs at the start (in its spec front-matter collaborators). More collaborators can be added later as needed.

Not every task needs every persona ​

Match personas to the work:

Work typeTypical collaborators
Architecture-significant changearchitect (+ PM, engineer)
UI/UX changedesigner (+ engineer)
Product/requirements workproduct-manager (+ architect)
Simple bug fixengineer only
Content/copy fixreviewer (often no engineer)
Release/QA sign-offqa, approver

Pull required reviewers/approvers for each phase from collaborators.yaml by role.

For UI/UX work, the designer reviews the UI/UX design artifacts produced in the design phase (Figma links / self-contained HTML prototypes under docs/specs/<id>/design/) — iterating on the rendered output until the designer signs off, with every opinion recorded as a ticket comment (paper trail). See reference/design-artifacts.md.

User-interaction principles (reviewing AI-authored work) ​

The human often did not write the code and their familiarity with the codebase keeps dropping, so how the-loop communicates is a first-class concern. These rules are fixed — the userInteraction block left the harness config in issue-352.

  • Give enough context to decide. Whenever user input is requested (a planning question, a design opinion, a review), include enough context that the user can make the right judgement call without digging.
  • Condensed, prioritized PR summaries. Every PR the-loop raises tells the reviewer where to focus and in what order — reviewing a huge AI-authored PR top-to-bottom is not realistic. Lead with the highest-priority items to scrutinize. This briefing is produced from the-loop's internal ${CLAUDE_PLUGIN_ROOT}/skills/the-loop/templates/pr-briefing.md and posted/updated in the PR BEFORE human review is requested — a required item of the ready-to-ship gate, so it triggers on every PR (see workflow.md).
  • RULE: all diagrams are mermaid. PR summaries, design docs and educational snippets use mermaid diagrams to explain low-level details.
  • Document insights & decisions in the PR description. Capture the insights from taking the spec to implementation and every low-level decision the harness had to make, so the user sees the reasoning, not just the diff.
  • RULE: educate the user (mandatory, not optional). As the user's familiarity with the code drops, use every opportunity to teach them the low-level design decisions. This is intentional and required, not a nicety. Enforcement: it is not left to chance — the reviewer briefing is a required item of the ready-to-ship gate (workflow.md), so "request human review" cannot happen without the education step having fired.

Working with other tools (MCP / CLIs / plugins) ​

the-loop is allowed to freely interact with the MCP tools, skills and plugins available in the harness — plugins such as superpowers. GitHub, and Jira where integrations.jira is configured, are reached through the-loop's own verbs (the-loop comment, ticket, pr, ask — issue-447, issue-475, see automation.md § Reaching GitHub), with gh, a GitHub MCP server or the Jira MCP tools only as the fallback when the CLI is not installed. Nothing declares them in the config: discover what the harness offers (its MCP servers, plugins, skills and CLIs) and check before assuming a capability is or isn't available.

Released under the MIT License.