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

@xhulz/nina

v0.55.0

Published

Composes an agent harness into a project from a versioned core, its surfaces, and the project's own layer.

Readme

NINA is a command-line compiler for the multi-agent pipeline I run inside Claude Code. It composes that pipeline into each project from versioned layers, and it uses hooks to enforce the rules a prompt can only ask for. It reads Claude Code's own transcripts to measure what the pipeline did. When the pipeline keeps making the same mistake, NINA turns the lesson into a rule in the next release.

At a glance

  • Projects don't copy the harness anymore. Each one pins a frozen release and composes from it. nina upgrade moves it forward, checks the result with the project's own detectors, and undoes the move if a check fails.
  • The pipeline is declared as a graph of stages, verdicts and edges, and it's validated on every check. Every loop-back edge has a cap.
  • Hooks count the rounds of each loop. When a dispatch would go past a cap, Claude Code asks the owner first, even in auto mode.
  • The numbers come from transcripts. That's how I found a "mandatory" rule that had been followed 2 times in 743 runs, and a loop counter whose cap fired 8 times when only one of those was a real repeat.
  • A lesson the pipeline learns three times becomes a request to the harness. The answer ships as a rule in a release, and the upgrade that installs it closes the request.
  • nina pipeline --view draws the pipeline as a pixel office: each agent at its stage's desk, walking the work over to the next one, a question mark over whoever asked, a party when a task passes, and beside the room a timeline of the session's tasks: who worked on each, and how long it took. It follows the session live, or replays one, in a page on this machine that the command prints a link to.
  • nina dashboard puts the office, the tasks, the pipeline, the pills, the learning cycle and the stats in one page, a tab each, drawn from the same data the commands print. The commands stay, since the hooks, CI and agents read them.

Why

My agent harness (the CLAUDE.md, the subagent specs, the routing rules) used to move between projects by copy-paste, and each copy started drifting the day it was made. I'd fix a rule in one project and it stayed broken in the others. After a while I couldn't tell which copy was current. NINA turns the harness into something a project composes and pins.

I built it one rung at a time, and every rung ended up as working code:

| Rung | In NINA | |---|---| | Prompts | rules written once, split into layers: core, surfaces, project | | Harness | the layers composed into a project and pinned to a frozen release | | Loops | stages that send work back, with a cap on every loop that hooks enforce | | Graphs | the pipeline declared once as data, and validated on every check | | Self-improving system | lessons that graduate into rules, with their effect measured |

The pipeline it composes

There are eleven subagents, and a project gets the ones its risks call for. Every project gets planner, architect, implementer, reviewer, qa, devops and secops. A database adds dba, external services add integration-tester, and smart contracts add solidity-dev and solidity-auditor, since contract code can't be patched once it ships. The chain also scales with the change. A one-file label fix gets a light chain or a direct edit, and anything on a critical path (auth, plus what the project's surfaces add: money, the database, an integration) goes through every gate.

flowchart LR
    planner -->|PLAN-READY| architect
    architect -->|SPEC-READY| implementer
    implementer -->|DIFF-READY| reviewer
    reviewer -->|APPROVED| qa
    qa -->|PASS| devops
    qa -->|PASS| secops
    qa -->|PASS| done((done))
    devops -->|DEPLOYED| done
    secops -->|SECURE| done
    reviewer -.->|"REJECTED · max 2"| implementer
    qa -.->|"FAIL · max 2"| implementer
    implementer -.->|"BLOCKED · max 2"| architect
    architect -->|BLOCKED| owner((owner))

Simplified. The core graph has 22 edges, 11 of them capped loop-backs (the dotted ones), and each surface adds its own stage and edges.

In a project, the graph lives in one file with one edge per line, and nina check validates it:

- `reviewer` → `implementer` on `REJECTED` — an implementation bug · max 2
- `reviewer` → `architect` on `REJECTED` — a design flaw, or no preview-deploy plan · max 2

Every stage needs a spec and every spec needs to be a stage. An edge has to leave on a verdict its stage can actually emit, every verdict has to go somewhere, and every loop-back needs a cap. A spec's prose can only mention a route the graph has.

How it works

Composition from layers

| Layer | Holds | |---|---| | core | what's true for every project | | surface | what's true for projects that have it: db, money, integrations, frontend, pii, edge-cf, blockchain, and the stacks prisma and drizzle on top of db and d1 on top of db and edge-cf | | project | what's true for one project only, kept in that project's repo |

