Skip to content

Repository files navigation

loopkit

Describe a change in plain English. loopkit plans it, builds it in an isolated git worktree, proves it with your own test suite, and merges it — and only interrupts you for the calls a human should actually make.

Every step is an immutable event in one append-only ledger. Everything you look at — the board, an item's timeline, the needs-you list — is just a projection of that log. It's what you get when you treat software delivery itself as an event-sourced system.

Who this is for

A solo operator on macOS, running the Claude CLI, who wants an agent's output gated by their own deterministic test suite before anything reaches main — not a team platform, not a hosted service, not a review-everything-yourself workflow either. If you already trust npm test (or your project's equivalent) to catch a real regression, loopkit is the harness that makes an agent prove it against that gate before it merges, and routes only the changes that actually need your judgment back to you. See "Honest scope" below for exactly what's proven today.

How this was built

loopkit is also a deliberate authorship experiment: one human operator directed the work; AI agents wrote the implementation.

The human owned the problem, product direction, operating constraints, risk boundaries, acceptance criteria, verification bar, and the decision to release and operate the result. The agents proposed much of the lower-level technical design and authored the code. This project does not claim hand-authorship of that implementation.

That boundary is part of the point. When producing code becomes cheap, the scarce engineering work moves toward making intent precise, keeping context trustworthy, deciding what evidence is enough, integrating safely, and remaining accountable for the outcome. loopkit is both the artifact and the experiment: AI can hold authorship of the implementation without holding authority or accountability.

$ loopctl new "Add a deleteNote(id) function to src/notes.js with tests"
  → captured WI-001

$ loopctl beat reactor      # routes + queues it
$ loopctl beat dispatch     # builds in a worktree, runs your gate, merges on green
  → WI-001  captured → queued → building → gated ✓ → merged into main

$ cd ~/my-notes-app && git log --oneline -2
  c4d2e81  feat(dispatch): WI-001 (target notes) ← exact merge candidate, gate-proven
  a1b9f4e  feat: add deleteNote(id) with tests   ← worker's scoped commit

The routing decision is still recorded as item.routed metadata. For this build path the folded work state presented to dispatch remains queued, so routed is not a separate lifecycle stop in the quickstart.

What just happened

  • You said one sentence. No ticket, no branch, no PR ceremony.
  • It built in isolation. A dedicated git worktree with an explicit file scope — parallel items can't trample each other, and a failed build leaves your tree clean.
  • Your test suite was the judge, not the model's self-report. Nothing merges unless the gate is green.
  • You weren't interrupted — because this change didn't need you. A change to money, auth, or a migration would have stopped and waited. That boundary is the whole point.

Why this exists

Autonomous coding agents fail in boring ways long before they fail in interesting ones. What broke for me was never the model's code — it was the coordination layer around it:

  • Mutable coordination state (queues in markdown, status files, chat threads) silently lost or double-applied work the moment two things ran at once.
  • Silent state loss — a crashed worker, an oversized payload, or a killed process could leave the system confidently wrong about what had happened.
  • No trust boundary — either a human reviewed everything (and became the bottleneck) or nothing (and shipped regressions unseen).

loopkit is the invariant I adopted after those scars: one append-only ledger, one fold, deterministic beats — and an explicit, configurable boundary between what the plane may ship on its own and what needs your eyes.

