Advance the LoopKit pipeline by exactly one stage — a lookup over state/triage.md, never a judgement. Use for "work the backlog", "advance the pipeline", any /loop tick.
Install
npx skillscat add luxtelos/loopkit/loop-tick Install via the SkillsCat registry.
loop-tick
Invoked with no arguments, this is not a document to read — it is a job to do.
Start at step 1 now and stop after step 4. Everything below the tick is
background for when something goes wrong.
Scripts live in the plugin. Every path below is ${CLAUDE_PLUGIN_ROOT}/scripts/…;
if that variable is not expanded in your shell, use the absolute scripts path the
session-start line printed.
Do this now
Run the commands. Do not ask which stage to work on, do not summarise this file
back, and do not do more than one stage.
The tick
1. Ask what stage is due. Do not decide this yourself — it is a lookup overstate/triage.md, and a model choosing freely once spent ~40 consecutive ticks
re-checking a single PR while 40 findings sat unclassified.
bash "${CLAUDE_PLUGIN_ROOT}/scripts/loop-next.sh"It prints the backlog counts, an optional POLL: line, a STAGE:, a TARGET:
and an ACTION:.
2. If there is a POLL: line, run the cheap check first.
bash "${CLAUDE_PLUGIN_ROOT}/scripts/loop-watch.sh" --pr <number>A few hundred bytes on a quiet tick against ~9,000 for a full read. VERDICT: ACT or ESCALATE preempts and becomes the tick. QUIET means ignore it and do
the work stage — a PR in flight must never starve the backlog.
3. Advance exactly that one stage — for every row at it.
loop-next.sh prints one TARGET: (the first row) and then a TARGETS: line
per row at that stage (capped at 8). A tick advances the STAGE, not one row of
it: dispatch one implementer (or reviewer, or assessor) per TARGETS: row, each
in its own worktree and branch, in parallel — that is what "one worktree per
finding" is for. The one-stage rule exists so transitions stay attributable;
reading it as one ROW per tick leaves a backlog of independent stories moving one
an hour. Rows that share a file or a table are the exception — say so in the row
and run those serially.
| Stage | What to run | Then set the row to |
|---|---|---|
new |
loopkit:loop-assess Mode A — baseline, classify, route |
spec-draft if code; otherwise inbox |
spec-draft |
loopkit:spec-writer — EARS criteria, carrying the control case |
spec-ready |
spec-ready |
implementer agent, in its own worktree and branch | fixing |
fixing |
reviewer agent + the stop gate | pr-open ONLY on a recorded PASS; stay fixing on FAIL, citing the criterion |
pr-open |
handled by the poll in step 2 | done on merge |
blocked |
nothing — it waits on a human ruling in inbox/needs-human.md |
whatever the ruling says |
discover |
loopkit:morning-triage |
rows appear as new |
On every transition into pr-open, if the project has a labelling rule, apply
it from a lookup (a script over the diff and the Part of #NNNN line), never
by hand. A scope label is how the owner finds the PR at all.
4. Record the transition and stop.
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/triage_state.py" update --state state/triage.md --source "<row source>" --status <new-status>update keys on --source, not --finding. Upsert keys on source too, so two
findings with the same source overwrite each other — give each row a distinct
source (state/<doc>.md §S14), never the bare doc path.
Then commit the tick THROUGH THE LOCK, never with a bare git commit:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/loop-commit.sh" \
-m "loop(tick): <row> -> <status>" -- state/triage.md state/<your notes>.mdParallel agents share one git index per worktree, so an unlockedgit add … && git commit publishes whatever anyone else has staged — that is
how a review verdict ended up inside commit 6989d63 on 2026-09-07. The
wrapper holds .loopkit/driver.lock across the add and the commit;triage_state.py takes the same lock for its own write. A bare git commit is
refused by the plugin's require_commit_lock.py.
# lane-scoped tick (lanes come from <project>/.loopkit/scopes.json):
bash "${CLAUDE_PLUGIN_ROOT}/scripts/loop-next.sh" --scope <lane>The ORDERING is the discipline; the volume never was. An earlier version of
this rule read "one stage per tick… stops a tick from turning into an unbounded
work session", and that last clause was a throughput cap doing damage. It was
written to cure a starvation problem — an agent burning ~40 ticks re-checking
one PR — but the cure for starvation is the ordering rule in step 2, not a limit
on how much work a turn may contain. An instruction that is routinely ignored is
worse than no instruction: it still costs context on every prompt and quietly
teaches that the file is optional.
So: pick the stage by lookup, advance it for every row at it, record each
transition keyed on --source, and stop when the stage is done — not when some
quota is met. What must stay bounded is the stage, so each transition is
attributable and the loop is resumable after a crash or a compaction.
Finishing means the owner only ever sees what is ready
A pull request the loop has not had reviewed is not finished work, and handing
it over is not a status report — it is passing the unfinished thing upward.
On 2026-09-07 a tick told the owner "READY TO MERGE: none — all four green but
unreviewed" and, in the same message, handed them all four. Both cannot be true.
Worse, the blocker loop-scan.py printed was needs a reviewer, and the loop
has reviewer agents. It reported the absence of a step it could have taken.
So:
A row reaches
pr-openonly when a reviewer has returned a verdict and that
verdict is PASS. "I opened a pull request" is not the transition; "a different
agent judged it and it held" is.This one is enforced, not merely asked for. The reviewer records its
verdict andtriage_state.pyrefuses the transition without one:# the reviewer, after judging: python3 "${CLAUDE_PLUGIN_ROOT}/scripts/triage_state.py" verdict \ --state state/triage.md --source "<row source>" \ --result PASS --by "<reviewing agent>" --evidence "<what proved it>"The verdict is one line in
state/ticks.jsonl; the latest verdict for a
source wins, so a PASS followed by a FAIL closes the door again. SetLOOPKIT_AGENTon each agent's runs and the recorder also refuses a PASS
from the agent that did the work; leave it unset and it cannot tell them
apart and does not pretend to. When a human decides otherwise, the door is--override-verdict "<reason>", which writes the reason to the ledger and
says so on stderr — use it, rather than routing around the gate in the dark.needs a revieweris never a thing to report. It is a thing to do. When the
scan prints it, dispatch the reviewer — that IS the tick.Report to the owner only what they can act on: what is READY TO MERGE, what
needs a RULING, and what needs a credential or an action only they hold.
Everything else is the loop's own work in progress and belongs in the queue,
not in their inbox.A state-only pull request is not exempt. A queue that misdescribes reality is
worse than no queue, because the loop reads it as memory and acts on it.
The test for any hand-off: could the owner act on this line right now, without
asking a question? If not, it was not ready to hand over.
Why new is where the value is
Forty unclassified rows is not a backlog of code — it is forty findings nobody
has established the truth of. loop-assess exists because a planner goes from
"extract the problem" straight to "outline the fix", so anything that enters
becomes code whether or not code was the answer. Classifying a new row is
usually the highest-value thing a tick can do, and it is cheap: most rows
resolve to inbox or a non-code layer without any implementation at all.
Cadence and stopping
Never sleep on work. At the end of a tick ask one question: what am I
waiting ON? If the answer is an external process — CI, a human ruling, a PR
review, a deploy, a background agent — arm a wakeup or a Monitor sized to that
process. If the answer is "nothing, the next stage is mine", run the next tick
NOW, in the same turn. A pipeline that naps for 30 minutes between a spec and
its implementer is not pacing itself, it is idling (owner correction,
2026-09-06: "don't sleep unless waiting on some process").
- A wakeup's delay is the process's time, not a clock: a CI run gets its
duration, a human gets 20–30 minutes, an agent gets a long fallback because
its completion is the real signal. - Stop the loop, rather than sleep it, when every row in the lane is
blocked,inbox, orpr-openunder a quiet watch — then say so. noop: falseonly when the tick advanced a row or produced an artifact.
Unrelated work in the same turn does not make the tick non-noop.loop-watch.shescalates after 24 unchanged ticks. A watch that quiet has
stopped producing information — chase the human or stop the loop.
The question "what am I waiting on?" is answered for you: loop-next.sh ends
every tick with NEXT: CONTINUE (rows still actionable — next tick now, no
wakeup), NEXT: WAIT (only a PR review, CI or a human ruling can move things
— the one case for a wakeup) or NEXT: IDLE (run morning-triage now). Read
the line; do not re-derive it.
Invoking it
From the skill picker: choose loopkit:loop-tick. No arguments, nothing to
type. That is the intended path, and it is why the top of this file is
imperative.
Everything reaches a tick on its own:
- The hooks fire on tool matchers, so merge, approve and stage-everything stay
blocked whatever the prompt said. CLAUDE.mdauto-loads, so the standing rules are already in context.- The plugin's
loop_doctrine.py(UserPromptSubmit) injects the discipline the
moment a prompt looks like a tick — including a bare/loop.
So a repeating loop needs no procedure pasted into it either. This is enough:
/loop work the loopkit backlogIf you are ever writing out the steps by hand to make a tick behave, that is a
bug in this file or in the doctrine hook, not something to work around.
If the tick is a status question
"What is in flight", "what needs review", "what can be merged" is not a stage to
advance. Use loopkit:loop-scan, or directly:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/loop-scan.py"READY TO MERGE is the only line to act on without opening anything.
Gotchas
triage_state.py updatekeys on--source, not--finding. Two rows with
the same source overwrite each other; give every row a distinct source.- Never pipe
loop-next.sh(or any gate) throughgrep -q,tailorhead
underpipefail: the reader exits early, the script takes SIGPIPE, and the
pipeline reports failure — or, piped throughtail, reportstail's
success. Capture to a variable, then read it. TARGETS:is capped at 8 rows per stage; theBACKLOG:counts are not. A
stage with 20 rows takes three ticks, not one.- A
blockedrow is never served and never polled. If a row is waiting on a
human, set it toblocked, notpr-open, or every tick spends a poll on it. - A scoped tick that prints
STAGE: discovermay still have work outside the
lane — read theunscoped new=Nnote before calling the loop idle. - A
NEXT: CONTINUEfollowed by a ScheduleWakeup is the bug this rule exists
for. The wakeup is for waiting, and CONTINUE means nobody is waiting.