Sends a scripted scenario to a server so a handle means something outside your own checkout
Zpush uploads a .zas script to a server under a handle, and optionally
creates a room to run it in. This is what lets you drive a scripted scenario
through the UI of a server that does not have the repo.
npm run zpush --workspace @zooly/z-agent-srv -- --scenario KenjiVsSolstice --create-room
The same push-and-create-room step is also available from the browser, with no terminal — see the Zauthor tab in the Playground.
A .zas you authored or edited is not automatically on a remote server.
Uploading stores the script's source text in z_agent_mock_tape, keyed by
handle, so any server can resolve it.
The checked-in fixtures under packages/z-agent/srv/scripts/test-scenario/
are a separate case. That folder is not copied into the Vercel serverless
function, so at build time the files are inlined into the server bundle.
That copy is what the Playground's Load an existing scenario dropdown
uses on production — see the Zauthor tab. You
still push after loading if you want the script stored under a handle (and
to create a room from it). A script that is not one of those fixtures still
has to be uploaded; there is nothing to inline.
A handle is looked up in three places, in order:
The source text is stored, not the compiled tape. Editing a scenario stays a single upload, and the tape is compiled against whichever room actually runs it — so a stored copy can never disagree with that room's participants or declared goals.
| Option | Default | What it does |
|---|---|---|
--scenario <name> | — | Folder holding <name>.zas. Required unless --script is given |
--script <path> | — | A script file directly; needs --id for the handle |
--id <handle> | the scenario name | Handle to store under |
--url <base> | http://localhost:3004 | Server to push to, or Z_AGENT_MOCK_URL |
--secret <token> | Z_AGENT_MOCK_SECRET | Bearer token for the upload |
--note <text> | — | Stored alongside the script, e.g. why it was pushed |
--create-room | off | Also create a room and print its id and URL |
--talent-account <q> | — | Bind the talent seat when creating a room |
--brand-account <q> | — | Bind the brand seat |
--real-room | off | The created room is a real deal room, visible in both accounts' own dashboards, instead of an admin-only playground room. Requires both account flags |
--guest-brand | off | Leaves the brand seat an anonymous guest instead of binding --brand-account, so every push and any offer-minting effect is composed but held. Not supported with --real-room or --brand-account |
--send-emails | off | Lets the created room's wake turns send their real push emails too |
--list | off | List the handles a server already has |
Pushing to a remote and creating a bound room in one go:
npm run zpush --workspace @zooly/z-agent-srv -- \
--scenario KenjiVsSolstice \
--url https://app.example.com \
--create-room \
--talent-account "Asi Meskin" \
--brand-account "Elia Weiss123" \
--note "checking the gray-zone path on staging"
Add --real-room to make that same room show up in both accounts' own
/dashboard/conversations instead of only the admin playground — see
Zreplay for what it makes visible (the conversation
and any minted offer — still flagged isDemo and marked Demo in Offers & Deals).
The upload routes accept either a bearer token or an admin session cookie. The token exists because a CLI has no cookie, especially when pointed at a remote server:
# on the server
Z_AGENT_MOCK_ENABLED=true
Z_AGENT_MOCK_SECRET=<a long random string>
# locally, so zpush picks it up without --secret
Z_AGENT_MOCK_SECRET=<the same string>
Two failure modes worth recognising:
Z_AGENT_MOCK_ENABLED unset, which hides these routes
entirely rather than leaving an upload endpoint exposed where scripting was
never wanted.The script is compiled before it is stored, so a malformed scenario is rejected with its line number rather than surfacing later as a tape miss halfway through a conversation:
Push to http://localhost:3004 failed (400):
Line 47: "@plan push brand" has no matching "@wake brand"; the woken persona
has no scripted line to deliver.
A successful push reports what the scenario will do, which is worth reading before you open the room:
Pushed "KenjiVsSolstice" to http://localhost:3004
from: .../test-scenario/KenjiVsSolstice/KenjiVsSolstice.zas
turns: 7
mints an offer: yes
goals: talent-setup=met, brand-offer=met
offer-gen 1
persona-brand 4
persona-brand:wake 2
persona-talent 3
persona-talent:wake 3
planner-guide 7
planner-distil-mem 7
Uploading also clears the server's in-memory tape cache, so a room already running that handle picks up what you just pushed on its next turn instead of the version the process first compiled.
--send-emails works from a remote pushzreplay --send-emails sets an environment variable for its own short-lived
process, which is fine because that process only ever drives one run. zpush
has no such luxury — it is a separate process from whatever server it is
pushing to, often on a different machine entirely, so there is no environment
variable to set. --send-emails here instead asks the room-creation route to
persist the choice on the room itself (mock_send_emails), which is also
what the Playground's Zauthor tab uses — a setting that belongs to one room
has to be stored somewhere that outlives any single request.
If the server is on the same machine as your checkout, you never need HTTP.
zreplay can store the script and create the room in one step:
npm run zreplay --workspace @zooly/z-agent-srv -- \
--scenario KenjiVsSolstice --push --create-only \
--talent-account "Asi Meskin" --brand-account "Elia Weiss123"
--push stores the source under its handle, and --create-only creates the room
and stops without driving the script, leaving the conversation at its first line
for you to drive by hand.
room handle: k91d04s43cbmhstxssfjv
tape handle: KenjiVsSolstice
first line: KenjiVsSolstice opens in the "brand" pane
Open it:
http://localhost:3004/z-agent/playground?room=k91d04s43cbmhstxssfjv
| Route | What it does |
|---|---|
GET /api/z-agent/mock-tapes | Lists stored handles |
POST /api/z-agent/mock-tapes?id=<handle> | Body is the .zas script verbatim; compiles, stores, returns the summary |
POST /api/z-agent/mock-tapes/<handle>/rooms | Creates a room running that handle; optional talentAccount / brandAccount in the JSON body |
GET /api/z-agent/rooms/<id>/script | The scripted human lines, for a "next line" affordance |
DELETE /api/z-agent/rooms/<id>/script | Rewinds the cursors so the room replays from the top |