warpline
v0.3.4
Published
Deterministic plugin runtime with LLM-judgment dispatch and side-effect approval gates
Readme
Warpline
A deterministic plugin runtime where the LLM is a step you dispatch, not a capability the code carries around.
Warpline runs the recurring work that keeps a business going, on a schedule. Outbound sequencing, market research pulls, content pipelines, competitor monitoring, lead qualification, report generation.
A plugin that declares a side effect (sending email, creating issues, writing
to a database, calling external APIs, modifying files) doesn't run without
explicit human session approval. That includes at autonomous, because the
autonomy level is dispatch autonomy. It decides whether the scheduler can
start a plugin without asking. It never decides whether that plugin can act on
the world without asking.
Blanket approval exists and it's deliberate. warpline approve --all writes a
wildcard grant. What it covers, what it prints before writing anything, when it
expires, and why a run can't widen its own scope:
Can the gate be bypassed?.
Published docs: warplinehq.github.io/warpline.
warpline plan — preview only; nothing was executed.
Grant: none — plugins with side effects would be SKIPPED this run
Plugins: /tmp/warpline-demo/plugins
Due (3):
anomaly-watch (level 0)
(no declared side effects)
feed-triage (level 0)
(no declared side effects)
metrics-rollup (level 0)
(no declared side effects)
Not due (3):
feed-monitor — skipped (unapproved): side effects require session approval
external_api: ⚠ unapproved — would be SKIPPED this run
github-poll — skipped (unapproved): side effects require session approval
external_api: ⚠ unapproved — would be SKIPPED this run
anomaly-issue — skipped (unapproved): side effects require session approval
creates_issue: ⚠ unapproved — would be SKIPPED this run
external_api: ⚠ unapproved — would be SKIPPED this runQuickstart
npm i warpline # or: bun add warpline
npx warpline --helpRuns on Node 22.18+ or 23.6+ (engines.node excludes 23.0–23.5), or Bun ≥ 1.3.
Node alone is enough. You don't need Bun to use warpline. v0.1 supports POSIX
systems (macOS, Linux). Windows is untested and unclaimed.
Every file warpline reads or writes lives under one home directory: the
WARPLINE_HOME env var, else the nearest ancestor .warpline/ directory,
else <cwd>/.warpline.
What you get
- Plugin runtime — Zod-validated manifests; per-attempt timeouts; bounded
retries with exponential backoff + jitter;
AbortSignalthreaded into your I/O; per-run artifacts (runs/<id>.json+ captured log with attempt delimiters). - Side-effect approval gate — a closed enum of side-effect types declared
per plugin; declared effects gate execution behind
warpline approve-style session approval, regardless of autonomy level. - Engine — TTL freshness (skip work that is still fresh), dependency
topological ordering, quiet hours, review gate, and idle-based degradation
tiers (
normal → degraded → extended → suspended). - Event board — append-only
events.jsonl+ acknowledgements; tasks with ack / defer / complete states and severity-FIFO ordering. - The
[needs-llm]contract — plugins emit judgment work as a typed handoff; a Claude Code companion skill picks it up. Deterministic work costs nothing to run; judgment work uses your existing Claude subscription. See docs/needs-llm-contract.md.
Writing a plugin
A plugin is a directory under <home>/plugins/<name>/ with two files:
my-plugin/
manifest.ts # export const manifest = PluginManifestSchema.parse({...})
handler.ts # export const handler: HandlerFn = async (manifest, args, signal) => SkillResultThe manifest declares what the plugin is allowed to do (side effects,
schedule, TTL, timeout, retries, minimum degradation tier). The handler does
the work and returns a structured SkillResult. Invalid manifests are a
hard-stop at load, so a misconfigured plugin never silently runs.
Worked examples in examples/plugins/:
| Example | Demonstrates |
| --- | --- |
| anomaly-watch | A pure deterministic check — the baseline shape |
| github-poll | external_api side effect gating an autonomous plugin |
| feed-monitor | Deterministic fetch/parse that emits the handoff — the producer half of the feed chain |
| feed-triage | The on_run consumer half — per-entry judgment handed off via [needs-llm], no declared side effects |
| metrics-rollup | daily schedule with retained state — append-only rows, a retention window, weekly rollups; writes only under the home |
| anomaly-issue | dependencies ordering after anomaly-watch, supervised autonomy, and a creates_issue side effect through the gate — irreversible, so the result says how to undo it |
Authoring guide: docs/plugin-authoring.md.
# Scaffold a plugin — also prepares the home directory
npx warpline scaffold my-plugin
# Preview what the next engine advance would do. Executes nothing.
npx warpline plan
# Invoke one plugin handler directly
npx warpline run my-plugin default
# Answer whichever gate is waiting — record a parked result, or grant a
# side-effecting plugin permission to run for this session
npx warpline approve my-plugin
# Say no to what a plugin proposed. The answer is bound to the proposal, so
# the question comes back if what it proposes changes.
npx warpline deny my-plugin
# Clear the session approval
npx warpline revokeThose six subcommands are the whole CLI surface. Running everything
that's due on a schedule is a library call, not a command. It's runAdvance()
from the package root:
import { runAdvance } from 'warpline'
const result = await runAdvance()Where the LLM fits
If you can write an
if/elsefor it, it's code. If it needs understanding or judgment, it's an LLM task, and the plugin hands it off instead of calling a model.
Read the full doctrine: docs/doctrine.md.
Nowhere in this repo. That's the point. Plugins that reach a judgment step
return a [needs-llm] handoff (mapped to the delegated run status, never
retried). An orchestrating Claude Code session consumes those handoffs via
companion skills, and a template lives in
skills/needs-llm-template/.
That directory is deliberately not shipped in the package, so the link is
absolute. It resolves the same from npm, from GitHub, and from node_modules.
Side effects that follow from judgment work still go through the approval
gate.
FAQ
Can the gate be bypassed?
Only deliberately, and only by a human at a keyboard. warpline approve --all
is the one route to a wildcard scopes: '*' grant, and it refuses to run if you
also name plugins, so no plugin name, glob or shell expansion can widen a grant
past what you typed. It prints the coverage it's about to grant before it writes
anything. The grant is session-scoped and it expires. See
the session approval file for
the default lifetime and the ceiling from first grant. And nothing inside a run
can widen its own scope. The grant helpers have no caller on the advance path,
so a run can only ever spend approval a human already gave.
Why not just let the plugin call a model?
Because a plugin that calls a model has quietly made every future rerun an
experiment. The deterministic half stops being reproducible, the judgment half
stops being reviewable, and you find out which was which when they disagree
with each other. If you can write an if/else for it, it's code. If it needs
understanding, it's a handoff. Keep the boundary in the manifest and you can
read a plugin and know which one you're looking at.
Do I need Bun / a Claude subscription?
No to both, in different ways. Node 22.18+ or 23.6+ is enough to install and
run warpline. 23.0–23.5 is excluded by engines.node, and npm reports
EBADENGINE there (a hard failure wherever engine-strict is set). Bun is the
development runtime for this repository's own test suite, not a user
requirement. The [needs-llm] half is a handoff, not an API call. It uses
whatever Claude Code session you already have, and if nobody ever picks a
handoff up, the deterministic work carries on running without it.
Docs
- docs/first-plugin.md — start here: build, run and gate a plugin in ten minutes
- docs/doctrine.md — the deterministic/LLM boundary
- docs/runtime-spec.md — manifest fields, retry/timeout/abort semantics, run artifacts
- docs/board-spec.md — the Board: objects, Ask lifecycle, places, form, file formats. The board is a repo-only surface at 0.1, so this spec is not shipped in the package and the link is absolute.
- docs/needs-llm-contract.md — the LLM handoff protocol
- docs/plugin-authoring.md — writing and testing plugins
- docs/why-the-gate-holds.md — the long argument: why the gate holds, and the objections it has to survive
From source
Cloning gets you the test suite and the board, neither of which ships in the package:
git clone https://github.com/warplinehq/warpline
cd warpline
bun install
bun run test # builds, then runs the full suiteRequires bun ≥ 1.3. This is the one place it's genuinely
required, because the suite is written against bun:test.
# The board — a repo-only surface at 0.1, not wired into the published bin
bun run src/cli/board-cli.ts status
bun run src/cli/board-cli.ts tasksProvenance
Warpline was extracted in August 2026 from the private automation engine that's run one company's marketing operations since early 2026. The domain plugins stayed behind. The runtime, gates, board, and doctrine are what generalised.
License
Apache-2.0
