npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

closedloop

v0.2.1

Published

Composable, fail-closed agent workflow blocks for TypeScript.

Readme

closedloop

closedloop is a fail-closed agent workflow package for TypeScript, with a JSON CLI built from the same composable blocks.

The completion invariant is strict:

Loop termination is determined by files and command measurements, never by an agent's stdout.

An agent may print any claim it wants. Conditions cannot read agent stdout, stderr, or RunResult, so those claims cannot authorize completion.

Prerequisites

  • Node.js 20 or newer.
  • Linux or macOS.
  • The Git working tree must be clean before a run. Commit or stash first. The CLI refusal is: git working tree is dirty; commit or stash changes first.
  • Codex, or another agent CLI, must be installed and authenticated separately. closedloop never handles agent credentials.

On WSL, run which npm and confirm it resolves under /usr/, not /mnt/c/. Installing with Windows npm creates a non-executable shim that fails with Permission denied; reinstall Node and npm inside Linux.

Install

npm install closedloop

JSON CLI

JSON remains the primary path for the built-in repository worker/reviewer preset.

{
  "repo": "/absolute/path/to/repository",
  "defaults": {
    "worker": {
      "command": "codex",
      "args": ["exec", "--sandbox", "workspace-write", "{{prompt}}"]
    },
    "reviewer": {
      "command": "codex",
      "args": ["exec", "--sandbox", "workspace-write", "{{prompt}}"]
    },
    "checks": ["npm test"],
    "maxAttempts": 3,
    "timeoutSeconds": 900,
    "isolate": true,
    "verbose": false,
    "context": ["docs/spec.md"],
    "constraints": ["Do not change the public response schema"]
  },
  "jobs": [
    { "name": "implementation", "task": "Implement the requested feature." }
  ]
}

Run it:

closedloop closedloop.json
closedloop --json closedloop.json
closedloop --keep closedloop.json
closedloop --quiet closedloop.json

defaults are merged into every job before validation; job keys override defaults. A v0.1.0 flat config is still accepted and becomes one job named default.

verbose controls how much detail is stored in the run log; it does not control terminal output.

Every resolved job must have at least one objective decider: a reviewer writing verdict.json, or one or more deterministic checks. Jobs run sequentially and stop at the first non-passing outcome. Accepted repository content accumulates, while every job receives a fresh measured baseline. isolate remains a job-level setting.

Only an argv element exactly equal to {{prompt}} is substituted. Missing placeholders, duplicate placeholders, and substring use are rejected. Commands are spawned directly with shell: false.

Library API

The four blocks are agent, run, seq, and loop:

import { agent, run, seq, loop, verdictPassed } from "closedloop";

const worker = agent("worker", {
  command: "codex",
  args: ["exec", "--sandbox", "workspace-write", "{{prompt}}"],
});
const reviewer = agent("reviewer", {
  command: "codex",
  args: ["exec", "{{prompt}}"],
});

export default loop({
  body: seq([run(worker), run(reviewer)]),
  until: verdictPassed(),
  max: 3,
});

The repository copy of this example is examples/review-loop.ts.

Run that repository example directly against Codex:

npx tsx examples/review-loop.ts "Implement the requested change and verify it."

Execute a workflow with task text supplied at invocation time:

import { execute } from "closedloop";
import workflow from "./review-loop.js";

const result = await execute(workflow, {
  task: "Implement the cache invalidation feature.",
  worktree: process.cwd(),
});

if (!result.ok) process.exitCode = 1;

The task lives at ctx.task; it is never baked into a reusable workflow.

The four blocks

  • agent(name, { command, args, context? }) declares a reusable command-backed agent. context is static prompt material, not a retained provider transcript.
  • run(agentDef, options?) invokes one agent. A custom prompt can read the current RunContext, for example run(reviewer, { prompt: (ctx) => Review this work.\n\n${ctx.task} }).
  • seq(blocks) executes blocks in order and stops at the first failure.
  • loop({ body, until, max }) repeats a block until measured state passes or the required positive integer max is reached.

Blocks implement:

interface Block {
  readonly kind: string;
  execute(ctx: RunContext): Promise<BlockResult>;
}

Blocks nest freely. A loop body may be a sequence, and a sequence may contain another bounded loop.

Step input and output

Every run() receives protected containers:

.git/closedloop/runs/<runId>/steps/<n>/in/
.git/closedloop/runs/<runId>/steps/<n>/out/

