@aicalas/loopy
v0.2.1
Published
Temporal-backed agentic workflow CLI
Maintainers
Readme
loopy
Loopy runs durable local agent workflows on Temporal. You author each workflow as a TypeScript function; Loopy discovers it, receives external or scheduled input, provisions isolated git worktrees, runs coding agents, validates structured output, and records orchestration state in Temporal.
Loopy is built for one developer running a local worker against a Temporal Service. Temporal owns workflow state, retries, schedules, histories, and generic execution operations. Loopy owns the workflow authoring API, agent sessions and worktrees, idempotent effects, human questions, and recovery of parked agent calls.
Loopy currently only supports receiving signals from GH. The long term goal is to provide a plugin system for users to build their own integrations.
Install
Loopy requires Node.js 24 and git.
npm install --global @aicalas/loopyAdd Loopy and a Standard JSON Schema validator such as Zod to the project that contains your workflow modules:
npm install @aicalas/loopy zodStart
The complete setup, configuration, authoring, storage, and recovery guide ships with the package and works offline without configuration or a Temporal connection:
loopy guide
loopy guide setup
loopy guide workflows
loopy guide recoveryAfter configuring the repository, workflow directory, working directory, and Temporal connection, start the local runtime:
loopy loopTypical Docker Compose setup
A typical local deployment runs the Temporal development server and Loopy as separate Compose services. This single-developer setup uses Temporal's embedded development server; deploy Temporal separately for production.
Create config.toml beside your workflows/ and prompts/ directories:
repository = "owner/repo"
default_branch = "main"
project_directory = "/opt/loopy"
workflows = "workflows"
working_directory = "/var/lib/loopy"
poll_interval_seconds = 60
[temporal]
control_task_queue = "loopy-control"
agent_task_queue = "loopy-agent"
[agent_worker]
max_concurrent_activities = 2Build Loopy into the same development environment that its agents need. Workflow modules are imported under Node before they are bundled, so the image installs the workflow project's locked dependencies under /opt/loopy rather than relying on globally installed packages. This minimal Dockerfile.loopy includes the three supported provider CLIs, but a real image must also install the target repository's language runtimes, package managers, build tools, test dependencies, MCP server executables, and system libraries:
FROM node:24-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates gh git make openssh-client python3 g++ \
&& rm -rf /var/lib/apt/lists/*
RUN npm install --global @anthropic-ai/claude-code @openai/codex opencode-ai@^1.18.3
RUN mkdir -p /opt/loopy /var/lib/loopy && chown -R node:node /opt/loopy /var/lib/loopy
WORKDIR /opt/loopy
COPY --chown=node:node package.json package-lock.json ./
USER node
RUN npm ci --omit=dev
ENTRYPOINT ["/opt/loopy/node_modules/.bin/loopy"]
CMD ["--config", "/opt/loopy/config.toml", "loop"]Use the official Temporal CLI image for a persistent local development service:
services:
temporal:
image: temporalio/temporal:1.8.1
command:
- server
- start-dev
- --ip
- 0.0.0.0
- --db-filename
- /home/temporal/temporal.db
ports:
- "7233:7233"
- "8233:8233"
volumes:
- temporal-data:/home/temporal
restart: unless-stopped
loopy:
build:
context: .
dockerfile: Dockerfile.loopy
depends_on:
- temporal
env_file:
- .env
environment:
TEMPORAL_ADDRESS: temporal:7233
init: true
volumes:
- ./config.toml:/opt/loopy/config.toml:ro
- ./workflows:/opt/loopy/workflows:ro
- ./prompts:/opt/loopy/prompts:ro
- loopy-work:/var/lib/loopy
- agent-home:/home/node
restart: unless-stopped
volumes:
temporal-data:
loopy-work:
agent-home:The three volumes provide durability: temporal-data holds Workflow histories and schedules, loopy-work holds the managed clone and open worktrees, and agent-home holds provider authentication, configuration, MCP definitions, and session transcripts.
Credentials and agent environment
Loopy does not create, broker, or store credentials. Supply only the credentials needed by the workflows and harnesses you use. For a local Compose deployment, put secret environment variables in an uncommitted .env file or inject them with your secret manager:
GITHUB_TOKEN=github-token
ANTHROPIC_API_KEY=anthropic-key
CODEX_API_KEY=openai-key
# Add the provider-specific variables referenced by your OpenCode configuration.Start the stack and open Temporal UI at http://localhost:8233:
docker compose up --build -d
docker compose logs -f loopyFunctionality
| Capability | Built-in behavior |
| --- | --- |
| Intake | Poll labelled GitHub issues or pull requests, or start from a Temporal cron/interval schedule. Edge-mode watchers consume the intake label before user code runs; level mode repeats while it remains attached. |
| Agents | Run Claude, Codex, or OpenCode in isolated git worktrees and resume the same provider session across calls. |
| Control flow | Use ordinary TypeScript branches, loops, sequential calls, and Promise.all fan-out inside the Temporal Workflow. |
| Results | Require every agent call to return a value validated by a Standard JSON Schema validator or literal JSON Schema. |
| Effects | Apply idempotent GitHub label changes once per effect ID. |
| Human input | Post a GitHub question, park without holding an agent slot, and resume from the reply or loopy answer. |
| Prompts and progress | Read prompt files with explicit scalar interpolation and publish the latest progress to Temporal Current Details. |
| Recovery | Drain safely on shutdown and reconcile or retry parked agent calls with loopy resolve. |
Workflow example
Each TypeScript module default-exports a workflow created with defineWorkflow from @aicalas/loopy/workflow. This example covers structured agent output, a prompt file, progress, human input, and an idempotent effect:
import {
claude,
defineWorkflow,
githubAttention,
githubComments,
githubLabels,
githubPullRequests,
promptFile,
triggerIssue,
} from "@aicalas/loopy/workflow";
import { z } from "zod";
const Review = z.object({
summary: z.string(),
question: z.string().nullable(),
});
export default defineWorkflow({
name: "review",
watcher: githubPullRequests({
repository: "owner/repo",
label: "agent:review",
onStart: { add: ["agent:running"] },
}),
attention: githubAttention({ target: "trigger" }),
async run(ctx) {
const issue = triggerIssue(ctx.trigger);
const reviewer = ctx.agent(
"reviewer",
claude({ model: "claude-opus-4-8", effort: "high", permissionMode: "acceptEdits" }),
);
ctx.progress("Reviewing the pull request");
let review = await reviewer.run({
output: Review,
prompt: [promptFile("prompts/review.md", { repository: ctx.trigger.repository, issue })],
});
if (review.question !== null) {
const answer = await ctx.ask(
"review-question",
githubComments({ repository: ctx.trigger.repository, issue }),
{ prompt: [review.question] },
);
review = await reviewer.run({
output: Review,
prompt: [`The human answered: ${answer}`],
});
}
await ctx.effect(
"mark-reviewed",
githubLabels({
repository: ctx.trigger.repository,
issue,
add: ["agent:reviewed"],
remove: ["agent:running"],
}),
);
return review;
},
});The GitHub watcher claims each matching pull request, then Temporal runs the workflow. The named agent keeps its provider session and worktree across calls.
Scheduled workflows
A workflow declares one input. Use a Temporal schedule instead of a watcher for time-based work:
import { defineWorkflow } from "@aicalas/loopy/workflow";
export default defineWorkflow({
name: "nightly-maintenance",
schedule: { cron: "0 2 * * *", timezone: "UTC" },
async run(ctx) {
return { source: ctx.trigger.source };
},
});Schedules also accept intervalSeconds and an overlap policy. Attention is optional for scheduled workflows; GitHub attention must target a fixed issue because there is no source issue or pull request.
Agent harnesses
| Factory | Provider | Required options | Session behavior |
| --- | --- | --- | --- |
| claude | Anthropic Claude Agent SDK | model, effort | Resumes a Claude session in the same worktree path. |
| codex | OpenAI Codex SDK | model, effort | Resumes a Codex thread in the same worktree. |
| opencode | OpenCode SDK v2 and OpenCode 1.18.3 or newer on PATH | model as provider/model, effort | Resumes an OpenCode session in the same worktree. |
Harness options also control provider isolation, permissions, approvals, and filesystem sandboxing. See loopy guide workflows for the authoring boundaries and remember that provider configuration and repository instructions can grant tools and credentials beyond Loopy's declared adapters.
Adapters
Import adapter factories from @aicalas/loopy/workflow. Each adapter declares its own repository, target, and credential source.
| Kind | Factories | Role |
| --- | --- | --- |
| Watcher | githubIssues, githubPullRequests | Poll a repository for a label and claim each match. |
| Harness | claude, codex, opencode | Run the agent behind ctx.agent(id, harness). |
| Attention | githubAttention | Report halted, parked, stuck, and awaiting-answer workflows. |
| Effect | githubLabels | Apply idempotent writes through ctx.effect(id, effect). |
| Channel | githubComments | Ask a human through ctx.ask(id, channel, request). |
| Prompt | promptFile | Read a prompt segment from disk outside the Workflow isolate. |
The GitHub adapters make label changes idempotent by converging on the desired label set. Before changing an issue or pull request, Loopy reads its current labels, applies the requested additions and removals, and writes the result only if it changed. Retrying the operation reaches the same state: adding a label that is already present or removing one that is already absent does nothing.
Agent output accepts a Standard JSON Schema validator such as Zod, Valibot, or ArkType, or a literal JSON Schema.
Prompt files interpolate explicitly supplied scalar values. Use named placeholders such as {{ repository }} in the file. Names must be identifiers; strings are inserted verbatim, while finite numbers, booleans, and null use their normal JSON scalar text. Every placeholder needs a supplied value, unused values are allowed, and malformed placeholders or unsupported values halt the workflow before Loopy contacts an agent or posts a question.
Preserve the Temporal Service state, Loopy config, configured working directory, and each agent provider's session data while executions are open. A session ID stored in Temporal cannot reconstruct a deleted provider transcript or worktree.
Use Temporal UI or CLI for execution inspection, Event History, cancellation, reset, termination, and Schedule administration. Use loopy resolve for a parked agent call and loopy answer for a workflow waiting on human input.
