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

ghostrail

v0.16.0

Published

Open source software factory for coding agents. Config-driven loops, pluggable isolation backends, human merge gate.

Readme

Ghostrail — open source software factory for coding agents

Config-driven loops, pluggable isolation backends (worktree, container, Firecracker, E2B), and a human merge gate. One light left on, on rails that hold.

Ghostrail turns a repo into a factory by dropping in one config file and one prompt. It finds eligible work in your tracker, isolates it, runs a sandboxed coding agent that only writes files, gates the result against a mirror of your CI, and opens a pull request. It never merges. That stays your call.

Status: pre-alpha. Phase 0 (the MVP that self-hosts and replaces a bespoke bash factory) is complete: the config-driven loop, the Linear source, the claude-code agent, the worktree and container backends, the gate, the git publisher, the respond pass, the run store, and the board all ship. See SPEC.md for the design.

Why

A software factory is thin glue over three primitive layers: where work comes from, what agent does the work, and where the work runs in isolation. Most people rebuild that glue per repo. Ghostrail extracts it once, so the tracker, the agent job, the gate, and the isolation backend are all configuration:

  • Config-driven. A ghostrail.toml plus a prompt file is the whole factory.
  • Pluggable backends. local-worktree and container-per-run ship first-class; Firecracker, E2B, and remote sandboxes are adapters behind the same five-verb interface.
  • Pluggable agents. Claude Code headless is the day-one agent, behind an [agent] seam so other agents can slot in.
  • Local and simple by default. The quickstart runs on your own hardware with zero infrastructure and no additional cost. Cloud backends and a hosted board are opt-in.
  • Human merge gate. auto_merge defaults to false. A person owns every merge.

Quickstart

npx ghostrail install         # CLI + skill, then offers to set up your credentials
ghostrail init code     # or: content
ghostrail run                      # one tick: claim, isolate, run, gate, open PRs
ghostrail respond                  # fold new human PR comments back into branches
ghostrail board                    # watch runs at http://127.0.0.1:5050/

ghostrail run does one tick and exits; ghostrail watch --interval <dur> and ghostrail start --interval <dur> run ticks on a loop. For exactly what a tick does end to end, and how the [guardrails] levers interact (including combinations that quietly do nothing), see how a tick works.

Configuration reference

ghostrail.toml is the whole factory: one file, one table per adapter seam. Every key below is read and validated in src/config/load.ts; the defaults come from src/config/schema.ts — this table is checked against that code, not against the illustrative templates under templates/ or examples/, which drift.

A few things apply across every table, worth knowing before you read them:

  • Durations are strings. timeout and claim_ttl are parsed by a small parser that accepts a whole positive number plus one unit: "30m", "2h", "90s". A bare number (30) is a validation error, not minutes.
  • An invalid value does not stop the rest of the file from validating. Every key is checked independently: a missing or malformed value is recorded as an error and that one key falls back to its default, so one typo doesn't hide problems elsewhere in the file. That said, ghostrail run/watch still refuse to start if any error was recorded — look for config error at <path>: ... lines on stderr. A couple of lookups made before a run starts (like the factory name in early log output) instead treat a config that fails to validate as absent and quietly fall back to their own hardcoded default, without printing anything.
  • An omitted section takes every default in it. [guardrails], [backend], [gate], and [output] can all be left out of the file entirely.

[factory]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | name | string | any non-empty string | none | yes | | base_branch | string | any non-empty string | "main" | no | | source | string | a git URL or local path | none — operates on --repo in place | no |

[source]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | kind | string | "linear" | "github-issues" | "beads" | "linear"* | yes | | team | string | tracker team key | "" | yes for linear, no otherwise | | project | string | project name within the team | none | no |

* A missing or invalid kind is recorded as an error, then falls back to "linear" rather than blocking the rest of the file — see the note above.

[source.eligible]

Filters which tracker items are eligible work. The same shape is reused, verbatim, by [triage.eligible].

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | state | string | a tracker workflow state name | none — not filtered | no | | label | string or array of strings | one or more label names (AND'd) | [] — not filtered | no | | assignee | string | "unassigned" | "me" | a user email or id | none — not filtered | no |

[agent]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | kind | string | "claude-code" today | "claude-code"* | yes | | prompt | string | path to a prompt file, relative to the config | none | yes | | allowed_tools | array of strings | tool names the agent may call | ["Read", "Edit", "Write", "Bash", "Grep", "Glob"] | no |

* Same fallback as [source].kind: missing or invalid is an error, and the field still resolves to "claude-code".

[backend]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | kind | string | "local-worktree" | "container" | "firecracker" | "e2b" | "remote" | "local-worktree" | no | | image | string | a container image reference | none | no — needed once kind = "container" | | managed_checkout | boolean | true | false | true | no |

managed_checkout only affects local-worktree: true makes worktrees from a bare clone ghostrail owns under its own state dir; false borrows your working checkout instead. See choosing a backend.

[gate]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | commands | array of strings | shell commands, run in order in the workspace | [] | no |

[guardrails]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | max_items_per_run | number | > 0 | 3 | no | | budget_usd_per_item | number or string | > 0, or "unlimited" | 3 | no | | max_cost_usd_per_run | number or string | > 0, or "unlimited" | 5 | no | | timeout | duration string | e.g. "30m" | "30m" | no | | claim_ttl | duration string | e.g. "2h" | "2h" | no | | concurrency | number | > 0 | 1 | no |

  • budget_usd_per_item does two jobs: it's the hard per-item cost ceiling, and it's the cost ghostrail assumes for an item that finishes without reporting any cost at all. Raising it to give one item more headroom also raises what an unreported-cost item gets charged.
  • Either budget key also accepts the string "unlimited" to disable its cap entirely. An item that reports no cost is then charged $0 rather than budget_usd_per_item, and the tick logs which cap is uncapped.
  • This table says what each key accepts. What each one bounds inside a tick, and how they interact with concurrency, is covered in how a tick works.

[output]

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | open_pr | boolean | true | false | true | no | | draft | boolean | true | false | false | no | | auto_merge | boolean | true | false | false | no | | branch_prefix | string | any non-empty string | "ghostrail" | no | | commit_type | string | a conventional-commit type, e.g. "feat", "fix", "chore" | "chore" | no |

auto_merge is policy, not just a default: it hard-defaults to false because a human owns every merge. No combination of keys makes ghostrail merge its own PR.

Top level

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | artifacts | array of strings | names of global artifacts under ~/.config/ghostrail/artifacts/ | [] | no |

[respond] (optional)

Enables ghostrail respond. Omit the whole section to leave it disabled.

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | prompt | string | path to a prompt file, relative to the config | none | yes, if [respond] is present |

[triage] (optional)

Required by ghostrail triage; without it, that command has nothing to run.

| Key | Type | Values | Default | Required | | --- | --- | --- | --- | --- | | prompt | string | path to a prompt file, relative to the config | none | yes, if [triage] is present | | eligible | inline table | same shape as [source.eligible] | { labels: [] } | no |

A complete example

[factory]
name = "fix-bugs"
base_branch = "main"            # default; shown for clarity

[source]
kind = "linear"                 # linear | github-issues | beads
team = "TJ"                     # required for linear: its team key
project = "myproject"           # optional: scope to one project
eligible = { state = "Todo", label = "factory", assignee = "unassigned" }

[agent]
kind = "claude-code"
prompt = "prompts/fix-bug.md"
# allowed_tools defaults to ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]

[backend]
kind = "local-worktree"         # default; container | firecracker | e2b | remote also declared
# managed_checkout = true       # default: local-worktree owns its clone

[gate]
commands = [
  "pnpm install --frozen-lockfile",
  "pnpm lint",
  "pnpm typecheck",
  "pnpm test:unit",
]

[guardrails]
max_items_per_run = 3           # default
budget_usd_per_item = 3         # default; also the assumed cost of an unreported-cost item
max_cost_usd_per_run = 5        # default
timeout = "30m"                 # default
claim_ttl = "2h"                # default
concurrency = 1                 # default: one item at a time

[output]
open_pr = true                  # default
draft = false                   # default
auto_merge = false              # hard default: a human owns every merge
branch_prefix = "ghostrail"     # default
commit_type = "chore"           # default

artifacts = ["style-guide"]     # optional: shared references staged into every run

[respond]                       # optional: enables `ghostrail respond`
prompt = "prompts/respond.md"

[triage]                        # optional: required by `ghostrail triage`
prompt = "prompts/triage.md"
eligible = { state = "Todo", label = "triage", assignee = "unassigned" }

Credentials

Ghostrail reads its credentials from two dotenv-style files before every run, watch, respond, and triage:

| File | Scope | | --- | --- | | ~/.config/ghostrail/secrets.env | every factory on your account | | .ghostrail.env in the repo you point -C at | that one factory |

Precedence is repo, then global, then the ambient environment, so a value in a file beats an exported variable of the same name. Nothing has to go in your shell profile.

ghostrail install offers to create this file for you at the end of a fresh install. You can also do it any time:

ghostrail secrets-setup            # report what it would add, change nothing
ghostrail secrets-setup --write    # create it (mode 600), then open your $EDITOR

It is append-only and safe to re-run: existing values are never rewritten, keys you added yourself are left alone, and a second run adds nothing. Run it again after upgrading and it tops the file up with placeholders for any credential a newer ghostrail has learned to read. Add --no-edit to skip the editor.

Uncomment only the lines you need. Leave the rest commented, because an uncommented empty value overrides, and therefore blanks out, a working credential from your environment.

LINEAR_API_KEY (required)

The tracker credential. Ghostrail acts as whoever owns this key: it reads eligible issues, assigns them, moves their state, comments, and creates sub-issues during triage.

  1. Go to linear.app/settings/account/security (Settings → Account → Security & access).
  2. Under Personal API keys, click New API key.
  3. Name it something you will recognize later, like ghostrail.
  4. Grant Write. Read alone is not enough: ghostrail assigns issues, moves them between states, and comments. You do not need Admin.
  5. Restrict it to the team your factory works in, if your workspace has several.
  6. Copy the key (it starts with lin_api_) into LINEAR_API_KEY.

Linear shows the key once. If you lose it, delete that key and make a new one.

GH_TOKEN (required for a code factory)

Used to clone the repo, push factory branches, open pull requests, label them, and read and post PR comments during respond. GITHUB_TOKEN is read as a fallback when GH_TOKEN is unset; set one, not both.

The simple option, a classic token. Go to github.com/settings/tokensGenerate new token (classic), set an expiration (90 days is a reasonable default), and tick the single repo scope. That covers everything ghostrail does. Add workflow only if you expect the agent to edit files under .github/workflows/, since GitHub rejects those pushes otherwise.

The tighter option, a fine-grained token. Go to github.com/settings/personal-access-tokensGenerate new token, set an expiration, select Only select repositories and pick the repo your factory targets, then grant these repository permissions:

| Permission | Access | Why | | --- | --- | --- | | Contents | Read and write | clone the repo, push the factory branch | | Pull requests | Read and write | open the PR, read its comments in respond | | Issues | Read and write | apply the ghostrail label, post PR comments | | Metadata | Read-only | mandatory, GitHub enables it for you |

Issues is the surprising one. PR labels and PR comments both go through GitHub's issues API, so a token with only Pull requests write cannot label a PR or comment on it.

One caveat before you choose fine-grained: gh has a known issue where some PR reads fail with Resource not accessible by personal access token, because the CLI asks for project-card fields no fine-grained permission covers. If respond fails that way, a classic repo token is the fix.

For a repo owned by an organization, an org owner may need to approve the token before it works, and fine-grained tokens can be blocked by org policy entirely.

Agent auth: pick one rail

Ghostrail runs Claude Code as the coding agent, and there are two ways to pay for it. ANTHROPIC_API_KEY wins whenever both are set, so if you mean to bill a subscription, leave the API key commented out and make sure it is not exported in your shell either.

CLAUDE_CODE_OAUTH_TOKEN, to bill a Pro or Max subscription. Run:

claude setup-token

It opens the same browser authorization flow as /login and prints a one-year token to the terminal. It does not save it anywhere, so copy it straight into CLAUDE_CODE_OAUTH_TOKEN. Requires a Pro, Max, Team, or Enterprise plan. Confirm that automated use fits your plan's terms before running it unattended, and note that your plan's rolling limits are shared with your claude.ai usage.

ANTHROPIC_API_KEY, to bill metered API usage. Go to platform.claude.comSettings → API keysCreate key, and copy it into ANTHROPIC_API_KEY. Straightforward billing and no shared subscription limits, but every run costs tokens.

Keeping them safe

secrets-setup writes the file mode 600 inside a 700 directory, so it is readable only by you. A few things worth knowing:

  • Never commit one. A repo-local .ghostrail.env belongs in .gitignore; the scaffolded templates do not add that line for you.
  • Ghostrail redacts values from its CLI output. Run logs and the JSON a tick prints replace any known credential value, and secrets-setup prints key names only. The run store behind ghostrail board keeps metadata rather than command output (gate step names, exit codes, PR links), so it has little to leak, but it is not passed through the redactor. Treat redaction as a safety net, not a reason to paste a key into a terminal.
  • Rotate on the provider, not in the file. Delete the old key at the source so a leaked copy stops working, then paste the new one in.

The factory image

Only the container backend needs one. Both scaffold templates default to local-worktree, which runs in a git worktree on your host with no image and no Docker, so a first setup usually needs nothing here.

When you do switch to kind = "container", build the toolchain image once per machine. The agent and the gate run inside the workspace, so the container needs node and pnpm for the gate, git and gh to publish, and the Claude CLI for the agent. The default alpine:3 has none of them.

ghostrail image build                  # -> ghostrail-factory:local
ghostrail image build --tag mine:v2    # or your own tag

The Dockerfile ships inside the npm package, so this needs no checkout. From a repo checkout, scripts/build-factory-image.sh does the same thing. ghostrail install offers to run the build for you, defaulting to no since most people start on local-worktree.

Point the config at whatever you built:

[backend]
kind  = "container"
image = "ghostrail-factory:local"

No credentials are baked into the image. They are forwarded per run from your environment after the secrets files are applied.

Drive it from your coding agent

ghostrail skill installs a /ghostrail skill into whichever coding agents you have configured, so you can work from inside them instead of the shell. The one worth knowing: /ghostrail customize reads the repo (stack, CI workflow, test conventions) and proposes the edits that turn the generic scaffold into a factory that fits this codebase. Also doctor, status, init, run, respond, triage.

It is idempotent — re-run it to install or update, whichever applies:

ghostrail skill              # install/update for detected agents
ghostrail skill --project    # commit it with this repo instead of your user config
ghostrail skill --check      # is my copy out of date?
ghostrail skill uninstall    # remove it (never touches another product's skill)
ghostrail self-update        # update the CLI, then its skill

Keeping up with template changes

ghostrail init records what it scaffolded in ghostrail.template.json, so a later release can tell a file you edited apart from one the template changed. Commit that file with the repo.

ghostrail init code --diff    # read-only: how do my files differ?

Each scaffolded file reports as up to date, locally customized, safe to take (the template moved, yours is untouched), a conflict (both moved), or missing. For anything that needs a look it prints the exact diff command against the shipped template. It never writes, so it is safe to run at any time.

Scaffolded before this existed? Re-run ghostrail init once. It adopts a baseline for every file that still matches the template exactly, which leaves only your genuinely edited files unknown.

ghostrail artifacts lists the shared references in your global store (~/.config/ghostrail/artifacts/), staged into each run so the same voice and rules apply across every factory instance.

Self-host and cutover

Ghostrail builds itself: ghostrail.toml + prompts/resolve-issue.md point the factory at its own repo. To replace the bespoke bash factories in echotime, tjwrite, and research, see docs/cutover.md and the configs in examples/.

Development

Requires Node 22+ and pnpm (via corepack: corepack enable).

pnpm install
pnpm test:unit   # run the unit tests
pnpm typecheck   # type-check src + tests
pnpm lint        # lint + format check (Biome)
pnpm build       # compile to dist/

Releasing

Releases publish from CI on a tag, so main and npm cannot drift.

  1. Bump version in package.json and add the release to CHANGELOG.md.
  2. Merge that to main.
  3. Tag and push: git tag v0.1.0 && git push origin v0.1.0.

.github/workflows/release.yml then verifies the tag matches package.json, runs lint/typecheck/tests/build, checks the package contents (the bin shebang, both templates and their prompts, the Dockerfile, and that every advertised command appears in help), and publishes with provenance.

Auth is npm Trusted Publishing (OIDC): the workflow exchanges its GitHub Actions OIDC token for a short-lived registry token at publish time, so there is no long-lived NPM_TOKEN secret to leak or rotate.

One-time setup on npmjs.com: open the ghostrail package → SettingsTrusted Publisher → GitHub Actions, and point it at this repository with workflow filename release.yml. Allowed action: npm publish.

Provenance (the Sigstore attestation binding a tarball to its source commit and build) requires a public repository, so the workflow gates that flag on visibility: releases cut while the repo is private publish unattested, and the first release after it goes public attests automatically. Provenance is per-version, so earlier versions stay unattested — it cannot be backfilled.

License

Apache-2.0.