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

smol-symphony

v0.4.4

Published

TypeScript orchestrator that runs coding agents (Claude, Codex) in per-issue Gondolin microVMs over ACP.

Readme

smol-symphony

Disclaimer: this project is written using AI — the orchestrator dispatches AI coding agents at itself, and the bulk of the code in this repository was authored by those agents under human review.

A small TypeScript orchestrator that reads issues off a local Markdown tracker, prepares per-issue workspaces, and runs coding agents (Claude Code, Codex) inside isolated per-issue Gondolin microVMs over the Agent Client Protocol.

You file issues; symphony dispatches one agent per issue and handles state, retry, and concurrency. The agent signals progress through an injected MCP server (transition, request_human_steering, propose_issue). When you point it at a GitHub remote it opens a pull request per issue; in local-only mode the per-issue agent/<id> branch is left in the workspace for review.

New here? Jump to Quick start — install, scaffold, run, with no repo checkout and no image to build.

┌──────────────────────────────────────────────────────────────────────────┐
│  symphony (node host)                                                    │
│                                                                          │
│    ./issues/<state>/*.md     ──┐                                         │
│    ./WORKFLOW.yaml             ├──▶  orchestrator  ──▶  agent runner     │
│    ./WORKFLOW.template.yaml  ──┘     poll · reconcile · dispatch         │
│                                                          │               │
│                                                          ▼  ACP/RPC      │
│                                       ┌───────────────────────────────┐  │
│                                       │ Gondolin  (per-issue VM)      │  │
│                                       │   adapter (claude / codex)    │  │
│                                       │   workspace mount             │  │
│                                       │   mcp client  ────────────────┼─┐│
│                                       └───────────────────────────────┘ ││
│                                                                         ││
│              symphony MCP server  ◀─────────────────────────────────────┘│
│      ( transition · request_human_steering · propose_issue )             │
│                                                                          │
│    HTTP dashboard (HTMX):  /                                             │
│      attention · sessions · on disk · new issue · totals                 │
└──────────────────────────────────────────────────────────────────────────┘

SPEC.md documents the contracts this repo's code references; for the original architectural narrative, see openai/symphony/SPEC.md.

Quick start

From zero to your first dispatched issue. No repo checkout, no image to build — the agent image is a prebuilt asset symphony fetches from a published release the first time it runs.

# 1. Install symphony. (Or skip this and prefix each command below with `npx`,
#    e.g. `npx smol-symphony doctor WORKFLOW.yaml`.)
npm i -g smol-symphony

# 2. In your project, scaffold a starter workflow. With no WORKFLOW.yaml present,
#    symphony offers to write a ~30-line starter (plus a prompts/Todo.md and an
#    issue-filing skill), then exits so you can glance at it. It runs as-is.
cd /path/to/your/project
symphony WORKFLOW.yaml

# 3. Check the host is ready before the first dispatch. `doctor` runs every
#    prerequisite probe — Node version, the agent image, your adapter credential,
#    a writable tracker, a bindable dashboard port — and prints a PASS/FAIL line
#    with a one-line fix for each failure, exiting 0 only if every check passes.
symphony doctor WORKFLOW.yaml

# 4. Run symphony. The first run downloads the prebuilt agent image (one-time,
#    surfaced on the dashboard); then it serves the dashboard and starts dispatching.
symphony WORKFLOW.yaml --port 8787

# 5. Open http://127.0.0.1:8787/ and add your first issue from the "new issue"
#    form. symphony picks it up, boots a per-issue microVM, and dispatches the
#    agent — watch the issue move from "on disk" into a running session.

That's the whole path. Two host prerequisites symphony can't install for you, both checked by doctor in step 3:

  • Node.js ≥ 23.6 (the engines.node floor; doctor reports your version).
  • One adapter credential. For the default claude adapter, a ~/.claude/.credentials.json on the host — run claude login once. (For codex, see WORKFLOW.template.yaml.) The credential never enters the VM — see Trust posture.

The scaffolded WORKFLOW.minimal.yaml runs as-is on the managed image; the only edits a different setup needs are that adapter credential and — if you'd rather pin your own image than fetch the managed one — gondolin.image. The first-run scaffold also drops an issue-filing skill at .claude/skills/symphony-issues/SKILL.md (best-effort — it never blocks the scaffold), so a coding agent in the freshly-onboarded repo can file well-formed tracker issues and decompose a large task into a blocked_by DAG from day one.

When you want a power feature the starter omits — typed action DAGs, PR autopilot, per-state adapters, egress allowlists, sleep-cycle reflection — graduate to the annotated WORKFLOW.template.yaml. Full detail behind each step is in Prerequisites in detail — you should not need it to get here.

Prerequisites in detail

  • Node.js ≥ 23.6. The engines.node floor; symphony doctor reports your running version against it.
  • The agent image — fetched, not built. Gondolin is the in-process microVM substrate (@earendil-works/gondolin), so there is no separate VM daemon to run. The scaffolded workflow sets gondolin.image: managed, which fetches a prebuilt agent rootfs from a published smol-symphony release on first run — one asset per host arch (x86_64, aarch64), keyed to the symphony version. No docker, no npm run build:image, no local OCI convert. The image ships only the mise (jdx/mise) binary; node + every ACP-capable coding agent CLI (claude-agent-acp, codex-acp) come from a mise SYSTEM config symphony stages into the guest at dispatch (assets/symphony-mise.system.toml), and a consuming repo's own mise.toml adds project toolchains (rust, go, kotlin+gradle, …) on top. So bumping an agent CLI is a one-line edit to the staged mise config + a restart — no image rebuild. On an unsupported arch (no published asset), or when you'd rather build/pin your own, set gondolin.image to a local build id / name:tag (npm run build:image from a checkout prints one — see images/agents/README.md) or gondolin.oci_image to an OCI ref symphony auto-converts on first reconcile. Tune the managed fetch and the mise data dir via the gondolin keys in WORKFLOW.template.yaml.
  • An adapter credential. For the default acp.adapter: claude: a credentials file at ~/.claude/.credentials.json on the host (run claude login once). Symphony reads it only on the host side: the guest holds only a token-shaped placeholder, and the host substitutes the real OAuth access token into the outbound request at Gondolin egress (TLS-MITM). The credential file itself is never staged into the VM. (For acp.adapter: codex, see WORKFLOW.template.yaml.)
  • Where state lives. The tracker, run logs, and per-issue workspaces default to ~/.symphony/{trackers,logs,workspaces}/<project> — outside your repo, so a delete / re-clone doesn't wipe them. <project> is derived from workspace.github_repo (or the workflow-file directory name). The startup banner prints the resolved tracker root, workspace root, dashboard URL, and log file; override any with the matching *.root key (e.g. tracker.root: ./issues to keep issues inside the repo).
  • Console output. With the default log file configured, symphony prints a one-line-per-field startup banner (workflow, tracker root, dashboard URL, log-file path) and routes the structured key=value stream to the log file only (~/.symphony/logs/<project>/symphony.log by default). tail -f that file to follow the detail. Pass --verbose (alias --foreground) to mirror the structured stream back to the console for interactive debugging. With no log file configured (the SYMPHONY_LOG_FILE="" override), the structured stream stays on stderr.

From a checkout

If you're hacking on symphony itself:

git clone https://github.com/dizk/smol-symphony.git
cd smol-symphony
npm install
npm run build
npx symphony WORKFLOW.yaml     # the local bin

npm run dev (via tsx watch) reruns on source edits.

Local Markdown tracker

Issues live as .md files under tracker.root. The parent directory is the issue state; the set of valid state directories comes from the states: block in WORKFLOW.yaml (see below) and is auto-mkdir'd on startup.

issues/
├── Todo/
│   ├── 1.md
│   └── 2.md
├── Review/
│   └── 3.md
├── Done/
│   └── 4.md
└── Triage/
    └── 5.md

The basename is the issue identifier. When a caller (dashboard form, MCP propose_issue) omits an explicit identifier, the tracker picks the next free positive integer by scanning every state directory under tracker.root. Operator-supplied identifiers (e.g. CACHE-7.md) pass through unchanged and coexist with the numeric ones.

Each file has YAML front matter and an optional body:

---
title: "Fix the login bug"
priority: 2
labels: [bug, auth]
blocked_by: [5]
---
Long-form description in the body.

State comparison is case-insensitive. Moving the file between state directories is the canonical state transition; the orchestrator does this itself in response to symphony.transition. The agent inside the VM does not have filesystem access to the tracker root: it signals progress through the MCP server and the orchestrator does the file move.

WORKFLOW.yaml

WORKFLOW.yaml is a pure YAML config file (the orchestrator config). Each active state's prompt lives in its own prompt_file — a Liquid-templated file under prompts/, wrapped in the shared prompt.preamble_file/footer_file. The shipped file in this repo is the canonical project workflow; see WORKFLOW.template.yaml for the annotated reference covering every supported option, its type, default, and example.

The workflow is a state machine. A required top-level states: block declares every state an issue can occupy, its role (active — dispatched; terminal — triggers cleanup and handoff; holding — sits outside the dispatch loop, e.g. Triage), and optional per-state adapter, model, max_turns, and allowed_transitions overrides. A single issue can travel through any number of states with distinct adapters and instructions; each active state names its own prompt_file, so the agent in each state sees only that state's instructions plus the shared preamble/footer. The shipped workflow uses a two-stage Todo → Review → Done flow (Claude implements, Codex reviews).

Symphony watches the file and its prompt files, and re-applies poll interval, concurrency, typed actions, prompts, gondolin settings, etc. on change without restart. In-flight runs keep the settings they started with.

Dashboard

When server.port is set (or --port <n> is passed), a single-page HTMX dashboard is served at /. Five live regions poll their own partials every 2s; idiomorph keeps unchanged DOM stable so polling doesn't twitch text.

  • header strip — workflow file + tracker root + status badge (working / attention / idle).
  • attention — only present when something needs you. Steering requests (question, original task, agent's context, reply textarea) and retry queue ease open with a CSS max-height transition so the page doesn't jump.
  • sessions — running issues. Two-line rows: identifier + pill + turn + tokens, then a dim last-message line.
  • on disk — active-state issues not currently dispatched.
  • new issue — collapsed <details> form. Posts JSON to POST /api/v1/issues.
  • totals — dim footer with token + runtime aggregate.

The steering reply form posts form-encoded with HX-Request: true and a same-origin check. The endpoint also accepts application/json for direct API clients. CSRF-relevant content types (text/plain, multipart/form-data) are rejected with 415.

MCP — how the agent talks back

Symphony injects an MCP server into each ACP session at http://<host>:<bound-port>/api/v1/issues/<id>/mcp, gated by a per-dispatch bearer token. Three tools:

  • symphony.transition({ to_state, notes? }) — canonical (and only) exit verb. Moves the issue into another declared state, optionally appending markdown notes to the issue body before the move so the next agent (in to_state) reads them as part of issue.description. A transition into a role: terminal state ends the run and triggers workspace cleanup; transitions between active/holding states preserve the workspace so the same agent/<id> git branch survives the handoff. Rejected transitions (unknown target, disallowed by allowed_transitions) return MCP tool-result errors the agent can read and retry.
  • symphony.request_human_steering({ question, context? }) — call when blocked on something only a human can answer. The turn ends immediately; the human's reply arrives as the prompt for the next turn. Steering-reply turns don't count against agent.max_turns.
  • symphony.propose_issue({ title, description?, labels?, priority? }) — call when the agent notices work that is out of scope for its current task. The proposal lands in the Triage/ state directory, which the orchestrator does not dispatch; the operator approves (→ first active state) or discards (→ first terminal state, prefers Cancelled) from the dashboard. The calling issue's identifier and a timestamp are stamped into the proposal's front-matter as proposed_by / proposed_at so provenance is visible.

In Gondolin, the VM's 127.0.0.1 transparently reaches the host's 127.0.0.1 (verified empirically), so the agent reaches the orchestrator without any mount or special host alias.

ACP — adapter registry

One ACP client (symphony's agent/acp.ts), two shipped adapter profiles. Each profile encodes the binary symphony launches and the credential path the adapter reaches for inside the VM:

| Adapter | Binary | Credential surface | | --------- | ------------------ | --------------------------------- | | claude | claude-agent-acp | placeholder + host egress swap | | codex | codex-acp | placeholder + host egress swap |

WORKFLOW.yaml:

acp:
  adapter: claude
  shell: bash
  prompt_timeout_ms: 1800000
  read_timeout_ms: 30000
  stall_timeout_ms: 300000

Selecting an adapter is enough — symphony auto-derives the launch command. For claude, a per-VM identity file (organization + account UUIDs only, no tokens) is staged into the workspace runtime dir and copied to ~/.claude.json inside the VM; the guest holds only a token-shaped placeholder, and the host substitutes the real access token into the outbound request at Gondolin egress. For codex, no identity file is staged (OpenAI ships no third-party fingerprint check); the VM is launched with OPENAI_API_KEY=<placeholder>, and the host substitutes the real OpenAI credential at egress. Set command: only to override (testing a forked adapter, a non-standard binary path).

Credentials never enter the VM for either adapter. For claude, the host's ~/.claude/.credentials.json is read only on the host side; the VM sees ~/.claude.json (identity-only) plus the placeholder value in its Authorization header, which the host swaps for the real token at egress. For codex, the host's ~/.codex/auth.json (access token / OPENAI_API_KEY, never the refresh token) is read only host-side; the real OPENAI_API_KEY is stripped from the forwarded VM boot env, so the VM holds only the placeholder.

After-run handoff: pull request

On transition into the Done state the orchestrator runs that state's typed actions: block — two records, push_branch then create_pr_if_missing — which push the per-issue branch and open a PR when a GitHub repo is configured: set workspace.github_repo: <owner>/<repo> in WORKFLOW.yaml (or export SYMPHONY_REPO=<owner>/<repo>, which overrides the file). Each action's if: $repo predicate short-circuits to a no-op in local-only mode (neither set), so the branch is simply left in the workspace until cleanup; pick the commits up with git log agent/<id> against your local clone. The PR is opened with gh pr create --base $base_branch ..., which requires gh auth status to be clean on the host; the token never enters the VM.

The orchestrator stages the action context — $branch, $base_branch, $pr_title (already id-prefixed), and $pr_body_file (a temp file holding the current issue body) — before the block fires, so the PR description carries every symphony.transition notes block accumulated across the run, the full handoff thread from implementer through reviewer to approval. Per-action retry and snapshot plumbing replaces the old opaque shell-exit surface: on a rate-limit the create_pr_if_missing action surfaces "retrying in 60s" on the dashboard rather than failing silently.

See states.Done.actions in WORKFLOW.yaml for the canonical record pair and WORKFLOW.template.yaml for the typed-action reference.

Trust posture

Sandbox isolation comes from running each agent inside a Gondolin microVM. The VM has no OAuth refresh tokens or long-lived access tokens: for both claude and codex, the guest holds only a token-shaped placeholder and the host substitutes the real credential into the outbound request at Gondolin egress, so no real Anthropic or OpenAI credential is present in the VM. The VM has no tracker filesystem access (the tracker is reached only through the MCP server) and stripped git remotes (set by the orchestrator's setupWorkspaceDir).

Within the ACP session, the orchestrator follows SPEC §6.1's "high-trust" posture:

  • Command execution and file change approvals: auto-approve.
  • User-input-required turns: end the turn (the orchestrator retries on backoff).
  • Unsupported dynamic tool calls: return failure; the session keeps running.

Tests

npm run typecheck    # tsc --noEmit
npm test             # ~1400 tests across the core deciders + shell adapters:
                     # workflow, tracker, prompt, workspace, adapters, http, mcp,
                     # acp-bridge, orchestrator, run log, and runner state resolution
npm run build        # tsc emit to dist/

npm run verify runs the full gate set CI enforces: typecheck, the eslint purity/budget rules, the dependency-cruiser layering walls, the ratcheted architecture gates, and the tests.

An end-to-end smoke run needs the built agent image (see images/agents/).

See CHANGELOG.md for operator-visible changes between releases.

License

MIT. See LICENSE.