The generated prompt supplies the absolute paths and requires the agent to read from in/ and write artifacts to out/. Repository edits still happen in the shared working tree.

By default, the previous step's entire out/ tree is copied into the next step's in/. Explicit wiring replaces that default:

run(reviewer, {
  inputs: { "spec.md": "steps/2/out/design.md" },
});

Step directories are mode 0700; copied and produced files are normalized to 0600. Symlinks and other special output entries are rejected. Agents receive private temporary in/ and out/ mirrors so sandboxed providers never write directly into Git metadata; validated outputs are copied into the canonical run directory before conditions execute.

For codex exec agents, ClosedLoop supplies the dynamic step output directory through --add-dir. If no Codex sandbox option was specified, it selects workspace-write; an explicit sandbox choice is preserved.

Conditions

Conditions receive only a StateView:

interface StateView {
  worktree: string;
  readFile(path: string): Promise<string | null>;
  exists(path: string): Promise<boolean>;
  runCommand(cmd: Command): Promise<{ exitCode: number | null; output: string }>;
  stepOut(stepIndex: number, path: string): Promise<string | null>;
}

Built-ins:

  • verdictPassed(file = "verdict.json") reads the latest step output and passes only for an exact lowercase "pass" verdict.
  • checksPass(commands) runs direct commands and passes only when every command exits 0.
  • fileExists(path) measures a repository-relative file.

StateView deliberately does not expose agent stdout, stderr, step command results, or the complete Run result. Command output obtained through runCommand is a measurement; an agent's own output is only a claim.

Reviewer verdict file

A reviewer must write this file into its step out/ directory:

{ "verdict": "pass", "feedback": "optional explanation" }

or:

{ "verdict": "fail", "feedback": "what must change" }

The following all fail closed:

  • missing verdict.json
  • malformed JSON or malformed fields
  • any verdict other than exactly "pass"
  • reviewer timeout, crash, or nonzero exit
  • reviewer mutation of the repository
  • missing, incomplete, ambiguous, or oversized evidence

Failure feedback from the verdict file and failing check output is appended to the next attempt's prompt. Reviewer stdout is retained only as diagnostic evidence and never parsed for completion.

See MIGRATING.md for the v0.1.0 breaking change.

Events and inspection

Events are appended to:

.git/closedloop/runs/<runId>/events.ndjson

Each line is written and fsynced while execution is active. v0.1.0 event shapes remain, with two additions:

  • block_started: kind, name, and stepIndex
  • block_finished: kind, name, stepIndex, and ok

Inspect without blocking:

closedloop runs
closedloop status [runId]
closedloop status [runId] --json
closedloop watch [runId]
closedloop watch [runId] --json

watch --json forwards the stored NDJSON exactly. Non-verbose jobs omit full prompt, output, feedback, and diff bodies.

Watching a run

Runs print live progress to stderr by default while keeping stdout unchanged:

run ms1example started · follow with: closedloop watch ms1example
[1] worker_started
[1] worker_exited exit=0 31.2s
[1] check_result npm test exit=0 12.0s
[1] reviewer_exited exit=0 18.1s
[1] verdict fail
[2] worker_started
run finished: passed (2 attempts, 94.8s)

Follow the stored event stream or inspect current state from another terminal:

closedloop watch <runId>
closedloop status <runId>

Use --quiet on the run command to suppress live progress and the watch hint. runs, status, and watch are unaffected.

Observable states are running, finished, and crashed. Terminal outcomes and exit codes remain:

| Outcome | Exit | |---|---:| | passed | 0 | | exhausted | 2 | | limitCycle | 3 | | aborted | 4 | | invalid configuration/usage | 5 |

Isolation and safety

Isolated jobs run in temporary Git worktrees. Failed worktrees are removed unless --keep is supplied; passing worktrees are retained for inspection and merge. Cleanup uncertainty aborts rather than claiming success.

Run directories are 0700; events and reports are 0600. Context files are repository-relative, loaded before execution, and rejected above 60 KiB rather than silently truncated. Runtime dependency: Zod only.

Troubleshooting

Upstream agent CLIs may print their own warnings, such as Codex model-cache messages. Those messages are not produced by closedloop and are usually non-fatal; use the recorded exit code and run outcome to determine whether the command failed.

Non-goals for v0.2

Not included: parallel blocks, branching, backtrack routing, persistent agent transcripts, multiple reviewers, crash resume, cost tracking, or a workflow-file loader.

Conditions cannot read agent stdout by design: a model's statement about completion is not a measurement of repository or artifact state.