Community harness presets
A community harness is a coding harness an outside contributor maintains. You install it as one small data file, and a
rig member runs it by naming it as its runtime. OpenRig doesn't test these harnesses. It shows them as community
harnesses, and it reads their activity only from what the screen shows.
For what an integration author owns end to end, including a runner's duties and running a harness somewhere else, see the draft integration author contracts.
What a community harness gets
What OpenRig does for a community-harness seat, capability by capability. Partly and no mean OpenRig shows that it doesn't know; it never reads an unsupported capability as idle, working, ready or verified.
| Capability | Supported | How it works, or what you see instead |
|---|---|---|
| Launch | yes | OpenRig types the preset's command into the seat's pane, with its variables passed through /usr/bin/env. The seat reads as ready once something other than a shell runs in the pane; that's a process check, not proof of which conversation it runs. With startup_assisted, the seat reports attention required until a person checks it. |
| Delivery | yes | rig send and queue wakes paste the text into the pane and press Enter, as for any seat. A pane that's back at a shell is refused (target_runtime_unverified) and nothing is typed. OpenRig doesn't confirm the harness took the message; rig send --verify checks the pane afterwards. |
| Identity | when declared | With launch_result, the conversation is shown as reported by its launcher; with names_conversation, as named by OpenRig. Neither is observed by OpenRig. Without either, the identity is unavailable. |
| Resume | when declared | resume: true with one of the two above (see Reopening a conversation). Without it, a resume says Resume is unsupported: community harness "<name>" is launch-only, and a restore stops and asks. |
| Activity | partly | Read from the screen only; there are no activity hooks. Work and attention the screen shows are kept, but a screen that looks idle reads unknown, never idle, because a quiet prompt can be an approval or a pause. A runner preset's own reports decide it instead. |
| Attention | partly | Read from the screen: a permission question or numbered choice that OpenRig recognizes reads as needing attention, and rig send won't type into it. A prompt it doesn't recognize reads unknown. |
| Approval | no | Approvals stay in the harness's own pane. OpenRig relays none, so a person answers them there. |
| Cancel | no | OpenRig has no verb that cancels a running turn. rig seat stop and rig down end the seat's tmux session; anything the harness runs outside its pane, such as a gateway, is its own, and the preset's limits should say what keeps running. Restarting in place and switching harness are refused. |
| Transport | yes | The seat is a tmux pane like any other, so rig capture, rig send and --host work as they do for any seat. |
Not placed: guidance, skills and startup files (a launch warns that they weren't delivered), and the model: set it in the harness's own
configuration, or let a launcher that opts in take the seat's declared model (passes_model, below).
Check that the harness answers on its own first. Before writing a preset, start the harness in a terminal outside OpenRig, with the
same configuration and credentials your launcher will give it, and send it one prompt. Use the folder your launcher gives the seat, not
the harness's default one: the example launchers keep one per seat under $OPENRIG_HOME/state/<harness>/ (HERMES_HOME,
OPENCLAW_STATE_DIR or CURSOR_CONFIG_DIR), so a check with your own defaults can pass while the seat's setup fails. If no answer comes
back, the problem is in the harness's own setup, such as its model, provider or login, and relaunching an OpenRig seat won't fix it. Fix
it in the harness, then connect it.
The preset
One YAML file per harness: $OPENRIG_HOME/community-harnesses/<name>.yaml. rig community-harness list shows what's
installed.
format: 1
name: example-harness # must match the file name: lowercase letters, digits and hyphens
command: example-launch # one program and its arguments, typed into the seat's pane
maintainer: Example Maintainer
limits: # what the maintainer says doesn't work
- fork unsupported
launch_result: true # optional: the launcher reports the conversation it created (below)
# names_conversation: true # optional, instead of launch_result: OpenRig names the conversation (below)
resume: true # optional, needs one of those two; declare it only once your harness's resume is checked
startup_assisted: true # optional: startup submission needs a person's check (below)
first_input: true # optional: launcher takes the first turn from OPENRIG_FIRST_INPUT (below)
# runner: true # optional, needs launch_result: the launcher reports the seat's activity (below)
# passes_model: true # optional: the launcher takes the seat's declared model from OPENRIG_MODEL (below)
commandis a single program and its arguments. Shell syntax such as|,&&,;, redirections or$(…)is refused: put it in a script and name the script.- Built-in runtime names are reserved. A preset can't replace
claude-code,codexor the other built-ins. - Unknown fields are refused, so a preset written for a newer OpenRig says so instead of half-working.
- Fork isn't supported, and resume only as described below. Neither is placing skills, guidance or startup files. Except for assisted startup below, the seat is called ready while something other than a shell runs in its pane, which says nothing about which conversation it is.
Assisted startup
Set startup_assisted: true when startup submission needs a person's check. The seat may start its first turn
automatically, including from input passed at launch with first_input: true. This declaration requires a check after
the submission attempt; it does not hold the agent back. Without first_input, OpenRig pastes the startup text and
presses Enter once.
After attempting startup input, OpenRig reports attention required: check the pane and submit any text still in the
composer. OpenRig does not resend the identity prompt. Once the seat responds, use the reported
rig seat clear-attention <session> --reason "Startup submission checked; seat responded" command to acknowledge the
check. A launch with no startup text needs no such check. Other presets keep their existing behavior.
Reporting the conversation a launch created (launch_result: true)
Some harnesses create their own conversation id before they start. Cursor's cursor-agent create-chat is one. With
launch_result: true, the harness's launcher tells OpenRig that id for each launch.
- OpenRig names one file per launch attempt. It starts
commandwith two variables:OPENRIG_LAUNCH_RESULT, a new file path inside$OPENRIG_HOME/state/launch-results/;OPENRIG_LAUNCH_ATTEMPT, this attempt's id.
- The launcher writes one JSON record to that path, then starts the harness:
{ "format": 1, "attempt": "<OPENRIG_LAUNCH_ATTEMPT>", "harness": "cursor", "nativeRef": "<the conversation id>", "producer": "openrig-cursor-launch 1.0" } - OpenRig reads that attempt's record once, waiting up to 20 seconds for it. It records
nativeRefon the session, and the seat shows its identity as reported by its launcher, not observed. That's what the launcher says, not proof that the harness used it.
The seat still starts, with its identity unavailable and the reason named, when the record:
- wasn't written;
- belongs to another attempt or another harness;
- isn't JSON, isn't format 1, or is larger than 64 KiB;
- has an unusable
nativeRef(empty, too long, or containing spaces or control characters); - names no producer;
- or is reached through a link.
None of these means the conversation is gone.
A reported id doesn't make the harness resumable on its own. Without resume: true, nothing resumes it: a restore
stops and asks, as it did before (re-run it with --fresh <seat> only if you want a new conversation), and elsewhere a
resume says it's unsupported, and a new conversation starts only where the caller allows a fresh start.
Good to know:
- The variables are passed with
/usr/bin/env, solaunch_resultneeds a POSIX system. - A launcher that never writes its record adds up to 20 seconds to each launch, while OpenRig waits for it.
- Records are kept, one small file per attempt, under
$OPENRIG_HOME/state/launch-results/<session>/. A record larger than 64 KiB isn't read.
Letting OpenRig name the conversation (names_conversation: true)
Some harnesses take a conversation name from the caller and create that conversation, or reopen it. Hermes's
hermes chat -c <name> --create-if-missing is one. With names_conversation: true, OpenRig names each new conversation
and the harness uses that name, so no record is needed.
- A new launch: OpenRig starts
commandwithOPENRIG_CONVERSATIONset to a new name,openrig-<seat>-<8 hex digits>(at most 100 characters). Each launch gets its own name, so two seats never share a conversation. - What the seat shows: OpenRig records that name on the session, and the seat shows its identity as named by OpenRig, not observed. That's the name OpenRig passed, not proof that the harness used it.
- Not with
launch_result: a preset declares one or the other. Either the launcher reports the conversation, or OpenRig names it.
Reopening a conversation (resume: true)
A named conversation: with names_conversation, a resume starts command with OPENRIG_RESUME_REF set to the saved
name. The harness reopens it and must never create one under that name. Nothing reports back, so:
- readiness is process-only: the seat reads as ready while something other than a shell runs in its pane. That doesn't show the named conversation exists: a harness (or a launcher's own check) that hasn't yet found it gone reads as ready, and the saved name stays unverified until something observes it;
- never creating one is the launcher's job: a resume must not create a conversation under the saved name. When it's gone, the harness or launcher ends with its error and the pane is back at a shell; the seat keeps its saved name, and OpenRig starts nothing new, though the seat may already have read as ready;
- a former runtime's id isn't passed to the harness, as below.
A reported conversation: with launch_result, a resume (a restore included) starts the launcher with OPENRIG_RESUME_REF set to the
conversation to reopen. The launcher reports what happened in the same attempt's file:
- Reopened: the usual record, with that conversation as
nativeRef. - Gone:
{ "format": 1, "attempt": "…", "harness": "cursor", "error": "missing", "producer": "…" }. The launcher starts nothing. OpenRig reports the conversation missing and starts nothing either: it never replaces it with a new one.
For a reported conversation, OpenRig also says so plainly when:
- no usable record is written: the resume isn't confirmed. The seat needs attention and keeps the conversation it asked for; nothing is restarted;
- the record names a different conversation: the seat needs attention and keeps the conversation it asked for. OpenRig doesn't switch it to the reported one;
- the saved conversation isn't this harness's own (a former runtime's id, say): it isn't passed to the launcher. A restore stops and asks, and elsewhere the resume says it isn't possible;
- the record's folder can't be made: the launcher couldn't be told what to reopen, so nothing starts.
Runner presets (runner: true)
A plain preset's seat reads its activity from the screen. A runner preset is for a launcher that hosts the agent itself and reports what it's doing, as OpenRig's own Pi runner does. The seat's activity then follows those reports.
- It needs
launch_result: true. A runner reports the conversation it opened in its launch record, sorunner: truewithoutlaunch_resultis refused. - The declaration is fixed at launch. Each launch, a restore's included, records on the session what the preset declares at that moment. Editing the preset changes the next launch, not a running seat. A session recorded before this field existed reads as plain.
- No record means attention. When a runner's launch record doesn't arrive within the wait, or its folder can't be
made, the seat needs attention instead of reading ready. A plain
launch_resultpreset still starts, with its identity unavailable. - Input is as for any seat. A send pastes the text and presses Enter, so a runner should submit everything up to that Enter as one prompt.
What the runner posts. The runner posts each event to POST /api/activity/hooks with the local activity hook token,
one at a time and in order, without making the agent wait. It finds the address and token in OPENRIG_URL (or
OPENRIG_PORT) and OPENRIG_ACTIVITY_HOOK_TOKEN, or else in $OPENRIG_HOME/activity-endpoint.json. Every post names:
- the seat, as
sessionNameandnodeId; runtime, the preset's name;generation, fromOPENRIG_OCCUPANT_GENERATION;- an increasing
sequence, andoccurredAt; hookEventandsubtype, as in the table.
| The runner posts | When | The seat reads |
|---|---|---|
UserPromptSubmit |
it gave the agent a prompt | working |
active or PreToolUse |
the agent is working | working |
Notification / permission_prompt |
the agent asked for a permission | needs input |
Stop / the turn's stop reason |
the turn ended | idle |
Stop / <preset name>_exited |
the agent's process ended | unknown, never idle |
- The screen still counts, but only as evidence of work: on its own it never reads the seat idle.
- The generation attributes a post; it isn't authentication. A post with an earlier occupant's generation doesn't count for the live seat. The hook token is one local token, so any process that holds it can post for any seat.
- A runner's full duties (liveness, input, cancellation and permissions) are in the draft harness and runner contract.
An example: Hermes
community-harness-examples/hermes/ has a preset and a small launcher for Hermes
Agent's CLI.
- The launcher: it creates the session OpenRig named (
-c <name> --create-if-missing), and on a resume reopens it without--create-if-missing, with its own Hermes home per seat. A name that matches nothing ends with Hermes's own "No session found" error. - Settings go in the seat's Hermes
config.yaml, not in flags: Hermes's CLI creates a named session with no launch flags and then reopens it, refusing any creation flag (model, provider,--max-turns) that differs. So the preset's command carries none, and each seat'sconfig.yaml, in its Hermes home, sets them before the seat's first start: the model and provider, andagent.max_turnsif you want a cap (nothing else sets it once the flag is gone). - Let Hermes run
rigas the seat: Hermes runs every tool command in its gateway, one per Hermes home, so with this launcher one per seat (a two-seat run showed separate gateways, homes and sockets, and stopping one seat left the other's running). It runs them with that gateway's environment, and swaps in a session's own values only for its toolchain names and the names its profile lists interminal.client_env_passthrough. So put the seat's OpenRig variables in that list, in each seat'sconfig.yaml(config.yaml.example). Without it,riginside Hermes runs as whichever seat started the gateway, or as no seat; with it, each seat runsrigas itself. To check, ask the seat's Hermes to runrig whoami --json. The launcher warns when a seat'sconfig.yamldoesn't passOPENRIG_SESSION_NAME. This covers Hermes's CLI and TUI, whose clients pass their environment; its ACP clients pass none. - The preset: it declares
names_conversationbut notresume, until reopening a named session, and a missing one, have been checked with a real Hermes. - Stopping: Hermes's CLI is a client of a gateway, so closing a seat's pane detaches it. Hermes finishes any turn it
accepted. Whether the seat's gateway then exits once idle hasn't been checked: check with
psthat a stopped seat's gateway has gone before relying on it. The preset says so in its limits.
Copy them and maintain them as your own.
An example: OpenClaw
community-harness-examples/openclaw/ has a preset and a small launcher for
OpenClaw's CLI.
- Choose the mode:
openclaw.yamlruns an embedded agent withtui --local.openclaw-gateway.yamlruns a TUI client of an existing Gateway. Configure that client's Gateway route and authentication throughOPENCLAW_CONFIG_PATH; the launcher never starts or stops the Gateway. - The conversation: both examples use only agent
main, mapping OpenRig's name to the full native keyagent:main:<OpenRig name>. Each seat has its own client state folder. - Its resume check: local mode checks
sessions --agent main --json --limit all; Gateway mode calls the selected Gateway'ssessions.describe. Both require the exact canonical key. An unavailable lookup stops the launch; a confirmed missing key is never recreated. A client's local cache cannot authorize Gateway resume. - Stopping: a Gateway client's pane closing detaches it. The Gateway and an accepted turn can keep running; stopping that service is separate. Local mode runs its embedded agent in the pane.
- Unattended seats: prefer Gateway mode within the capabilities qualified for your installed version. Local interrupted-turn recovery is not supported by these launchers.
- Local interruption: Finish the current local turn before stopping; an interrupted local turn may retain native recovery work and refuse the next request. This launcher does not repair or retry it. OpenRig recognizes the recorded local “changed while starting work” error frame as needing attention: “OpenClaw turn not recovered; inspect the pane before retrying”. Use
rig capture <session>to inspect the native conversation. An ordinary send still submits a new turn, with an advisory that the last local turn was not recovered; it is not an automatic replay of that turn. It does not clear native recovery records, repeat the request or start a fresh conversation; a successful process launch is not proof that the turn recovered. - Local startup after resume: OpenClaw may run its database integrity check during startup. A reported resumed process does not confirm readiness for the next request; check that the TUI has finished starting before sending work.
- Resets must stay off: automatic session resets can create a new conversation under the same key. Typing
/newalso moves to a fresh key while the seat keeps the name OpenRig gave it. - The presets: both declare
names_conversation,resumeandfirst_input. The launcher passes the generated first startup text through OpenClaw's--messageoption. OpenRig labels the saved name as passed, not observed by its adapter.
Copy the launcher and the preset for your mode, and maintain them as your own.
Stopping a community seat stops its pane. rig down names those seats ("Pane stopped: …"), since a harness's own
processes, a gateway say, may keep running after it. OpenRig doesn't stop them, and whether they survive isn't exercised
by OpenRig's tests, so check the harness's own processes after rig down. Each preset's limits say what to expect.
Not supported for community harnesses: restarting a seat in place (rig seat restart is for Codex seats) and
switching harness on a running seat. Each says so before anything changes.
An example: Cursor
community-harness-examples/cursor/ has a preset and a small launcher for Cursor's
CLI.
- The launcher: it reports the chat
cursor-agent create-chatmakes, then reopens it. Each seat gets its own Cursor config folder unlessCURSOR_CONFIG_DIRis already set. Cursor keeps its chats andcli-config.jsonthere, and its login elsewhere. - The preset: it declares
launch_resultandresume. Cursor's--resumewith an id it doesn't hold starts a new, empty chat without saying so, so on a resume the launcher first checks that the chat's store exists. When it doesn't, the launcher reportserror: "missing"and starts nothing. - First input at launch: the preset declares
first_input: trueandstartup_assisted: false. The launcher passes the generated startup text as Cursor's positional first prompt. This route and same-chat resume were checked with Cursor Agent 2026.10.22-27dcf5f. Later tool approvals use Cursor's native prompts; startup submission does not answer them. - Sender names come from the sending session. A command run from an unattached operator shell can arrive as
<unknown sender>; that doesn't mean the receiving Cursor seat lost its identity.
Copy both and maintain them as your own.
An example: Pi's terminal UI
community-harness-examples/pi-tui/ has a preset and a small launcher that run Pi's
own terminal UI as a community harness. pi is the built-in managed Pi runtime, so the preset is named pi-tui. OpenRig
delivers to this seat as to any community seat (the Delivery row above), not as to a plain runtime: terminal seat, which
queue wakes don't reach. Checked once, on Pi 1.4.0 with OpenRig 0.7.4 and one model: a parked row's wake reached Pi in
the same session (the same session file) and Pi resumed the work, and a handoff reached a second pi-tui seat, which
picked it up unattended. The first seat's task named the commands to use, so this shows a wake reaching Pi and Pi acting
on it, not Pi choosing by itself to park or hand work on.
- The launcher: each seat gets its own Pi agent folder (
$OPENRIG_HOME/state/pi-tui/<node>/agent, unlessPI_CODING_AGENT_DIRis set) and its own session folder. At each launch it copies the installedopenrig-coreskills it's given (queue-handoffby default) into that agent folder. A new launch startspi --session-id <the name OpenRig gave>and passes the first input as Pi's first prompt, byte for byte. - Resume isn't declared. The launcher can reopen a named session: it opens the one session file whose header has that
id, and starts nothing when there isn't exactly one, because Pi's own
--session-idcreates a session for an id it doesn't find. Declareresume: trueonly after a real Pi has reopened a named session and refused a missing one. - Its limits: run a
pi-tuiseat unattended, not where a person also types into it.- With the seat's typing guard at its default, a wake submits a person's half-typed draft in Pi's editor together with the wake. The guard's draft-aware mode isn't a known fix for this.
- A send while a Pi dialog OpenRig doesn't recognize is open, such as
/settings, types into the dialog. - Not shown: how Pi takes a wake that arrives while it's busy (a message sent during a busy turn was answered after it), and other Pi versions.
- Give the seat a model before its first start: its provider key in its environment, or its login and settings in its agent folder.
Copy both and maintain them as your own.
The launcher is the contributor's code, in the contributor's package. OpenRig runs no code from a preset and loads no
module into its daemon. A preset that sets launch_result, names_conversation, startup_assisted, first_input, runner or passes_model is refused by an older
OpenRig that doesn't know the field.
Pass the first startup input at launch
A format-1 preset may set first_input: true (a boolean, default false). OpenRig composes the same initial identity turn it would otherwise paste, including the first merged text file, durable-obligation guidance and any selected orientation challenge. Before launch it writes that text to a per-attempt 0600 file under its own state/first-input/ folder and passes the path as OPENRIG_FIRST_INPUT. The typed launch command carries the path, not the text; an argument-based launcher still puts the text in the native harness’s argv.
The contributor-owned launcher reads the file and uses its harness’s supported initial-prompt route: the Cursor example passes one positional prompt; OpenClaw uses --message. Core knows neither flag. If no initial identity turn is due (an ordinary resume, for example), the variable is absent and the launcher starts without a first message. Later startup files and actions use their existing delivery paths; they are not folded into the launch file.
OpenRig records node.startup_first_input_passed with the path and submission: unconfirmed. This says the input was passed at launch, not that the native harness submitted or read it. An uncertain launch is not permission to replay it. Readiness remains process-only. startup_assisted is independent: when declared, startup still requires a person’s acknowledgement, including when the first input was passed at launch.
Presets without first_input and built-in runtimes keep their existing behavior. Older OpenRig versions reject this new field as unknown. The private input file remains for diagnostics; it can contain private startup context. A launcher that turns its contents into one argument is subject to the platform’s argument-size limit; the launcher must report a launch error rather than silently truncate the turn. The examples’ argument transport is covered with stand-ins; native unattended startup still requires qualification with the installed harness.
Pass the seat's declared model at launch
A format-1 preset may set passes_model: true (a boolean, default false). Then a model declared for the seat (in the rig
spec, or set later with rig seat set-model) reaches its launcher as OPENRIG_MODEL, on new and resumed launches alike. A seat that declares no model gets none,
and an inherited OPENRIG_MODEL is cleared. The launcher maps it into its harness's own setting; Core knows no harness's
model flag or config key.
OpenRig keeps three things apart:
- requested: the declared model, as
rig ps --nodesshows it ("configured", or "not-declared"); - passed: the launcher was handed the model. OpenRig records
node.startup_model_passedwith the model and whether the launch was a resume only when the launch command's send succeeded, and a later failure of the launch doesn't undo that. It never says passed from the declaration alone or for a launch that failed before anything was typed. When typing began and the send then failed, the model's dispatch is uncertain: it isn't recorded as passed, and it isn't retyped; - effective: the model that actually runs. OpenRig doesn't observe it for a community harness, so it reads UNKNOWN.
Passing a model never makes OpenRig pick a default, substitute another model, or retry with one. A model the harness refuses is the harness's own error.
The examples apply the request for a new session only; on a resume, the example launcher doesn't apply it, and which model a resumed session runs stays unknown unless the harness's own evidence says otherwise:
- Cursor:
--model "$OPENRIG_MODEL"for a new chat. What a resumed chat runs hasn't been checked: the launcher leaving out--modeldoesn't show that it keeps its earlier model. - Hermes: Hermes takes its model only from the seat's
config.yaml, so for a new session the launcher writesmodel: "<id>"there only when that file is absent or empty. A seat that has its ownconfig.yamlkeeps it byte-for-byte, and the launcher says the model wasn't applied: set it in that file. A seat set up fromconfig.yaml.examplealready has a config, so set its model there. Hermes v0.22.4 fixes a session's launch settings when it creates it (its own source), so a resumed Hermes session runs with what it was created with.
Both example presets ship with passes_model: false until that mapping has been checked with the real harness. Presets
without the field and built-in runtimes are unchanged, and older OpenRig versions reject it as unknown.
Interactive Pi in a terminal seat: the coordination skills
This is for a seat that runs interactive pi from a runtime: terminal member's startup, or from a community preset's
launcher. It isn't for a managed runtime: pi seat, which already copies its selected skill directories into the seat's own
Pi agent directory ($OPENRIG_HOME/state/pi/<session>/agent/skills/), or for Workshop, whose Pi profiles carry pinned skill
copies (openrig-world's PI-SKILLS.md). The pi-tui example above does step 1 for its
seats in its launcher.
OpenRig installs no skills for a terminal or community seat: it skips their resources and startup files, and rig skill loadout covers Claude Code and Codex only. Without the coordination skills, the agent there doesn't know the queue's habits,
such as parking a row that waits on CI instead of leaving it in progress, or handing work on so the next row names its
source. The seat's setup does two things.
-
Make the released skills readable. The installed OpenRig's own plugin carries them, matching the installed release:
rig plugin listshowsopenrig-coreand its path (usually$OPENRIG_HOME/plugins/openrig-core), and the skills are in itsskills/folder. Copy whole skill directories, keeping their helper files and modes, into theskills/folder of the Pi agent directory that seat uses (PI_CODING_AGENT_DIR, or~/.pi/agentby default). At leastqueue-handoff:CORE="$(rig plugin list | awk '$1 == "openrig-core" {print $NF}')" PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" mkdir -p "$PI_AGENT_DIR/skills" cp -Rp "$CORE/skills/queue-handoff" "$PI_AGENT_DIR/skills/"Copy again after each OpenRig upgrade: a copy doesn't follow the plugin.
-
Point the agent at it when it starts. Don't count on Pi finding the skill by itself; name it in the seat's first turn. For example:
Before you work your queue, read
<Pi agent directory>/skills/queue-handoff/SKILL.mdin full: it's how this team hands work on, parks it and waits. When you wait on CI or a long job, park the row withrig queue block <id> --on external:<what> --wake-after <duration> --continuation "<what resumes>".- A community preset with
first_input: truecarries it in the seat's first input when the file is declared withdelivery_hint: send_text(see "Pass the first startup input at launch" above). A.mdfile left at theautohint counts as guidance, which a community seat doesn't take. - A plain terminal seat can pass it as Pi's initial message in the command its startup runs. Check
pi --helpfor that argument in your Pi version. first_inputcarries text, not files: it doesn't install the skills.
- A community preset with
In a plain terminal seat, a queue wake doesn't reach Pi. OpenRig doesn't treat a runtime: terminal seat as an agent
seat. When a parked row's wake fires, OpenRig doesn't type it into Pi, and a handoff to that seat records a failed wake
(failed:terminal-seat). Parking still records the wait, but --wake-after won't bring Pi back. Check the seat's parked
rows yourself (rig queue whoami --session <seat>, or rig queue show <id>), or tell Pi to continue the named row.
What this shows, and what it doesn't. A copied folder and a startup line put the text in front of the agent. They don't
show that Pi discovered the skill by itself, or that the agent follows it. Check those separately: that the agent read the
whole file (its tool trace), and what its rows record, such as a CI wait parked on external: with a wake, and a handoff
made with handoff-and-complete, whose next row names it as its source.
Write your own
Start from the example closest to your harness, and change it. First check that the harness answers a prompt on its own (above).
- Choose how the conversation is named.
- Your harness creates an id you can read before it starts, as Cursor's
create-chatdoes: declarelaunch_result: true, and the launcher writes the record. - Your harness takes a name from its caller and creates or reopens it, as Hermes's
-c <name>and OpenClaw's session key do: declarenames_conversation: true, and the launcher passesOPENRIG_CONVERSATIONon. - Neither: leave both out. The seat starts and works, its identity shows as unavailable, and it can't be resumed.
- Your harness creates an id you can read before it starts, as Cursor's
- Write the launcher: a small script your package installs on the seat's
PATH. It reads OpenRig's variables, starts the harness withexec, and does nothing else.OPENRIG_CONVERSATIONwithnames_conversation, orOPENRIG_LAUNCH_RESULTandOPENRIG_LAUNCH_ATTEMPTwithlaunch_result.- On a resume,
OPENRIG_RESUME_REF: reopen exactly that conversation. Check that it exists first, and when it doesn't, end with an error (or reporterror: "missing") instead of creating one. - With
first_input: true,OPENRIG_FIRST_INPUT: pass the file's exact bytes through your harness's own first-prompt route. - Keep each seat's harness state apart (Cursor's config folder, Hermes's home, OpenClaw's state folder), so two seats never share a conversation.
- Write the preset at
$OPENRIG_HOME/community-harnesses/<name>.yaml, and check it withrig community-harness list.- Declare
resumeonly after checking, with the real harness, that a reopen works and that a missing conversation is refused. - List what doesn't work under
limits.
- Declare
- Try it in a rig: give a member
runtime: <name>, then go through the checks in the promotion test below. - Publish the preset and the launcher in your own package, and maintain them. OpenRig runs no code from a preset, so the launcher is yours.
The promotion test
A community harness stays a community harness: you maintain it, and OpenRig shows it as one. Promotion means OpenRig takes a harness on as a built-in runtime that it maintains and tests itself. Promotion is decided by the OpenRig maintainers, on evidence from OpenRig's own testing. Passing these checks isn't automatic promotion, and it isn't a promise of upstream support.
What the maintainers look at:
- Everything the preset declares works on a real install, end to end through OpenRig:
- a seat starts and takes its first turn (
rig up); - a message arrives (
rig send), and the reply can be read (rig capture); - the seat stops (
rig down) without leaving work running that the person can't see, or the preset'slimitssay what keeps running; - with
resume,rig up <rig> --existingreopens the same conversation, and a missing one is refused, never replaced.
- a seat starts and takes its first turn (
- What it doesn't support reads as not known, never as idle, working, ready or verified.
- Its
limitsare true, and name anything a person would otherwise be surprised by. - The built-in runtimes are unchanged.
- It's maintained: a named maintainer, and a launcher that keeps working with the harness's current release.
There's no score or threshold. The maintainers weigh the evidence.
From docs/reference/community-harness-presets.md at OpenRig v0.7.7