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

@bluewombat/slots

v0.4.0

Published

Shipped mason slots: Planners, Builders, Gates, publishers, and refreshers a Project names in its config.

Readme

Slots

The Planner, Builder, and Gate commands that ship with mason. A Project points its config at one of these paths; nothing here is imported.

planner:
  cmd: [node, ./node_modules/@bluewombat/slots/planners/one-subtask.mjs]
builder:
  producer:
    cmd:
      - node
      - ./node_modules/@bluewombat/slots/builders/producer.mjs
      - --prompt-file
      - ./build.md
      - --
      - node
      - ./node_modules/@bluewombat/slots/agents/claude.mjs
    timeoutMs: 600000
  repair:
    cmd:
      - node
      - ./node_modules/@bluewombat/slots/builders/repair.mjs
      - --prompt-file
      - ./repair.md
      - --
      - node
      - ./node_modules/@bluewombat/slots/agents/claude.mjs
    timeoutMs: 600000
  gates:
    defaultTimeoutMs: 120000
    gates:
      - id: workspace-changed
        argv: [node, ./node_modules/@bluewombat/slots/gates/workspace-changed.mjs]

A slot is an opaque command: it reads argv and writes one JSON line on stdout. These are examples of that contract, not a required layer — a shell script that prints the right line is a Gate. What every slot written in Node would otherwise write again lives in @bluewombat/slot-kit; these all use it. How Host wires a slot into a run: @bluewombat/runtime.

Contents

Catalogue

| Slot | Kind | Answers | | ----------------------------- | --------- | ------------------------------------------------------------------------------------------ | | agents/cursor.mjs | Agent | Runs Cursor CLI with a filled prompt | | agents/claude.mjs | Agent | Runs Claude Code with a filled prompt | | builders/producer.mjs | Builder | First pass of a Subtask: fill {{task}}, then spawn the agent | | builders/repair.mjs | Builder | A Gate refused the Subtask: fill {{task}} and {{report}} | | assembly/fix.mjs | Builder | A judgement refused the assembled feature: fill {{task}} and {{report}} | | assembly/validate.mjs | Builder | Read-only review of the assembled feature: fill {{task}}; answers MASON_VERDICT: VALIDATED or MASON_VERDICT: REFUSED: … | | gates/workspace-changed.mjs | Gate | The Attempt left an uncommitted change | | gates/parent-clean.mjs | Gate | Isolator's Parent working files did not move | | gates/sensitive-path.mjs | Gate | No path matching a glob was touched | | gates/gitignore-leak.mjs | Gate | The feature's commits add nothing its own .gitignore excludes | | gates/ci-green.mjs | Gate | The work line's own checks came back green | | planners/one-subtask.mjs | Planner | Bootstrap: one Subtask that is the FeatureStandard itself | | publishers/git.mjs | Publisher | Pushes the feature's branch to the work line's own remote | | refreshers/git.mjs | Refresher | Fast-forwards the work line copy onto what the Authority holds | | planners/producer.mjs | Planner | An agent CLI splits the FeatureStandard | | messages/conventional.mjs | Message | <type>: <title> for the feature's fold, no agent; --scope, --type | | messages/git-producer.mjs | Describer | An agent reads the git diff and writes the feature's subject and body under --rules-file |

Agents

A vendor CLI is not a Builder. agents/cursor.mjs and agents/claude.mjs take a prompt that is already filled, run the vendor, write a transcript if asked, and print the serialized run on stdout with writeContract (so a large stream-json transcript survives process.exit). They do not know --intention or --report. A role slot after -- is what names one:

- --
- node
- ./node_modules/@bluewombat/slots/agents/cursor.mjs
- --transcript-dir
- ./.mason/transcripts

cursor.mjs needs cursor-agent on PATH (cursor-agent login); claude.mjs needs claude, signed in once.

| Option | Meaning | | ---------------------------------- | -------------------------------------------------------------------------------- | | --prompt TEXT | The filled prompt. Mutually exclusive with --prompt-file | | --prompt-file PATH | The same text, from a file. No interpolation | | --bin PATH | The CLI to run. Default: the vendor's own name, on PATH | | --model NAME | Model for that run | | --output-format FMT | stream-json (default), json, or text. Skill extraction needs stream-json | | --permission-mode MODE | claude.mjs only. Default bypassPermissions | | --transcript-dir PATH | Write one file per turn under here. Off unless given | | --transcript-part NAME | prompt, stdout, stderr, timing. Repeatable. Default: all | | --agent-arg VALUE | Appended to the CLI argv, before the prompt. Repeatable | | --id / --attempt / --context | How a transcript is filed. Forwarded by the role |

The agent CLI prints one JSON line — a SerializedRun from @bluewombat/slot-kit — with the vendor's raw streams plus two extras the wrapper fills when it can:

| Field | Meaning | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | skills | null = unknown (not stream-json, or unparseable). [] = looked, none used. Otherwise skill names in order | | usage | null = unknown. Otherwise token counts (input, output, cacheRead, cacheWrite) and costUsd (null when the vendor did not report a cost) |

Cursor extracts skills from stream-json readToolCall paths ending in /skills/<name>/SKILL.md. Claude extracts them from tool_use blocks named Skill (input.skill), and adds --verbose whenever the format is stream-json. A future vendor agent (codex.mjs, …) uses the same fields: leave them null when it cannot tell.

When --transcript-dir is set, the transcript header repeats skills and usage (unknown, (none), or the values).

A global npm install belongs to one Node version: after nvm use, a claude installed under another version is off PATH. Give --bin the absolute path, or install it under the version the run uses.

The vendor's output streams to stderr as it comes. A CLI that cannot start, whose sign-in is gone, or that refuses the invocation is fail-blocking once the role classifies the run — an unusable CLI is not worth another Attempt. The agent failing at the work is fail-retryable. An agent that declines in prose still exits 0: that is what workspace-changed is for.

The outcome is read from the CLI's own result object, not from its subtype: a claude -p run whose sign-in had expired reported "subtype": "success" beside "is_error": true.

Do not pass --worktree: Isolator already isolated the workspace.

Builders

A role slot fills its placeholders and spawns the agent command after --. --prompt-file and --rules-file belong here. Vendor flags belong after --.

--report on retry is what chooses builders/repair.mjs; the first-pass producer refuses it.

The prompt

The shipped prompt speaks to an agent that only knows the directory it woke up in: it names no part of mason, because none of it is visible from there. What changes from one Project to the next is that text, not the agent, so it is a template file on the role:

builder:
  producer:
    cmd:
      - node
      - ./node_modules/@bluewombat/slots/builders/producer.mjs
      - --prompt-file
      - ./mason-prompt.md
      - --
      - node
      - ./node_modules/@bluewombat/slots/agents/cursor.mjs
    timeoutMs: 600000

A path that names a file next to the config resolves against the config; any other relative path resolves inside the Subtask workspace, so a template committed in the Project travels with it.

Each role owns a closed set. An unknown name, or a required name the template never places, is fail-blocking. {{done_when}} is Done when: plus the definition of done, and vanishes whole when there is none — which is every assembly, so assembly/fix does not fill it at all.

| Role | Required | Also fills | | ------------------- | ------------------------------------- | ---------------------------------------------------------- | | builders/producer | {{task}}, {{rules}} | {{done_when}}, {{id}}, {{attempt}} | | builders/repair | {{task}}, {{report}}, {{rules}} | {{refused_by}}, {{done_when}}, {{id}}, {{attempt}} | | assembly/fix | {{task}}, {{report}}, {{rules}} | {{refused_by}}, {{id}}, {{attempt}} | | assembly/validate | {{task}}, {{rules}} | {{id}}, {{attempt}} |

A first-pass command that receives --report is fail-blocking: that report belongs to the repair command. A repair or fix command without --report is the same.

The repair prompt

A pass a Gate refused is a different job from a first pass: not "do this" but "this was refused, resolve it". So it is a different role — builders/repair.mjs — with its own template. The agent after -- can be the same.

A repair template must place {{report}} — one that never says what was wrong is a run spent on nothing. A command that arrives with a report and no --prompt-file of its own falls back to the shipped repair template.

The fix prompt

A judgement of the assembled feature that refused it is not a Subtask retry. The units already passed. assembly/fix.mjs tells the agent to change what the refusal names and leave the rest. Same placeholders as repair except {{done_when}}, which an assembly has nothing to fill.

The validate prompt

assembly/validate.mjs is a different kind of Builder: it never writes. It fills only {{task}} and {{rules}} — there is nothing to fix yet, so no {{report}} — and asks the agent to end its final message with one line: MASON_VERDICT: VALIDATED or MASON_VERDICT: REFUSED: <one paragraph>. Its own {{rules}} default is not PROMPT_RULES: a review has no work to leave uncommitted and nothing to push, so it says "read-only" instead of "do not commit, do not push".

The slot does not trust the CLI's exit code alone — a review can complete cleanly and still find a problem. It reads the agent's own final answer (readResult(run.stdout)?.result) for the last line starting with MASON_VERDICT: — not a bare search for "VALIDATED" or "REFUSED" anywhere in the text, which a model's own reasoning can contain by accident ("the units were already validated on their own"). It maps a REFUSED: verdict through emitFailure("fail-retryable", …) itself, the report being what followed REFUSED: on that line. No MASON_VERDICT: line, or one that is neither form, is treated the same way, naming what the agent said: a review that did not answer as asked is not a silent pass.

The rules

{{rules}} holds the bounds of the slot: one directory, work left uncommitted, nothing pushed. A producing template must place it — one that leaves it out is fail-blocking before any agent runs, naming the file. Without the bounds the Gates are what says no, which is slower, reaches the tracker, and spends a run.

--rules-file replaces them with the Project's own. The last three are not a matter of taste: the Gates read the working tree of the Subtask directory, so an agent that commits, stashes or writes elsewhere leaves them nothing to see and the pass is refused for having done nothing. Replacing the rules does not lift that; it only stops saying it out loud.

Transcripts

The journal films the system: which phase ran, what each Gate answered. It cannot film the turn itself — the prompt and the agent's answer exist nowhere but inside the slot, and by the time a result reaches Host it is one word. So a loop that misbehaves cannot be read back, only guessed at.

--transcript-dir writes one Markdown file per turn: the invocation, the prompt as sent, stdout, stderr, how long it took, and — when the agent wrapper passed them — skills and usage (unknown, (none), or the values). Files are filed under the Feature the Task belongs to, named for the Subtask and the Attempt. It belongs on the agent command, after --.

builder:
  producer:
    cmd:
      - node
      - ./node_modules/@bluewombat/slots/builders/producer.mjs
      - --
      - node
      - ./node_modules/@bluewombat/slots/agents/cursor.mjs
      - --transcript-dir
      - ./.mason/transcripts
    timeoutMs: 600000
.mason/transcripts/github-owner-repo-34/s1-attempt-2-20260908T134501Z.md

Off unless the directory is given, and that is not caution for its own sake: a transcript is the Project's own material — its code, its conventions, its failures — written to disk in the clear. --transcript-part narrows what is kept; timing alone costs nothing and says nothing about the Project. Whatever directory is chosen belongs in .gitignore.

Failing to write a transcript never fails the run: it is a record of the turn, not part of it.

Gates

Every Gate answers with one JSON object on stdout — {"verdict":"pass"}, or fail-retryable / fail-blocking with a report. Which sequence a Gate belongs in, and what mason appends to its argv, is in @bluewombat/runtime.

builder:
  gates:
    defaultTimeoutMs: 120000
    gates:
      - id: workspace-changed
        argv: [node, ./node_modules/@bluewombat/slots/gates/workspace-changed.mjs]
      - id: parent-clean
        argv: [node, ./node_modules/@bluewombat/slots/gates/parent-clean.mjs]
      - id: sensitive-path
        argv:
          - node
          - ./node_modules/@bluewombat/slots/gates/sensitive-path.mjs
          - "**/.env"
          - "**/secrets/**"