How it works

   intent (plain English)
      │
      ▼
 ┌──────────┐   append    ┌────────────────────┐   fold    ┌─────────────┐
 │ reactor   │──────────▶ │  work ledger        │─────────▶│ projections  │
 │ beat      │  events    │  (append-only,      │  (one     │ board · item │
 └──────────┘             │   monthly segments) │   fold)   │ timeline ·   │
      ▲                   └────────────────────┘           │ needs-you ·  │
      │ gate/merge/tier            ▲                        │ health       │
 ┌──────────┐   append             │                        └─────────────┘
 │ dispatch  │─────────────────────┘
 │ beat      │  builds each item in an ISOLATED GIT WORKTREE,
 └──────────┘  runs the target's deterministic gate, merges on green
  • The ledger is the only truth. Beats and the CLI append events; nothing mutates in place. A crashed process changes nothing retroactively — recovery is just re-reading the log.

  • The item lifecycle is executable. One typed state machine defines the allowed, event-caused transitions used by the fold: capture, route, queue, build, gate, park, approve, merge, accept, reject, reopen. It is runtime policy, not a diagram that can drift away from behavior.

  • Dependencies are first-class scheduling facts. A queued item may depend on one or more other work items. Dispatch admits it only after every active blocker is merged or accepted; missing references, malformed conditions, and cycles fail closed. Waiting changes no lifecycle state—the item stays queued.

  • Worktree isolation. Every build runs in its own git worktree with its own file scope (Touches). Parallel work items are disjoint by construction.

  • Deterministic gates. An item merges only when the target's own gate command (its test suite) passes in the worktree. The gate is the arbiter, not the model.

  • Tiered acceptance — the human boundary. Every merged item is classified by what it actually changed — the real git diff at merge time, not the item's own declared metadata, so a change that touched real code can never slip through as "nothing changed":

    tier what it is what happens
    auto framework-internal, gate-proven auto-accepts after a short window
    optional non-surface code auto-accepts after a longer window
    review a declared product surface surfaces for your test
    must money · auth · migrations, or a failed quality judge waits for you, forever

    Acceptance routes attention. Merging and deploying are separate steps — v0.1 proves capture → build → gate → merge with deploy off by default. Merge ≠ ship.

  • Two orthogonal trust axes, not one list. Merge-trust (what may auto-merge) and test-visibility (what you want to eyeball) are declared separately — a path can be trusted to merge and still surface for your test. Getting this collapsed into one list is how changes ship unseen; here the boundary is explicit config, not convention.

  • Self-heal basics. Doctor handles orphaned and stalled builds; ledger appends reclaim dead-owner locks and bound oversized events. The plane degrades and reports instead of wedging.

  • Sensitivity-aware model routing. Every item carries a data-sensitivity tier (public/internal/private); the provider registry gates which model may serve which tier (private → local model), fail-closed. A fresh plane uses the exercised Claude CLI worker for public/internal work; ordered provider chains can be configured by sensitivity. Stages have separate model settings, but independent provider-per-stage assignment is not built yet. Full boundary semantics: docs/trust-boundaries.md.

  • Verified knowledge promotion. The worker's REPO PLAYBOOK baseline is a rebuildable projection, not a hand-typed file. A harvested lesson is gate-proven, audited, and operator-ratified: a strict, default-reject auditor harvests candidates only from merges that already passed the gate, and the operator ratifies each one through the same approve/reject verbs used everywhere else on the plane before it ever reaches a prompt. An imported lesson — the one-time loopctl knowledge import migration of a pre-existing hand-curated playbook — is explicitly operator-ratified only, by the act of running the command, never gate-derived or audited. Either way, a deterministic check on every materialize beat expires a lesson once its cited file or command no longer exists. With the flag off (knowledgePromotion.enabled, default false) the playbook file is left exactly as a human last committed it — untouched, not projected. See ADR-015.

Get pinged when it needs you

The point of tiered acceptance is that loopkit only interrupts you for the calls that need a human — a parked decision or a halted build. When that happens it runs one configurable command, your notify hook, with the message as its only argument (exit 0 = delivered):

// loopkit.config.json
{ "notifyHook": ".ai/notify-phone.sh" }   // a script in your plane repo; called as: hook "<message>"

There's no bundled Telegram or email client, on purpose — the hook is bring-your-own-channel, so no third-party SDK or secret lives in the framework. Point it at whatever you already use:

# .ai/notify-phone.sh — Telegram bot; $1 is the message
#!/usr/bin/env bash
curl -fsS "https://api.telegram.org/bot$TG_BOT_TOKEN/sendMessage" \
  --data-urlencode "chat_id=$TG_CHAT_ID" \
  --data-urlencode "text=🔔 loopkit: $1" >/dev/null
