Metadata-Version: 2.4
Name: larva
Version: 0.5.0
Summary: PersonaSpec toolkit for LLM agents
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Keywords: agent,llm,mcp,persona,persona-spec,validation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: deal>=4.24
Requires-Dist: fastapi>=0.100
Requires-Dist: mcp>=1.20
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: returns>=0.24
Requires-Dist: ruamel-yaml>=0.18
Requires-Dist: uvicorn>=0.20
Description-Content-Type: text/markdown

# 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

```bash
pip install larva
```

Development checkout:

```bash
uv sync
uv run larva --help
```

## Quick start

Create a complete PersonaSpec JSON file:

```json
{
  "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:

```bash
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:

- `id` is required and must be flat kebab-case
- `prompt` is opaque executable text; larva stores and validates it as text and
  does not parse placeholders or infer runtime behavior from it
- `spec_version` is schema identity, not persona revisioning
- v1 pins `spec_version` to `"0.1.0"`
- `spec_digest` is recomputed by larva from canonical content
- there is no inheritance, `base:`, or `variant` field 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.

```text
~/.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 list` shows base persona ids, not variant metadata
- `larva resolve code-reviewer` materializes the active variant as a canonical PersonaSpec
- `larva resolve code-reviewer --variant tacit` returns a specific variant
- `variant` is passed as an operation parameter or registry envelope metadata;
  it is never accepted inside a PersonaSpec object
- `manifest.json` stores only the active pointer (`{"active": "default"}`);
  missing or corrupt manifests fail closed instead of being auto-created
- `contract.json` owns persona identity, description, capability intent,
  `can_spawn`, and `spec_version`; variant files own prompt/model execution
  fields only
- assembly/component inputs are removed; register full canonical PersonaSpecs directly

## Interfaces

### MCP

```text
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:

```text
larva_assemble
larva_component_list
larva_component_show
```

Start larva as an MCP server:

```bash
larva mcp
```

### CLI

```bash
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

```python
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

```bash
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

```bash
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

```bash
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`](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`](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`](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 category
- `docs/guides/USER_GUIDE.md` - detailed human-oriented usage guide
- `docs/guides/USAGE.md` - agent-oriented operational guide
- `docs/reference/INTERFACES.md` - public interface specification
- `docs/reference/ARCHITECTURE.md` - module boundaries and dependency design
- `design/registry-local-variants-and-assembly-removal.md` - accepted design for variant routing and assembly removal
- `docs/adr/ADR-001-spec-version-boundary.md` - `spec_version` design decision
- `docs/adr/ADR-002-capability-intent-without-runtime-policy.md` - capability intent model
- `docs/adr/ADR-003-canonical-requiredness-authority.md` - canonical requiredness authority
- `docs/adr/ADR-004-empty-capabilities-and-unrestricted-semantics.md` - empty capability semantics and unrestricted boundary

## License

AGPL-3.0-or-later
