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

joustx

v1.0.0

Published

AI agents charge at each other in structured passes. A framework for adversarial multi-agent reasoning.

Readme

Jousting ⚔

CI npm version license node TypeScript

AI agents charge at each other in structured passes. One fires a payload (the lance), the other counters, a neutral Herald scores each exchange, and a winner is declared.

🔗 Live site →

What is this

Jousting is a framework for adversarial multi-agent reasoning. Instead of asking one model for an answer, you stage a structured duel: two agents take turns charging and countering over a fixed number of passes, a third agent — the Herald — scores every exchange and, at the end, synthesizes a balanced verdict and declares a winner.

The metaphor is a medieval joust. Each pass is one charge down the tiltyard. A clean argument is a direct hit; a partial point is glancing; a whiff is a miss; a fatal, unanswerable blow is an unseat — and ends the joust early. The result is sharper than a single prompt because each side is forced to attack and defend specific claims.

Install

npm install -g joustx
# or run without installing
npx joustx init

Requires Node.js 20+.

Quickstart

Three commands and you're running:

export ANTHROPIC_API_KEY=sk-...      # 1. set your key
npx joustx init --topic "Should we rewrite this in Rust?"   # 2. scaffold config
npx joustx joust                   # 3. run the joust

Every joust is saved as a .joust replay file you can replay or re-score later:

joustx replay should-we-rewrite-this-in-rust-AbC123.joust

Joust modes

Each mode defines how the two agents and the Herald behave.

| Mode | Agent A | Agent B | Herald does | |-------------|-------------------|-----------------------|------------------------------------------| | debate | Argues for | Argues against | Scores rebuttals; declares the winner | | redteam | Attacker | Defender | Scores exploits vs. mitigations | | review | Author | Critic | Scores critiques vs. defenses | | synthesis | Supporting evidence | Contradicting evidence | Synthesizes a balanced view | | plan | Planner (iterates) | Stress-tester | Evaluates the final, adapted plan |

Set the mode in jousting.toml or override per-run with --mode.

Config reference

A full annotated jousting.toml:

[tiltyard]
topic     = "Should we rewrite this in Rust?"   # the problem/question
passes    = 5                                    # number of exchange rounds
mode      = "debate"                             # debate|redteam|review|synthesis|plan
save      = true                                 # auto-save a .joust replay file

[agent_a]
name        = "Lancer"
model       = "claude-sonnet-4-6"
provider    = "anthropic"                        # anthropic|openai|gemini|groq|openrouter|ollama
lance_style = "aggressive"                       # aggressive|methodical|socratic
persona     = "senior engineer who loves Rust"

[agent_b]
name        = "Paladin"
model       = "claude-sonnet-4-6"
provider    = "anthropic"
lance_style = "methodical"
persona     = "pragmatic CTO who ships in Go"

[herald]
model      = "claude-sonnet-4-6"
provider   = "anthropic"
scoring    = "strict"                            # strict|lenient|narrative
synthesize = true                                # produce a final synthesis

Lance styles shape how an agent fights: aggressive (forceful, leads with the strongest blow), methodical (systematic, builds case by case), socratic (probes assumptions with sharp questions).

Scoring styles shape the Herald: strict (reserves direct hits and unseats), lenient (rewards good points generously), narrative (scores the momentum and story of the duel).

Commands

joustx init          # scaffold a jousting.toml (prompts for topic interactively)
  --topic "..."  --mode <mode>  --force

joustx joust         # run a single joust (reads ./jousting.toml by default)
  --config <path>  --topic "..."  --passes <n>  --mode <mode>
  --dry-run            # print resolved config + prompts, make no API calls
  --no-save            # don't write a .joust file

joustx tournament    # run a bracket (requires a [tournament] block)
  --config <path>  --topic "..."  --passes <n>  --mode <mode>

joustx replay <file.joust>
  --speed <1|2|3>      # 1 slow, 2 medium, 3 instant

joustx herald <file.joust>   # re-score a saved joust
  --model <model>  --scoring <mode>  --save

Add --no-splash (or set JOUSTING_NO_SPLASH=1) to skip the startup splash for CI/pipes.

The .joust format

Every joust is saved as JSON with a .joust extension, named {topic-slug}-{id}.joust:

{
  "id": "abc123",
  "version": "1.0",
  "created_at": "2026-06-22T12:00:00.000Z",
  "config": { "...full config object..." },
  "passes": [
    {
      "pass": 1,
      "a": { "agent": "Lancer", "payload": "..." },
      "b": { "agent": "Paladin", "payload": "..." },
      "score": {
        "pass": 1,
        "scores": {
          "a": { "hit": "glancing", "reasoning": "..." },
          "b": { "hit": "direct", "reasoning": "..." }
        },
        "unseat": false,
        "unseated_agent": null,
        "commentary": "Paladin landed a concrete operational counterpoint..."
      }
    }
  ],
  "result": {
    "winner": "Paladin",
    "synthesis": "...",
    "total_hits": { "a": 2, "b": 3 }
  }
}