# …or email, same contract
#!/usr/bin/env bash
printf 'Subject: loopkit needs you\n\n%s\n' "$1" | sendmail you@example.com

Set up the Telegram side once — message @BotFather/newbot for a token, then read your chat_id from .../getUpdates. Keep the token in your shell env, never in the repo.

You watch the same blockers in the console's needs-you lane — the hook is just the push to your phone:

the loopkit console — the operating picture, with the lane that needs you

Try it

Clone this repository into a directory named loopkit. The exact sequence below then drives a real worker end-to-end — intent → worktree build in the target repo → its own test gate → merge into its main:

Prerequisites: macOS, Node.js 22+, git, and one installed and authenticated Claude CLI (claude auth status). That is enough for the exercised path; Codex, Ollama, and multi-provider chains are optional experimental configuration. If the only eligible provider is unavailable, routing/build work waits, parks, or fails closed — the plane does not silently widen the sensitivity allowlist.

# 0. Build the engine
cd loopkit && npm install && (cd packages/core && npm run build)
LOOPKIT_REPO_ROOT="$(pwd)"
LOOPCTL="node $LOOPKIT_REPO_ROOT/packages/core/dist/cli.js"

# 1. Materialize the demo target (a tiny notes app with its own tests + loopkit.target.json)
bash examples/setup-demo.sh ~/loopkit-demo/notes

