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

@nanobpm/urban-pr-review-codefirst

v1.3.0

Published

Code-first (defineFlow) expression of the Nano Urban PR-review convergence app: one durable multi-round loop per PR, BPMN model derived from TypeScript.

Readme

urban-pr-review-codefirst

The code-first expression of urban-pr-review: a durable, multi-round loop that drives a GitHub PR to convergence against an automated reviewer, escalating to a human when the reviewer is stuck or a round cap is hit.

The model-first app draws this loop in convergence-loop.bpmn. Here the same loop is written with the code-first declarative surface (defineFlow, ADR 0044/0045), consumed through the published @nanobpm/urban runtime (which re-exports defineFlow/WorkflowClient/Worker alongside the data

  • pages + actions surfaces). The SDK derives the executable BPMN (with DI), the job types, and the message names — no diagram, no task-type wiring, no correlation plumbing.

Feature parity with the model-first app. This app persists to the same SQLite schema and serves the same schema-driven web frontend as urban-pr-review — the two run identically. The only difference is how the loop and its record steps are expressed: model-first draws them (convergence-loop.bpmn + workers/*/worker.ts), code-first writes them (workflows/convergence-loop.ts, with the record steps hosted in-process). See Persistence and Frontend below.

HOWTO: run agents that converge PRs for you, all day

The problem this solves

Driving a PR to convergence is mostly waiting: open a review, wait 5–15 min for the reviewer, address the comments, re-request, wait again — often ten rounds deep, escalating to a human only when the reviewer is stuck. If a single agent babysits one PR, it spends almost all of its time idle-polling a review that hasn't landed yet. That is wasted wall-clock and a wasted worker slot.

This app inverts that. The BPMN process the SDK derives owns the durable wait between rounds (a real w.signal message-catch event), so no agent is ever parked holding a job slot while a review is pending. Instead you hire a couple of agent workers once, and they pull senior:pr-review jobs from whatever PR is ready right now — alternating across all in-flight reviews. Two agents can keep a dozen PRs converging in parallel, and they only run when there is actual work to do.

Because the review-round task overrides its job type to senior:pr-review (via w.task("review-round", { jobType: "senior:pr-review" })), the same hired reviewer services both this code-first app and the model-first urban-pr-review. Set it up by: (1) running this app, (2) submitting PRs, and (3) hiring agent workers with c8ctl nano.

0. Prerequisites

  • A running Nano gateway/engine (default http://localhost:8080). This is what the app deploys to and what agents pull jobs from.
  • Node ≥ 22.6 (to run this app — it hosts on Node's built-ins: node:sqlite, node:http; @nanobpm/urban declares engines.node >=22.6). Deno is optional, used only to cross-compile a standalone binary (npm run compile). Also install the c8ctl CLI with the nano plugin (to hire/run agents).
  • On each machine that will host an agent: the GitHub CLI logged in (gh auth login) or a GITHUB_TOKEN/GH_TOKEN in the environment, and the agent harness itself — e.g. the Copilot CLI (copilot).

1. Install the app into your Nano IDE

Open the Nano console and Projects → Import by reference, pointing at this app's folder (ADR 0041), or drop a *.project-ref.json next to your other projects. nano-ide.ext.json marks it as an example. You can also just run it standalone (next step) — the IDE import is only needed to launch/manage it from the console.

2. Start the app

npm start              # → http://localhost:3000

That applies the DB migrations, deploys the derived flow, hosts the in-process w.run record steps, serves the web UI at http://localhost:3000, and runs the review-ready poller (which reads GitHub via the host gh CLI by default, or a GITHUB_TOKEN — without either, re-reviews come only from the UI/CLI). Point it at a non-default gateway with NANOBPMN_BASE_URL; change the port with PR_REVIEW_PORT.

3. Submit a PR

From the web UI, or from the CLI:

npm run submit -- https://github.com/owner/repo/pull/42 5   # 5 = maxRounds

Each submitted PR starts one durable convergence-loop instance that parks until an agent services its senior:pr-review round.

4. Hire an agent worker

An agent is a CLI harness (Copilot CLI here) turned into a Nano job worker. Hire a profile whose rank + capability produce the senior:pr-review token this flow's task emits:

c8ctl nano hire \
  --name reviewer \
  --rank senior \
  --capabilities pr-review \
  --command 'copilot -p - --allow-all-tools' \
  --model <your-model>
  • --rank senior + --capabilities pr-review makes the worker subscribe to the senior:pr-review job type — exactly the token this flow's review-round task emits (thanks to the SDK job-type override), so one profile serves both apps.

  • --command 'copilot -p - --allow-all-tools' starts the Copilot CLI reading its prompt from stdin (-p -). The harness pipes the whole job JSON (prompt + job.variables: prUrl, repo, prNumber, round, answer?) to stdin; the review-round instructions tell the agent how to read it and where to write its result.

  • --allow-all-tools is the crucial flag. Without it, Copilot pauses to ask permission before each tool call — and an unattended worker has no human to answer, so the job stalls. --allow-all-tools lets it run the whole round non-interactively. (Pair with --deny-tool to blocklist specific tools.)

    ⚠️ Only enable --allow-all-tools for code and hosts you trust. It grants the agent unattended, broad permissions (shell, file writes, network). Each job runs in a throwaway per-job workspace (see below), but the worker still runs as your user on the host — don't point it at untrusted PRs on a shared machine. Use --deny-tool to narrow it, or a container sandbox for stronger isolation.

If you'd rather not override the job type in the flow, you can instead leave the derived convergence-loop:review-round type and point a worker at it explicitly with c8ctl nano work reviewer --job-type convergence-loop:review-round. This app ships with the senior:pr-review override so it shares a reviewer with the model-first app out of the box.

5. Put the agent to work

c8ctl nano work reviewer      # polls for senior:pr-review jobs until Ctrl-C

Now every PR you submit gets picked up automatically. Start a second worker (same command, another terminal or another machine) and the two alternate across whichever PRs are ready — that is the idle-time you reclaim. Run more than one job at once per worker with --max-parallel 2.

Isolation — each job gets its own clean workspace

In the default host mode (--sandbox none), the worker provisions a throwaway, per-job workspace: a fresh clone under <state>/agent-runs/run-* checked out on the PR's head branch, exposed to the agent as AGENT_WORKSPACE / REPO_URL / REPO_BRANCH / REPO_REF, and reaped after the job. So multiple agents on one host don't step on each other. Host workers inherit your gh/GITHUB_TOKEN login, so no extra auth is needed. Docker/podman sandboxes exist (--sandbox docker --image …) for stronger isolation, but container-side git provisioning is a later increment — container jobs don't clone yet, and don't inherit your host login (pass credentials via --secret-resolver host / secretRefs). For the review loop, host mode is the recommended setup.

Run it across spare hardware (incl. a Raspberry Pi)

Nano ships ARM binaries (arm64/armv7/armv6), so the whole thing scales down nicely:

  • All-in-one: run the gateway, this app, and one or two workers on your laptop.
  • Distributed: run the Nano gateway on a Raspberry Pi (always-on, low power), run this app anywhere, and put agent workers on spare machines — each worker just needs the c8ctl CLI, the Copilot CLI logged in, and NANOBPMN_BASE_URL pointed at the Pi. Add or remove workers at will; the BPMN process holds all durable state, so workers are stateless and disposable.

The payoff: instead of one agent burning wall-clock polling a single PR, a small pool of always-available workers keeps every open review converging — and idles to zero cost when there's nothing to do.

The flow

workflows/convergence-loop.ts:

loop:
  review-round            (EXTERNAL agent — a coding-agent harness)
  switch status:
    converged  -> persist-converged; break
    addressed  -> if round >= maxRounds:
                     persist-escalation-maxrounds        (fall through)
                  else:
                     persist-round (round+1); wait-review; continue
    default    -> persist-escalation                     (fall through)
  wait-answer             (both escalation paths park here for a human)
  • review-round is a w.task — an external worker. Point a coding-agent harness at the job type senior:pr-review (e.g. a c8ctl nano hired rank senior + capability pr-review profile — the same one that services the model-first app). The app never hosts or names it; it only owns the durable orchestration around it.
  • persist-* and persist-converged are w.run — app-hosted handlers. persist-round returns { round: round + 1 }; that return value (not an ioMapping) is the loop's only state mutation.
  • wait-review / wait-answer are w.signal — real, engine-visible catch events correlated on prKey. Both resume at the loop head (review-round): wait-review via an explicit continue, wait-answer via the loop's natural repeat. The two escalation branches converge on the single trailing wait-answer (step names must be unique, so the signal is not repeated). This durable wait is the surface a Temporal-style code-first API cannot draw.

Because every step declares a typed envelope() contract, this flow can be ejected to model-first without losing its I/O shapes — they are lifted into the generated BPMN as nano:shape + io.nanobpm.dataEnvelope properties.

Derived names

| Step | Derived job type / message | | --- | --- | | review-round (external) | senior:pr-review (overridden via w.task("review-round", { jobType: "senior:pr-review" })) | | persist-round | convergence-loop:persist-round | | persist-escalation | convergence-loop:persist-escalation | | persist-escalation-maxrounds | convergence-loop:persist-escalation-maxrounds | | persist-converged | convergence-loop:persist-converged | | wait-review (signal) | convergence-loop:wait-review | | wait-answer (signal) | convergence-loop:wait-answer |

Run it

Requires a running Nano gateway (default http://localhost:8080; override with NANOBPMN_BASE_URL).

# The one service: applies DB migrations, deploys the flow, hosts the app-owned
# w.run record steps, AND serves the web frontend + review-ready poller.
npm start              # → http://localhost:3000

npm start now does everything: it applies the db/migrations/ on boot (creating app.db), deploys the flow, hosts the persist-* handlers, serves the schema-driven page runtime at http://localhost:3000, and runs the review-ready poller. Submit and answer PRs directly from the web UI, or drive the same actions from the CLI:

# submit a PR (parks after review-round at wait-review or wait-answer)
npm run submit -- https://github.com/owner/repo/pull/42 5   # 5 = maxRounds

# nudge a re-review (resumes an instance parked at wait-review)
npm run review-ready -- owner/repo#42

# answer an escalation (resumes an instance parked at wait-answer)
npm run answer -- owner/repo#42 "merge as-is"

# wipe all persisted PRs/rounds/escalations (drops + re-migrates app.db)
npm run purge

The reviewer itself is not started by npm start — it is the external senior:pr-review job. Host it with a coding-agent harness so the automated review is fully decoupled from the durable orchestration.

Environment overrides: PR_REVIEW_PORT (default 3000), NANOBPMN_BASE_URL (gateway), NANO_APP_DB_URL (default file:./app.db), NANO_PR_POLL_MS (poller interval, default 60000), NANO_PR_MAX_ROUNDS (default 10), NANO_PR_GITHUB_TRANSPORT (auto | gh | token, default auto — prefer the host gh CLI, else the token), GITHUB_TOKEN (token for the token/auto transport — without any usable transport the poller idles and re-reviews come only from the CLI/UI).

Compile to a native binary (no runtime install required)

The app is authored on Node's built-ins (node:sqlite, node:http, process.env), which lets Deno cross-compile it into a native executable that bundles the Deno runtime (its denort binary, which supplies the node:* compatibility APIs), the npm dependencies and the app's entry code — so the target box needs no Node/Deno/node_modules install:

npm run compile        # → dist/urban-pr-review-codefirst  (via `deno compile`)

Not (yet) a single self-contained file. The binary bundles the runtime + deps

  • entry code, but the Urban runtime loads the app's declarative files — nano.app.json, pages/, db/migrations/, prompts/ and the actions/ modules — from the working directory at runtime (via readTextFile + dynamic import). deno compile can embed data dirs into its virtual filesystem with --include, but embedded files are only reachable relative to import.meta.dirname, whereas the runtime resolves them against Deno.cwd() — so today you must ship the binary alongside those app files and run it from that directory (it then boots identically to npm start, same env vars). Making a truly self-contained binary needs an embeddable Urban app capability in @nanobpm/urban (resolve the app root from import.meta.dirname + --include the non-statically-analyzable actions/ dynamic imports), tracked separately. Add --target in the deno.json compile task to cross-compile for another OS/arch. (Only Node's built-in node:sqlite survives this — a native npm addon like better-sqlite3 would not.)

Persistence

Parity with the model-first app: the same three-table SQLite schema (pull_requests, rounds, escalations), the same db/migrations/*.sql, and the same typed rows (app/rows.ts), written through the @nanobpm/urban data layer (DataLayer.table<T>(name, pk), exposing pull_requests / rounds / escalations). The nano.app.json manifest declares the app sqlite datasource (${NANO_APP_DB_URL:-file:./app.db}) and the migrations directory; the Urban runtime provisions the datasource and applies migrations on boot.

The record steps that write these rows are the code-first counterpart of the model-first workers/{persist-round,persist-escalation,finalize} — but instead of standalone defineWorkers, they are the w.run handlers in workflows/convergence-loop.ts, hosted in-process by the @nanobpm/urban Worker that npm start runs. Those handlers get no AppApi, so main.ts injects the provisioned data layer into them via setPersistData(app.data) before the worker starts. The thin CLI scripts (submit/answer/review-ready) import the flow to start/signal instances without ever opening the DB — only the worker host injects the data layer.

Frontend

The same schema-driven page runtime as the model-first app (pages/home.page.json), served by the @nanobpm/urban pages surface at http://localhost:3000. It lists active + historical PRs, submits new ones, and answers or cancels runs inline. Because both apps commit the identical page schema, the UI is byte-for-byte the same; the code-first action bindings in actions/* (startwf.start, escalation messagewf.signal wait-answer, cancel → gateway cancellation) sit behind it, mounted by Urban ahead of the generic page actions.

Link it into your Nano projects

This is a standalone Node project (Deno is used only as the cross-compiler). Import it by reference (ADR 0041) so the console reads it live from this directory:

  • In the console Projects → Import by reference, point at this folder, or
  • drop a urban-pr-review-codefirst.project-ref.json in your projects root.