Skip to content

shashankanil/agent-loops

Repository files navigation

loopctl

A lightweight, text-first orchestrator for agentic loops: small, human-operated workflows that wire coding harnesses (Claude Code today; Codex, Gemini, OpenCode, Aider planned) to your work platforms (Jira, Linear, Slack, GitHub, mail, calendar) via MCP.

Not n8n. No canvas, no nodes. Loops are plain files you edit, run, grep, and commit — the feel of a human operating an orchestration from the terminal.

loop run feature-intake "users should be able to export their data as CSV"

…and the loop researches your repos and Jira backlog, reports what exists / what's missing / what's possible, asks you clarifying questions in your terminal, and files a fully-contexted ticket. Then next: implement-ticket chains straight into the execution loop: one harness plans, another writes the code, a third reviews it — with approval gates between phases.

Harnesses

claude-code is first-class (Agent SDK: streaming, per-run MCP servers, cost tracking). codex, gemini, opencode, and aider work out of the box as headless CLIs, and any other tool is one config entry away:

harnesses:
  claude-code: { model: claude-opus-4-8 }
  codex: {}                                  # uses your codex login + config
  mytool: { command: mytool, args: [run, "{prompt}"] }   # generic adapter

Pick the harness per loop (harness: in frontmatter) or per step in YAML pipelines — so implement-ticket can plan with Claude, code with codex, and review with gemini.

Install

Requires Bun >= 1.1.

bun install
bun run setup        # links `loop` onto your PATH (or: loop setup)
loop --help

Other ways to get loop:

Method Command Notes
Dev (linked) loop run ... bun run setup once after clone
Compiled binary bun run setup:binary Standalone dist/loop~/.local/bin/loop, no Bun at runtime
Global install bun install -g loopctl When published to npm
Ephemeral bunx loopctl run ... Bun's npx equivalent

If loop isn't found after setup, add to ~/.zshrc:

export PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH"

Run loop doctor anytime to verify Bun, PATH, config, and optional tools (gh, docker).

Quick start

loop init                 # scaffold loopctl.config.yaml, loops/, skills/
loop new my-first-loop    # scaffold a loop file
loop run my-first-loop "some input"
loop ls                   # list loops
loop ls --runs            # list past runs
loop logs last            # event log + artifacts of the latest run

How it works

  • Configloopctl.config.yaml is discovered by walking up from your cwd (or ~/.loopctl/config.yaml).
  • Featuresloopctl.features.yaml gates platform capabilities and integrations per environment (loop features).
  • Runs — every execution is a directory under ~/.loopctl/runs/<id>/ (logs, artifacts, status).
  • Shell loops.loop.sh scripts get a temporary loop shim on PATH for loop ask / loop agent primitives.

Full guide: guides/creating-loops-and-integrations.md. Add any MCP integration: guides/adding-custom-integrations.md or loop run add-integration "describe your integration".

Command cheat sheet

Command Purpose
loop run <name> [input] Run a loop (and its next: chain)
loop serve Webhook + schedule daemon
loop ui Local ops dashboard
loop trigger <source> Simulate a webhook locally
loop logs [run] Event log + artifacts
loop validate Static checks before spending tokens
loop setup / loop doctor Install PATH link / verify toolchain
loop features Platform feature flags + integration gates

loops/ has 14 ready-to-edit examples — PR review on webhook, bug triage, standup digests, inbox triage with memory, dependency updates in worktrees, multi-model consensus, flaky-test hunting, Ralph-style backlog draining, and more.

Loops are files

A loop is a markdown file: frontmatter declares the wiring, the body is plain-language steps a driver agent executes.

---
name: feature-intake
integrations: [jira, github]   # MCP servers from loopctl.config.yaml
skills: [jira-conventions]     # markdown instruction packs from skills/
harness: claude-code
ask:
  enabled: true                # set false to run hands-off
  channel: terminal            # slack coming in M4