Each layer mirrors the project's file tree, and nina compose merges them:

  • <!-- nina:slot db.1 --> in a core file is a hole the db surface fills. A project with no database drops the slot, so its agents never read a word about migrations, and one whose database is not reached through Prisma never reads a word about Prisma.
  • <!-- nina:requires db --> on a file's first line makes the whole file conditional. That's why a frontend-only project gets eight agent specs instead of twelve.
  • {{PLACEHOLDERS}} get filled from the project's profile, so the core can state a rule without naming one project's provider, packages or models. A release can answer a name itself, like the typecheck and test commands, and a project declares it only to change it.
  • core/defaults/ holds the release's own text for a project slot a file can't do without. The one that needed it is an agent's description:, which Claude Code requires before it loads the agent at all. The project's own text still wins; until it writes some, the default composes, and nothing counts it as owed. An empty default marks a place a project may add to, like the last rows of each agent's skills table.
  • A passage marked <!-- nina:why --> tells how a rule came to be. It stays in the layers for whoever maintains them and is never composed, so no dispatch pays to read the history of a rule.
  • Every composed file opens with a notice naming the layer to edit instead. Claude Code's Edit tool refuses a file the agent hasn't read, so any agent editing an existing file sees the notice first. Seeing it isn't the same as obeying it, so an edit guard hook refuses the edit and quotes the notice back.
flowchart LR
    core["core<br/>every project"] --> compose{{"nina compose"}}
    surfaces["surfaces<br/>the ones the project declares"] --> compose
    project["project layer<br/>this project only"] --> compose
    profile["profile.json<br/>release pin · surfaces · vocabulary"] --> compose
    compose --> out["CLAUDE.md · agent specs · graph.md<br/>router.md · hook scripts"]

Composition gets tested like code. Four fixture projects, one for each shape worth testing, have to pass ten properties each: no placeholder survives, every unfilled slot belongs to the project, nothing leaks in from a surface the project didn't declare, gated files show up exactly when they should, rule references resolve, the notice never lands above a frontmatter block, every agent spec declares its name, description and tool allowlist, the graph validates, every numbered list counts 1, 2, 3 in every profile, and every file fits a size budget. An eleventh check looks at the layers themselves: a surface's technology, role or domain may only be named in files gated on that surface. The first time it ran, it found 37 places where ungated core prose handed work to a role that only some projects have.

Releases and upgrades

A release freezes the core and the surfaces, and a frozen release is never rewritten. Each project pins one in .nina/profile.json, so I can keep changing the harness without moving a project that's shipping.

nina upgrade --to <version> starts with a report of what the move would cost. The real risk is text the project wrote that the new core has no place for, and the command won't apply while any of it would lose its meaning. --apply runs the whole move and then checks it with the project's own detectors:

  pinned 0.13.0
  ✓  composing the harness files — 17 file(s)
  ✓  checking the declaration — check: declaration is sound
  ✓  validating the pills — pills: all 13 well formed
  ✓  running the project's own detectors — harness: current (4 detectors clean)

  upgrade: 0.7.0 → 0.13.0 applied and verified.

If a step fails, the project goes back byte for byte, including the old pin, the old composition and the owner's own edits. Every step also runs before the move, so a problem the project already had gets reported as pre-existing instead of blamed on the upgrade.

Three things recorded in the project's own repo fully determine its harness: the NINA version in its lockfile, the release pin and the project layer.

Loop caps that hold

A cap written as an instruction only works if the model counts its own rounds, so NINA's loop gate counts them instead. Claude Code hooks keep a small ledger for each session, with metadata only: which stage reported which verdict and the short ids it gave the issues it sent back, which dispatch went out and when, and when the owner last spoke. On PreToolUse, if a dispatch would go past a cap, the gate answers permissionDecision: "ask" and Claude Code puts the dispatch in front of the owner. Before relying on it, I checked that this also works in auto mode.

sequenceDiagram
    participant M as Model
    participant C as Claude Code
    participant G as Loop gate
    participant O as Owner
    M->>C: dispatch implementer, round 3 on reviewer → implementer, max 2
    C->>G: PreToolUse hook
    G-->>C: permissionDecision ask
    C->>O: confirm this dispatch?
    alt owner allows
        C-->>M: the round runs, and the next one asks again
    else owner refuses
        C-->>M: denied, so the model stops and hands over every round's report
    end

It asks instead of denying because it can't be sure two rounds are about the same issue. The stages now name their issues, but a model can rename one between rounds, so a wrong count should still cost one click.

I worked out what counts as a round by replaying six weeks of a real project's history. The obvious rule was to count a dispatch to an edge's target after a loop-back from its source. It hit a cap 8 times, and 7 of those were different issues that happened to travel the same edge. The rules that replaced it:

  • a round is a dispatch that acts on a declared loop-back;
  • parallel dispatches acting on the same verdicts count as one round;
  • a pass cancels a rejection only if a fix went out in between;
  • when the owner speaks, every count starts over;
  • while every report in a loop names its issues, each issue gets its own count, and the edge still asks once it goes past twice its cap, in case an issue was renamed along the way. One report that names nothing puts the rest of that loop back on the edge's count.

Three separate reviews each found a case the counting still got wrong before it shipped.

Probing the live system changed the design twice. UserPromptSubmit fires when a subagent's report is delivered, and not only when a person types. Resetting on every prompt would have wiped every count, so only human prompts reset. The gate also never reads the session transcript to decide anything. Claude Code writes that file asynchronously, and a resumed session rewrites its own history. In one 385 MB transcript, 74% was replay.

The gate fails open and logs its errors. Every project runs a self-test among its detectors. It pushes a whole loop through the project's own hook command and expects the round past the cap, and only that round, to reach the owner.

Findings that reach the model

Each project runs NINA's detectors from two hooks, and each hook reaches a different reader:

| Hook | Reaches | |---|---| | Stop | the person, with what the turn left behind | | UserPromptSubmit | the model, as additionalContext, before it answers |

For a long time only the first one existed, so every finding went to the person, who wasn't the one about to act on it. The detectors report drift in the composed files, what a project still has to declare, lessons that are owed, and the health of the loop gate itself.

The two readers are told differently. The model gets a finding in full the first time, and after that, while it stays the same, only its summary and an instruction not to repeat it. The person hears one line from Stop, and only about what the turn itself left behind, since whatever the model was handed before the turn it has already relayed. A new project's first answer opens with what the project is, read from the brief its owner wrote during init, and proposes writing the architecture together, instead of reciting what is missing.

Measurement and the learning cycle

nina snapshot reads Claude Code's transcripts and records each dispatch: which stage ran, the verdict it declared, whether it got sent back, which skills it used and whether it read its lessons. The store keeps metadata only. Report text, source code and personal data never go in.

nina stats reads that store back: the loop-back rate of each stage, what each stage costs at list prices (a subscription pays nothing per token, but the unit still compares one stage with another), which model each stage actually ran on against the one its spec declares, how each stage's rounds spent their context, and the size of each change against the chain that carried it. The first cost reading, over 825 runs, had the implementer at 41% of the spend and the gate whose value was in question for weeks at 2%. The first context reading found resumed fix rounds starting at nearly half a million tokens, which is why a fix now goes to a fresh run.

The same history can go to Langfuse on its own. nina langfuse login asks for a project's keys once, and nina langfuse on makes a project send its runs after every turn, from the detector that already snapshots it. Each stage becomes a trace named for its role, with its tokens, estimated cost and verdict. With --content, the trace also holds what the stage did: its prompt and report, each message it wrote, and each tool call with what came back, read from the transcript as it is sent. Obvious secrets are masked, but the code the stages read goes with it, so context is a switch per project. With --prompts, only what passed between the agents goes: the prompt each stage was given and the report it handed back, beside how many turns it took and how large its context grew. Each run goes once, after it settles, because Langfuse keeps what it is first sent and a second send would count twice.

When a stage gets sent back, the pipeline can write a lesson for that role. nina learn checks the cycle one link at a time, and its first audit found four of the five links broken. Here's the same project before and after I redesigned it:

  observe   826 run(s) in 843 round(s) recorded, 2026-08-12 → 2026-09-22
  capture   ✗ reviewer: 24 loop-back(s) since its newest lesson (2026-09-03)
  apply     592 of 646 run(s) of roles that have lessons opened their pills (92%)
  graduate  0 lesson(s) at 3+ occurrences · 0 request(s) open
  verify    reviewer/…-never-write-to-repo-files.md (2026-09-03): 2% → 29% loop-back (n 43 → 104)
  observe   826 run(s) in 843 round(s) recorded, 2026-08-12 → 2026-09-22
  capture   ✓ no role has looped back 3+ times without a lesson in the last 14 days
  apply     581 of 720 run(s) of roles that have lessons read one (81%); 81 only listed the directory — measured since 2026-08-12. Reading is not obeying; nothing here can see that.
  graduate  3 lesson(s) at 3+ occurrences · 0 request(s) open
            → nina learn --graduate .claude/pills/integration-tester/regenerate-the-premise-index-after-editing-an-integration-doc.md
            → nina learn --graduate .claude/pills/qa/exit-code-is-the-verdict.md
            → nina learn --graduate .claude/pills/shared/test-must-name-the-mutation-that-fails-it.md
  verify    qa/vitest-file-filter-needs-no-double-dash.md (2026-09-01): 8% → 19% loop-back (n 13 → 79); every other role 17% → 20%
            reviewer/reviewer-never-write-to-repo-files.md (2026-09-03): 2% → 29% loop-back (n 43 → 104); every other role 22% → 16%
flowchart LR
    A["loop-back"] --> B["lesson for the role"]
    B -->|"learned a 3rd time"| G{"whose rule?"}
    G -->|"this codebase's"| H["rule in the project's own layer"]
    G -->|"every project's"| C["request to the harness"]
    C -->|"answered"| D["rule in a core or surface layer"]
    D -->|"next release"| E["nina upgrade --apply"]
    E -->|"closes the request, retires the lesson"| F["verify: loop-back rate before and after"]

A lesson learned for the third time has to graduate, and the project is asked where until it says. A convention of its own codebase becomes a rule in its own layer, right there, and the lesson retires. A rule every project needs becomes a request against the pinned version, because a project can't edit the harness. It can only ask. It used to file every lesson that way, and a convention of one codebase went all the way to me and back only to stay a lesson. I answer requests in the harness repo, into a file the next release freezes, and the upgrade that installs the answer closes the request and retires the lesson. The first full cycle has already happened: the three lessons in the second output became harness rules in 0.20.0, and the upgrade closed all three requests by itself.

Counting loop-backs doesn't say what they were about. nina learn --deep reads each loop-back's report and has a small model, on the same login, group them by cause and set the causes against the lessons already written. It proposes a lesson for each recurring cause that none covers, and it writes nothing itself. The first run, over one project's 70 loop-backs, took six calls and proposed five lessons nobody had written.

Evaluating a release

A loop-back rate moves with the work as much as with the rules, so it can't show whether a rule change helped. nina eval runs an experiment instead. The reviewer each release composes reviews the same change, which carries twelve planted defects, and the report counts what each one caught. It runs claude -p on the Claude Code login, so it uses the subscription and never a per-token key, and the grading is deterministic. The first run caught 11 of 12, and it also showed the reviewer writing a sentence before its verdict line, which the gate would have read as no verdict at all. An optional judge, a second model on the same login, reads each report for what line numbers can't show, and has to quote the report for every defect it says was found. It found the twelfth defect described in words. It's also asked to call each other finding real or noise, and before any of that it judges a report that found nothing, which has to come out at zero.

What measuring found

None of this showed up until I counted.

  • Mandatory skills were invoked 2 times in 743 subagent runs. Skill was missing from every role's tools: allowlist, so the rule could never run. The prose looked fine for three months.
  • Lessons were captured at 3% of the rate the rules asked for, 2 lessons from 78 loop-backs. "Write a lesson on every loop-back" had trained everyone to skip them. The rule is now "ask whether this would happen again", and the detector fires on an event instead of watching a rate.
  • A metric for "agents apply their lessons" said 92%, because listing a directory counted as reading. Counting only real reads, it's 81%. Even that only measures reading. Nothing in a transcript shows whether an agent obeyed.
  • The reviewer sent work back 4% of the time on Sonnet and 39% on Opus 5, at $0.69 and $3.18 a run. Those are different weeks of different work, so it isn't a result. It is the question nina eval exists to answer.
  • The first before/after readings went up after their lessons, with every other role as a control. Maybe the reviewer got better at catching things, or maybe the lesson didn't help. It isn't proof, but without the numbers I couldn't even ask.

What the first new project found