workspace-changed fail-retries when the workspace has no uncommitted change — the Attempt produced nothing, or committed and hid it. It takes no option. Put it first: every Gate after it then judges work that exists. It passes at --stage assembly, where "nothing changed" is a correct outcome rather than a producer to send back.

parent-clean fail-blocks if Isolator's Parent working files changed (--parent DIR, or the other git worktrees of this Child).

sensitive-path fail-retries if the workspace changed a path matching a glob. * is one segment, ** any depth. It looks at what a fold can carry out of the workspace — tracked paths, and new ones .gitignore does not exclude — so what an install left under an ignored directory (a dependency's own .github/) is not a change to guard.

gitignore-leak --base REF fail-retries if the feature's commits add a path the workspace's own .gitignore excludes — a Builder's node_modules/, a build's dist/, a generated file. REF is the work line the feature is offered to (main): only what the feature adds since it left that line is judged, and only what is committed, so a path tracked on purpose before the feature began is not blamed on it, and a removal staged but not committed does not pass. The report is written for assembly.fix: untrack the paths, or change .gitignore if tracking one is the point. It passes at --stage unit, where no commit holds a Subtask's work yet.

It is a second line, not the first: the isolation-git fold already keeps an ignored path out of the commits it makes. It catches a leak that came some other way — another isolation strategy, an agent that committed. Like every assembly Gate, with an Authority it judges what was published, so it stops the fold into the work line, not the push.

assembly:
  gates:
    defaultTimeoutMs: 900000
    gates:
      - id: gitignore-leak
        argv: [node, ./node_modules/@bluewombat/slots/gates/gitignore-leak.mjs, --base, main]
      - id: ci-green
        argv:
          - node
          - ./node_modules/@bluewombat/slots/gates/ci-green.mjs
          - --token-env
          - MASON_GITHUB_TOKEN
          - --require-checks

ci-green reads the work line's own checks on what this workspace published, and answers on them: GitHub Actions' check runs and the commit statuses a CI outside Actions reports (Vercel, Netlify, Jenkins…) — the two lists GitHub shows on a pull request. A pending status is still running; a failed one's report is its context, what it said, and its link. It reads and nothing else — it does not push, does not open a pull request, does not merge. Whoever put the work in front of the checks did that before the Gate ran; a workspace with nothing published is fail-blocking, because no Attempt of that Task can change it.

It waits while checks are running, so give it a ceiling of its own. On a red check the report carries the failing job's log, cut at the error the runner marked — a check name and the word "failure" tell the next Attempt nothing it can act on. Right after a push the checks may not be created yet, and an empty list reads as a pass: on a repository with CI, give it --require-checks, and it waits for a check to appear instead. --token-env is required and names the variable holding a token that can read the checks — the same one the manager's tokenEnv names. It has no default on purpose: GITHUB_TOKEN is what gh reads ahead of its own login, so an operator must not export it, and a Gate that fell back to it refused only at the first assembly of a real run. --remote, --poll-ms and --api-base (GitHub Enterprise) are there too. It talks to GitHub, and knows nothing about @bluewombat/manager-github: a Project may run either one without the other.

sensitive-path, workspace-changed, gitignore-leak and ci-green need a git workspace, which is what Isolator makes when WorkLineStable is a git tree. They fail-block on a workspace Isolator had to copy.

Planners

one-subtask.mjs is the bootstrap Planner: one Subtask that is the FeatureStandard itself. It is what mason init writes into a fresh config, and it takes no option.

planners/producer.mjs hands the split to an agent CLI. It reads the project before answering and writes nothing into it. --prompt-file and --read belong on the role; the vendor is named after --, the same way a Builder names it.

planner:
  cmd:
    - node
    - ./node_modules/@bluewombat/slots/planners/producer.mjs
    - --read
    - ./work-line-stable
    - --
    - node
    - ./node_modules/@bluewombat/slots/agents/cursor.mjs
  timeoutMs: 600000

| Option | Meaning | | -------------------- | ------------------------------------------------------------- | | --read PATH | The project the agent may read before splitting. Default: cwd | | --prompt-file PATH | Prompt template. Must place {{intention}} and {{out}} |

--bin, --model, --transcript-dir and --agent-arg are the agent's, after --. Optional placeholder: {{max_units}}.

A Plan it cannot use — a cycle, a dangling dependency, more Subtasks than --max-units — is this slot's own failure: it exits non-zero so the Breakdown is retried. A FeatureStandard that cannot be split is a refusal on stdout, which is a verdict on the intention instead.

Publishers

The slot that puts an assembled feature where the Authority can read it. It runs in the work line, and mason appends --id, --ref — the name Isolator gave the feature — and --target, the work line it is offered to.

git.mjs pushes that name to the work line's own remote. A plain push, no force of any kind: Integrator only ever adds history, so republishing after a refusal is a fast-forward. A rejection means the name moved under us, which is the one case worth refusing rather than overwriting.

It takes no option, and the remote is origin. After the Authority folds, Host reads the work line back from origin on its own, with no way to learn what this slot was told — so a --remote here would publish to one place while the refresh read another. It comes back when that half is a slot too.

The answer is one JSON object on stdout, like every slot:

{ "ref": "issue/feature-42" }
{ "outcome": "refused", "reason": "the remote rejected the push" }

The reference is opaque above this line — a branch, a directory, a URL. Whatever the Publisher answers is what the FeatureManager is told to judge, so a Project that places its work some other way writes its own Publisher and changes nothing else. A refusal stops the Submission and travels back to the tracker; a slot that could not run at all exits non-zero and says why on stderr.

git.mjs runs git with LC_ALL=C: the refusal quotes git, and it is posted on a tracker a team reads, whatever language the machine that ran it speaks.

Refreshers

The other half of the Publisher's seam: what the Authority accepted has moved the reference work line, and the copy this system works in has not. Left behind, the next Isolation starts from a version that no longer exists — so this runs before anything else in a pass. mason appends --target.

git.mjs fetches that branch from origin and fast-forwards onto it. Fast-forward only: this system never rewrites that line, and a copy that cannot fast-forward has been written to by somebody else. A plain directory has no reference held anywhere else, so it answers ok with nothing to do.

{ "ok": true }
{ "outcome": "refused", "reason": "<copy> has diverged from origin/main: …\nPut it back with: git -C <copy> reset --hard origin/main\nor delete the directory and start again; it is fetched afresh." }

A refusal stops the pass: nothing starts, and the next one tries again. The reason is written for the person who reads mason run's trace: the copy is the system's, nothing in it is worth keeping, and the reason says the one command that puts it back. Git runs with LC_ALL=C here too, for the same reason as the Publisher.

Stack

Plain .mjs, run by Node. No build step. Repository stack: docs/DEVELOPMENT.md.

Commands (development)

| Task | Command | | --------- | ---------------- | | Lint | npm run lint | | Format | npm run format | | Typecheck | npm run tsc | | Test | npm test |

Every slot is tested by spawning it, the way mason does — test/ holds those suites and fixtures/ the stand-in agent CLIs they run against.

The slots are plain .mjs, so the path in a Project config is the file in this repository and nothing is built. tsc still checks them: checkJs reads the kit's types through the imports, and a helper that takes an argument names its type in JSDoc.

Layout

README.md      this file
agents/        vendor CLIs: filled prompt in, serialized run out
builders/      first-pass and repair roles for a Subtask
assembly/      the same two roles for the assembled feature
lib/           what the shipped templates say, shared by the roles
gates/         checks on the workspace, and on the work line
planners/      FeatureStandard in, a Plan out
messages/      what a commit says: a rule, or an agent under the team's guideline
test/          one suite per kind; each spawns the real command
fixtures/      fake agent CLIs the Builder and Planner suites run against