outputs:
  ticket: The created issue key/URL.
next: implement-ticket
---

1. Research the repos and Jira backlog for the requested use case.
2. Report what exists, what's missing, what's possible.
3. Ask the user clarifying questions.
4. Create a Jira ticket with the full context and emit it as `ticket`.

The driver agent gets two loop primitives as tools: ask_user (blocks on your answer — terminal prompt today; auto-proceeds with defaults when ask.enabled: false or --no-ask) and emit_artifact (saves named outputs for chaining). Everything else it does with its own tools plus the MCP integrations the loop declares.

YAML pipelines (.loop.yaml)

When you want determinism instead of agent interpretation, write the steps explicitly. Outputs of earlier steps feed later ones via {{steps.<id>}}:

name: file-ticket
integrations: [jira]
steps:
  - id: research
    agent: Search the repos and backlog for {{input}}; summarize what exists.
  - id: clarify
    ask: "Priority?"
    choices: [low, medium, high]
    default: medium
    # enabled: false      # per-step toggle — skip the question, use the default
  - gate: "File the ticket?"
  - id: create
    act: jira.create_issue      # direct MCP tool call — no LLM in the path
    args: { summary: "{{input}}", description: "{{steps.research}}" }
  - emit: ticket
    content: "{{steps.create}}"

agent runs a harness, ask/gate go through the human channel (auto-proceed when disabled), act calls an MCP tool deterministically, emit saves an artifact.

Shell loops (.loop.sh)

Maximum unix: the loop is a bash script and the CLI is the API. loopctl injects LOOP_INPUT, LOOP_RUN_ID, and a loop shim on PATH; prompts render on stderr so command substitution stays clean:

#!/usr/bin/env bash
# ---
# name: file-ticket
# integrations: [jira]
# outputs: { ticket: the issue key }
# ---
set -euo pipefail

research=$(loop agent "Search the repos and backlog for $LOOP_INPUT" --integrations jira)
priority=$(loop ask "Priority?" --choices low,medium,high --default medium)
loop gate "File the ticket?"
ticket=$(loop act jira.create_issue summary="$LOOP_INPUT" description="$research")
loop emit ticket "$ticket"

loop tools <integration> lists what an MCP server exposes so you know what to act on.

Integrations

MCP-first: register any MCP server once in loopctl.config.yaml, and loops opt in by name. Secrets stay in env vars ($JIRA_TOKEN), so loops are safely shareable.

mcpServers:
  jira:
    type: http
    url: https://mcp.atlassian.com/v1/mcp
    headers: { Authorization: "Bearer $ATLASSIAN_MCP_TOKEN" }
  github:
    command: docker
    args: [run, -i, --rm, -e, GITHUB_PERSONAL_ACCESS_TOKEN, ghcr.io/github/github-mcp-server]
    env: { GITHUB_PERSONAL_ACCESS_TOKEN: $GITHUB_TOKEN }

Triggers

Loops don't have to be started by a human. Declare what fires them — a commit, a PR, a Jira/Linear state change, a Slack message, a schedule — and run the daemon:

trigger:
  - webhook: github
    match: { "workflow_run.conclusion": failure, "workflow_run.head_branch": main }
  - webhook: jira
    match: { "issue.fields.status.name": Ready }
  - every: 30m
loop serve --port 8484 --token $LOOPCTL_TOKEN
# point your GitHub/Jira/Linear/Slack webhooks at:
#   POST http://your-host:8484/hooks/github   (any source name works: /hooks/<source>)

match: is dotted-path equality on the event payload — enough to express "this Jira ticket moved to Ready" or "this PR opened against main" without a rules engine. The matched event payload becomes the loop's input. Test trigger wiring locally without a server:

echo '{"issue": {"status": "Ready", "key": "ENG-9"}}' | loop trigger jira

Daemon-triggered runs are hands-off (asks auto-proceed with their defaults) until the Slack ask channel lands.

Goals — loops that actually loop

