CLI
The pleach binary ships three subcommands — dev boots a self-contained local playground; init scaffolds a route, page, and plugin stub for your framework; schema copies the Postgres bundle.
@pleach/core ships one binary, pleach, registered through the
package's bin field. It has three subcommands today:
pleach dev— boot a self-contained local playground in your browser: the four-stage lattice visualizer, the event log, an inspector, a runtime console, and the audit ledger, all driven by a real graph turn.--demoruns keyless against a canned model.pleach init— interactive wizard. Detects your stack and scaffolds a starter project (route handler, page, optional plugin stub, optional schema copy). This is the default;npx pleachwith no args runs it.pleach schema— copy the harness Postgres SQL bundle into your project so you can apply it against your database.
The binary name is pleach. The script lives at
scripts/harness-init.mjs in the package — that internal path
shows up in stack traces, but the user-facing name is the bin
name.
pleach dev — the local playground
pleach dev boots a self-contained local playground in your
browser — no host app, no build step, no database. It runs a
real graph turn through the shipped createPleachRoute() and
folds the resulting StreamEvent stream into five DX panels:
- Lattice visualizer — the four stages (
anchor-plan,tool-loop,synthesize,post-turn) light up live as nodes fire and channels are written. - Event log — the raw
StreamEventstream, the same shape the ledger and@pleach/reacthooks consume. - Inspector — per-node / per-channel state at each superstep.
- Runtime console — the bracketed runtime log breadcrumbs.
- Audit ledger — one
AuditableCallrow per LLM call, rendered as it lands.
npx pleach dev # → opens the playground in your browser
npx pleach dev --demo # keyless preview — REAL graph, canned model text
npx pleach dev --headless --prompt "…" # emit-stream capture, no browserFlags
| Flag | Default | Effect |
|---|---|---|
--demo | off | Keyless mode. The real graph runs end-to-end; only the model text is scripted (canned, deterministic). Use it to see the lattice execute with no API key. |
--sql | off | Durable persistence. Back the route's storage + checkpoint + event-log seams with a local pglite (Postgres-in-WASM) store, so chats and checkpoints survive a restart — no Docker, no cloud account. Needs the optional peer @electric-sql/pglite (npm install @electric-sql/pglite); without it, falls back to the in-memory default with a hint. |
--sql-dir <path> | ./.pleach-dev-sql | Where the pglite store lives. Implies --sql. Point it somewhere stable so a restart resumes the same chats. |
--headless | off | No browser. Drive one turn and dump the captured emit stream to stdout — for CI, sharing, or piping into another tool. |
--prompt <text> (alias --once) | a built-in multi-tool chain prompt | The user turn to run. Pairs with --headless; omitted, headless runs the default chain so the full lattice fires. |
--markdown (alias --md) | off | With --headless, render the emit capture as a Markdown session document instead of raw events. |
--plugin <path> | – | Load YOUR HarnessPlugin module into the runtime (repeatable). Test it in the browser, or headlessly with the flags below. The module exports its plugin as default, a named plugin, or a zero-arg factory. |
--scenarios <path> | – | A JSON battery of prompts (array of strings, or { name, prompt }, or { scenarios: [...] }). Drives each headlessly and prints a per-scenario report. Implies headless. |
--report <json|md> | json | Harness report format when running a battery. |
--assert <path> | – | An expectations file. Exits non-zero on any failed expectation — gate it in CI or an agent loop. |
--watch | off | Re-run the battery and DIFF the report on every --plugin file edit (run → observe → edit). |
--port <n> (alias -p) | auto | Port for the playground server. |
--no-open | off | Start the server but don't auto-open the browser. |
--help, -h | – | Print the dev help banner and exit 0. |
Key handling
pleach dev reads the active provider's <PROVIDER>_API_KEY from
the environment first, then from the in-page key panel you can
paste into without restarting. Precedence is process.env first,
then the opt-in persistence store, then none.
| Provider id | Env var |
|---|---|
openrouter | OPENROUTER_API_KEY |
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
google | GOOGLE_GENERATIVE_AI_API_KEY |
deepseek | DEEPSEEK_API_KEY |
mistral | MISTRAL_API_KEY |
moonshot | MOONSHOT_API_KEY |
Persistence is opt-in. When you paste a key into the panel you
can choose to save it to ~/.pleach/dev.env (written 0600, one
<PROVIDER>_API_KEY=<value> line per provider); the key is never
logged. Without saving, the key lives only for the running session.
If no key is detected and you didn't pass --demo, the playground
prints guidance and points you at --demo for a keyless run.
Keyless demo
--demo is the zero-config entry point: the real compiled graph
executes a full turn — anchor-plan, tool-loop, synthesize,
post-turn, channel writes, audit rows — and only the model's text
output is canned. It's the honest preview: you watch the actual
lattice run, not a mock of it. Headless demo (--headless --demo,
no --prompt) runs the default multi-tool chain so the whole
lattice fires for a CI smoke or a shareable capture.
Plugin-test harness — headless, continuous
Point the runtime at your own HarnessPlugin and drive a battery
of prompts with no browser. Each scenario folds the same surfaces
the playground tabs expose — conversation, runtime log, event log,
channel snapshot, audit ledger, and the inspectRuntime report
(capabilities wired n/total) — into one structured report, plus
derived signals (reachedTerminal, stalled, notContributed,
eventTypes, terminalSyntheses, auditFamilies, …):
# load your plugin into the live playground (test it in the browser)
npx pleach dev --plugin ./my-plugin.mjs
# headless battery → a JSON (or --report md) report per scenario
npx pleach dev --plugin ./my-plugin.mjs --scenarios ./scenarios.json --report md
# gate in CI / for an agent loop: non-zero exit on a failed expectation
npx pleach dev --plugin ./my-plugin.mjs --scenarios ./scenarios.json --assert ./expect.json
# iterate: re-run the battery + DIFF the report on every plugin edit
npx pleach dev --plugin ./my-plugin.mjs --scenarios ./scenarios.json --watchscenarios.json is an array of prompts (or { name, prompt });
expect.json declares per-scenario or all expectations. Each
predicate maps to a documented guarantee, so a green battery is a
continuously-enforced value-prop contract:
- Lifecycle —
reachedTerminal(the turn reaches a terminal answer — assert this, not the rarer naturalconverged),notStalled,finalContentIncludes. - Capability wiring —
minCapabilitiesWired,contributes,eventTypes. - Channels —
snapshotChannels(the channels a feature promises are written, e.g.costEvents/costRollup). - Audit ledger —
minAuditRows,everyAuditRowClassified(every LLM call carries acallClass),everyAuditRowTenantScoped(every row carries atenantId— isolation on every row). - Call classes —
exactlyOneTerminalSynthesis(one final answer per turn — countsterminalSynthesisrows, so a thin-draft recovery retry doesn't trip it),atMostOneTerminalSynthesis(the robust "never two final answers"; passes a passthrough turn too),maxSynthesizeCalls(caps the retry budget),auditCallClassesIn(every call's class stays within theutility | reasoning | converse | synthesizetaxonomy). - Stage lattice —
auditStagesIn(every call is stamped with a stage from theanchor-plan | tool-loop | synthesize | post-turnlattice). - Family lock —
singleFamily(the turn never silently widens across matrix families — at most one resolvedfamilyacross its audit rows).
The report is exactly the feedback an agent needs to run a
run → observe → edit loop on your plugin. To run the same loop
inside your own runtime (no CLI), flip the harness chatinit
flag.
pleach init — the wizard
The wizard detects what you already have and asks four questions:
- Template — chatbot (route + page), headless smoke script, or plugin-only.
- Provider —
anthropic,openai,openrouter,google. The default is whichever provider env var it finds first. - Plugin stub? — optional
pleach.plugin.ts(or.mjs) demonstrating three HarnessPlugin hooks. - Schema scaffold? — optional. Runs the
schemasubcommand inline. Default is on when asupabase/directory already exists.
Then it prints a write plan, asks for confirmation, and writes only files that don't already exist. Existing files are skipped, not overwritten.
npx pleach init [--template <name>] [--yes] [--cwd <dir>] [--help]Flags
| Flag | Default | Effect |
|---|---|---|
--template <name> | chatbot (with --yes) | Preselect template. One of chatbot, headless, plugin-only. Also flat / bundles to override the plugin-stub shape (default: bundles). |
--facets <list> | prompts,safety,tools | Comma-separated facet list for the plugin stub. Valid: prompts, safety, tools, stream, lifecycle. |
--facets-file <mode> | inline | Single-file inline (default) or split to scaffold facets/<name>.ts files. |
--license <SPDX> | Apache-2.0 | License for the consumer's scaffolded package.json (only when --publishing is not none). Independent of the @pleach/* substrate's own FSL-1.1-Apache-2.0 posture. |
--publishing <mode> | none (under --yes) | One of none, npm-private, npm-public. Controls package.json + README.md scaffold. |
--author <name> | null (under --yes) | Author for scaffolded package.json. Interactive mode reads npm config get init-author-name as a default. |
--repository <url> | null (under --yes) | Repository URL for scaffolded package.json. Interactive mode reads npm config get init-repository. |
--yes, -y | off | CI mode — accept all defaults without prompting. |
--cwd <dir> | process.cwd() | Run in a different directory. |
--help, -h | – | Print the init help banner and exit 0. |
What it detects
All best-effort, all from the filesystem and env:
| Signal | Values | How |
|---|---|---|
| Framework | next-app, next-pages, astro, sveltekit, remix, vite, react-other, node | package.json deps + presence of app/ |
| Package manager | bun, pnpm, yarn, npm | Lockfile presence |
@pleach/core installed | yes / no | package.json deps |
| TypeScript project | yes / no | tsconfig.json exists |
| Supabase migrations dir | yes / no | supabase/ exists |
| Provider env vars | which provider keys are reachable | process.env first, then .env.local, .env.development, .env in that order |
Detection runs most-specific to least-specific. Next.js wins over Vite over plain React, because plain React often hides under a Next or Vite shell. The fallback for "nothing matched" is plain Node.
The .env* walk is intentionally minimal — no interpolation, no
multi-line values, first occurrence wins. Just enough to surface
a provider key you set but never exported.
Provider env-var matrix
| Provider id | Env var |
|---|---|
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
openrouter | OPENROUTER_API_KEY |
google | GOOGLE_GENERATIVE_AI_API_KEY |
The wizard offers every provider whose env var it can find. The
first reachable one is the default selection; if none is set,
the default is anthropic.
The wizard's 4-provider list is narrower than the runtime's. The
quickstart subpath's
providerDetection covers 7 providers (adds deepseek,
mistral, moonshot). If only a non-wizard provider env var is
set, the wizard reports "none detected" and defaults to
anthropic — but the runtime createPleachRoute your app boots
will still detect and use the provider correctly. The wizard's
list only drives the prompt question; the deployed handler reads
the broader runtime list.
Templates
| Template | Framework | Files written |
|---|---|---|
chatbot | next-app | app/api/chat/route.ts, app/page.tsx |
chatbot | next-pages | pages/api/chat.ts, pages/index.tsx |
chatbot | astro | src/pages/api/chat.ts, src/pages/index.astro |
chatbot | sveltekit | src/routes/api/chat/+server.ts, src/routes/+page.svelte |
chatbot | remix | app/routes/api.chat.ts, app/routes/_index.tsx |
chatbot | vite / react-other | src/App.tsx (no server route — pair with a Node server) |
headless | any | agent.mjs — smoke script using SessionRuntime |
plugin-only | any | pleach.plugin.{ts,mjs} (bundled stub by default; --template flat for legacy single-facet) |
The chatbot route templates import createPleachRoute from
@pleach/core/quickstart; the page
templates import ChatBox from the same subpath. The headless
template uses SessionRuntime from
@pleach/core directly. The plugin stub uses createPlugin,
appendPrompt, and gatedPrompt — three of the
plugin contract hooks. For the full
quickstart subpath surface (createPleachRoute, useChat,
<ChatBox />, defaultPlugin, createBenchmarkPlugin,
providerDetection), see
Getting started → The quickstart subpath.
Idempotence
Files that already exist are skipped, never overwritten. The
wizard's done block reports written, skipped, and failed
counts. For a clean re-run, delete the files the wizard wrote
the first time.
CI mode
--yes runs without prompts. Defaults: template = chatbot,
provider = whichever the env detection picked (or anthropic),
withPlugin = true for chatbot or plugin-only,
withSchema = true if supabase/ exists.
npx pleach init --yes --template plugin-only --cwd ./my-appNon-TTY invocations (CI runners, piped stdin) take the --yes
path automatically even without the flag.
Exit codes
| Code | When |
|---|---|
| 0 | Success, help, or --yes with zero write errors |
| 1 | One or more files failed to write |
Worked example — Next.js App Router
$ npx pleach init
pleach init — charm-style wizard for @pleach/core
┌─ environment
│ directory › /Users/you/my-app
│ framework › Next.js (App Router) (tsconfig.json)
│ package mgr › pnpm
│ @pleach/core › not yet installed
│ provider env › anthropic (.env.local)
│ supabase/ › found
└─
? Template
● 1) Chatbot (route + page) default — streaming UI
○ 2) Headless smoke script node agent.mjs, no UI
○ 3) Plugin stub only add to an existing app
› (1-3, default 1)
? Provider
● 1) anthropic ANTHROPIC_API_KEY set
○ 2) openai OPENAI_API_KEY unset
○ 3) openrouter OPENROUTER_API_KEY unset
○ 4) google GOOGLE_GENERATIVE_AI_API_KEY unset
› (1-4, default 1)
? Generate a sample plugin stub? (y/N) y
› plugin name (default: my-plugin) triage-plugin
? Scaffold Postgres schema into supabase/migrations? (Y/n)
┌─ write plan
│ • app/api/chat/route.ts
│ • app/page.tsx
│ • pleach.plugin.ts
│ • supabase/migrations/20260608120000_pleach_<bundle-file>.sql (from schema bundle)
└─
? Write 4 file(s)? (Y/n)
done
✓ wrote app/api/chat/route.ts
✓ wrote app/page.tsx
✓ wrote pleach.plugin.ts
✓ wrote supabase/migrations/20260608120000_pleach_<bundle-file>.sql
next steps
1. pnpm add @pleach/core
2. ANTHROPIC_API_KEY loaded from .env.local (framework auto-loads at dev time)
3. pnpm dev — chat lives at the root pageThe "next steps" block adapts to the package manager the wizard
detected. With bun it suggests bun add and bun run dev; with
npm, the npm equivalents. Same with the provider-key line — if
the key was found in process.env, the line is dropped from
next-steps entirely.
Plugin stub shape
When template = plugin-only (or chatbot with --add-plugin), the
wizard scaffolds a pleach.plugin.ts file. The default shape is now
bundle-aware: each facet (prompts, safety, tools by default)
gets its own definePleachPlugin const, composed into a single
exported plugin array.
// pleach.plugin.ts (default — bundle-aware shape)
import { definePleachPlugin } from "@pleach/core";
const promptsFacet = definePleachPlugin("my-plugin", { prompts: [] });
const safetyFacet = definePleachPlugin("my-plugin", { safetyPolicies: [] });
const toolsFacet = definePleachPlugin("my-plugin", { tools: [] });
export const myPlugin = [promptsFacet, safetyFacet, toolsFacet];Wire at the runtime registration site via array spread:
new SessionRuntime({ plugins: [...myPlugin] });This shape teaches the multi-facet pattern from day 1 — when a facet
grows past ~50 lines, move its const to facets/<facet>.ts per
plugin authoring standards.
The single-facet shape (one definePleachPlugin call covering all
hooks) is still available via --template flat:
npx pleach init --template flatSee plugin bundles for the rationale behind
the multi-facet pattern and the planned composePlugin upgrade path.
pleach init plugin — monorepo subcommand
For an existing project that needs to add a plugin without re-running framework detection:
npx pleach init plugin --name corpus-safety --facets safety,promptsScaffolds plugins/corpus-safety/ with four files:
| File | Content |
|---|---|
src/pleach.plugin.ts | Bundle-aware stub with the requested facets |
package.json | License + peer-dep + scripts, standards-aware defaults |
README.md | Install + use + standards reference seed |
tsconfig.json | TypeScript config seed (ES2022, strict, declaration emit) |
If a tsconfig.json exists at the top level, the wizard also inserts
a path-map entry (compilerOptions.paths["corpus-safety"]) as a
courtesy. Best-effort: skips and warns if the tsconfig can't be parsed.
Subcommand flags
| Flag | Default | Effect |
|---|---|---|
--name <kebab> | required | Plugin identity. Required under --yes. |
--facets <list> | prompts,safety,tools | Same vocabulary as pleach init. |
--template flat|bundles | bundles | Stub shape override. |
--facets-file inline|split | inline | Single-file vs split-file layout. |
--at <path> | plugins/<name>/ | Scaffold target. |
--license <SPDX> | Apache-2.0 | License id for the consumer's generated package.json. (Independent of the @pleach/* substrate's own FSL-1.1-Apache-2.0 posture.) |
--publishing <mode> | npm-private | One of none, npm-private, npm-public. |
--author <name> | (none) | Skipped under --yes unless supplied. |
--repository <url> | (none) | Skipped under --yes unless supplied. |
--yes, -y | off | CI mode. --name becomes required. |
--cwd <dir> | process.cwd() | Run in a different directory. |
--help, -h | – | Print the subcommand help banner and exit 0. |
The subcommand never modifies the top-level package.json or app
code beyond the optional path-map insertion. It writes only inside
the target directory.
pleach schema — copy the SQL bundle
Scaffolds the harness storage schema into a target directory.
This is the surface that used to live under pleach init;
pleach init --apply still routes here so existing scripts
don't break.
npx pleach schema [--target <dir>] [--dry-run|--apply] [--timestamped] [--help]Flags
| Flag | Default | Effect |
|---|---|---|
--target <dir> | ./supabase/migrations if it exists, else ./harness-migrations | Destination for the copied SQL files |
--dry-run | on | Print the file-copy plan; write nothing |
--apply | off | Actually write the files |
--timestamped | off | Prefix each file with YYYYMMDDHHMMSS_ (UTC) so Supabase migration ordering is stable |
--help, -h | – | Print the banner and exit 0 |
What it does
- Locates the schema bundle inside the installed package
(
node_modules/@pleach/core/dist/schema/postgres/). Falls back to the in-repo source tree when running against an unbuilt checkout. - Optionally rewrites each filename with a UTC timestamp prefix. Multiple files in the same second get monotonically incremented suffixes — Supabase's lexicographic order stays stable.
- Copies the files into your target directory (creating it if needed).
What it doesn't do
- Connect to your database.
- Run the migrations.
- Overwrite existing files. If a target file is already there,
schema --applyrefuses and exits 3 — read the manual-apply block below.
The full apply path is intentionally not in the CLI. Until a real consumer asks for it, the dry-run + manual-apply flow is the supported path and the surface stays small.
Exit codes
| Code | When |
|---|---|
| 0 | Success or dry-run completed |
| 2 | Schema bundle missing (is @pleach/core installed?) or no SQL files in the bundle |
| 3 | A target file already exists — refused to overwrite |
Applying the migrations by hand
After pleach schema --apply:
for f in supabase/migrations/*pleach*.sql; do
psql "$DATABASE_URL" -f "$f"
doneIf you're using the Supabase CLI:
pleach schema --apply --timestamped
supabase db pushThe schema bundle is idempotent at the DDL level — each file
uses CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS,
so re-applying is safe but won't migrate column shapes. Schema
evolution lands as new files in the bundle; the package's
CHANGELOG.md records the version each new migration shipped
in.
Local development without a database
If you don't want to run any migrations during development, skip the CLI entirely and set:
HARNESS_MOCK_MODE=trueThe runtime wires MemoryAdapter + MemorySaver automatically.
See environment variables for the full list of
runtime-read env vars.
Project layout — after the wizard
The baseline plus
the files pleach init writes. This is what a Next.js App
Router project looks like one command after npx pleach init
on the chatbot template with plugin stub and schema scaffold:
my-app/
app/
api/
chat/route.ts # wizard-written — createPleachRoute({ provider, plugins })
page.tsx # wizard-written — <ChatBox apiUrl="/api/chat" />
pleach.plugin.ts # wizard-written — createPlugin + appendPrompt + gatedPrompt
supabase/
migrations/
20260608120000_pleach_*.sql # wizard-copied from the schema bundle
.env.local # ANTHROPIC_API_KEY (or your provider key)
package.json
tsconfig.jsonWhat changes from the baseline:
- The route handler is one line.
createPleachRoute({...})is what you'd normally write by hand; the wizard writes it for you. Wire your ownHarnessPluginset into theplugins:array as you grow. pleach.plugin.tsis the starting point, not the final shape. The stub shows three hooks —prompts,runtimePrompts, and the plugin-construction call. The plugin contract covers the full set (contributeTools,contributeSafetyPolicies, etc.); replace the stub when you outgrow it.supabase/migrations/holds the schema bundle. Filenames are timestamped so Supabase's lexicographic migration order matches the bundle order. Without--timestamped, the files keep their bundle names — fine if you're applying them by hand but loses ordering when fed throughsupabase db push.
Sibling-SKU binaries
Sibling SKUs (@pleach/compliance@0.1.0,
@pleach/eval@0.1.0, @pleach/gateway@0.1.0,
@pleach/replay@0.1.0, @pleach/mcp@0.1.0,
@pleach/coding-agent@0.1.0) are shipping today. The
CLI convention is one bin per package under its own name in the
@pleach/ scope — @pleach/compliance exposes
pleach-compliance, @pleach/eval exposes pleach-eval, and so
on. The pleach bin in core is reserved for substrate-level
operations. Per-sibling bins land alongside each SKU's first
useful command surface; check the package's npm page for the
current bin inventory.
Where to go next
Getting started
Install, wire a SessionRuntime, stream your first message.
Project layout
The orienting layout the wizard produces — described, not prescribed.
Plugin contract
The full HarnessPlugin surface the wizard's plugin stub demonstrates three hooks of.
Environment variables
Every env var the runtime reads, with defaults.
Subpath exports
The schema bundle and every other named subpath you can import.
DevTools
`window.__HARNESS_DEVTOOLS__` — the browser-console surface for inspecting sessions, walking checkpoints, forcing syncs, and listing tools during development.
Agent shapes
Reference agent designs Pleach is built for — each pairs a recurring production pattern with the runtime primitive that handles it and the team shape it shows up in.