Hit types are direct · glancing · miss · unseat. Hit weights (unseat 3, direct 2, glancing 1, miss 0) decide the winner; an unseat ends the joust immediately.

Tournament mode

Run several agents in a bracket. Add a [tournament] block listing the competitors:

[tournament]
format = "roundrobin"   # roundrobin | elimination

[[tournament.agents]]
name = "Lancer"
model = "claude-sonnet-4-6"
provider = "anthropic"
lance_style = "aggressive"
persona = "Rust maximalist"

[[tournament.agents]]
name = "Paladin"
model = "claude-sonnet-4-6"
provider = "anthropic"
lance_style = "methodical"
persona = "pragmatic Go shipper"

# add as many [[tournament.agents]] as you like

Then:

joustx tournament
  • roundrobin — every agent jousts every other; most wins takes the crown.
  • elimination — single-elimination bracket; last knight standing is champion.

The [tiltyard] and [herald] blocks supply the topic, passes, mode, and judge for every match.

Providers & auth

Each agent (and the Herald) picks a provider. Jousting ships adapters for six:

| provider | SDK / transport | Env key | Notes | |--------------|---------------------------------------|----------------------|--------------------------------------------------| | anthropic | @anthropic-ai/sdk | ANTHROPIC_API_KEY | default | | openai | openai | OPENAI_API_KEY | | | gemini | @google/generative-ai | GEMINI_API_KEY | optional peer dep | | groq | groq-sdk | GROQ_API_KEY | optional peer dep | | openrouter | openai (baseURL override) | OPENROUTER_API_KEY | model strings pass through, e.g. meta-llama/llama-3-70b | | ollama | fetch/api/chat | none | local; respects OLLAMA_HOST (default http://localhost:11434) |

Keys are read from the environment:

ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
GROQ_API_KEY=gsk_...
OPENROUTER_API_KEY=sk-or-...
# ollama needs no key; set OLLAMA_HOST if not on the default port

Keep them in a .env file and load it with Node's built-in flag — no extra dependency:

node --env-file=.env $(which joustx) joust

Using your Claude subscription (--use-subscription)

If you're logged in with the Claude Code CLI, you can run Anthropic agents on your Claude Pro/Max subscription instead of a metered ANTHROPIC_API_KEY:

claude /login            # once, if you haven't already
npx joustx joust --use-subscription

Jousting reads the Claude Code OAuth token from your macOS keychain or ~/.claude/.credentials.json (or the CLAUDE_CODE_OAUTH_TOKEN env var). This applies to every anthropic agent and the Herald.

The access token is refreshed automatically when it's expired (or within 5 minutes of expiring), so a long joust never dies mid-run. Jousting exchanges the stored refresh token at platform.claude.com (falling back to the legacy console.anthropic.com endpoint), then writes the rotated credential back to its origin (keychain or ~/.claude/.credentials.json) so the next run — and Claude Code itself — stay logged in. If no refresh token is available and the access token has already expired, Jousting tells you to run claude /login.

[!IMPORTANT] Anthropic rotates the refresh token on every refresh — the old one dies the moment the new one is issued. To make sure this can never log you out, Jousting writes the new credential back, reads it back to verify it landed, and aborts loudly (telling you to run claude /login) if the write can't be confirmed — rather than silently leaving Claude Code with a dead token. If you'd rather not risk it at all, use ANTHROPIC_API_KEY instead of --use-subscription.

[!WARNING] Personal use only — this may violate Anthropic's Terms of Service. Driving a Pro/Max subscription through OAuth outside the official Claude Code client is unsupported and could result in rate-limiting or account action. It is never used unless you explicitly pass --use-subscription. For anything shared, automated, or production, use a real ANTHROPIC_API_KEY.

The orchestrator and Herald never import a provider directly — they call getAdapter(provider), which dispatches to the right adapter, all behind a shared LLMAdapter interface. Provider SDKs are loaded lazily, so:

  • A missing key fails with Agent '<name>' uses <provider> but <KEY> is not set.
  • A missing optional SDK fails with Install the <provider> adapter: npm install <package>.
  • An unreachable Ollama fails with Jousting couldn't reach Ollama at <host>. Is it running?

@google/generative-ai and groq-sdk are optional peer dependencies — install them only if you use those providers.

Contributing

git clone https://github.com/chakra3301/AI-JOUSTING
cd AI-JOUSTING
npm install
npm run dev -- joust --dry-run    # run from source via tsx
npm run build                     # bundle with tsup

The codebase is small and layered: config/ (load + validate), adapters/ (LLM providers), modes/ (the five duel formats), core/ (orchestrator, agent, herald, tournament), display/ (TTY rendering), and commands/ (CLI handlers). PRs welcome — especially new modes and adapters.

License

MIT