A loop with a goal: doesn't run once; it iterates. After each pass an evaluator agent judges the result against the goal, sends concrete feedback into the next iteration, and — because plans change once you see reality — can revise the goal itself (e.g. "fix the test" becomes "quarantine the flaky test and file a ticket"):

goal: The test suite on main passes locally and the fix is pushed.
maxIterations: 4
evaluator: gemini   # optional: judge with a different model than the worker

Iteration context flows into every format: markdown drivers get the goal and feedback in their prompt, YAML steps get {{goal}} / {{iteration}} / {{feedback}}, shell loops get LOOP_GOAL / LOOP_ITERATION / LOOP_FEEDBACK. Events (iteration:start, iteration:verdict, goal:updated) land in the run log like everything else.

See loops/fix-failing-ci.loop.md for the two combined: CI fails on main → webhook fires → the loop iterates until the suite is green.

Isolation, review, merge

Set isolation: worktree and a run never touches your live working tree: it gets its own git worktree on branch loop/<run-id> (the consensus pattern across coding-agent orchestrators — and the thing that makes parallel and webhook-triggered runs safe). Then:

loop review last        # full diff of what the run changed (--stat for the short version)
loop merge last         # squash-merge into your current branch, clean up worktree + branch
loop discard last       # throw it all away

Durability

steps:
  - id: research
    agent: ...
    retry: { max: 2, backoffSeconds: 10 }   # per-step retries with backoff
  - parallel:                               # branches run concurrently; asks stay serialized
      - id: code
        agent: ...
      - id: docs
        agent: ...

budget: 5.00          # USD cap — run fails with budget:exceeded when crossed
onFailure: notify     # n8n-style error workflow: runs with the failed run's full context

Failed runs are checkpointed: loop resume last continues a YAML pipeline from the failed step (completed step outputs replay from disk), and re-prompts markdown/shell loops with the previous attempt's context. Budgets are enforced natively mid-run for claude-code (Agent SDK maxBudgetUsd) and between steps for everything else; give CLI harnesses an estimated costPerRun in config to include them.

The daemon queues triggered runs FIFO with daemon.maxConcurrent (default 2), dedupes repeat events for an already-queued loop, and daemon.debounceSeconds collapses webhook bursts. Schedules can be intervals (every: 30m) or real cron (cron: "0 9 * * 1-5").

Durable human-in-the-loop

Daemon-triggered runs don't skip their questions anymore — they suspend. A loop whose ask channel is the terminal parks as status: waiting with the question persisted to disk (it survives daemon restarts), until someone answers:

loop ls --runs            # ...20260610-bug-triage-x1  waiting  ? Severity for this one?
loop logs last --follow   # live-tail any running or waiting run
loop answer last "high"   # the run wakes up and continues

Slack-channel loops still get asked live in a thread. Crashed daemons recover on restart: the run queue is persisted, and stale running runs get marked interrupted with a loop resume hint.

Composition: loops calling loops

steps:
  - id: tickets
    act: jira.search          # get the list…
    args: { jql: "sprint = active AND status = 'To Do'" }
  - id: triage
    foreach: "{{steps.tickets}}"   # …fan out over it (JSON array or lines)
    as: ticket
    concurrency: 3
    do:
      loop: implement-ticket       # each item runs a whole sub-loop
      input: "{{ticket}}"
  - emit: report
    content: "{{steps.triage}}"    # joined; {{steps.triage.0}} addresses one item

