@0xtiby/looper
Standalone AI loop orchestration engine
Package details
Install @0xtiby/looper from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@0xtiby/looper- Package
@0xtiby/looper- Version
0.3.1- Published
- Apr 30, 2026
- Downloads
- 319/mo · 253/wk
- Author
- 0xtiby
- License
- MIT
- Types
- extension
- Size
- 111.3 KB
- Dependencies
- 3 dependencies · 4 peers
Pi manifest JSON
{
"extensions": [
"pi-extension"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
What Is Looper?
A standalone engine for looping an AI coding CLI against a prompt until it signals it's done:
- You give Looper a prompt and pick a CLI (
claude,codex,opencode, orpi). - Looper spawns the CLI, streams its output, and watches for a sentinel string.
- It re-spawns from scratch each iteration until the sentinel fires,
maxIterationsis reached, or the CLI exits non-zero.
Looper is stateless — every iteration is a fresh spawn with no shared conversation state. It has no opinions about specs, plans, trackers, git, or project structure. The prompt is the instruction.
Under the hood, Looper drives the CLIs via @0xtiby/spawner.
Prerequisites
- Node.js 20+
- At least one supported AI CLI installed and authenticated:
- Claude Code (
claude) - Codex CLI (
codex) - OpenCode (
opencode) - pi (
pi)
- Claude Code (
Quick start
Install the package:
pnpm add @0xtiby/looper # or: npm install @0xtiby/looperScaffold a
.looper/config.jsonwith sensible defaults:looper initRun a loop with an inline prompt:
looper run -p "Refactor src/auth to remove dead code. Emit :::LOOPER_DONE::: when finished."
The package ships both a looper binary and a library (import { loop } from "@0xtiby/looper"). Both ESM and CommonJS are supported.
CLI
looper init
Creates .looper/config.json populated with the built-in defaults.
looper config
Prints the resolved config (defaults merged with your file).
looper run
Runs a loop. Exits 0 on clean completion, non-zero on CLI error, and 130 on SIGINT (Ctrl+C).
# Inline prompt, or a path to a file (auto-detected)
looper run -p ./plan.md --cli claude --max-iterations 5
# Or pipe in
cat plan.md | looper run --prompt-stdin
| Flag | Description |
|---|---|
-p, --prompt <value> |
Inline string OR path to an existing file (auto-detected). |
--prompt-stdin |
Read the prompt from stdin. |
--cli <name> |
One of claude, codex, opencode, pi. Overrides config. |
--model <name> |
Model override (e.g. opus, sonnet). |
--max-iterations <n> |
Cap the number of iterations. |
--sentinel <string> |
String the AI must emit to stop the loop. |
--cwd <path> |
Working directory passed to the spawned CLI. |
--var KEY=VALUE |
Template variable (repeatable — see Template variables). |
looper resume
If a session is aborted with Ctrl+C, the session file is marked interrupted. Continue it with:
looper resume # lists interrupted sessions
looper resume <session-id> # resumes the specific session
Resume reuses the original prompt, CLI, and model and runs the remaining iterations (up to maxIterations). New iterations are appended to both the session JSON and the transcript log. Stdin-origin prompts cannot be resumed.
Library usage
Basic
import { loop } from "@0xtiby/looper";
const result = await loop({
cli: "claude",
prompt: `
1. Run: gh issue list --repo {{REPO}} --state open --json
2. Pick the next unblocked issue labeled "ready".
3. Implement it: write code, tests, commit, open a PR,
4. Exit.
When no unblocked issues remain, emit :::DONE:::
`.trim(),
cwd: process.cwd(),
maxIterations: 20,
sentinel: ":::DONE:::",
vars: { REPO: "0xtiby/looper" },
});
console.log(result.stopReason); // "sentinel" | "max_iterations" | "error" | "aborted"
The library is stateless and does no file I/O. Callers own persistence; the looper CLI is a thin consumer of the library.
All options
import { loop } from "@0xtiby/looper";
const result = await loop({
cli: "claude",
prompt: "Fix the failing tests. Emit :::DONE::: when finished.",
cwd: process.cwd(),
model: "opus",
maxIterations: 5,
sentinel: ":::DONE:::",
vars: { PROJECT: "my-app" },
onOutput: (chunk) => process.stdout.write(chunk),
signal: AbortSignal.timeout(60_000),
});
LoopOptions
| Option | Type | Default | Description |
|---|---|---|---|
cli |
"claude" | "codex" | "opencode" | "pi" |
— | Required. The AI CLI to spawn. |
prompt |
string |
— | Required. Prompt string passed to the CLI. |
cwd |
string |
process.cwd() |
Working directory for the spawned CLI. |
model |
string |
— | Model override (e.g. opus, sonnet). |
maxIterations |
number |
10 |
Hard cap on iterations. |
sentinel |
string |
":::LOOPER_DONE:::" |
String the CLI must emit to end the loop. |
vars |
Record<string, string> |
{} |
Template variables for {{KEY}} substitution. |
onOutput |
(chunk: string) => void |
— | Streaming callback for raw CLI stdout. |
signal |
AbortSignal |
— | Aborts the loop between or during iterations. |
LoopResult
| Field | Type | Description |
|---|---|---|
iterations |
IterationResult[] |
Per-iteration records, in order. |
stopReason |
StopReason |
"sentinel" | "max_iterations" | "error" | "aborted". |
IterationResult
| Field | Type | Description |
|---|---|---|
number |
number |
1-indexed iteration number. |
exitCode |
number |
Exit code of the spawned CLI. |
sentinelDetected |
boolean |
Whether the sentinel was observed in stdout. |
stdout |
string |
Full captured stdout for the iteration. |
startedAt |
string |
ISO timestamp. |
durationMs |
number |
Wall-clock duration. |
tokensIn |
number |
Input tokens reported by the CLI. |
tokensOut |
number |
Output tokens reported by the CLI. |
Template variables
Prompts can reference {{VAR}} placeholders. Substitution is flat (values are not themselves re-substituted). Unknown placeholders are left as-is.
Built-in variables:
| Name | Value |
|---|---|
ITERATION |
Current iteration number (1-indexed). |
MAX_ITERATIONS |
Configured maxIterations. |
SESSION_ID |
UUID of the session (CLI only). |
Precedence (higher wins): --var KEY=VALUE CLI flag → config vars → built-in.
Config (.looper/config.json)
{
"cli": "claude",
"model": "opus",
"maxIterations": 10,
"sentinel": ":::LOOPER_DONE:::",
"vars": {}
}
All fields are optional. Missing fields fall back to the defaults shown above. CLI flags on looper run override the resolved config.
Sessions
Each looper run (and resume) writes two files under .looper/sessions/<uuid>:
<uuid>.json— session metadata and per-iteration metrics.<uuid>.log— raw CLI stdout, separated by--- ITERATION N [ISO_TIMESTAMP] ---markers.
Session state machine: active → completed | interrupted. Session files are never auto-deleted.
Example session JSON:
{
"id": "…",
"prompt": "./plan.md",
"cli": "claude",
"model": "opus",
"maxIterations": 10,
"state": "completed",
"startedAt": "2026-04-16T18:00:00.000Z",
"completedAt": "2026-04-16T18:07:42.000Z",
"stopReason": "sentinel",
"iterations": [
{
"number": 1,
"exitCode": 0,
"durationMs": 45000,
"tokensIn": 8000,
"tokensOut": 12000,
"sentinelDetected": true
}
]
}
Pi integration
If you use pi, looper ships a built-in extension for fire-and-forget background runs inside Zellij panes or tmux sessions.
Install looper as a pi package:
pi install git:github.com/0xtiby/looper
Then in a pi session:
/looper-run
This opens an interactive wizard that walks you through picking a prompt (from .looper/*.md or writing a new one), choosing the AI CLI, and spawning looper in a background pane/session while you keep chatting with pi.
The LLM can also call the looper_run tool directly:
{
"name": "looper_run",
"parameters": {
"prompt": "Refactor auth module. Emit :::LOOPER_DONE::: when finished.",
"cli": "claude",
"maxIterations": 5
}
}
Features
- Auto-detects Zellij (preferred) or tmux
- Scans
.looper/*.mdfor reusable prompts - Zellij: pane direction and floating mode support
- tmux: new detached sessions
License
MIT