The harness was extracted from a project that was already running, so the path a new project takes had never been walked. The first project started from nothing walked it in two days, and each thing it found became a release:

  • Stop printed twenty-two lines under every answer, repeating the pending items the model had just explained. It says one line now, and only about what the turn left behind.
  • The first answer to "hello" was an inventory of what the project lacked. It now opens with what the owner said the project is.
  • Once the architecture existed, the model closed every answer with the same pending line, because it was told before every message to mention it. It now gets the full list once per session.
  • Seven agents, the reviewer, qa and secops among them, took their description: from the project, and Claude Code doesn't load an agent without one. Every new project had no reviewer until it wrote one, and nothing said so, because the fixtures fill no project slot and composed the same broken specs. The release now supplies a default, and the fixtures check for it.
  • The integration recipe told the project to add a row to skills tables it had no way to edit, and a region written in backticks was reported as a skill that wasn't installed.

Getting started

You need Node 20 or newer. NINA has no dependencies, and a project installs it from npm at an exact version, which its lockfile pins:

pnpm init                          # a new project needs its own package.json first
pnpm add -D -E @xhulz/nina
pnpm nina init

Each release merged to main is published by a GitHub Actions workflow through npm's trusted publishing, so every version carries the provenance of the build that made it.

init works out the surfaces a repo reveals (a Prisma schema means db, a wrangler config means edge-cf), then asks what the project is and one yes-or-no question for each surface no file can settle. It writes .nina/profile.json, the owner's answer as .nina/BRIEF.md and a to-do list, wires the Claude Code hooks and composes the harness. The first Claude Code session opens from the brief and works through what is still missing with the owner.

Run alone on a terminal, pnpm nina opens a session: the banner once, then a nina › prompt where commands (stats, check, upgrade --to <version>) run one under another, with Tab, history and clear.

The test suites run from this repo:

node scripts/compose-test.mjs     # the fixtures against their properties, plus the layer audit
node scripts/cli-test.mjs         # the CLI end to end, in scratch projects
node scripts/harness-check.mjs    # this repo's own detectors

Repository map

| Path | What's there | |---|---| | bin/nina.mjs | entry point and command table | | src/commands/ | one file per command | | src/graph.mjs | parses and validates a composed pipeline graph | | src/gate.mjs | the loop gate: the ledger, what counts as a round, one answer per hook event | | src/office.mjs, src/office.html | pipeline --view: the ledger as beats, and the pixel office that draws them | | src/dashboard.mjs, src/dashboard.html | nina dashboard: each report's data as a tab, and the page that draws them | | src/serve.mjs | what every page served here shares: loopback, the host check, the port, the opener | | src/wiring.mjs | the hooks and npm scripts a project needs, read by init, wire, check and upgrade | | src/transcripts.mjs | the transcript parser | | src/detectors.mjs | runs a project's detectors from its hooks | | src/tools.mjs | what an agent spec's frontmatter must carry: name, description, tools, model, effort, skills | | src/langfuse.mjs | a run as an OpenTelemetry trace and a verdict score | | core/, surfaces/ | the harness layers, and in core/defaults/ the release's own text for a slot | | releases/ | the frozen releases projects pin | | fixtures/ | projects that exist to be composed and checked | | evals/ | a change with planted defects, for comparing one release's reviewer with another's | | scripts/ | the test suites and this repo's own checks | | docs/ | every mechanism, and why it ended up the way it did |

docs/ is the long version. CLAUDE.md is the short one Claude Code reads when it works on this repo, with a table of which document to read before changing what.

Engineering

  • Node.js with zero runtime dependencies. About 9.8k lines of source and 4.2k lines of tests.
  • 48 frozen releases so far. The package ships all of them, so an upgrade can compose its target and report the cost before it changes anything.
  • The tests check properties over the fixture projects, audit the layers, run the whole CLI in scratch projects, and use mutation checks for the rules that matter: undo the rule and some test has to fail.
  • Known limits are written down. The loop gate can't see a fix the model makes by itself without a subagent, for example, and a scheduled /loop prompt resets the counts the same way a person's reply does.
  • Commands: init, status, compose, check, where, pipeline, pills, learn, requests, wire, gate, upgrade, release, snapshot, stats, runs, eval, export, langfuse.

© 2026 Marcos Schulz. All rights reserved. The source is public so it can be read, and forking it on GitHub is fine, but no license to use, copy, modify or distribute it is granted. See LICENSE.