loop: runs another loop inline as a step (own run dir, cost charged to the parent's budget, depth-guarded); foreach: fans any step out over a list with bounded concurrency.

Safety for unattended runs

  • Webhook signatures — set webhookSecrets: { github: $GH_WEBHOOK_SECRET } and /hooks/github verifies GitHub's HMAC (Slack signing and Linear signatures too; X-Loopctl-Signature for everything else). Tampered or unsigned payloads are rejected.
  • sandbox: strict | workspace | off — mapped to each harness's native mechanism (claude-code permission modes, codex -s, gemini --sandbox).
  • repos: in config — a registry of repositories agents may research, injected into agent context (and additionalDirectories for claude-code, LOOP_REPOS for shell loops). Multi-repo research loops finally know where the code lives.
  • loop validate / loop run <name> --dry-run — catch a typo'd integration, broken {{steps.x}} reference, or missing skill before any agent spends a cent.

Asking humans on Slack

Point ask.channel: slack at a Slack app (Socket Mode — no public URL needed) and human-in-the-loop steps post to a channel and block on a threaded reply, with timeouts falling back to defaults. Daemon-triggered loops get real approvals instead of auto-proceeding:

# loopctl.config.yaml
slack:
  botToken: $SLACK_BOT_TOKEN     # xoxb-, chat:write
  appToken: $SLACK_APP_TOKEN     # xapp-, connections:write
  channel: C0123456789

Memory, observability, sharing

  • memory: true — the loop keeps notes across runs (~/.loopctl/memory/<loop>.md). Markdown drivers get a remember tool plus past notes in context; YAML steps see {{memory}}; shell loops get LOOP_MEMORY_FILE.
  • loop stats — cost/success rollups per loop and per day; --json for scripts.
  • loop ui — a tiny read-only dashboard (run list, live events, artifacts) over the runs dir. No build step, CLI stays primary.
  • loop mcp — loopctl as an MCP server: any agent (Claude Code, Claude Desktop…) can list_loops, run_loop, trigger_event, get_run, get_stats. Loops driving loops.
  • loop install <git-url> — loops and skills are files; share them as repos. Copies loops/*.loop.* and skills/*.md in, skipping existing files unless --force.

loops/ralph-backlog.loop.sh shows the Ralph-loop pattern done properly: goal iteration with an evaluator instead of while true, in an isolated worktree, with memory across sessions.

State is shell-greppable

Every run is a directory under ~/.loopctl/runs/<id>/: meta.json, an append-only events.jsonl, a plain-text status, and artifacts/ with one file per emitted output.

loop logs last --json | jq 'select(.type == "ask")'
cat ~/.loopctl/runs/$(cat ~/.loopctl/runs/last)/artifacts/ticket

Roadmap

  • M1 ✅ — markdown loops, Claude Code harness, terminal ask channel, MCP registry, file-based runs.
  • M2 ✅ — YAML + shell loop formats, deterministic act steps (direct MCP tool calls, no LLM in the path), gate, loop ask/gate/emit/agent/act/tools CLI primitives.
  • M3 ✅ — codex/gemini/opencode/aider + generic harness adapters; loop chaining (next:); the plan → code → review execution loop (loops/implement-ticket.loop.yaml).
  • M4 ✅loop serve trigger daemon (webhooks + intervals + cron + loop trigger simulation), goal-driven iterative loops with evaluator feedback and revisable goals, Slack ask channel, loop mcp server mode.
  • M4.5 ✅ — worktree isolation + loop review/merge/discard, parallel steps, per-step retries, loop resume, daemon run queue with concurrency control and debounce, cost budgets, onFailure error loops.
  • M5 ✅ — per-loop memory, loop stats, loop ui, loop install <git-url>, mail/calendar/transcript example loops.
  • M6 ✅ — durable waits (waiting runs + loop answer), daemon crash recovery + persistent queue, webhook signature verification, loop:/foreach: composition, repos registry, loop validate + --dry-run, loop logs --follow, terminal ask timeouts, sandbox: passthrough.
  • Open — docs site, community templates, OTel/Langfuse export, container-level isolation.

Development

See guides/dev-quickstart.md for a 2-minute smoke-test workflow and guides/creating-loops-and-integrations.md for loops, MCP integrations, and feature flags.

bun install && bun run setup
loop validate
loop run smoke "hello"
bun test
bun run typecheck

MIT.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages