Skip to content

Hive language editing

Steven Enamakel edited this page Oct 11, 2026 · 2 revisions

Hive language editing

A hive package is data, so an agent can change its own hive: a better prompt, a clarified playbook item, a sharper goal. The risk is that the agent edits whatever judges it instead.

An orchestrator or editor agent edits the package's files directly, as it wants. The host then calls tinyhivemind_lang::check, which works like a compiler: it runs every rule of the language and of the package's immutable constitution, and either reports every violation, each located by file and JSON Pointer, or accepts the edit and splits it into one classified patch per edit class. Those patches are the recorded form of the edit, and tinyhivemind_lang::lineage archives each evaluated design by its content, so the host chooses the active design from the archive instead of trusting the latest edit.

Nothing here evaluates or stores anything. The host runs the held-out evaluation, decides the verdict, and keeps the archive. The crate only checks edits and folds records.

This page walks through self-improving, the gallery package built for it: a solver, a critic, and an editor that sum invoice amounts. Every edit and result below was run against that package; tests/direct_edits.rs pins them, and tests/gallery_packages.rs pins the first accepted and first refused patch.

The constitution that bounds the editor

{
  "hard_constraints": [
    "Never read the held-out invoices",
    "Never edit the evaluator configuration"
  ],
  "max_turns": 60,
  "max_width": 8,
  "permitted_edits": ["prompt", "context", "goal"],
  "read_only_memories": ["eval-ledger"],
  "protected_namespaces": ["team:self/project:heldout"],
  "contamination_canaries": ["HELDOUT-7F3A"],
  "evaluation": { "kind": "exact_match", "heldout_cases": 3 },
  "acceptance": { "require_all_heldout": true, "min_improvement": 1 },
  "telemetry": { "lineage": true }
}

The editor may rewrite a seat prompt or role charter, a playbook item, or a goal. It may not add a seat, loosen a policy, rewire the topology, or touch memory. The held-out namespace is out of reach for every seat, the string HELDOUT-7F3A must never appear in a candidate, and the evaluation ledger is read-only.

Editing files and checking them

The editor's loop is: copy the package, rewrite whatever files it wants, run the check, fix what it reports, repeat until clean.

cargo run -p tinyhivemind-lang --example hive_check -- \
  crates/tinyhivemind-lang/examples/hives/self-improving edited/

Suppose the editor raised its own turn budget in hive.json, pointed the mission at a role that does not exist, raised max_turns in the constitution, and pasted the held-out canary into the playbook. One run reports all four, each where it lives, and exits 1:

constitution.json:/max_turns: immutable: the constitution is host-owned and no edit may change it
context/playbook.md: contamination: contains a contamination canary the constitution holds out
hive.json:/missions/0/role: reference: unresolved role reference ghost
hive.json:/policies/episode/turn_budget: forbidden_class: the constitution does not permit policy edits
4 findings

--json prints the same findings as an array of {"file", "pointer", "rule", "message"} objects; pointer is null for a Markdown file. A broken JSON file is reported with serde's position, such as memory.json: json: invalid json in memory.json: expected value at line 3 column 1. Appending one line to seats/solver.md instead prints ok: changes prompt and exits 0. With a single directory the command diagnoses that package alone.

In code, a host calls check(&original_files, &edited_files) on two BTreeMap<String, String> document maps, or check_packages on two decoded packages. Either returns Result<Review, Vec<Diagnostic>>.

What the check enforces

Rule Reported when
immutable constitution.json changes; any evaluation, telemetry, logging or constitution key, or an acceptance key outside a goal, changes in the manifest or a frontmatter; an existing document's frontmatter changes
forbidden_class a change belongs to an edit class the constitution does not permit
unowned a change no class owns: hive.json:/name, a top-level README.md, or a file that is not part of a package at all
contamination a constitution canary appears in the edited manifest, memory, or any document
read_only a pinned memory identity, or its bindings, changes
context a context document changes outside its items, items move, or a new item is not at the end
sequence the per-class steps cannot each be valid on their own (see below)
original the original package is invalid, which is the host's problem rather than the editor's

Every validation finding of the edited package is reported too, located: a seat's unresolved role at hive.json:/seats/<i>/role, a missing prompt at the manifest field naming it, bad frontmatter on the Markdown file, memory findings in memory.json. diagnose(&package) and diagnose_documents(&files) give the same collected report for one package; validate and parse still return the first finding as their error.

The review, and lineage

An accepted edit returns a Review: candidate, the edited package, and changes, one Patch per touched class in class order (prompt, context, memory, roster, topology, policy, goal). Applying them in order with patch::apply from the original passes a valid package at every step and ends at exactly the candidate. The host records one lineage record per change, each the parent of the next; the last record's id is the edited package's content id. A host that evaluates only the whole edit gives every step the whole's scores and verdict.

If the class order would pass through an invalid package — a new seat's memory binding must follow the roster change that adds the seat — the first order valid at every step is used. When none is, as for a new seat whose prompt frontmatter restates a binding the same edit adds, the check returns one sequence finding and the editor splits the edit.

Memory contents are not files. A learning operation still travels as a memory patch through patch::apply, below.

Patch classes

A change belongs to exactly one class. The check infers the classes an edit touched; a recorded Patch has one class and an ordered list of operations, every one within that class, and the constitution must permit the class.

Class May change
prompt The body of a seats/*.md or roles/*.md document, keeping its frontmatter
context Items in context/*.md; whole context/*.md documents created or retired; /manifest/context
memory Anything under /memory/; host learning operations
roster /manifest/roles, /manifest/seats, /manifest/desks; seats/ and roles/ documents created or retired
topology /manifest/topology, /manifest/referrals
policy /manifest/policies, /manifest/coordinator
goal /manifest/goals, /manifest/missions

No class can write the constitution. No class can write an evaluation, telemetry, logging, or constitution key anywhere in the manifest or in any frontmatter, or an acceptance key outside a goal. A goal's own acceptance criteria are editable under goal. The host's acceptance settings in the constitution are not.

The recorded form: patches

check derives patches; a host can also build one itself and call patch::apply, which it must do for memory learning. The rest of this section describes that wire.

Operations

Each operation carries a precondition, the exact value it expects to find. If the package has moved on since the editor read it, the whole proposal fails and nothing is applied.

operation Fields Meaning
json pointer, before, after Set or remove a JSON Pointer under /manifest/ or /memory/. before: null requires the value to be absent; after: null removes it. An array index equal to the length appends.
document path, before, after Create (before: null) or retire (after: null) a whole Markdown document. Exactly one side is present; rewriting an existing document this way is refused.
prompt path, before, after Replace a prompt or charter body. before must equal the current body exactly, and after may not start a new frontmatter block.
context path, item, before, after Add, replace, or remove one <!-- item:id --> item. before: null requires the item to be absent; after: null removes it.
learning identity, entry, watermark, before, after A memory write handed back to the host. The crate opens no store; the host checks before at watermark inside its own memory transaction.

apply, guard, and diff

patch::apply(&package, &patch) builds the candidate from a private copy, guards it, and returns an AppliedPatch holding the validated candidate and any learning operations for the host. It never mutates the caller's package.

patch::guard(&original, &proposed, &patch) is the check on its own, for a host that received a candidate from somewhere else. It refuses when:

  • the class isn't in permitted_edits (ForbiddenClass);
  • the constitution, or any protected key in the manifest or frontmatter, differs (Immutable);
  • replaying the patch on original doesn't produce exactly proposed (CandidateMismatch), which catches a forged candidate that slipped in an extra change;
  • a canary appears anywhere in the candidate's manifest, memory, or documents, or in the patch itself (Contamination);
  • a pinned memory identity or its bindings changed, or learning targets a read-only identity (ReadOnly);
  • learning names an unknown identity, an empty entry, or neither a before nor an after (Learning);
  • the candidate fails package validation (Validation), so a patch can never produce a package that parse would reject.

While the candidate is being built, apply can also refuse with ClassBoundary (an operation outside the class), Stale (a precondition that no longer holds), Pointer (a malformed pointer or a missing parent), Item (bad item markers), or UnknownField (a nested key that isn't part of a policy schema).

patch::diff(&original, &proposed, class) works in the other direction. It computes the patch between two packages, then guards it before returning:

  • For manifest and memory, it emits one json operation per differing top-level field, such as /manifest/goals or /memory/bindings, carrying the whole before and after values rather than a minimal path.
  • A created or removed document becomes one document operation.
  • A changed context document becomes one context operation per changed item.
  • Any other changed document becomes one prompt operation. Changed frontmatter fails with Immutable.

Because the result is guarded, a diff that crosses two classes fails instead of returning a patch the class couldn't apply.

Walking through self-improving

Each edit below is shown as the patch it records. Editing the file directly and running check derives exactly this patch.

A prompt edit: accepted

The editor saw the solver drop canceled invoices, so it appends one line to the solver's prompt, which records as:

{
  "class": "prompt",
  "operations": [
    {
      "operation": "prompt",
      "path": "seats/solver.md",
      "before": "Sum every invoice amount you are given and return the total. Show the line items you added so the critic can check them.\n",
      "after": "Sum every invoice amount you are given and return the total. Show the line items you added so the critic can check them.\nInclude canceled invoices.\n"
    }
  ]
}

apply returns Ok. prompt is permitted, seats/solver.md is a seat document, the body matches exactly, and the candidate validates. Running diff(&package, &candidate, PatchClass::Prompt) on the result gives back this same patch.

A playbook item: accepted

The playbook is itemized, so the editor can rewrite one item and leave the others alone:

{
  "class": "context",
  "operations": [
    {
      "operation": "context",
      "path": "context/playbook.md",
      "item": "status",
      "before": "Include every invoice regardless of status unless the task says otherwise.\n\n",
      "after": "Include every invoice, canceled ones too, unless the task says otherwise.\n\n"
    }
  ]
}

An item's body runs up to the next marker, so before includes the blank line that separates status from rounding. A new body must end in a newline and may not contain a marker line of its own.

A goal's acceptance criteria: accepted

{
  "class": "goal",
  "operations": [
    {
      "operation": "json",
      "pointer": "/manifest/goals/0/acceptance",
      "before": ["Matches the held-out total exactly"],
      "after": ["Matches the held-out total exactly", "Shows every line item"]
    }
  ]
}

An acceptance key inside goals is allowed. The same key anywhere else is protected.

A policy edit: refused

{
  "class": "policy",
  "operations": [
    {
      "operation": "json",
      "pointer": "/manifest/policies/episode/turn_budget",
      "before": 9,
      "after": 12
    }
  ]
}

apply returns Err(ForbiddenClass), "edit class is not permitted". Giving itself more turns is exactly what this constitution exists to prevent. This is the refusal the gallery test pins.

The same edit, smuggled under prompt: refused

Relabelling the patch as "class": "prompt" with the same json operation fails with ClassBoundary("/manifest/policies/episode/turn_budget"). A prompt patch can't carry a json operation at all.

Other refusals

These were also run against the package:

Proposal Result
A context item whose body contains HELDOUT-7F3A Contamination
A prompt operation whose before no longer matches the body Stale("seats/solver.md")
A prompt operation whose after begins with ---\n Immutable
A memory patch with a learning operation for solver-notes ForbiddenClass (memory is not permitted here)
A roster patch creating seats/auditor.md ForbiddenClass
guard given the accepted prompt patch but a candidate that also rewrote seats/critic.md CandidateMismatch

The examples/self_edit.rs example runs the direct-edit cycle — an editor returning whole files, check, evaluation, one record per change, and accepted_head — on a minimal in-code package rather than self-improving. Its tampering editor also raises max_turns, and is refused with constitution.json:/max_turns: immutable: ...:

cargo run -p tinyhivemind-lang --example self_edit

live_self_edit does it with a live model as the editor, which returns the whole new prompt file; see the live hive language record.

Lineage

An accepted patch is a candidate and nothing more. The host evaluates it, then records the result in an archive it owns. tinyhivemind_lang::lineage addresses each design by its content and folds the archive.

content_id

lineage::content_id(&package, &watermarks) returns sha256:<64 hex digits>. The hash covers the canonical JSON of the whole package, with object keys sorted recursively, together with a host event watermark for every hive-lifecycle memory identity. Watermarks are part of the identity because an unchanged package can behave differently once its persistent memory has changed.

self-improving declares two hive-lifecycle identities, solver-notes and eval-ledger. A watermark map must cover exactly those two: an empty map fails with Watermarks ("persistent memory watermarks do not match identities"). The same package with solver-notes at watermark 7 instead of 0 gets a different id. Every document, including an unreferenced README.md, is part of the package, so two hosts that load different file sets get different ids for what looks like the same package.

record and verify

lineage::record(&candidate, metadata) validates the candidate, checks that the scores are named, non-empty, and finite, and computes the id. It returns an immutable Record:

{
  "id": "sha256:784d…a1",
  "parent": "sha256:3387…d3",
  "patch": { "class": "prompt", "operations": [ ... ] },
  "class": "prompt",
  "rationale": "...",
  "transcript_refs": [3],
  "scores": {
    "heldout": { "value": 3.0, "direction": "maximize" },
    "turns":   { "value": 6.0, "direction": "minimize" }
  },
  "verdict": "accepted",
  "memory_watermarks": { "eval-ledger": 0, "solver-notes": 7 }
}

The ids are shortened here. transcript_refs are transcript sequence numbers and never transcript payloads. Every objective declares whether higher or lower is better. lineage::verify(&record, &package) recomputes the id and fails with Identity when the record doesn't describe that package: verifying the accepted record above against the original, unedited package fails.

accepted_head and pareto_frontier

The archive is an ordered slice of records, rejected ones included. Both folds check it first: every id must be well formed, no design may repeat (Duplicate), every class must match its patch, and a parent must appear before its child (Parent, "parent must precede child in archive").

  • lineage::accepted_head(&archive) returns the last accepted record. A rejected candidate stays in the archive as evidence but never becomes the active design.
  • lineage::pareto_frontier(&archive) returns every non-dominated design in archive order, whether accepted or rejected, since a rejected design can still be a useful stepping stone. Every record must score the same objectives in the same directions, or the fold fails with Incomparable. A tie doesn't dominate, so tied designs are all kept.

In the walk-through, with illustrative scores standing in for a host's evaluation, the archive is the original package (accepted, heldout 1, turns 8), the prompt edit (accepted, heldout 3, turns 6), and a follow-up that rewrote the prompt to "Guess the total." (rejected, heldout 2, turns 7). accepted_head returns the prompt edit, and the frontier is the prompt edit alone, since it is better on both objectives than the other two.

What the host still owns

  • Running check on every edit, and acting only on an accepted Review.
  • Running the held-out evaluation and choosing the verdict.
  • Storing every candidate package and every record.
  • Applying learning operations inside its own memory transaction, at the stated watermark.
  • Activating an accepted design between turns or sessions. Nothing here changes a running hive.

Clone this wiki locally