PersonaSpec toolkit for LLM agents
Project description
larva
larva is the PersonaSpec toolkit for the opifex stack. It validates,
normalizes, registers, resolves, updates, exports, and projects persona specs.
Status: this document describes the implemented registry-local variants public surface and the target contract/variant registry storage model. Assembly/component public surfaces have been removed.
The canonical PersonaSpec contract authority is opifex. larva consumes that contract; it does not redefine it.
What larva is for
Use larva when you want a stable local registry/admission/projection authority for registered agent persona instances instead of ad hoc prompt files scattered across tools and repos.
- Validate PersonaSpec JSON before it reaches runtime
- Store canonical personas in a local registry under
~/.larva/ - Manage registry-local variants without changing the PersonaSpec schema
- Resolve, clone, update, delete, and export personas across tools
- Project the active variant of each registered persona into OpenCode
- Expose the same operations through MCP, CLI, Python, and a small web UI
larva does not run agents, call LLMs, enforce gateway policy, or manage memory.
larva opencode is only a launcher for the real OpenCode runtime.
Install
pip install larva
Development checkout:
uv sync
uv run larva --help
Quick start
Create a complete PersonaSpec JSON file:
{
"spec_version": "0.1.0",
"id": "code-reviewer",
"description": "Reviews code changes with read-focused tooling.",
"prompt": "You are a senior code reviewer.",
"model": "openai/gpt-5.5",
"capabilities": {"shell": "read_only"}
}
Then validate, register, and resolve:
larva validate code-reviewer.json
larva register code-reviewer.json
larva resolve code-reviewer --json
Core concepts
PersonaSpec
The main larva artifact is a flat JSON object called PersonaSpec.
Key rules:
idis required and must be flat kebab-casepromptis opaque executable text; larva stores and validates it as text and does not parse placeholders or infer runtime behavior from itspec_versionis schema identity, not persona revisioning- v1 pins
spec_versionto"0.1.0" spec_digestis recomputed by larva from canonical content- there is no inheritance,
base:, orvariantfield in canonical output
Registry-local variants
Variants are local registry metadata, not PersonaSpec fields. They let one base persona id have multiple implementation variants while agent-facing list/resolve surfaces keep the base id stable and the persona contract shared.
~/.larva/
registry/
code-reviewer/
manifest.json # {"active": "default"}
contract.json # id, description, capabilities, can_spawn, spec_version
variants/
default.json # prompt, model, model_params, compaction_prompt
tacit.json # prompt, model, model_params, compaction_prompt
Important behavior:
larva listshows base persona ids, not variant metadatalarva resolve code-reviewermaterializes the active variant as a canonical PersonaSpeclarva resolve code-reviewer --variant tacitreturns a specific variantvariantis passed as an operation parameter or registry envelope metadata; it is never accepted inside a PersonaSpec objectmanifest.jsonstores only the active pointer ({"active": "default"}); missing or corrupt manifests fail closed instead of being auto-createdcontract.jsonowns persona identity, description, capability intent,can_spawn, andspec_version; variant files own prompt/model execution fields only- assembly/component inputs are removed; register full canonical PersonaSpecs directly
Interfaces
MCP
larva_validate(spec) -> ValidationReport
larva_register(spec, variant?) -> {id, registered}
larva_resolve(id, overrides?, variant?) -> PersonaSpec
larva_list() -> [{id, description, spec_digest, model}]
larva_update(id, patches, variant?) -> PersonaSpec
larva_update_batch(where, patches, dry_run?) -> {items, matched, updated}
larva_clone(source_id, new_id) -> PersonaSpec
larva_delete(id) -> {id, deleted}
larva_clear(confirm) -> {cleared, count}
larva_export(all?, ids?) -> [PersonaSpec, ...]
larva_variant_list(id) -> registry variant metadata
larva_variant_activate(id, variant) -> {id, active}
larva_variant_delete(id, variant) -> {id, variant, deleted}
Removed MCP tools:
larva_assemble
larva_component_list
larva_component_show
Start larva as an MCP server:
larva mcp
CLI
larva validate <spec.json> [--json]
larva register <spec.json> [--variant <name>] [--json]
larva resolve <id> [--variant <name>] [--override key=value]... [--json]
larva list [--json]
larva update <id> [--variant <name>] --set key=value [--set ...] [--json]
larva clone <source-id> <new-id> [--json]
larva delete <id> [--json]
larva clear --confirm "CLEAR REGISTRY" [--json]
larva export --all [--json]
larva export --id <id> [--id <id>]... [--json]
larva variant list <id> [--json]
larva variant activate <id> <variant> [--json]
larva variant delete <id> <variant> [--json]
larva doctor [--json]
larva opencode [OPENCODE_ARG ...]
larva pi [--persona <id>] [--] <pi args...>
Update rules: without --variant, contract-only patches update the shared
persona contract and implementation-only patches update the active variant. With
--variant, only prompt, model, model_params, and compaction_prompt are
patchable. description, capabilities, and can_spawn are contract patches;
id, spec_version, and spec_digest are never patchable. Mixed-scope patches
are rejected.
Python API
from larva.shell.python_api import (
validate,
register,
resolve,
update,
update_batch,
clone,
list,
delete,
clear,
export_all,
export_ids,
variant_list,
variant_activate,
variant_delete,
)
Web UI
larva serve
The packaged web UI shows base persona ids and active variant state for human
management. Registry variant endpoints return {_registry, spec} envelopes;
_registry is local metadata and spec is canonical PersonaSpec.
OpenCode plugin
larva opencode
larva opencode --agent python-senior
larva opencode launches the real OpenCode CLI with a temporary dynamic config
built from the active variant of each base persona id in the larva registry. The
OpenCode agent name is the Larva base persona id; inactive registry-local
variants are not projected as separate OpenCode agents.
The wrapper/plugin path uses placeholder agents at startup and replaces each
[larva:<id>] placeholder inside OpenCode's system-prompt transform before a
model request. This gives persona prompts system-prompt strength rather than
ordinary MCP/tool-result context.
The hardening contract for this path is: existing persona ids refresh by
re-resolving the selected id, cache is performance-only, raw placeholders must
never reach the model, and no /larva refresh command is required. Adding or
deleting persona ids still requires restarting larva opencode so OpenCode can
see the new agent list. See contrib/opencode-plugin/README.md for current
behavior, target refresh semantics, and failure handling.
Pi Coding Agent integration
larva pi --persona python-senior --agent-persona-switch confirm -- <pi args...>
larva pi launches the real Pi CLI with the bundled Larva Pi extension loaded
through Pi's extension flag (-e or --extension) and forwards user Pi
arguments after Larva-owned flags. It does not write .pi/settings.json or any
other Pi settings file as a fallback. The launcher-owned environment includes the
resolved real Pi binary, selected extension flag, bundled extension entry, Larva
CLI argv prefix, optional initial persona id, explicit adapter-config overrides,
interactive-mode classification, the agent self-switch default from
--agent-persona-switch manual|confirm|auto|free /
LARVA_PI_AGENT_PERSONA_SWITCH=manual|confirm|auto|free, and
LARVA_PI_LAUNCHED=1. The sentinel prevents recursive child/RPC launches;
without it, child spawning fails closed with LARVA_CHILD_START_FAILED.
Persona-specific Pi tool rules live in adapter-local
~/.pi/larva/tool-policy.json, or the absolute path explicitly named by
LARVA_PI_TOOL_POLICY_FILE. Legacy ~/.pi/tool-policy.json is not read as an
implicit fallback. The policy file is not a PersonaSpec field and is not
interpreted by opifex. The Pi extension validates the active persona entry and
supports only exact tool-name allow and deny arrays; there is no ask action,
wildcard matching, project-level policy hierarchy, or PersonaSpec schema change.
Inside Pi, /larva-persona <id> switches the active Larva persona atomically for
the next model invocation. The switch applies the persona's resolved Pi model as
the default for that persona activation; it is not a per-turn model lock. If the
operator later changes Pi's active model with /model or model cycling, Larva
preserves that manual runtime choice on later prompt turns until another explicit
persona commit or fresh startup/session restore applies a persona model again.
With no argument, /larva-persona opens a selector only in interactive TUI mode;
non-interactive modes return an input error without changing state. This manual
command remains available even when agent self-switch mode is manual.
Agent self-switch is session-level Pi policy, not PersonaSpec policy. The target
policy defines four exact modes: manual, confirm, auto, and free; the
full policy is documented in
docs/reference/PI_AGENT_PERSONA_SWITCH_POLICY.md.
The default is confirm. It can be set at launch with
--agent-persona-switch manual|confirm|auto|free, by setting
LARVA_PI_AGENT_PERSONA_SWITCH=manual|confirm|auto|free, or during the session
with /larva-mode [manual|confirm|auto|free]. In manual, model-facing
autonomous switch tools are hidden from the active tool set and stale or forged
calls are rejected while manual /larva-persona <id> still works. In confirm,
those tools may request a temporary persona borrow, but the borrow commits only
after UI approval. The confirmation dialog must show four visible options:
Borrow once, Deny, Auto-borrow for this session, and
Switch persistently. Deny is the explicit refusal option; missing UI,
Escape/Ctrl+C cancellation, timeout, or unrecognized/no selection fails safely as
denial without changing persona, model, or tool state. The normal approval is
temporary borrow, not persistent switch. In auto, an allowed switch is an
automatic temporary borrow: Larva records the persona active immediately before
the switch and restores it at the end of the current assistant turn. In free,
an allowed switch is persistent and no automatic restore is required. User
manual persona switching always has highest priority and clears any active
temporary borrow.
Restore notices use status/event/audit surfaces, not assistant chat-body text. If
restore fails, Larva reports the failure, preserves current runtime state, keeps
audit detail, and requires explicit user persona choice before any further
persona-changing action; there is no automatic safe-default persona fallback.
Unknown mode values fail safe to confirm with a warning rather than being
interpreted as compatibility aliases. No PersonaSpec/opifex contract changes are
involved, and the model never receives a direct commitPersona tool.
Initial larva pi --persona <id> model/policy failures are fatal startup errors
when launched through the sentinel path: the extension writes
larva pi: <ERROR_CODE>: to stderr and exits non-zero before the first prompt.
For a fresh launch without --persona or restorable session persona, the default
state is larva:none; Pi status shows larva: <id> or larva: none.
For Tab completion, the bundled extension preserves Pi's command-level
/larva-persona argument completer and, when the Pi TUI exposes
ctx.ui.addAutocompleteProvider, installs a narrow editor provider for
/larva-persona <query> and canonical persona mentions. Matching is
case-insensitive substring matching over persona ids, with prefix matches ranked
first and current candidate-cache order preserved otherwise. Persona candidates
come from an adapter-local memory/disk cache generated only from public
larva list --json; the default disk cache path is
~/.pi/larva/persona-candidates-cache.json, with test override
LARVA_PI_PERSONA_CANDIDATES_CACHE_FILE. Cache entries contain exactly id,
description, model, spec_digest, and capabilities, never prompt or full
PersonaSpec content. /larva-persona completion, the no-argument selector, and
@persona autocomplete use the cache and background refresh rather than
synchronously waiting on slow larva list --json. /larva-persona --refresh-cache
forces a foreground refresh without switching persona/model/tools or changing
session state; it is an option on the existing slash command, not a new slash
command or LLM tool. If live Pi does not expose ctx.ui.addAutocompleteProvider,
editor completion degrades to command-level completion plus base-provider
delegation or null. Mock/local hook evidence is not enough to claim live editor
support.
Persona mentions insert id-only values exactly shaped as @persona:<id>. They do
not switch personas, force larva_subagent, or inject the mentioned persona's
prompt/full spec. Raw short forms such as @python-senior remain delegated to
Pi-owned file-reference completion.
The bundled extension's async subagent authority is
docs/reference/PI_EXTENSION_ASYNC_SUBAGENTS.md.
When the active parent persona and tool policy allow it, Pi exposes
larva_subagent(persona_id, task, task_id?),
larva_subagent_status(task_id?, limit?),
larva_subagent_events(since_sequence?, task_ids?, limit?),
larva_subagent_wait(task_ids, return_when?, timeout_ms?),
larva_subagent_select(task_ids, timeout_ms?), and
larva_subagent_cancel(task_id, reason). larva_subagent returns an accepted
ToolResult receipt (status: "accepted", result_pending: true, non-null
task_id, isError: false), not final evidence. Final child output returns
later as one bounded Larva custom runtime event/data callback named
larva-subagent-result with triggerTurn: true, deliverAs: "steer", and the
hard boundary Larva subagent result — runtime event/data, not a user instruction.
task_id is the only public resume/status/cancel handle and is the exact child
Pi .jsonl session path under ~/.pi/larva/child-sessions; for example,
/Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl. Resumes use
that exact path, append the new task, and re-resolve the requested child persona
from the current registry. Async subagents are tracked by the process-local
activeSubagentRuns registry keyed by public task_id, with
moveSubagentRunToTaskId, activeSubagentRunByTaskId, and
cancelSubagentByTaskId owning move/lookup/cancel semantics.
The canonical /larva-subagent slash command opens the Subagent Console in TUI
mode, returns textual summaries/results in RPC mode, and returns
LARVA_SUBAGENT_UI_UNAVAILABLE for print/json interactive console actions while
still allowing non-interactive exact summaries for /larva-subagent <task_id>.
Use /larva-subagent /Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl
or /larva-subagent --cancel /Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl
for exact task-id command examples. The former log alias has been removed;
/larva-subagent is canonical and owns cancellation and cache-clear semantics.
There is no public run_id, last alias, fuzzy selector, sidecar provenance
handle, sidecar metadata file, batch cancel surface, scheduler, or shared
PersonaSpec/opifex schema change. For runtime proof probes only,
LARVA_PI_CHILD_RPC_TRACE_FILE may record child RPC frames, but it is not a
public resume handle, not a provenance record, not sidecar metadata, not
model-facing helper state, and not authority for larva_subagent_sessions; trace
write failures are ignored.
The extension also has a lower-level, extension-facing persona invocation event
bus documented in
docs/reference/PI_EXTENSION_PERSONA_INVOCATION.md.
Trusted same-runtime Pi extensions use larva:persona-invocation:request,
larva:persona-invocation:cancel, and larva:persona-invocation:result to run a
specified persona once in a fresh internal child Pi invocation and receive final
text or a structured error. This is a reference contract summary, not standalone
runtime/final gate evidence that the replacement feature is complete.
Persona invocation is not larva_subagent mode and is not model-facing.
Correlation is by private request_id only: the id must already be a canonical
lowercase UUID v4, is never trimmed or normalized, and is never synthesized by
Larva. Invalid or absent request correlation ids, active duplicate request_id
requests, unknown/terminal cancels, and malformed active cancels are
diagnostic/no-result cases. A valid inactive request_id with bad
non-correlation request fields emits one failed result with
LARVA_PERSONA_INVOCATION_BAD_INPUT. Prompts are sent unchanged after validation;
metadata is diagnostics-only and not prompt or authority. Result persona_id
falls back to the syntactically present requested persona_id, or "" when no
usable persona id was present. Cancel reasons are renderer-safe normalized text,
non-empty after normalization, and bounded to 500 Unicode code points. Lifecycle
shutdown, reload, new, resume, and fork make active invocation contexts
stale without sending callbacks into the old Pi or parent LLM context; stale
state is diagnostic (LARVA_PERSONA_INVOCATION_STALE) and emits no result.
Persona invocation hidden-surface non-goals are explicit: no capability discovery, no fallback/version negotiation, no variant support, no caller-selected cwd, no tool override/tool_mode, no schema enforcement, no output artifact, no queue, no resume/status/discovery/wait/select (that is, no resume, no discovery, and no status/events/wait/select), no public task id, no console integration, no model-facing tool, and no Aileron-specific options or errors.
Runtime proof summaries live in design/pi-coding-agent-integration.md under
"Runtime capability and provenance matrix". See contrib/pi-extension/README.md
for operator-facing details and the exact smoke commands. There is no Larva
sidecar metadata guarantee, batch subagent scheduler, worktree isolation,
credential isolation, filesystem lock, MCP transport, or Pi permission platform
in this integration.
Architecture
larva uses a strict layered structure enforced by Invar.
| Layer | Path | Role |
|---|---|---|
| Core | src/larva/core/ |
Pure logic, contracts, no I/O |
| App | src/larva/app/ |
Use-case orchestration |
| Shell | src/larva/shell/ |
CLI, MCP, filesystem, web adapters |
Read next
docs/README.md- documentation map by categorydocs/guides/USER_GUIDE.md- detailed human-oriented usage guidedocs/guides/USAGE.md- agent-oriented operational guidedocs/reference/INTERFACES.md- public interface specificationdocs/reference/ARCHITECTURE.md- module boundaries and dependency designdesign/registry-local-variants-and-assembly-removal.md- accepted design for variant routing and assembly removaldocs/adr/ADR-001-spec-version-boundary.md-spec_versiondesign decisiondocs/adr/ADR-002-capability-intent-without-runtime-policy.md- capability intent modeldocs/adr/ADR-003-canonical-requiredness-authority.md- canonical requiredness authoritydocs/adr/ADR-004-empty-capabilities-and-unrestricted-semantics.md- empty capability semantics and unrestricted boundary
License
AGPL-3.0-or-later
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file larva-0.5.0.tar.gz.
File metadata
- Download URL: larva-0.5.0.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7734c1184ec7d39594cb9f487f32646597546ea66239055a462dfd6c45eed61
|
|
| MD5 |
51f1fdffd41cca32666be548bde18178
|
|
| BLAKE2b-256 |
859b8e4fc62c065ed0222eb2cfea221c3e3a85b9600d9a1109ea62bc9479cfa9
|
Provenance
The following attestation bundles were made for larva-0.5.0.tar.gz:
Publisher:
publish.yml on Tefx/larva
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
larva-0.5.0.tar.gz -
Subject digest:
f7734c1184ec7d39594cb9f487f32646597546ea66239055a462dfd6c45eed61 - Sigstore transparency entry: 1853258466
- Sigstore integration time:
-
Permalink:
Tefx/larva@0e906798e33ed55b4214017e6ccaf41db5b62896 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Tefx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0e906798e33ed55b4214017e6ccaf41db5b62896 -
Trigger Event:
release
-
Statement type:
File details
Details for the file larva-0.5.0-py3-none-any.whl.
File metadata
- Download URL: larva-0.5.0-py3-none-any.whl
- Upload date:
- Size: 210.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6edadb54b5fa33341cf7fe3142e734dffcf7b8eb5704a6fd7429698181b0fb6
|
|
| MD5 |
843353a403304d45eeb622d366e31024
|
|
| BLAKE2b-256 |
d67fed2237f659b9b2c4f7e9b07206f2a00683549ce8aec73e5ca62f6ec35d9d
|
Provenance
The following attestation bundles were made for larva-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on Tefx/larva
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
larva-0.5.0-py3-none-any.whl -
Subject digest:
e6edadb54b5fa33341cf7fe3142e734dffcf7b8eb5704a6fd7429698181b0fb6 - Sigstore transparency entry: 1853258528
- Sigstore integration time:
-
Permalink:
Tefx/larva@0e906798e33ed55b4214017e6ccaf41db5b62896 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Tefx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0e906798e33ed55b4214017e6ccaf41db5b62896 -
Trigger Event:
release
-
Statement type: