Docs
Declare what a loop is for before it runs
RepoOps reads each loop contract in your repository, judges it valid, incomplete or unparseable, and shows the declared goal, the check that proves it done and the stop rules beside the runs the loop has reported. It reads the contract. It never runs the loop.
For: the engineer who runs a recurring agent loop, and the lead who wants to know what each loop was supposed to do before reading what it did
What it does, and why it helps
A declared intent is one file per loop, .claude/loops/<id>.loop.md, whose front-matter states five things: a goal a reviewer can call met or not met, a check with the command that proves progress and the pass condition for it, a stop map with at least one of success, maxIterations, budgetUsd or noProgress, an escalation that says what is handed back and to whom, and a handoff that says what a two-minute review needs. A sixth field, secondCase, is optional. The validator returns one of three verdicts: valid, incomplete with every missing field named, or unparseable with the reason. It never throws, on any input.
The Declared intent tab joins those contracts to the loop registry, the list of loops you told RepoOps you run. A registered loop with a contract reads registry+contract; a registered loop with none reads registry-only and carries the badge uncontracted; a contract with no registered loop reads contract-only. Each card shows the goal, the check that proves done, the stop rules, the escalation, the second-case evidence, the missing fields if any, a health badge when the loop has reported runs, and a runs summary that links onward to Agent traces. A repository that declares neither shows the zero state and what to do next. On the hosted dashboard the team page shows one row per published loop with the contract verdict and the caps, never the contract text.
The pain. A loop runs on a schedule and its runs pile up. When one goes wrong, nobody can say whether it did what it was for, because what it was for was never written down, or lives in a prompt only its author remembers.
The point of view. Judge a run against what the loop declared, not against what it seemed to be doing. The declaration comes first, is small enough to check by eye, and stays in the repository next to the loop it governs.
What gets easier. Reading. One card per loop answers what it is for, what proves it done, when it gives up and what it hands back, with the missing fields named when the answer is incomplete, and the runs it produced one link away.
When it helps. Any loop you run on repeat, agent or script, in a repository RepoOps tracks. It helps most before a loop is trusted to run unattended, and again when a run needs judging.
Its limits. It reads and judges the contract. It runs nothing, blocks nothing and holds no loop to its stop rule; the executor you arm separately does that. The desktop tab reads the contract from the mirrored branch, so an unpushed file is invisible to it. The hosted table carries the verdict and the caps only; the goal and check text never leave the machine.
Understand it in 30 seconds
Read the narration
- 0:00 A loop runs every night.
- 0:02 Nobody wrote down what done would look like.
- 0:06 Declare the goal, the check that proves it, and when to stop.
- 0:13 RepoOps reads the contract and names every missing field.
- 0:16 Runs sit beside it, so you judge them against what was declared.
- 0:23 Contract first. Then every run has something to answer to.
- 0:27 RepoOps never runs it.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Remediation in the sidebar, then Declared intent under All tools, in the Approvals and workflows group. - Hosted:
repoops.ai/team/declare, from Remediation in the sidebar, then Declared intent under All tools, in the Approvals and workflows group. - Keyboard: ⌘ K, then type “Declared intent”.
When to use it
Pinning down a loop that runs every night
Situation. A nightly triage loop is in the loop registry and has reported runs, but its card on Declared intent reads uncontracted with No declared goal yet, and its health badge is the only thing on the card.
What you do. Ask the loop-spec skill to write a contract with the goal, check.command, check.pass, at least one stop condition, the escalation and the handoff. Save the file into .claude/loops/<id>.loop.md, where the id is the loop's id or the slug of its name, and push it to the branch RepoOps mirrors.
What you see. After the next mirror sync the card's source badge reads registry+contract and the verdict badge reads valid. The goal sits at the top of the card, the check and the stop rules fill the two columns, and the runs summary still reads the loop's own count and last status.
What it establishes. The loop has a written definition of done and of giving up, in the repository, joined to its run history by id. Nothing about the loop's behaviour changed; what changed is that a run can now be judged against a declaration.
A contract that is not yet a contract
Situation. Someone wrote a loop file with a goal and a check but no stop map, and the card carries the badge incomplete.
What you do. Read the line Incomplete contract, missing: on the card. Add the fields it names; for stop, one of success, maxIterations, budgetUsd or noProgress is enough. Push the file.
What you see. The missing line disappears and the badge reads valid. On the hosted table, once the desktop's next hourly publish lands, the Contract column moves from contract incomplete to contract valid.
What it establishes. The verdict changed because the file changed. RepoOps did not fill in a stop rule for you; an empty stop map stays incomplete until a person declares one.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. The hosted table needs a desktop on a release that publishes the contract verdict (a loop from an older publisher reads not reported there).
- Where it runs
- Local: Remediation, then Declared intent under All tools, in the Approvals and workflows group, one repository at a time. Hosted: Remediation, then Declared intent under All tools, in the Approvals and workflows group (/team/declare), shows every published loop for the repositories the member may see, with the counters Declared loops, Contract valid, No stop rule, Not reported, Ran here and Last run failed, and the columns Loop, Repo, Contract, Schedule, Iteration cap, Budget cap, Runs and Last outcome.
- Permissions
- The local tab needs no permission beyond the app itself. The hosted page is session-authed and the team comes from membership; the loop read is narrowed to the repositories the member is allowed. Propose to repo (PR) uses this machine's own git and GitHub credentials to open the pull request.
- Connections
- A tracked repository whose mirror is ready; the tab reads contracts from the mirrored branch. For the hosted table: a bound device and the Loop runs toggle on in Settings. The tab itself makes no network call and no model call.
- Plan
- The local tab has no plan gate. Publishing the loop set to the team store needs the Team tier: the hosted publish route checks the team's plan and refuses when it is below that rung, so a free team's hosted table stays at No loops declared yet.
Configure it
- Register the loop, if you have not.
The Register a loop form was on Loop Engineering, which the focused desktop app no longer opens; POST /api/loops?repo=<id> registers one. Only the name is required; the command, a schedule, an iteration cap and a budget are optional. Registering describes a loop you already run so its runs can be measured; RepoOps still runs nothing. A contract can also stand alone, with no registered loop, and reads contract-only.
- Author the contract.
Ask the loop-spec skill in Claude Code to interview you and write the file, or write it by hand from docs/specs/loop-contract-format.md. The Author a contract panel was on Loop Library, which the focused desktop app no longer opens.
- Name the file after the loop.
The join is by id: the filename stem must equal the registered loop's id, or the slug of its name. Nothing fuzzier matches; a mismatch reads uncontracted on one card and contract-only on another.
- Deliver it into the repository and push.
Copy file or Download, then commit to .claude/loops/ and push to the branch RepoOps mirrors. Or press Propose to repo (PR), which opens a pull request against the tracked repository through the routing proposer; only a valid contract is proposed, and the mirror is never written.
- Read the card.
The Declared intent tab paints its last cached view at once, then refreshes from GET /api/declare. Reload fetches again. The verdict badge, the source badge and the runs summary are the three things to read first.
- Only then, decide about the team store.
Settings, What this machine publishes to your team, the Loop runs toggle. With it on and the device bound, the loop set is published on the hourly cycle when it changed: ids, names, commands, schedules, caps and the contract verdict. Never the goal, the check or the stop text.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
goal | .claude/loops/<id>.loop.md front-matter | one sentence a reviewer can call met or not met | Required. The goal is what every run is judged against; a vague one cannot be met or missed. |
check.command, check.pass | the contract's check map | the literal command, and the exit code or output shape that means pass | Required, both. A check with only one of the two reads incomplete and names the missing half. |
stop.success, stop.maxIterations, stop.budgetUsd, stop.noProgress | the contract's stop map | a success condition plus one give-up bound | At least one is required; an empty stop map is incomplete. A missing cap reads null, never zero. |
escalation, handoff | the contract front-matter | who gets what when it stops without success; what a two-minute review needs | Required. The tab shows them under Escalation / handoff; the proof bundle reads handoff. |
secondCase | the contract front-matter | leave it out until the loop has closed a second case | Optional. Absence reads as no second-case evidence, never as a defect; never invent it. |
iteration cap, budget $ | the loop registry, POST /api/loops or PUT /api/loops/<id> | set both for a loop that runs unattended | These are the caps the hosted table shows as Iteration cap and Budget cap. Neither declared reads unbounded there and is counted under No stop rule. |
syncIntervalMs | repos.config.json | 30000 (the default) | How often the mirror fetches the tracked branch, and so how soon a pushed contract reaches the desktop tab. A branch that has not moved for a while is checked less often, up to every 5 minutes (REPOOPS_MIRROR_IDLE_MAX_MS); the repo sync chip checks at once. |
looppublish.enabled (Loop runs) | Settings, What this machine publishes to your team | off until the team needs the hosted table | Only 1 publishes. It also needs a bound device, and the hosted route needs the Team tier. |
REPOOPS_LOOP_CONTRACT_SYNC | the data directory's .env | unset (the default) | 1 also reads the contracts into reviewable proposed brain records, locally, bounded at 256 KiB per file and 8,000 characters per record. Not needed for the tab. |
REPOOPS_LOOP_LOCAL_EXECUTE | the data directory's .env | unset (the default) | Not needed here. This tab never runs a loop; an isolated run on this machine is a plan-only dry run until this is exactly 1. |
What you should see
The normal case
Configuration. A registered loop with reported runs, a valid contract at .claude/loops/<id>.loop.md pushed to the mirrored branch, the Loop runs toggle off.
Expect. One card with the badges valid and registry+contract, the goal on top, Check that proves done and Stop rules filled, and a runs summary such as 3 runs with the last status and time.
Verify. The count above the cards reads 1 declared loop. Health reads healthy, degrading, stale or unknown with fewer than three samples. Agent traces opens from the card inside the app shell. The hosted table is untouched.
Declared, never registered
Configuration. A valid contract in the repository and no registered loop with that id or name slug.
Expect. A card whose source badge reads contract-only, whose runs summary reads no runs yet, and which carries no health badge.
Verify. GET /api/declare returns the loop with source contract-only and runs total 0. Registering a loop with the same id or a name whose slug matches turns the card into registry+contract on the next load.
Registered, never declared
Configuration. A loop in the registry, no file under .claude/loops/ on the mirrored branch.
Expect. A card with the badges uncontracted and registry-only, the line No declared goal yet (this loop runs, but has no loop contract stating its intent), and not declared under both the check and the stop rules.
Verify. The health badge, when present, says basis heuristic rather than contract. The hosted table, if publishing is on, shows the loop as no contract with its caps, or unbounded when it has none.
Data and cost
- What is captured
- Nothing new. The contract is a file in your repository, read from the mirror. The registry is loops/<repoId>.jsonl under the app's own brain root, an append-only log of register, update, remove and run events; the folded view keeps the latest 20 run results per loop. The tab caches its last response in the browser under the key declare:<repo>.
- Who can see it
- Local by default, one repository at a time. With the Loop runs toggle on and the device bound, the team store holds one row per loop for the repositories a member may see: id, name, command, schedule, next run, disabled, credential names, iteration cap, budget cap, contract verdict. Never the goal, the check text, the stop text, the escalation or the handoff.
- How long it is kept
- The contract lives as long as the file does in git. The registry log has no retention knob; the folded view is bounded at 20 runs per loop and the log keeps everything. In the team store a publish fully replaces that device's loop set for the repository; the hosted run rows in cloud_loop_runs have no purge route, see Delete below.
- What leaves the machine
- None from the tab. With publishing on, an hourly POST of the loop set to the team store over the device token, skipped when the set has not changed by hash, with a 20-second timeout. Propose to repo (PR) uses your own git and GitHub credentials to push a branch and open the pull request.
- What it costs
- No model call anywhere on this path: the validator is a parser and the join is by id. Nothing meters.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| The tab shows No declared loops yet and a Do this next block. | No contract on the mirrored branch and no registered loop for this repository. | Write a contract and push it, or register the loop with POST /api/loops?repo=<id>. The card appears on the next load. |
| I wrote the file, and the card is still uncontracted. | The desktop tab reads the mirror, which tracks the remote branch and fetches every syncIntervalMs. An uncommitted or unpushed file is not there. | Commit, push to the tracked branch, click the repo sync chip (or wait up to 5 minutes for the next check), press Reload. The hourly publish reads the working copy instead, so the hosted verdict can lead the desktop badge until the push lands. |
| Two cards for one loop: one uncontracted, one contract-only. | The filename stem matches neither the registered loop's id nor the slug of its name. | Rename the file to the loop's id, or to the slug of its name, and push. |
| The card reads Incomplete contract, missing: stop. | The stop map is absent or empty; at least one of success, maxIterations, budgetUsd or noProgress is required. | Add one condition and push. The card names what is still missing on the next load. |
| The badge reads unparseable. | No front-matter fence, or a line the grammar cannot place: only top-level key: value scalars and one level of two-space nesting under check and stop are read. | Move anything richer into the markdown body below the closing fence; the validator never judges the body. |
| health: unknown on a loop that has run. | Fewer than three scored samples, or no schedule to judge staleness against. | Nothing to fix; unknown is the honest reading until more runs report. |
| The hosted Contract column reads not reported. | That loop was published by a desktop that sent no verdict at all, which is an older client, not a missing contract. | Update the desktop; the next changed publish carries the verdict. No contract means the publisher looked and found none. |
| The hosted table reads No loops declared yet while the desktop tab has cards. | The Loop runs toggle is off, the device is not bound, the team is below the Team tier, or nothing has changed since the last publish. | Turn the toggle on in Settings, bind the device, and wait for the hourly cycle. The route refuses a team below the rung. |
| Couldn't load declared intents: unknown repo. | The page was opened outside the dashboard shell without ?repo=<id>, so the server could not resolve a repository. | Open the tab from the sidebar, or add ?repo=<id> to the URL. |
- Disable
- Not provided as a switch. The tab reads whatever exists; remove the file from the mirrored branch or the loop from the registry, and the card goes. The Loop runs toggle in Settings stops the hosted publish.
- Roll back
- Not provided. A contract is a file in your repository; revert it in git and push. A contract proposed through Propose to repo (PR) is a pull request you can close.
- Revoke access
- Disconnect in the desktop app forgets the device binding and revokes the device token on the server, which ends the hourly publish. The pull-request path uses your own GitHub credential; revoke it at GitHub.
- Delete
- Not provided on the hosted side: no route deletes a team's loop rows or the run rows in cloud_loop_runs; a later publish replaces the loop set for that device and repository. Locally, removing a loop appends a remove event to loops/<repoId>.jsonl; the log itself is not rewritten.
Related tasks
Maintenance evidence
- Feature id
declare(spine leafdeclare)- Owner
- Cross-surface strategic alignment program (WS-7, owner UX Foundation) for the surface; the LCON program (RepoOps core) for the contract format; G128 and G119 for the hosted table. Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/declare.html, public/loop-library.html, public/loop-engineering.html, public/settings.html) and the hosted page source; no isolated instance was started for this guide.
- Example fixtures
- No fixture file. Contract texts are inline in lib/loop-spec.test.mjs (the three verdicts, every missing field named, one stop condition is enough, never throws) and lib/loops/contracts.test.mjs (the read, the join by id then name slug, the honest empty result); lib/loop-publisher.test.mjs covers the published verdict; website/lib/declared-intent.test.ts and website/app/team/(home)/declare/page.test.tsx cover the hosted table. No test exercises lib/routes/declare.mjs itself.
- Source references
lib/loop-spec.mjs,lib/loops/contracts.mjs,lib/routes/declare.mjs,lib/loops/registry.mjs,lib/loops/health.mjs,lib/mirror.mjs,lib/loop-publisher.mjs,lib/routes/loop-contracts.mjs,lib/zero-states.mjs,public/declare.html,public/loop-library.html,public/loop-engineering.html,public/settings.html,website/app/team/(home)/declare/page.tsx,website/lib/declared-intent.ts,website/lib/cloud-loops.ts,docs/specs/loop-contract-format.md- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Narrated story rendered and published 2026-09-26 (render 9ba07aed7919, LDG-1014) with the breadcrumb Remediation, which lists the feature under Moved here, checked against main at 7aab4cd82 with LDG-1014 part 1. Six frames, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. Narration is the provisional Windows voice until LDG-0721.
Last updated