# 2. Create a plane (its own state lives here, separate from the target)
mkdir -p ~/loopkit-demo/plane && cd ~/loopkit-demo/plane && git init -b main
mkdir -p .ai/loops/prompts
cp "$LOOPKIT_REPO_ROOT"/packages/core/prompts/*.md .ai/loops/prompts/
echo 'LOOPKIT_AUTONOMY=off' > .ai/loops/config.env  # fail-safe while you inspect it
source "$LOOPKIT_REPO_ROOT/scripts/load-plane-env.sh"

# 3. Connect the target (prints its manifest — gate command, branch — for your review)
$LOOPCTL target add ~/loopkit-demo/notes

# 4. Check the read-only health views, then arm deliberately
claude auth status
$LOOPCTL state
$LOOPCTL slo
# If you run the optional console, inspect its read-only /observability page too.
echo 'LOOPKIT_AUTONOMY=on' > .ai/loops/config.env
source "$LOOPKIT_REPO_ROOT/scripts/load-plane-env.sh"

# 5. Drop intent, then run the two beats (normally these run on a scheduler)
$LOOPCTL new "Add a deleteNote(id) function to src/notes.js with tests"
$LOOPCTL beat reactor     # routes + queues the item
$LOOPCTL beat dispatch    # builds in a worktree OF THE TARGET, gates, merges into ITS main

# 6. See what happened
$LOOPCTL state && $LOOPCTL events --item WI-001
$LOOPCTL flow --item WI-001       # generated lifecycle + dependency-readiness view
cd ~/loopkit-demo/notes && git log --oneline -2   # gated merge + worker commit are in YOUR history

For two existing work items, make the second wait for the first without parking or rewriting it:

$LOOPCTL dependency add WI-002 WI-001      # dependent first, blocker second
$LOOPCTL flow --item WI-002
$LOOPCTL dependency remove WI-002 WI-001   # append-only removal fact

dependency add refuses missing items, self-dependencies, and cycles. These item-level edges are live today; a named plan container and bounded plan run window are separate roadmap features. Canonical event and compatibility semantics live in docs/event-model.md.

Notes from real runs: the plane refuses to run agents until LOOPKIT_AUTONOMY=on (a fail-safe, not a bug) · a target may carry .claude/settings.json to grant its workers project-scoped permissions · avoid hosting targets under /tmp on macOS (symlink canonicalization confuses worker sandboxes) · don't run the beats from inside another sandboxed agent session.

Scheduled beat and console launchers must source scripts/load-plane-env.sh before exec-ing Node. The helper deliberately exports assignment-only env files to the child process; merely sourcing LOOPKIT_AUTONOMY=on without exporting it makes Node see an unset variable and report the fail-safe halted state. Set LOOPKIT_ENV_FILE first when the env file is not .ai/loops/config.env; standalone plane-home installs may use $LOOPKIT_HOME/config/autonomy.env.

The target contract

A repo becomes buildable by declaring a small, non-secret manifest (loopkit.target.json): default branch, gate command, worktree prefix, and its three boundary lists (merge-trust prefixes, test-visible surfaces, risk patterns). The plane's own state lives in a separate plane-home directory — itself a git repo, so runtime state gets the same durability treatment as code. targetId is stamped at capture and retained on the folded item; v0.1 drives one target. Multi-target is an activation, not a rewrite. Design notes: docs/event-model.md.

The method, not just the machinery

The plane encodes an opinionated delivery discipline:

  1. Event-model before coding — a feature is mapped left-to-right (events → screens → commands → read models) before any code; the model doubles as the spec.
  2. Make operational flow executable — allowed item transitions live in one typed state machine, while cross-item ordering lives in explicit dependency events. Prose and diagrams explain those rules; they do not define a second workflow.
  3. Vertical slices — every work item is a thin, end-to-end, independently revertable slice.
  4. One writer per boundary — ledger appends are single-writer with PID-aware locking; parallel builds are Touches-disjoint by construction.
  5. The gate is the reviewer of record — human attention goes to product judgment (the review/must tiers), not to re-checking what a test suite already proved.

The discipline itself — stated so it outlives the code — is docs/method.md; docs/hardening-audit.md is it applied: a 10-class incident catalog run proactively against the framework.

More: the method · the vision · operating model · event model · trust boundaries · hardening audit · agent integration · knowledge index · verified knowledge promotion (ADR-015).

For attended agent sessions the repo includes optional repo-local helpers. Claude Code gets three handwritten commands (.claude/commands/): /drive (attended coordinator mode over claims), /plane-check (health triage), /board (the status window). Open a session in this repo and they load automatically. Codex gets matching repo-local source-command-drive and source-command-board skills under .agents/skills/; deterministic health triage remains available through loopctl doctor, summary and slo. These helpers are not external prerequisites or a provider-neutral plugin system, and loopctl does not install them into target repos. Workers run from the target worktree; discovery of its AGENTS.md / CLAUDE.md is provider-native rather than Loopkit-enforced. See agent integration.

Honest scope

This is an experimental v0.1 preview — built and exercised by one operator against one target at a time, on macOS, with the Claude CLI as the worker. It delivers the slices of that one workflow, and that is the entire claim. Not a product, not production-anything, not provider-agnostic yet, no support SLA.

The target registry supports registering more than one repo (target add mints a distinct targetId per repo, per ADR-001), but the exercised, day-to-day workflow — the demo, the docs, the author's own usage — has only ever driven one target at a time. Multi-target scheduling (dispatch deliberately prioritizing/interleaving several targets' queues under one set of beats) is not yet built or proven; see docs/event-model.md for the contract.

Works today Not yet / not claimed
macOS (beats via launchd) Linux / systemd runners
Claude CLI workers (Codex/Ollama adapters present, lightly exercised) provider-agnostic guarantees
registering and building against one target at a time, end-to-end multi-target scheduling, or any multi-target run in anger
executable item lifecycle + queued-item dependency DAG plan containers, run windows, generic workflow engine
solo-operator workflows teams, RBAC, hosted anything

This repo is published read-only as a reference / build-in-public project: fork, clone, and star freely — see CONTRIBUTING.md. Pull requests are closed automatically.

License

Loopkit is MIT-licensed. The bundled Inter font is licensed separately under the SIL Open Font License 1.1; see packages/ui/canonical/fonts/OFL.txt.

About

Event-sourced agent-plane framework: an append-only work ledger, one fold, two beats — LLM workers build in isolated worktrees, diffs gate, green merges.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages