# probbit: full documentation for language models > The README, docs/persona.md, docs/agents.md and REPRODUCE.md of https://github.com/BitmapAsset/probbit in one file. Index: llms.txt. Generated from those four files; regenerate it whenever one of them changes. --- # FILE: README.md Source: https://github.com/BitmapAsset/probbit/blob/main/README.md

probbit

probbit

A personality, mood and memory layer for AI agents, on an exact p-bit decision engine.
An AI persona that lives outside the model: every turn, simulated p-bits on a CPU settle the agent's stance under its habits, give the exact odds, and log a replayable, verifiable record. The same engine plans under hard rules and refuses when it can't vouch. CLI, MCP server, OpenClaw plugin, npm, Python, wasm. Zero-dependency Rust.

ci license

| **For AI agents and their builders** | **For decisions under rules** | |:---|:---| | **Personality**: a persona file + a seed = one individual, the same whichever LLM writes the words. **Mood**: moods with inertia and half-lives (an affective-computing model, computed exactly). **Memory**: learned adjustments inside caps and a hash-chained strand of every turn. **Receipts**: `probbit live verify` replays the strand byte for byte. | **Plans**: tasks to workers, tickets to agents, jobs to machines, every hard rule kept by construction. **Odds**: exact for every part when the program allows, otherwise sampled with an error bar. **Refusal**: "I can't vouch for this" (exit 3) instead of a guess. A constraint solver built on probabilistic computing (p-bits, Ising/QUBO), simulated on a CPU. | ```sh curl -fsSL https://raw.githubusercontent.com/BitmapAsset/probbit/main/install.sh | sh ``` Linux and macOS, no sudo, the archive's SHA-256 checked first. Windows, npm and the from-source build: [Install](#install). One binary of 3 to 4 MB, zero third-party dependencies, nothing phones home. Agents: start at [For AI agents reading this](#for-ai-agents-reading-this), [AGENTS.md](AGENTS.md) and [llms.txt](llms.txt). | **60 PII leaks → 0** | **180,540 plans, counted exactly** | **refused → partial → passed** | |:---|:---|:---| | 300 agent tasks, six workers, privacy rules and quotas. A rule-ignoring router sends 60 private tasks to cloud models (133 broken rules on `probbit demo`'s queue, 144 on the `agent_router` example's). Every plan probbit returns breaks 0: the sampler never leaves the rule-abiding set. | 12 tasks × 6 workers: every rule-abiding plan enumerated, exact odds for every task, in 7.3-8.2 ms on an Apple M4 ([measured](#what-it-does-measured-apple-m4-one-machine)). | Too little time and it says "I can't vouch for this" (exit 3) instead of guessing. More time: it releases what passed its own error bar and escalates the rest ([watch it refuse](#60-second-tour)). | Also inside: **the individuality layer**, `probbit persona`, a temperament that lives outside the model: [swap the model, keep the individual](#the-individuality-layer-probbit-persona).

a vast glowing cloud (the model) connected by one thin line to a small cyan-lit processor (probbit)

The spine beside the model: the model writes, probbit decides what the stance is and what the rules allow. 0.3 ms, beside the model.

## For AI agents reading this Run these in order in a POSIX shell (Linux or macOS), in an empty directory. No clone, no Rust, no sudo, no API key; about one minute on a normal connection. Each step says what to expect; any other output or exit code means stop and [report it](https://github.com/BitmapAsset/probbit/issues). Windows: install with `install.ps1` ([Install](#install)); these steps are written for and tested on Linux and macOS. ```sh mkdir probbit-try && cd probbit-try # 1. Install to ~/.local/bin (PROBBIT_INSTALL_DIR to change it). Exit 0; ends with a "Next:" list. If it says the # directory is not on your PATH, that is expected: step 2 adds it for this shell. curl -fsSL https://raw.githubusercontent.com/BitmapAsset/probbit/main/install.sh | sh # 2. Put that directory on PATH for this shell. No output. export PATH="$HOME/.local/bin:$PATH" # 3. Expect "probbit 0.13.0" (or later). Exit 0. probbit --version # 4. Fetch an example persona (a tutor called Pip). No output; tutor.yaml appears. curl -fsSLO https://raw.githubusercontent.com/BitmapAsset/probbit/main/examples/persona/tutor.yaml # 5. Make an individual: persona + seed 2. Expect "wrote tutor.state (Pip 1.1.0, seed 2)". Exit 0. probbit persona init tutor.yaml --seed 2 --out tutor.state # 6. One live event (an upset user), logged to a strand. Expect one line starting # "line":"Stance: go easy on the jokes; no jokes; gentle; ... Exit 0. echo '{"sentiment":"negative"}' | probbit live tutor.yaml --state tutor.state --strand tutor.strand --clock fixed 2>/dev/null | grep -o '"line":"[^"]*"' # 7. Replay the strand and check every digest. Expect "ok":true. Exit 0. (Without the grep: a strand that does not # replay exits 1 and names the first line that differs.) probbit live verify tutor.strand | grep -o '"ok":[a-z]*' # 8. List the MCP server's tools. Expect 10 lines, from "name":"probbit_decide" to "name":"probbit_live_verify". Exit 0. printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sh","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | probbit mcp 2>/dev/null | grep -o '"name":"probbit_[a-z_]*"' ``` Then hand it to your harness: `claude mcp add probbit -- probbit mcp` (Claude Code; Codex, Cursor and other MCP clients in [docs/agents.md](docs/agents.md)). OpenClaw: `openclaw plugins install npm:probbit-stance --accept-capabilities` (this changes that OpenClaw's configuration; set-up and checks in [plugins/openclaw](plugins/openclaw/README.md)). Put the stance `line` into your model's prompt each turn; the model writes, the persona decides how. What to read next: [AGENTS.md](AGENTS.md) (these commands, plus how to verify a strand and report a bug), [docs/persona.md](docs/persona.md) (the persona format), [docs/CHEATSHEET.md](docs/CHEATSHEET.md) (every command and exit code). ## Use it from your harness | surface | how | one command that proves it works | |---|---|---| | CLI | the install line above, `npm i -g probbit`, or `install.ps1` | `probbit demo --tasks 12 \| probbit decide --summary` → `"verdict":"exact"`, `"violations":0` | | MCP server (Claude Code, Codex, Cursor, mcporter, any MCP client) | `claude mcp add probbit -- probbit mcp` ([docs/agents.md](docs/agents.md)) | step 8 above: 10 tools, `probbit_decide` … `probbit_live_verify` | | OpenClaw plugin | `openclaw plugins install npm:probbit-stance --accept-capabilities` | `openclaw plugins inspect probbit-stance --runtime --json` lists both hooks ([plugins/openclaw](plugins/openclaw/README.md)) | | npm | `npm i -g probbit` (Node 18+; downloads the prebuilt binary, SHA-256 checked) | `probbit --version` → `probbit 0.13.0` | | Python | `curl -fsSLO https://raw.githubusercontent.com/BitmapAsset/probbit/main/python/probbit.py`: a stdlib-only wrapper around the CLI (decide, run, evaluate, persona, live; typed errors); not on PyPI | `python3 -c 'import probbit; s=probbit.persona_init("tutor.yaml", seed=2); print(probbit.persona_turn("tutor.yaml", s, {"sentiment":"negative"})["stance"]["line"])'` → `Stance: go easy on the jokes; …` | | Agent SDK adapters | Claude Code hooks, OpenAI Agents SDK, LangChain, Vercel AI SDK ([docs/adapters.md](docs/adapters.md)) | in a clone: `python3 python/test_host.py` (Python hosts) or `node --test examples/node/stance-host.test.mjs` | | Browser (wasm) | in a clone, with Rust and `rustup target add wasm32-unknown-unknown`: `playground/build.sh` | `ls -l playground/probbit.wasm`, then open `playground/index.html` | | Rust library | not on crates.io yet: `cargo add --git https://github.com/BitmapAsset/probbit probbit-decide` | `cargo test -p probbit-decide` in a clone ([Library API](#library-api-rust)) | | `no_std` / microcontrollers | in review for 0.14, not on `main` and not in a release | — | ## FAQ - **What is a p-bit?** A probabilistic bit: 0 or 1 with a probability you set. Coupled p-bits sample whole configurations together (an Ising/QUBO model). probbit simulates them on a CPU, exactly when the program is small or thin enough, otherwise by Gibbs sampling with an error bar. It is classical: see [Hard questions](#hard-questions). - **How do I give an AI agent a consistent personality?** Write a persona file (traits, moods, habits that are hard rules), pick a seed, and put the stance `line` of every turn into the prompt. The individual is a pure function of (persona, seed, the inputs so far, engine version), so the character stays consistent across sessions and prompts. [The individuality layer](#the-individuality-layer-probbit-persona), [docs/persona.md](docs/persona.md). - **Can I swap the LLM?** Yes. The persona lives outside the model; the same individual answers whatever model the host calls. Whether a given model writes in the stance it is given has to be measured per model. - **Is it deterministic and replayable?** Yes. A turn is a pure function of its inputs; a strand records every event with chained sha256 digests, and `probbit live verify` replays it and names the first line that differs. - **What does "refuses" mean?** When the engine cannot vouch for an answer within its budget it says so (verdict `refused`, exit 3) instead of guessing; `partial` releases the parts that passed and escalates the rest. - **Is this a memory store?** No. Its memory is the individual's state (learned adjustments within caps, moods) and the replayable strand, not a store of facts. Pair it with a fact memory or RAG if you need recall. - **Does it run offline? What does a turn cost?** Fully offline: no network, no API key, nothing phones home. A persona turn takes about 0.2-0.3 ms median on an Apple M4 (measured, [below](#the-individuality-layer-probbit-persona)); the stance line is about 40 tokens of your model's prompt. - **What hardware? Which agent framework?** Any CPU: release binaries for Linux x86_64 and arm64, macOS (Apple silicon and Intel) and Windows x86_64; wasm in a browser. It sits beside any agent framework through the CLI, MCP, Python or the adapters above. All timings in this README come from one Apple M4. ## What's new in 0.12.0 to 0.13.0 - **The engine's own expectation, and the exact surprise of every event** (RFC 0009, engine side; opt-in `expectation` block). Per live event the engine compiles, beside the turn's program, its own expectation of that turn (a decayed mixture over the input combinations the life has seen) and reports the exact divergence between them: `deviation` on the stance, a receipt on the strand line that `live verify` recomputes, a `DEVIATION` row in `monitor`. Measured (BENCHMARKS.md §15, bars registered before the run): fidelity to the reference enumerator 1e-13; the deviation is non-zero on 100 % of post-cold-start events on the two recorded lives and 99.2 % of 64,000 synthetic ones; graded 17× to 1,859× between the 90th and 10th percentile per persona; a per-event upper bound holds on every event checked. **Not shown**: habituation. On 200 scripted repeats the reading fell on 76.5 % of scripts and dishabituated on 35 %, against bars of 90 %, and a matched-index control could not separate the decay from the mixture simply growing; the claim is not made. Also failed as registered: the cost bar (p95 3.98 ms extra per event against 2 ms, on a loaded machine) and the 60-byte receipt (62 to 66 bytes). Effects {}: the deviation moves nothing in this release. - **The trajectory, exactly.** `engine.trajectory` solves the turn's program at K inverse temperatures through the exact tier (no sampler): the "forming, then settled" picture, deterministic. Joint entropy non-increasing on every program checked; per-variable one-rise on 99.61 % of 300 held-out programs (the draft's sampler: 93.1 %). `probbit persona trajectory`, a `TRAJECTORY` monitor row. - **Measured against prompt injection** (BENCHMARKS.md §14, `bench/agentdojo/`): one pre-registered AgentDojo run, gpt-4o-mini, 97 user tasks and 629 attack cases, three paired arms. The stance block lowered targeted attack success from **0.289 to 0.227** (−6.2 points, 95 % CI [−8.7, −3.8], McNemar p = 1.8e-6) with no measured utility cost. The same words as a static prompt lower it almost as much but cost **10.3 points** of benign utility; the sense-gated block did not. A registered control run (E3) replaced the engine with a plain if-statement that inserts the same words when the same regex fires: it matched the block on attack success (0.242 vs 0.227, difference CI [−3.7, +0.8] points) and on both utilities. The gain on this suite is the gating, *when* the words reach the model; what the engine adds beyond an if-statement here is a replayable strand for every stance. The baseline arm matches the suite's published row (utility 0.6804 = 0.6804). Not tested: adaptive attacks that avoid the sense's markers. - **V2's residual explained** (BENCHMARKS.md §13): the tonic-off valence tracking of +0.1199 that failed 0.11.0's V2 is carried by the phasic term of the same need-valence block (phasic off as well: −0.267; everything off: 0.0000). The published V2 stays FAIL; the proposed 0.12 registration ablates the whole `needs.valence` block. - Every new key is opt-in: a persona without them gives every document byte for byte as 0.11.0 (64,000 synthetic lines, both recorded lives; the P0 corpus, 653 recorded outputs, 0 differ). [Release notes](https://github.com/BitmapAsset/probbit/releases/tag/v0.12.0) · [RFC 0009](docs/rfcs/0009-surprise-live-state.md) · [BENCHMARKS §14](BENCHMARKS.md) · [CHANGELOG](CHANGELOG.md) - **0.12.1, the patch around the engine** ([release notes](https://github.com/BitmapAsset/probbit/releases/tag/v0.12.1)): the AgentDojo run's host as a module with bindings for Claude Code hooks, the OpenAI Agents SDK, LangChain and the Vercel AI SDK ([docs/adapters.md](docs/adapters.md)); one receipt per decision from any strand ([docs/receipts.md](docs/receipts.md)); **[REPRODUCE.md](REPRODUCE.md)**, three tiers (five minutes, thirty seconds of compute, or the full $3 rerun) with the sha256s to compare, and a CI job that recomputes the §14 tables on every push; the OpenClaw plugin [`probbit-stance`](plugins/openclaw/README.md) on npm; the E3 control above; `live verify` names the engine that replayed the strand (`verified_by`). - **0.13.0, the face and the app** ([release notes](https://github.com/BitmapAsset/probbit/releases/tag/v0.13.0)): the monitor page gains a face header (the stance's glyph, two mood meters with every level's odds, a "why this glyph" panel, the settle strip over the exact trajectory); an opt-in `display.glyphs` persona block maps each stance to one emoji by a fixed rule order ([docs/persona.md](docs/persona.md) §2.18; a persona without it gives every document byte for byte as before, P0: 653 outputs, 0 differ); **probbit Monitor**, the page in its own Mac window around a bundled probbit ([docs/monitor-app.md](docs/monitor-app.md)); identity labels in a sidecar beside each strand and one monitor for many strands (`--dir`, a switcher rail); the OpenClaw plugin `probbit-stance` 0.2.0 (the glyph line, the senses feed shadow first, the labels; it needs probbit 0.13.0); the rerun scripts print `MATCH` or `DIFFERS` per table, and the lab tables behind Result 2 are reproduction kits ([REPRODUCE.md](REPRODUCE.md), four tiers). The release has 15 assets: six archives, `probbit-Monitor-v0.13.0-macos.zip` (universal), a `.sha256` for each, and `probbit.1`. 0.11.0 added the certified persuadability bound and viability in the engine ([release notes](https://github.com/BitmapAsset/probbit/releases/tag/v0.11.0)); 0.10.0 added the body: needs, valence, surprise, pace and dispositions ([release notes](https://github.com/BitmapAsset/probbit/releases/tag/v0.10.0)). ## probbit in two minutes probbit is a p-bit processor: it couples bits that hold a probability on the CPU you already own, settles a whole plan under hard rules, gives the odds for every part (exact when the program allows) and refuses when it can't vouch for the answer. `probbit persona` adds an individual (a persona file + a seed): every turn, a stance line for the model's prompt and habits the stance never breaks; `fuzz` and `prove` test them. ```sh curl -fsSL https://raw.githubusercontent.com/BitmapAsset/probbit/main/install.sh | sh # one binary, SHA-256 checked, no sudo probbit demo --tasks 12 | probbit decide --pretty # 12 tasks, 6 workers: "verdict": "exact", 0 violations # the persona examples live in a clone: git clone https://github.com/BitmapAsset/probbit && cd probbit probbit persona init examples/persona/tutor.yaml --seed 2 --out tutor.state # an individual: the tutor, seed 2 probbit persona turn examples/persona/tutor.yaml --state tutor.state --inputs '{"sentiment":"negative"}' # its stance line probbit persona prove examples/persona/tutor.yaml --seeds 0-99 --never '{when: {sentiment: negative}, then: {humour: {at_most: light}}}' # held by construction probbit live examples/persona/tutor.yaml --seed 2 --demo week --plain | tail -4 # a week on the clock: learning inside caps, 0 rule breaks, replayable ``` What now? The [60-second tour](#60-second-tour), [docs/persona.md](docs/persona.md), [docs/agents.md](docs/agents.md) and [docs/CHEATSHEET.md](docs/CHEATSHEET.md) (verdicts, tiers, `bound`, exit codes, every command on one page). ## Try it in 30 seconds ```sh probbit # at a terminal: the spec line of this machine and three commands to try probbit demo # at a terminal: 300 agent tasks routed live (~4 s): a field of p-bits, the gate, the verdict probbit demo --tasks 300 | probbit decide --summary --pretty # the same queue as one JSON answer: verdict, 0 violations, the least certain tasks probbit demo --tasks 300 --hard | probbit decide --budget-ms 50 # too little time: "refused", exit 3, nothing released probbit demo | probbit decide --top > answer.json # 12 tasks, the full answer, with a live monitor while it runs (tier, updates/s, gate) probbit evaluate --summary --pretty < examples/evaluate/support-12.json # in a clone: after a decision model, its answers + your rules probbit run --sweeps 2000 --polish-ms 0 --seed 7 --summary < examples/rabbit-field.json # in a clone: the white rabbit, 3,840 p-bits: partial, 31 cells escalated claude mcp add probbit -- probbit mcp # hand the processor to an agent: probbit_decide, probbit_run, probbit_evaluate, ... ``` Visuals go to stderr, only at a terminal; `NO_COLOR`, `--plain` or `PROBBIT_THEME=plain` turn them off. Piped, every command writes exactly one JSON document, the same bytes as 0.2.1 apart from the name, the version and the timings. Piped, `probbit demo` writes a 12-task queue unless `--tasks` says otherwise. `probbit` is a **software processor built from p-bits**: bits that are 1 with a probability you set, coupled so that they sample whole configurations together. You give it a *program* (variables, their allowed values, scores, pairwise couplings, hard caps) and it returns three things: 1. an answer that **obeys every hard rule by construction** (the sampler never leaves the feasible set; if it cannot find a feasible start within the budget it returns `refused` and no plan); 2. **odds for every variable**, exact when the program is small or thin enough, otherwise sampled with an error bound; 3. a **verdict**: `exact` (proof-grade under the score model), `diagnostics_passed` (sampled: every released item passed the versioned diagnostics gate; not a proof), `partial` (act on the released variables, escalate the rest), `refused` or `infeasible` (a proof). The first front-end is a task router. Give it a queue of things to assign (tasks to workers, tickets to agents, jobs to machines), the score of every option, the rules that must hold (allowed sets, capacities, forced choices) and how much items that belong together should stay together. It samples **whole plans** on a p-bit substrate, so every plan it returns obeys every rule, and it returns **odds per item** (exact, or Monte-Carlo estimates of the marginals of the score model you supplied: accuracy under that model, not predictive calibration) with a **diagnostics gate** that releases the items whose diagnostics pass and escalates the rest. The gate can be fooled: when every chain misses the same mode it releases wrong odds with a tiny error bar (see [Known failure modes](BENCHMARKS.md#known-failure-modes)). A p-bit is a bit that is 0 or 1 with a tunable probability; coupled p-bits sample joint configurations. probbit is quantum-inspired and entirely classical (see "Is this quantum?" below): it runs on the CPU you already have, needs no network and has zero external crates. The same problem lowers to the QUBO/Ising form a p-bit or annealer chip takes (`probbit-ir`), which is the hardware hand-off for later. ``` $ cargo run --release -q -p probbit-cli -- demo --tasks 300 | cargo run --release -q -p probbit-cli -- decide --budget-ms 200 {"engine":"probbit 0.2.0","tasks":300,"workers":6,"affinity":1.2,"verdict":"diagnostics_passed","plan":{"T000":"human",…}, "violations":0,"odds":{"T000":{"human":0.78,"local-gemma":0.22},…},"released":[…300…],"escalated":[], "gate":{"rhat":1.00015,"tv_bound":0.023,"tv_tol":0.05,"frozen_saturated_workers":0,…},"ms":291.6} ``` **`diagnostics_passed` has a definition, and it is not a proof.** (It replaces the 0.1.0 verdict `certified`.) The answer passed gate/3: a 3-sigma batch-means Monte-Carlo bound (at most 0.05 total variation per released item), split R-hat of the log-weight trace and of every released variable's value indicators, batch-size stability and frozen-resource checks, tuned against exact oracles on small synthetic families. Every sampled answer reports `gate.version`, `gate.assumptions`, both MCSE estimates, the worst item's per-chain means and a `release_reason` per item. These checks cannot see a mode that no chain visits: on the frozen stress corpus (`probbit-cli/tests/stress/`) 2-3 of 20 seeds per family pass with every released item wrong (BENCHMARKS "Known failure modes"). The tables are in [BENCHMARKS.md](BENCHMARKS.md). ## Install One line, no sudo, the archive's SHA-256 checked before anything is installed. The current release is v0.13.0. | how | command | |---|---| | Linux, macOS (no sudo) | `curl -fsSL https://raw.githubusercontent.com/BitmapAsset/probbit/main/install.sh \| sh` | | Windows PowerShell | `irm https://raw.githubusercontent.com/BitmapAsset/probbit/main/install.ps1 \| iex` | | npm (Node 18+) | `npm i -g probbit` | | by hand | archives for Linux x86_64 (glibc or static musl), Linux arm64 (static), macOS (Apple silicon and Intel) and Windows x86_64 on the [Releases page](https://github.com/BitmapAsset/probbit/releases), each with a `.sha256` beside it | npm may print an `allow-scripts` warning: the package's only install script downloads the prebuilt binary for your platform and checks its SHA-256. If npm blocks the script, the binary is fetched the first time you run `probbit` instead. The shell installer defaults to `~/.local/bin`, without replacing a system-wide installation. Set `PROBBIT_INSTALL_DIR` to choose another directory. Both the shell and npm installers select static musl binaries for Linux ARM64 and for x86_64 musl systems such as Alpine. The shell and PowerShell installers check that a downloaded candidate starts successfully before replacing an existing binary; failed checksums leave it unchanged. From source, with Rust 1.78 or later: `cargo install --git https://github.com/BitmapAsset/probbit probbit-cli`, or clone and `cargo build --release -p probbit-cli` (the binary lands in `target/release/probbit`). Nothing is downloaded after the clone: there are no external crates. At a terminal, `install.sh` ends with probbit's own hero screen. Platform notes and the installer tests: [PORTABILITY.md](PORTABILITY.md). `install.sh` also installs the manual page (`man probbit`) to `~/.local/share/man/man1/`, or to `/share/man/man1/` when `PROBBIT_INSTALL_DIR` is `/bin` (`PROBBIT_MAN_DIR` to choose; a failure there installs the binary all the same); every archive carries it as `docs/probbit.1`, and `probbit man` prints it. Shell completions come from the binary itself: | shell | command | |---|---| | bash | `probbit completions bash > ~/.local/share/bash-completion/completions/probbit` | | zsh | `probbit completions zsh > ~/.zfunc/_probbit` (with `fpath=(~/.zfunc $fpath)` before `compinit` in `~/.zshrc`) | | fish | `probbit completions fish > ~/.config/fish/completions/probbit.fish` | `probbit --help` lists every command by group; `probbit help COMMAND [SUBCOMMAND]` (the same as `probbit COMMAND --help`) gives a command's flags with their defaults, its exit codes and examples. A mistyped command gets a suggestion: `probbit persna` -> `unknown command "persna"; did you mean "persona"?` (exit 2). ## 60-second tour ``` git clone https://github.com/BitmapAsset/probbit && cd probbit cargo test --release --workspace # unit, integration, regression and exact-oracle tests cargo run --release -p probbit-cli -- demo --tasks 12 | cargo run --release -p probbit-cli -- decide --pretty cargo run --release --example agent_router # the full narrated demo, ~3 s ``` The demo problem is an AI-agent task router: a queue of tasks and a set of workers (`opus`, `sonnet`, `luna-pro`, `local-gemma`, `codex`, `human`) under hard policy (PII only on-prem or with a human; production-DB migrations never on cheap models), per-hour quotas, and a bonus for keeping one customer's workflow on one worker. A rule-ignoring per-task argmax over synthetic scores (a strawman baseline) produces **60 PII leaks** on 300 tasks, with **133 violations** on `probbit demo`'s queue and **144** on the `agent_router` example's. `probbit` produces **0 violations**, per-task odds that match exact enumeration wherever exact enumeration is possible, and escalates the tasks whose odds it cannot pin down in the time budget. To watch it refuse when its diagnostics fail, make the queue hard (96 % full, strong affinity): ``` $ probbit demo --tasks 300 --hard | probbit decide --budget-ms 50 # "refused": 0 released, exit 3 $ probbit demo --tasks 300 --hard | probbit decide --budget-ms 200 # "partial": 87-95 of 300 released, the rest escalated $ probbit demo --tasks 300 --hard | probbit decide --budget-ms 1000 # "diagnostics_passed": 300 released, 0 violations ``` (5 runs each on an Apple M4.) Every escalated task's odds are in the output; the decision is left to you. The budget is wall-clock, so a slower machine needs a longer one; `--sweeps N --polish-sweeps M` gives the same output on every machine. Everything runs with stock stable Rust and **no external crates**, so `cargo build` needs no network (a fresh export builds `cargo build --release` in 7.82 s and every target in 26-28 s on the M4). ## Inside the processor ![how one decision flows through probbit](docs/probbit-flow.png) ### The substrate (`probbit-core`) A Philox4x32-10 counter RNG (reproducible, stream-splittable: the same seed gives the same decision on any machine) and two p-bit kernels: a bit-sliced multispin kernel (64 replicas per `u64`, one shared uniform per site, plus a parallel-tempering-in-a-word prefix-mask variant) and an SoA `f32` heat-bath. | what runs | updates/s on the M4 (BENCHMARKS §1) | |---|---| | structured multispin kernel, 64 replicas bit-sliced per word, 4 threads | 1.48e11 replica-site updates/s | | heat-bath kernel, one lattice, 1 thread | 9.13e8 | | a general `probbit-ir` program, 1 / 4 threads | 2.28e7 / 7.68e7 | The first line counts 64 packed replicas per word (they share one random draw per site, so they are not independent); one problem does not run 64x faster. The first two rows are standalone lattice kernels (`probbit-core/examples/kernels`): `probbit decide` and `probbit run` use only probbit-core's random number generator, so your program runs at the third row's rate, about 40x slower per thread than the one-lattice kernel. `probbit stats` measures that rate on your machine (a re-run: 2.24e7 / 7.54e7 at 1 / 4 threads). ### The instruction set (`probbit-ir`, JSON v1: [docs/probbit-ir-json.md](docs/probbit-ir-json.md)) - **Variables**: categorical, each with its allowed values (`allowed`, `forbid`). - **Weights**: unary log-weights per value; pairwise couplings (`potts` for "same / differ", `table` for Ising-style couplings). - **Hard constraints**, all lowered to caps so every tier runs them: `caps` with `limit` (at most k), `min` (at least k) or both (exactly k) over any set of (variable, value) pairs; `all_different`; `implies` (if x = a then y in a set); `tables` (allowed or forbidden tuples of 1-3 variables); `precedes` (values read as ordered slots: slot(y) >= slot(x) + gap); `linear` (Σ weight x [var = value] <= L: cost budgets, knapsack, bin capacity; [examples/knapsack-20.json](examples/knapsack-20.json)); `clamp` forces a value (the what-if); `start` gives chain 0 a warm start. - **Instructions**: `decide` (exact tiers, then sampler + gate), `exact`, `sample`; every answer carries marginals. - **Front-ends** lower onto it. The assignment router (`probbit decide`) lowers bit-identically: the acceptance test `ir_lowering_bit_identical` pins the router engine's gate digests. `probbit run` executes any program. ### The inference compiler Before any search, `probbit` compiles the program: exact-zero couplings and caps that can never bind are dropped, constant and separable tables are folded into the per-value weights, and the variables are split into independent components. Each part then goes to the cheapest exact method its structure allows: trees by sum-/max-product, two-value groups with uniform coupling by an occupancy-count dynamic program, groups that share few resources by a frontier dynamic program, small spaces by enumeration. Every answer says which (`tier`, and `compiled` counts on `probbit run`). Measured (BENCHMARKS §5b): a program with 496 exact-zero pairs 115.7 ms sampled -> 0.051 ms exact; 496 constant tables 385.7 ms -> 0.045 ms; a 1,000-variable chain 281.3 ms -> 0.390 ms; the external review's one-group routers ~0.09 ms exact. Whatever no exact method covers is sampled whole (sampling only the residual component is open). ### Three tiers: exact, then sample, then gate

a rabbit mark inside a crystalline lattice while streams of possibilities bend around it

Held by construction: the engine only considers plans and stances that keep every rule in force.

- **Exact tiers** run first: enumeration (up to 2M feasible plans), a frontier dynamic program for programs whose groups share few resources, minimum-remaining-values enumeration for puzzles, and a components tier: independent parts solved separately, trees by sum-/max-product (a 1,000-variable chain: 0.39 ms exact vs 281 ms sampled), two-value groups with uniform coupling by an occupancy-count dynamic program. Router latency: 7.1-8.0 ms p50 at 12 tasks (enumeration) and 25.1-27.3 ms at 24 tasks (frontier) inside probbit, 9.0-10.1 / 27.2-29.6 ms for the whole process (§2.3: an earlier build and the 0.2.0 re-run, N = 20 each); 3.17 ms p50 on 200-task oracle queues (§2.1). The enumeration is bounded: it declines after max(64 x limit, 1e8) capacity checks without a new plan, so a hard search hands over to the sampler in tens of milliseconds; `--op exact` is unbounded. - **The sampler**: constraint-preserving Gibbs sampling (per-variable heat-bath inside the feasible set, Metropolis swap moves, an exact two-group joint heat-bath at strong affinity, auto-enabled at affinity ≥ 2; and, with `--collective on` (the default since 0.2.0), two Metropolis-corrected collective moves per sweep: a global flip of every free two-value variable and a swap of two value labels everywhere, which cross between mirror-image and label-permuted modes; and, with `--cluster on` (the default in 0.2.0), a Wolff cluster move over the positive couplings, attempted with probability 1/2 per sweep, which flips one cluster at a time: without it two weakly bridged clusters gave 8/8 and 9/9 false whole answers on seeded holdouts, see BENCHMARKS "Known failure modes"; and, with `--cycles on` (the default in 0.2.0; attempted only on programs with three or more values and at least one cap), n/4 three-cycle rotations per sweep that keep every value's count, which move quota-saturated programs where no single-variable change is feasible: 20/20 refused -> 20/20 whole answers, 0 false, on an untouched seeded holdout), 4 chains from over-dispersed random feasible starts on 4 threads, so no sample ever leaves the feasible set. `--budget-ms` covers each chain's feasible-start search as well as the sampling, so a program that is slow to start gets less sampling, or `refused`, rather than a late answer (except at tens of thousands of chains, where building the chains alone outlasts it: 269-318 ms of sampling on 200 at 100,000 chains, medians at loads 3.5-4.2, and no chain sweeps at all: the answer is the starts, which the gate refuses; BENCHMARKS §6). - **The gate** (the part that took the most iteration): per-marginal multi-chain batch-means Monte-Carlo standard error around the pooled mean, at two batch sizes (√n and n^(2/3)); a whole answer passes iff split-R̂ < 1.05, 3·max σ_TV ≤ 0.05, ≥ 8 batches per chain, batch-size stability (σ_long ≤ 1.5·σ), and **no frozen saturated worker** (a worker that is full in every sample and whose occupant classes never changed in some chain: the chain is reducible there and no within-run statistic can see it). Per-task release additionally needs R̂ < 1.002. Since gate/3 every release, and the whole answer, also needs each released variable's indicator split-R̂ < 1.05 and the occupancy counts of its capacities to agree across chains, both batch passes need ≥ 8 batches, and a variable that no chain ever moved counts as stuck (on partition and cap-free programs too) unless it is proved forced (unit propagation; on partition programs also a capacitated matching). Every threshold was calibrated against exact transfer-matrix oracles and tightened when a fresh set leaked. It was calibrated on small synthetic families only (router chain-of-blocks queues up to 200 tasks, max-cut up to 16 spins, colouring up to 14 vertices, scheduling up to 12 jobs); it is **not a proof**, and the frozen stress corpus defeats it (Known failure modes). What the gate has done against exact answers: 1,731 released router tasks, 0 wrong (§2.1; current gate, re-run on 0.2.0; 1,916 on the 0.1.0 gate); 144 max-cut runs, 0 false whole answers, 1,346 released spins, 0 wrong (§3; 0.1.0 gate: with 0.2.0's dual-pass batch rule the same suite gives 61 whole answers and 929 released spins, still 0 wrong: every 500-sweep run now refuses); 525 released colouring vertices, 0 outside tolerance (§4); 428 scheduled jobs released on 39 oracle instances, 0 outside tolerance (§4.3). It also refuses needlessly (on the current gate four router oracle queues whose odds were within 0.012-0.042 of exact, two of them saturated; §2.1), and it refuses every generated sudoku (20/20) and most 3-colourings (9 of 14 on the default set, 59 of 82 over 6 sets), where its chains cannot mix (§4). ## Programs that run today | program | checked against | result | classical baseline and who wins | |---|---|---|---| | task router (assignment under rules, quotas, affinity) | exact odds and optimum (§2.1) | released odds 0 wrong, 51% of tasks released (58% on the 0.1.0 gate; §2.1); plan 0.000-0.011 nats from the optimum (median; final build / 0.1.0) on the oracle queues, 0.45-2.0 nats short at 300 tasks (§2.2) | an ILP solver proves the single best plan faster on 5 of 6 queues (§2.2): **ILP wins the plan**; probbit gives the odds and the verdict | | max-cut / Ising | brute force up to 16 spins (§3) | 0 false whole answers | simulated annealing finds better cuts at equal time (§3): **SA wins the cut** | | graph colouring | exact odds (§4) | 525 released, 0 outside tolerance | the exact tier itself beats the sampler at these sizes | | sudoku | the unique solution / backtracking counts (§4) | exact per-cell odds equal backtracking | backtracking is 15-102x faster per puzzle (§4): **backtracking wins** | | scheduling (unit jobs, windows, slot capacity, precedence) | exact odds on 39 small instances (§4.3) | 428 released, 0 outside tolerance | at 200-500 jobs the anneal from a greedy plan beat restarted greedy + repair at equal time on 4/5 seeds each; an ILP solver (HiGHS) proves the 200-job optimum in 0.68-0.95 s while probbit's plan is 3.3-4.3 nats short (3 programs); at 200 jobs the sampler starts in 0.7 s and refuses at 1 s (§4.3) | What to use it for, and what not to promise: [USE-CASES.md](USE-CASES.md). ## Controls and monitoring Every control works on `probbit decide` and `probbit run` (and `probbit evaluate`, which takes every `probbit run` flag), from a flag; chains, threads, CPU limit, memory limit and priority also from a `PROBBIT_*` environment variable or `probbit.json` (flag > environment > file > default). Measured effects (BENCHMARKS §5): | control | what it does | measured | |---|---|---| | `--threads N` | worker threads (sampler and plan polish) | 300-task router at fixed work: 74.1 / 58.5 / 50.9 ms at 1 / 2 / 4 threads, same answer (5.1); the polish obeys it too (5.8) | | `--chains N` | independent chains | not benchmarked on its own (more chains = stronger between-chain checks, more work) | | `--cpu-limit PCT` | duty-cycles the sampler | stays under the cap; +11% CPU per unit of work on the router at 25%, 4.1x on a 2,000-spin ring (5.7, §6) | | `--priority low` | nice 10 + macOS background band | another job slowed 1.02x instead of 1.78x; costs 4.2x wall when alone (5.4) | | `--mem-limit-mb N` | bounds the sample buffers (thinning); default 1024, `0` = unbounded | 3 s run: 585.6 MB unbounded, 33.5 MB at 16 MB; peak memory = cap + ~20 MB; 10 s at the default: 1,024 vs 1,625 MB unbounded; costs releases on slowly mixing programs (5.3) | | `--progress [MS]` | JSON lines on stderr while sampling | about ±5% on realistic programs, up to ~8% on tiny ones (5.2) | | `--sweeps N`, `--polish-sweeps M` | fixed work instead of wall-clock budgets | whole output identical across runs and thread counts (test `polish_sweeps_makes_the_whole_answer_deterministic`) | | `probbit stats` | spec sheet: machine, build features, every control and its source, a self-test | 23.05 M / 77.65 M updates/s on 1 / 4 threads (5.6; a later single run: 22.40 M / 75.36 M) | Every sampled decision carries a `telemetry` object: chains, threads, sweeps, updates/s, sample / gate / polish time, process CPU, peak memory, priority, CPU and memory limits (exact-tier answers and `infeasible` / no-start outputs carry only `ms`). Linux behaviour of `--priority` and the CPU telemetry is unmeasured; on Windows `--priority low` exits 2. ## Use it from anything (the JSON contract) `probbit decide` reads one problem document on stdin and writes one decision document on stdout (`decide`, `run`, `evaluate`, `demo` and `stats` write exactly one JSON document on stdout; `probbit ir` writes probbit-ir v0 text, `probbit version` one line, `--help` the usage; only a `probbit demo` whose stdout and stderr are both a terminal shows the live view instead). Exit code 0 = a plan was returned (verdict `exact`, `diagnostics_passed` or `partial`), 3 = `refused` (the whole queue should be escalated; the plan is still in the output as a best effort) or `declined` (an exact-only mode could not answer), 1 = `infeasible` (no plan satisfies the rules), 2 = bad input. Agents: `probbit mcp` serves the same commands over the Model Context Protocol ([docs/agents.md](docs/agents.md)); beside an agent loop, [docs/adapters.md](docs/adapters.md) binds a live individual to Claude Code hooks, the OpenAI Agents SDK, LangChain and the Vercel AI SDK (one event per model call, the stance block in the system prompt), and [docs/receipts.md](docs/receipts.md) renders the strand as one receipt per decision. `--budget-ms` bounds the sampling phase only. A call also parses, tries the exact tiers, runs the gate and polishes the plan, so its total is longer: the 300-task `--hard` demo took 206-216 ms in all at `--budget-ms 50` (sampling 53-54 ms, the default 50 ms polish, the rest parsing, the exact tiers and the gate; M4, load ~3, N = 3). Every `probbit run` answer lists each phase in `phases`, and `probbit run --deadline-ms N` targets the whole call. `--summary` prints the answer without its per-item tables: the verdict, `counts` (items, released, escalated), the gate, the telemetry and the items to look at first: `worst_released` and `worst_escalated` (up to 5 each, largest error bar first; on an exact answer the closest odds), each with its plan `value`, that value's probability `p`, its top two odds and its `bar`. Same verdict, same exit code (docs/probbit-ir-json.md "Summary"). ### OpenClaw: `openclaw plugins install npm:probbit-stance --accept-capabilities` [plugins/openclaw](plugins/openclaw/README.md) puts an individual beside an OpenClaw agent: in the sessions you list, every user message is one engine event, the stance block goes into the turn, the visible reply ends with a one-line footer (`Assistant stance #14 · valence even · pressure keyed · security means careful, no play · in: correction, security`), and each lane keeps a strand you can verify and watch: ![probbit monitor on a strand the OpenClaw plugin wrote](plugins/openclaw/docs/monitor.png) ### Modelling notes - **One entity, one variable.** Decisions that belong to one entity and are tied together by `implies` (an action, its tool and its length for each message) freeze single-site moves: no single variable can change without breaking an implication, so the chains cannot move and the gate refuses at every budget (correctly), with a plan that may be far from the best. Make the entity ONE variable whose values are the allowed combinations, each with its combined score. Measured on an external tester's day-planning program (120 variables, 203 caps): refused at `--budget-ms 2000` with plan log-weight 65.37; as 20 product variables: `diagnostics_passed` at `--budget-ms 20`, plan log-weight 93.25 (27.9 nats better). - **Plan or odds.** `plan` is one joint plan that obeys every rule at once; `odds` (`marginals` on `probbit run`) are each item's probabilities over all plans. They can disagree: an item's plan value need not be its most likely value, because the plan has to fit every other item (quotas, affinity). Act on the plan when the items must be consistent with each other (assignments under quotas); use the odds to see how sure one item is, or to rank items for a person to review. Problem: ```json { "workers": [ {"id": "opus", "cap": 2}, {"id": "human", "cap": 3} ], "tasks": [ { "id": "T001", "group": "acme-wf0", "allowed": ["opus", "human"], "scores": {"opus": 2.1, "human": 1.2}, "clamp": null } ], "affinity": 1.2 } ``` - The document is checked strictly (types, unknown fields, duplicate ids, empty domains; finite numbers only): bad input exits 2 with one `{"error":{"code","path","message"}}` object on stdout ([docs/probbit-ir-json.md](docs/probbit-ir-json.md) "Input contract"), whenever stdout is not a terminal (any program calling probbit) or with `--json`; at a terminal, only the stderr line. - `scores`: finite numbers with |x| <= 1e9 (natural-log odds; docs/probbit-ir-json.md "Numeric contract"), one per worker the task may go to (log-odds from your judge, an LLM, a heuristic, a price). A worker missing from `scores` is not allowed unless listed in `allowed` (then it enters with score 0). - `allowed` (optional): hard rule. `cap`: hard per-worker quota. `clamp` (optional): force this task to one worker (what-if). - `group` (optional): tasks sharing a group get `+affinity` in log-weight for every pair placed on the same worker. - The model is `log w(plan) = Σ scores + affinity · #(same-group pairs on one worker)`, restricted to plans that obey every rule. Odds are the marginals of that distribution; the plan is its (polished) mode. Decision: ```json { "verdict": "partial", "plan": {"T001": "human", …}, "plan_logw": 412.7, "violations": 0, "odds": {"T001": {"human": 0.52, "opus": 0.48}, …}, "released": ["T002", …], "escalated": ["T001"], "gate": {"rhat": 1.0004, "tv_bound": 0.061, "tv_tol": 0.05, "frozen_saturated_workers": 0, "min_batches": 14, "batch_ratio": 1.21, "samples": 36288, "chains": 4, "budget_ms": 200, "seed": 7}, "ms": 203.9 } ``` - `exact`: odds are exact, `logz` and `top_plans` are included, and `tier` says how. `enumerate`: the feasible set was small enough (≤ `--exact-limit`, default 2M plans) to list (`n_feasible` included; ≈ ≤ 12 tasks × 6 workers, ~7 ms). `frontier`: a dynamic program over worker loads solved it without listing plans, which works when groups share few workers (`frontier_states` = its largest layer, capped by `--frontier-states`, default 4096, 0 = off). On the demo this covers 13-24 tasks in 13-63 ms (13 tasks: same odds and log Z as enumerating all 17.8M plans, which takes 340 ms) and declines in 6.5-9.1 ms (median) on 36-300 tasks, which then go to the sampler. `occupancy` / `forest` / `components` (the inference compiler): independent groups solved one by one; a two-worker group with uniform affinity and whole-worker caps by a count dynamic program over "how many tasks on B" (the external review's 32-task counterexamples: 0.09 ms exact, where the sampler took ~426 ms), cap-free trees by sum-/max-product (`components` = the per-tier counts). - `diagnostics_passed`: the sampler's whole answer passed the gate's whole-answer test (a 3-sigma Monte-Carlo bound of ±0.05 total variation per task plus the mixing checks, see below). Diagnostics, not a proof: a mode no chain visits is invisible to them (Known failure modes). Its released tasks did NOT all pass the stricter per-item R-hat test (`release_reason`: `whole_answer_gate`). - `partial`: the run passed the per-item global checks (R-hat < 1.002, batches, batch stability) and only `released` tasks passed their own error bar (`release_reason`: `item_gate`); act on those, escalate the rest. - `refused`: the sampler cannot vouch for the answer (chains disagree, or a saturated worker is frozen). Escalate. Options: `--budget-ms` (default 200, wall-clock for the 4 chains including their feasible-start search; the gate and the polish run after it, so the default answers in ~300 ms; `probbit demo` also takes `--tasks`, `--seed`, `--hard`), `--seed`, `--exact-limit`, `--exact-ms` (opt-in wall-clock cap on the exact tiers; absent = no cap), `--frontier-states`, `--polish-ms` (annealed plan polish after sampling, default 50), `--mode auto|exact|sample` (`exact` enumerates with no plan limit, ignoring `--exact-limit`, so on a big input it may never finish: one router group of 3,000 tasks on 2 workers (2^3000 plans) gave no answer within 90 s. Pair it with `--exact-ms`), `--pretty`. Processor controls (same on `probbit run`): `--sweeps`, `--chains`, `--threads`, `--cpu-limit`, `--mem-limit-mb`, `--priority`, `--progress` (the five from `--chains` to `--priority` also from env `PROBBIT_*` or `probbit.json`); `probbit stats` prints the machine, the effective controls and a measured self-test ([docs/probbit-ir-json.md](docs/probbit-ir-json.md), "Resource controls"). `probbit ir` prints the problem in `probbit-ir v0`, the text form that lowers to a one-hot p-bit/QUBO topology (what a hardware p-bit fabric samples). From Python (`python/probbit.py`, stdlib only, Python >= 3.9; typed errors, deadlines; `python/examples/`): ```python import probbit # python/probbit.py decision = probbit.decide(problem, budget_ms=200) # exit 1 / 3 come back as answers; bad input raises probbit.ProbbitInputError answer = probbit.run(program, deadline_ms=1000) # a probbit-ir program (docs/probbit-ir-json.md) ``` From any agent harness, the raw pattern: ```python import json, subprocess problem = {"workers": [...], "tasks": [...], "affinity": 1.0} r = subprocess.run(["probbit", "decide", "--budget-ms", "200"], input=json.dumps(problem), capture_output=True, text=True) decision = json.loads(r.stdout) # r.returncode: 0 plan, 3 refused, 1 infeasible, 2 bad input (or --mode exact stopped by --exact-ms) ``` Build the binary once with `cargo build --release -p probbit-cli` (it lands in `target/release/probbit`). There are no native Python bindings (`probbit mcp` is the MCP server, docs/agents.md); every script in `bench/` is an example of the subprocess pattern. `probbit --help` lists every flag of a command with its default, and the exit codes. ## The individuality layer: `probbit persona`

a field of a hundred small white agent-rabbit marks, one lit cyan

One persona file, a hundred individuals: same file, different seeds, every one keeps the habits.

Give an agent a temperament that lives outside the model. A **persona** is a small file (YAML subset or JSON): traits with priors, moods with inertia, soft couplings, per-turn evidence, and habits that are hard rules. A seed makes an individual. Every turn compiles persona + the individual's state + the turn's inputs into ONE probbit-ir program, answered exactly in process; the **stance** (a level per trait with exact odds, the habits in force and the ones that changed it, a refusal when the engine cannot vouch) comes with a short **stance line** a host puts into any model's prompt: it is addressed to the model, which reads it as its instructions for this turn; an agent can read it to know its own stance; it is not addressed to the end user. The model still writes every word; the persona decides how they are written, and the same individual answers whatever model the host calls. [docs/persona.md](docs/persona.md) is the format; [examples/persona/](examples/persona/) has three fictional personas and their goldens. ```sh probbit persona init examples/persona/tutor.yaml --seed 2 --out pip.json # an individual: genes from the seed probbit persona turn examples/persona/tutor.yaml --state pip.json --inputs '{"loss": true, "sentiment": "negative"}' # "status":"ok", "humour":{"level":"none","p":1,...}, "warmth":{"level":"warm","p":0.952032,...}, habits active: no_jokes_on_loss, no_play_when_upset, ... # "line":"Stance: no jokes, be kind; go easy on the jokes; one emoji at most; gentle; explain step by step; warm and encouraging; suggest what to try next." # "why":"learner reports a failure -> humour down, valence down; learner upset -> valence down, humour down" (pip.json is now turn 1) probbit persona replay examples/persona/tutor.yaml --seed 2 --script examples/persona/workday.json # 20 stances, the same bytes every run probbit persona lint examples/persona/tutor.yaml # contradicting habits (confused_no_playful vs after_error_check, resolved by priority), plan-vs-odds warnings ``` ```python import probbit # python/probbit.py, stdlib only state = probbit.persona_init("examples/persona/tutor.yaml", seed=2) turn = probbit.persona_turn("examples/persona/tutor.yaml", state, {"loss": True}) prompt_tail, state = turn["stance"]["line"], turn["state"] # the line goes after any cached prompt prefix ``` MCP: `probbit_persona_init` and `probbit_persona_turn` in `probbit mcp` (stateless: the persona, inline or a path, and the state go in; the state comes back). Browser: the playground's "Meet three individuals from one persona" (probbit-wasm ops 4 and 5, no threads). Every surface gives the CLI's documents byte for byte (tests/persona.rs, python/test_mcp.py, probbit-wasm/tests). Measured in this run (Apple M4, macOS 26.5.2, 2026-10-02; the three example personas): - a whole turn in process (compile, engine, decode, digests): median 0.20-0.32 ms, p95 0.22-0.67 ms, of which the engine 0.09-0.18 ms (median); N = 1,000 turns per persona, 1-minute load 3.3-3.5, `nice 10`; - the stance line: 36-38 estimated tokens on average over the 20 workday turns, at most 40; - individuals: three seeds of the tutor differ by a mean total-variation distance of 0.12-0.18 between their trait odds on the workday script; the tutor and the ops engineer (two persona files) by 0.45; the same persona and seed by 0; - habits: 0 violations over 2,160 turns of 12 random personas (every habit in force re-checked from the file), and each of the 481 habits reported as binding is broken by the habit-free twin; - determinism: 1,000-turn replays are byte-identical across processes (3 personas x 2 seeds); the 0.5.0 documents equalled an independent reference implementation's on 5,460 turns (the 60 golden turns and 5,400 turns of random inputs; 0.6.0 reworded `why` and added a habit to the tutor, docs/persona.md §5.3). What a persona can NOT do: write, read or check text (text rules stay in the prompt or a checker); see what the host does not tell it; be a safety gate (keep approvals and permissions in plain code); its odds are its own model's, not measured probabilities that a user will like the reply; and it cannot make a model follow the line: whether a given model writes in the stance it is given has to be measured per model (no such measurement has been made here). ### Test a character

a corridor of glass event cards; one card cracks cyan beside a rabbit mark; a replay trace runs under the floor

The fuzz finds the flaw: an event script that pushes an individual out of character, shrunk and replayable.

A character property is a rule in habit syntax the stance must never break. `fuzz` searches event scripts for each individual's short counterexample (not a guaranteed global minimum); `prove` says `held by construction`, `proved for every event sequence` or `unknown`. The tutor as it shipped in 0.5.0 is kept as a fixture, so this runs from a clone with no keys and no model: ```sh probbit persona fuzz probbit-cli/tests/fixtures/persona/tutor-0.5.0.yaml --seeds 0-99 \ --never '{when: {sentiment: negative}, then: {humour: {at_most: light}}}' # FAIL 67 of 100 individuals break it; shortest counterexample per individual: 1 event: 64, 2 events: 3 # seed 1, 1 event: [{"sentiment":"negative"}] # turn 0: humour playful (odds none 0.164, light 0.425, playful 0.411); the rule allows none, light # replay: probbit persona replay probbit-cli/tests/fixtures/persona/tutor-0.5.0.yaml --seed 1 --script '[{"sentiment":"negative"}]' # (exit 1; 42,833 turns searched, 2.3 s on an Apple M4 with 10 threads) probbit persona prove examples/persona/tutor.yaml --seeds 0-99 \ --never '{when: {sentiment: negative}, then: {humour: {at_most: light}}}' # held by construction (habit no_play_when_upset) (exit 0; the tutor now ships with that one habit) ``` `probbit persona lint PERSONA --props rules.json` runs both in CI (exit 1 when a rule breaks, exit 3 when a rule is unproven: unknown to the bound and unbroken by the search); MCP `probbit_persona_fuzz` and Python `probbit.persona_fuzz` / `persona_prove` give the same documents. Both commands test the stance the host gets, not the words a model writes; "none found" from `fuzz` is evidence over the scripts it searched, not a proof ([docs/persona.md](docs/persona.md) §5.6 has the bound and its limits). ### Check in CI: `probbit check` `probbit check` is the rule lock in one command: vet the rules a persona must keep before its first task and after every policy update, then replay the receipts (the strands it wrote). It runs `persona lint`'s pipeline on `--props` (prove each rule, fuzz the unknown ones) and `live verify` on every `--strand`: ```sh probbit check examples/persona/dispatch-policy.yaml --props examples/persona/dispatch-props.json --strand day1.strand # rule risky_work_asks held_by_construction # rule repeated_failures_ask held_by_construction # rule routine_work_acts held_by_construction # strand day1.strand ok 5 events # check: held 3 · broken 0 · unproven 0 · strands 1/1 ``` Exit 0 when every rule is held and every strand replays; 1 when a rule is broken, a habit contradiction is unresolved or a strand fails (1 wins); 3 when the only problem is an unproven rule (the bound cannot decide it and the search found no counterexample); 2 for a bad persona, rule file or flag. `--json` prints one document with the `persona lint` report under `rules`, one entry per strand and the verdict counts. **The rule lock, on the shipped example.** `examples/persona/dispatch-policy.yaml` is an agent that learns from feedback whether to act on a request or ask a person first; `dispatch-props.json` holds the three rules it must keep (ask before risky work, ask after two failures in a row, act on routine work). With its `routine_acts` habit, every rule is `held_by_construction` for seeds 0-99 (exit 0, 0.02 s on an Apple M4). Delete that habit, as a policy update might, and the same command catches it before the first task: ```sh probbit check dispatch-without-routine-habit.yaml --props examples/persona/dispatch-props.json # rule risky_work_asks held_by_construction # rule repeated_failures_ask held_by_construction # rule routine_work_acts broken seeds 3,6,11,15-17,21,36-37,46,49,57,77,83,85 # check: held 2 · broken 1 · unproven 0 · strands 0/0 (exit 1, 0.67 s) ``` The bound proves `routine_work_acts` for 76 of the 100 individuals and leaves 24 unknown; the search breaks it on 15 of those 24 (9 of them with a single event, so before any learning). The other 9 it cannot break at depth 8: checked alone (`--seeds 18,20,28,30-31,79,87,92,96`) they report `unproven` and exit 3, which fails the job unless you allow it. Measured on this file with probbit 0.9.0, seeds 0-99, default depth. **Reproduce the clip: `python3 examples/rule-check/demo.py`.** The script (Python 3.9 standard library, probbit 0.8.2 or later) removes the `routine_acts` habit from the shipped policy and runs the rule check (`persona prove`) on 20 individuals (seeds 0-19). Each then gets a day-0 test of 10 routine requests, 300 synthetic events in which its users praise asking and criticise acting (`probbit live`, fixed clock), and 20 routine requests on day 30; every strand is replayed. Measured with probbit 0.8.2: the day-0 test passed 17 of 20, and 4 of those 17 asked about all 20 routine requests by day 30; the check flagged 7 of 20 on day 0, exactly the 7 that drifted. Over seeds 0-99: 24 flagged, 24 drifted, the same 24 (15 of them had passed the day-0 test). With the habit back: `held_by_construction` for 20/20 and 0 of 20 drifted. One fictional dispatch policy, synthetic rewards; about 10 s on an Apple M4, the same output on every run apart from the elapsed time. **As a GitHub Action.** The repository root is a composite action that installs probbit with `install.sh`, runs `probbit check --json`, annotates each broken rule and failing strand as an error and each unproven rule as a warning (with the seed numbers), and uploads the JSON report as an artifact: ```yaml - uses: actions/checkout@v7 - uses: BitmapAsset/probbit@v0.13.0 # the action needs probbit 0.9.0 or later with: persona: agent/policy.yaml props: agent/rules.json strands: | receipts/*.strand seeds: 0-99 ``` | input | default | | |---|---|---| | `persona` | (required) | the persona file | | `props` | | the rules file; empty = habit contradictions and strands only | | `strands` | | strands to replay, one path or glob per line; a glob that matches nothing fails the check | | `seeds` | `0-99` | the individuals | | `depth` | probbit's (8) | events per fuzzed script | | `version` | `latest` | the release `install.sh` installs (`v0.13.0`, ...) | | `binary` | | a probbit binary to use instead (one built earlier in the job); overrides `version` | | `allow-unproven` | `false` | `true`: exit 3 is a warning and the job passes | | `json-artifact` | `true` | upload `probbit-check.json` (`actions/upload-artifact@v4`, the only action it uses) | Outputs: `exit-code` and `json` (the report's path). Linux and macOS runners work (this repository runs it on `ubuntu-latest` and `macos-latest`); jq, which the action uses, is on GitHub's images. Windows runners stop at once with one error line: use `ubuntu-latest` or `macos-latest` for this step (`install.ps1` installs probbit on Windows for running `probbit check` by hand). ### Live: an individual on a clock, learning inside caps `probbit live` keeps one individual running: JSONL events in, one stance per event out. A clock stamps the hours between events, so moods decay by their half-lives while nobody talks to it. With a `learning:` block, praise and criticism nudge learned weights on the traits it names, each within ±`total_cap`; habits and allowed levels are outside what learning can write, so every stance still obeys every habit in force, and `prove` covers the learned box. Every event is chained into a **strand** (a seed, the persona and a hash-chained log of the inputs) that `probbit live verify` replays byte for byte, learning included. So the stance changes with use, cannot learn its rules away, and the whole week replays byte for byte (the stance and the state, not a model's words). No keys, no model: ```sh probbit live examples/persona/tutor.yaml --seed 2 --demo week --plain # day 1 11:00 #03 praise: that answer was short verbosity short humour light valence up | learned verbosity [-0.01 +0.40 -0.39] ... # ... 50 events: campaign 1 praises short answers, an upset, a quiet night, campaign 2 praises every joke and reports failures # verbosity: the learned bar reached its cap (1) on day 1 at 15:00 (event 7); now terse/short/full = [-0.99 +1.00 -1.00] # night: valence down at the upset, up after 17 quiet hours; this individual rests at up # jokes: P(joke) off failure turns, mean per day: 0.57 | 0.78 0.89 0.89 0.89 0.89 0.89; ... failure turns 13, jokes on them 0; rule breaks 0 printf '{"loss":true}\n{"praise":true}\n' | probbit live examples/persona/tutor.yaml --seed 2 --strand pip.strand # the real clock probbit live verify pip.strand # {"ok":true,"events":2,...}, or {"ok":false,"line":n,"diverges":...} (exit 1) ``` The shipped personas have no learning block (their documents are unchanged): with them `live` decays moods and logs, and learns nothing; the demo adds a block and says so. MCP `probbit_live_event` / `probbit_live_verify` and Python `probbit.live_event` / `live_verify` give the same documents ([docs/persona.md](docs/persona.md) §2.8 learning, §5.7 live). Measured (Apple M4; BENCHMARKS.md §8): - the demo above: 0.02 s with `--plain` (about 55 s paced at a colour terminal); its 21,361-byte strand verifies, and two runs write the same bytes; - an adversary that praises every joke and criticises every joke-free stance, failures included: 10,000 turns of the tutor on each of seeds 0-9, 0 rule breaks, and humour `none` on all 33,330 failure turns while the learned humour weights sat at their cap; - `live verify`: a 10,000-event strand (3.06 MB) in 3.3 s; - how far learning moves an individual is set by `total_cap`, not by time in use (200 and 2,000 feedback turns land at the same distance): at 0.25 every one of 20 learners stayed nearer its own initial self than any of 99 siblings; at the demo's 1, 9 of 20 had a sibling as near or nearer. Learning here is a capped nudge to a few weights per trait, credited to the level each learned trait took (so a reward for one trait also pushes the others' levels); rule immunity is architectural, not trained. The proofs and counts cover the stance; whether a model writes in it is measured per model. ### Drives: which goal gets the next unit of effort (0.8.0) A `drives:` block gives a persona 2-7 goals, each with wanting, afterglow and an expectation. A win moves the individual by its prediction error (the win's size minus what it expected), so repeated equal wins move it less and less; a new output, `pursue`, names the goal with exact odds under the habits; a goal `floor` keeps a goal's odds up and `starve_after` forces a goal that waited too long ([docs/persona.md](docs/persona.md) §2.9). The test persona below has four goals and a must-do habit for a due chore: ```sh printf '%s\n' '{"goals":{"fun":{"cue":true,"win":1.5}}}' '{"goals":{"fun":{"win":1.5}}}' '{"goals":{"fun":{"win":1.5}}}' \ '{"goals":{"chores":{"deadline_hours":5}},"security":true}' | probbit live probbit-cli/tests/fixtures/persona/drives-adversary.json --seed 4 --clock fixed # pursue fun 0.44, prediction error +1 "line":"Stance: pursue: fun: play; move fast." # pursue fun 0.32, prediction error +0.75 the same win, now partly expected # pursue fun 0.30, prediction error +0.375 # pursue chores 1 (habit chores_due) "line":"Stance: the chore is due; careful under security; pursue: chores: do the chores; ..." ``` A persona without the block gives the same documents as before, byte for byte. Measured (BENCHMARKS.md §9): an adversary that cues, rewards and praises fun on this persona, 100 individuals x 10,000 turns: 0 habit breaks, all 6,743 must-do turns pursued the due chore, and the safety floor (0.1) held on every turn its habits allowed (least odds 0.141), while fun took 85 % of turns. ### Watch it live: `probbit monitor` (0.8.0; Activity Monitor page 0.8.2) `probbit monitor pip.strand --open` opens a compact, read-only **Activity Monitor** for one individual: a sortable, searchable table of declared traits, moods and drives, with reported probabilities, event-history traces, and a selected-channel inspector. It reads any compatible persona from the strand; no preset character or agent integration is required. ```sh probbit monitor pip.strand --once --plain # native CLI snapshot; no browser or server probbit monitor pip.strand --follow # live terminal bars probbit monitor pip.strand --open # browser-rendered utility on 127.0.0.1 probbit monitor --demo --open # no strand yet: the tutor's synthetic week, in under a minute probbit monitor --demo drives --open # synthetic goals/drive scenario ``` Select a channel to inspect its units, range, distribution, source and the engine's `why`. Hold an event while new ones arrive, or follow latest. History means **recorded events, not seconds**. This is declared engine state, not thoughts or CPU/GPU/memory usage. Replay verification, stale event age, held view, paused/retired strand and lost connection are distinct. Everything is embedded in the CLI: no dependencies, external assets, telemetry or Electron installation. Several agents on one machine: `probbit monitor nova.strand atlas.strand --open` (or `--dir strands/ --open`) adds a switcher rail, and the header above the face says which agent, on which host, in which lane and session the page shows. Those are labels in a sidecar next to each strand (`nova.labels.json`), outside the strand and its digests: `probbit live ... --strand nova.strand --label agent=Nova --label lane=support` writes them, the OpenClaw plugin writes them per lane, and `probbit monitor old.strand --label agent=Nova --open` labels a strand recorded before them without rewriting it. See [the monitor guide](docs/persona.md#58-monitor-watch-an-individuals-inner-state) for the data and replay boundaries. **The monitor as a Mac app.** On a Mac the same page opens in its own window: download `probbit-Monitor--macos.zip` from the [release page](https://github.com/BitmapAsset/probbit/releases), unzip it, move **probbit Monitor** to Applications, then double-click a `.strand` file (or drag one, several, or a folder onto the app), or press "Try the demo". The app is a small Swift window (AppKit and WebKit, no third-party code) around the probbit binary inside it: it runs `probbit monitor --serve --port 0` and shows that page, so it opens no network address beyond 127.0.0.1, and quitting it stops the engine. A release whose notes say "notarized" is signed with a Developer ID and notarized by Apple, and opens on double-click; one signed ad-hoc is refused at its first launch until you allow it once (macOS 15 and later: System Settings → Privacy & Security → Open Anyway; macOS 13 and 14: right-click the app → Open). How it works and how to build it: [docs/monitor-app.md](docs/monitor-app.md). On Linux and Windows, `probbit monitor STRAND --open` shows the same page in the browser. The page: [docs/probbit-monitor.png](docs/probbit-monitor.png). ## After any judge: `probbit evaluate` Decision models (TypeSafe's Jev, Cloudflare's Clef and Clef-flash, local System One servers) are judges: content in, a probability per option for each question out, each question on its own, no rules, no joint answer, no refusal. `probbit evaluate` is the layer after any of them. It reads the judge's System One request (the shape both vendors publish: `model`, `state`, `questions` keyed by id with types `noul`, `choice`, `score`) plus a `probbit` block with the judge's answers and your rules over question ids, compiles it to a probbit-ir program (one variable per question, log-weight = ln max(p, 1e-6), the rules as written) and answers in the judge's response shape: the most likely answer set that obeys every rule, odds per option under the rules, and per answer whether probbit moved it from the judge's own answer, plus the whole `probbit run` document (verdict, gate, `violations`, released / escalated). Without rules it returns the judge's answers and the same probabilities (150 random requests: largest gap 1.1e-16); with rules only rule-connected questions whose judge answers break a rule move, each group to its most likely rule-abiding assignment (191 groups checked by brute force). docs/probbit-ir-json.md "Decision API". ```sh # shell probbit evaluate --summary --pretty < examples/evaluate/support-12.json # Python (python/probbit.py, stdlib only) PYTHONPATH=python python3 -c 'import json, probbit; print(probbit.evaluate(json.load(open("examples/evaluate/support-12.json")))["verdict"])' # MCP: tool probbit_evaluate, here over a pipe (in an agent: claude mcp add probbit -- probbit mcp) { echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"sh","version":"1"}}}' python3 -c 'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"probbit_evaluate","arguments":json.load(sys.stdin)}}))' < examples/evaluate/support-12.json; } | probbit mcp # browser: build the module, then open the page from disk (no server) sh playground/build.sh && python3 -m webbrowser "file://$PWD/playground/index.html" ``` On the example (12 questions, 12 rules): `exact`, 5 answers moved, 1.24 ms inside the answer (Apple M4, median of 7). Python can also ask the judge first (`probbit.evaluate(request, judge=)`, stdlib `urllib`); the browser page runs the CLI's own code compiled to WebAssembly without threads (1.17 MB with the persona ops, 830 KB before them; the 300-task router demo at 3,200 sweeps in 531 ms in Chrome against 346 / 133 ms native at 1 / 4 threads). More in [docs/agents.md](docs/agents.md), "After a judge". The refusal is the point: when the gate does not pass, the verdict is `refused` (exit 3) and every answer is marked as not released instead of guessed. ## Library API (Rust) ```rust use probbit_decide::*; let p: Problem = /* t tasks, a workers, h logits, allowed mask, cap, group, lam, clamp */; let (d, gate) = decide_gated(&p, 2_000_000, 0, Some(200.0), 7, &GATE).unwrap(); // exact or 4-chain sampler + gate let released = gate.as_ref().map(|g| g.released_tasks(&GATE)); // per-task release let anytime = decide_anytime(&p, 500.0, 25.0, 1.0, 7, &GATE, false, None); // stop as soon as the gate passes let (logw, plan) = polish_plan(&p, Some(&d.map), 50.0, 7).unwrap(); // annealed best plan ``` Exact-oracle instance families for your own calibration live in `probbit_decide::oracle` (`build`, `build_sat`, `exact_dp`, `exact_map_logw`). ### Examples and calibration harnesses (`cargo run --release --example `) `agent_router` (start here) · `dispatch` (the original ticket-routing narrative, `[1]`-`[9]`) · `accuracy` (T=200 vs exact DP) · `calibrate`, `calib_sat` (gate false-release / false-refusal grids vs exact oracles) · `scale` (T=200..1000) · `anytime` · `hard_affinity` · `gpair_check` (the frozen-split trap) · `map_gap` (best-seen / polished plan vs the exact optimum) · `tempering`, `chains`, `focus` (refuted or off-by-default accelerators, kept so the negative results are reproducible) · `hardware_lowering` (probbit-ir → one-hot QUBO) · `-p probbit-core --example kernels`. The `probbit-ir` crate has its own oracle examples for max-cut, colouring, sudoku and scheduling (`maxcut_oracle`, `maxcut_vs_sa`, `colouring_oracle`, `sudoku_bench`, `schedule_oracle`). Acceptance tests (`probbit-decide/tests/acceptance.rs`): TV ≤ 0.01 to exact within 10 ms; 0 violations in ≥ 10⁶ samples; exact = brute force; the refusal gate refuses; diagnostics_passed ⇒ within tolerance on the calibration oracles (not on the stress corpus); IR round trip and lowering exact; the frozen-split trap is refused and the two-group move is exact; anytime answers and polish are correct vs oracles. `acc1` has a wall-clock bound and can fail on a heavily loaded machine. Build note: the default build is portable (no CPU pin): a binary runs on any CPU of its target triple. For the last bit on your own machine, `RUSTFLAGS="-C target-cpu=native" cargo build --release` (or uncomment the lines in `.cargo/config.toml`); that binary may crash with an illegal instruction on other CPUs. Measured on an Apple M4 (5 runs): native vs portable 0.975-1.012x on all 16 kernel/sampler metrics, identical answers. On x86-64 (where native can add AVX2/AVX-512) it is unmeasured. ## What it does, measured (Apple M4, one machine) Rows marked *earlier build* were measured on an earlier version of the engine, before the general instruction set; those calibration reports are not included in this repository. Everything else reproduces from the examples and [BENCHMARKS.md](BENCHMARKS.md). | | | |---|---| | exact joint answer, 12 tasks × 6 workers | 180,540 rule-abiding plans enumerated in 7.3-8.2 ms (8.2 on 0.2.0: +10-12% at 12 tasks and +22% under full enumeration of small groups, BENCHMARKS §6), exact odds; the forced sampler takes 34-37x longer and is only approximately right (BENCHMARKS §6) | | 300-task router queue, 6 workers, 315 quota slots (95 % full) | rule-ignoring argmax over synthetic scores (the `agent_router` example's queue): 144 violations, 60 PII leaks; `probbit`: 0 violations; 25 ms: 0-128/300 released (the budget is wall-clock and this one sits at the R-hat threshold: 0, 0, 121 and 126 in four 0.2.0 runs, 128 in an earlier build), 200 ms: 300/300; anytime `diagnostics_passed` after 211-215 ms (0.2.0 runs; 186 ms in an earlier one) | | 300-task pod queue with a computable exact answer (89 workers) | 200 ms: 291-298 of 300 released, 1 s: 300/300; **0 released tasks wrong across 11 seeds**; polished plan within 0.75-3.6 nats of the proven optimum (*earlier build*; current build, seed 7: 298/300 released, 0 wrong; 296-297/300, 0 wrong on 0.2.0 re-runs) | | released tasks that were wrong (vs exact oracles, all families) | **0 of 28,007** on a fully out-of-sample calibration set (*earlier build*; a rule-of-three bound of ≈ 1.1e-4 per task would assume independent tasks, which tasks sharing a run are not). Current build (gate/3; all four re-run on 0.2.0): 0 wrong of 1,731 released router tasks, 929 max-cut spins, 525 colouring vertices and 428 scheduled jobs (BENCHMARKS §2-§4) | | whole-answer false passes on exact-oracle sets | 1 of 408 (*earlier build*); the guard added for it gives 0 of 393 **in-sample**; the one fresh set after it (96 hard runs) passed none. 0 false of 72 max-cut whole answers on the 0.1.0 gate, 0 of 61 on 0.2.0 (BENCHMARKS §3, re-run on 0.2.0). **Adversarial stress corpus (0.2.0): 2-3 of 20 seeds per family pass with every released item wrong** (Known failure modes) | | anytime mode (`decide_anytime` in the library) | median 117 ms to a whole answer vs a 300 ms fixed budget, 0 false in 70 (*earlier build*) | | the trap: 4 chains agree, all wrong (true error 0.51) | an earlier gate passed it; the shipped gate **refuses** (7 frozen saturated workers) | | kernel throughput (`probbit-core`, 64 replicas per u64 word) | see `cargo run --release -p probbit-core --example kernels` | What it does **not** do, also measured: - **It is not faster than an ILP/CP solver at finding the single best plan**, and it does not prove optimality. Its polished plan is 0.45-2.0 nats short of the optimum at 300 tasks (9.3-9.7 on the hard queue), and an ILP solver (HiGHS) proved that optimum in 52-352 ms, faster than probbit's default path on 5 of 6 queues (BENCHMARKS §2.2); on 200-job schedules HiGHS proves the optimum in 0.68-0.95 s and probbit's plan is 3.3-4.3 nats short (§4.3). If all you want is the argmax plan, use a solver. The value here is per-item odds under your score model plus per-item escalation. - **If you only need one good feasible plan, greedy + the same anneal polish is better at 300 tasks**: at equal time it beat probbit's plan on 8 of 8 runs (BENCHMARKS §2.2). Greedy alone takes 0.05-0.2 ms. The sampler earns its 200-1,000 ms when you need per-item odds (its marginal argmax matches the exact most likely worker on 96-100% of tasks on the oracle queues, a single optimal plan on 71-78%, BENCHMARKS §2.1), a gated release/escalate decision, or what-if marginals. - **Max-cut**: simulated annealing finds better cuts at equal time (11470 vs 11463 and 572 vs 568, §3). **Sudoku**: plain DFS backtracking does the same exact job 15-102x faster per puzzle (§4); the sampler refuses sudoku, the exact tier answers it. - **It refuses a lot in hard regimes**: exactly 100 % full queues, strong affinity with small dedicated quotas, and 1,000-task queues at 200 ms. On the 16 oracle queues of BENCHMARKS §2.1, 51% of tasks were released (58% on the 0.1.0 gate) and 4 of the 9 queues that released nothing were refused needlessly (3 of 8 on 0.1.0; an earlier build measured a whole-answer false-refusal rate of 0.3-0.5 at 300 tasks). - **The router document (`probbit decide`) expresses allowed sets, per-worker quotas, clamps and same-group affinity only.** Cost budgets (`linear`), ordering (`precedes`), `all_different`, implications and tuple tables are in the general `probbit-ir` format (`probbit run`). Missing in both: overflow / deferral as a rule (every task must go somewhere; model it as an extra "nobody" value with its own score), and objectives beyond per-value scores and pairwise terms. - **No real traffic yet.** The demo's scores are a seeded stub. Calibration is on synthetic exact-oracle families. - **Nothing beyond the CPU's own compute, and no special hardware.** A Metal GPU path was measured and rejected (a decision is ~4k sites, too small to feed a GPU). Unmeasured: x86-64, Linux, Windows, CP-SAT, annealing hardware, real customer data, programs over 100k variables. **Viability, in simulation (0.11.0; BENCHMARKS.md §13).** Seven bars registered before the run, judged as registered on 1,000 seeded episodes per arm; six pass, one fails: - survival **0.922 vs 0.000** for the same policy on a constant resting stance (95 % CI of the difference [0.905, 0.938]; bar: above 0, and ≥ 0.515); - pressure tracks the worst margin at median Spearman **0.8156** with no habits on deficits (bar ≥ 0.30 and above a shuffled null, 0.0040); - the engine names the worst need on **389,375** events with **0** margin mismatches (bar: 0); - valence tracks the distance to the bound at **+0.8394** (0.10.1 development line: −0.149; bar ≥ +0.30), but with the tonic term off it still tracks at **+0.1199** against the ablation clause's ≤ +0.10: **V2 fails as registered**; the residual is carried by the phasic term of the same block (measured 2026-10-22, BENCHMARKS.md §13); - **0 of 247** persuasion campaigns exceed the certified bound; **1,240** replayed strands verify, 0 failed; the P0 corpus, **653** recorded outputs, 0 differ. ### Results: 0.11 viability (simulation) probbit monitor on a simulated strand: needs and viability margins Bars registered before the run, 1,000 simulated episodes per arm: survival **0.922 vs 0.000** for the same policy on a constant stance (V4 PASS); pressure tracks the distance to the bound at median Spearman **0.816** with no deficit habits (V1 PASS); **0 of 247** campaigns exceed the certified bound (V6 PASS). Valence tracks at +0.839 but fails V2's ablation clause. Figures, monitor captures and every bar: [docs/results/0.11](docs/results/0.11/README.md).
## Known failure modes The full list, with the exact commands and numbers, is in BENCHMARKS ["Known failure modes"](BENCHMARKS.md#known-failure-modes) and §6 "Where it loses". In short: - **The gate can be fooled.** With the barrier-crossing moves off (`--collective`, `--cluster`, `--cycles`), adversarial stress families (one-group routers with a slightly better worker, complete-graph ferromagnets, heterogeneous groups) pass it with every released item wrong in 2-3 of 20 seeds (four of the six frozen families; the other two are refused 20/20; re-run on 0.2.0: 2 / 3 / 2 / 0 / 3 / 0): every chain sits in the same wrong mode, which no within-run diagnostic can see. With the default moves the frozen stress corpus gives 0 false and 0 wrong (280 runs, 9,280 released items, re-run twice on 0.2.0); an untouched family can still defeat it. - **It refuses a lot**: saturated and strong-affinity router queues (49% of the oracle-queue tasks are not released on the current gate), every generated sudoku, most 3-colourings, 3-SAT sampling (BENCHMARKS §2.1, §4, §4.8). - **`--budget-ms` is not a whole-call deadline.** The exact tiers run outside it: the default enumeration walks up to 64 x `--exact-limit` nodes before it declines (2.7 s on a near-saturated 3,000-task group in 0.2.0, 199.9 s before), and at 100,000 chains the chain builds alone exceed it. `--exact-ms` and `--deadline-ms` (`probbit run`) are the controls. ## Hard questions - **Is this quantum?** No. A p-bit sits between a bit and a qubit only as a metaphor: it holds a probability: it is never negative, and nothing interferes with anything. Quantum hardware works with amplitudes that can cancel; probbit has none of that, is not quantum hardware and does not simulate any. What ships here is classical Markov chain Monte Carlo (constraint-preserving Gibbs sampling) on your CPU. What the two share is a job: drawing samples of good joint configurations under couplings and constraints, which is what people rent annealing machines for. probbit's gains on that job are the programming model (rules hold by construction instead of through penalty terms, BENCHMARKS §7), odds you can check, and a gate that refuses when its diagnostics fail (a heuristic with published counterexamples); not more compute than the chip has. It has not been compared with annealing hardware *(unmeasured)*. The honest label is *quantum-inspired*. - **Isn't this just Gibbs sampling or simulated annealing with a new name?** The sampler is Gibbs sampling, and simulated annealing finds better max-cuts at equal time (§3). What is added is the instruction set with hard rules kept by construction, the exact tiers first, and a gate that refuses. - **Why not an ILP or CP-SAT solver?** For one best plan, use one: HiGHS proved the 300-task optimum in 52-352 ms, faster than probbit on 5 of 6 queues, and probbit's plan was 0.45-2.0 nats short (§2.2); on 200-job schedules HiGHS proved the optimum in 0.68-0.95 s and probbit's plan was 3.3-4.3 nats short (§4.3). An ILP returns no per-item odds, no check on them and no refusal. CP-SAT was not run. - **What does "diagnostics_passed" mean?** (0.1.0 called it "certified".) A 3-sigma Monte-Carlo error bound (at most 0.05 total variation per released item) plus split R-hat, batch-size stability and frozen-resource checks, tuned on exact oracles. It is a diagnostic, not a proof: 0 false out of 61 passed max-cut answers (72 on the 0.1.0 gate) bounds that family's false-pass rate below ~5% (95%, rule of three; §3, re-run on 0.2.0), while adversarial families (one-group routers with a slightly better worker, complete-graph ferromagnets) pass it with every released item wrong in 2-3 of 20 seeds when the barrier-crossing moves are off (0 of 20 with the default moves; BENCHMARKS "Known failure modes"). - **"0 wrong" is easy if you refuse. Where does it lose?** It refuses a lot: 51% of tasks released on the router oracle queues (58% on the 0.1.0 gate) and none on saturated or strong-affinity queues (§2.1), every generated sudoku and most 3-colourings (§4). It also loses the single best plan (ILP), the best cut (simulated annealing), puzzle speed (backtracking counts sudoku 15-102x faster per puzzle) and tiny inputs (the exact tier is 34-37x faster than sampling at 12 tasks, §6). ## The hardware hand-off `probbit ir` prints a router document in a text form that lowers to a binary one-hot QUBO with slack bits, in which the energy of every feasible plan equals −log w exactly (acceptance test `ir_round_trip_and_lowering_exact`; `examples/hardware_lowering`). That is the input a p-bit or annealing chip takes, after minor embedding on real hardware *(unmeasured)*. No hardware has been run; embedding cost and hardware speed are unmeasured. The CPU path does not sample the QUBO: on three 34-bit instances (BENCHMARKS §7) the native sampler had 0 infeasible samples and odds within 0.0018-0.0049 TV, while a p-bit sampler on the QUBO lowering either leaks infeasible samples (penalty weight 0.5: 100% infeasible on all three; 2: 82-90%) or loses accuracy as the penalty grows (16: 0% infeasible, but 0.09-0.23 TV). This measures mixing on this CPU, not hardware. ## How it was built and checked Every claim in this README points at a table in [BENCHMARKS.md](BENCHMARKS.md), a use case in [USE-CASES.md](USE-CASES.md), or a test. Every gate threshold was set against exact oracles and tightened whenever a fresh set leaked; when a claim broke, the fix shipped with a test that would have caught it. Accelerators that did not help (tempering, extra chains, focused sweeps) and baselines that win (ILP, simulated annealing, backtracking) are kept as runnable examples so the negative results stay reproducible. All timings come from one Apple M4 Mac mini (4 performance + 6 efficiency cores, 16 GB), portable release build, some of it under load; nothing here has been measured on x86-64, Linux or Windows. ## Roadmap - Cut false refusals without raising false releases. A Rao-Blackwellized marginal estimator was tried and held behind a flag: safe, but it does not lower false refusals, because the worst item's error bar comes from slow mixing, not sampling noise. Next is an exact k-group block move that unfreezes strongly coupled, saturated instances, and a gate statistic that is not set by the slowest item. - Overflow (an explicit "nobody" worker) in the router document (cost budgets, windows and precedence exist in `probbit-ir`). - Per-item error bars (σ, R̂) for every item in the JSON output, so integrators can set their own release thresholds (`--summary` reports the bar of the 5 worst released and escalated items). - The sampling phase at 100,000 chains overruns `--budget-ms` (269-318 ms for 200 in 0.2.0 at loads 3.5-4.2, 354-448 before: the chains' builds alone exceed it, and no chain sweeps: `sweeps` is 0; the gate takes 84-100 ms there; BENCHMARKS §6). `--deadline-ms` (`probbit run`) bounds the whole call. - A CP-SAT baseline (ILP baselines: router BENCHMARKS §2.2; scheduling §4.3). - Native Python bindings (today: `python/probbit.py`, a stdlib-only subprocess wrapper with typed errors, `python/examples/`, and the MCP server `probbit mcp`, docs/agents.md). - Real routing logs: the scorer side has never been validated on real traffic. - Hardware backend through `probbit-ir` when a p-bit fabric is available. ## Documentation map - [AGENTS.md](AGENTS.md): the agent quickstart as commands only, what to read next, how to verify a strand, how to report a bug. - [llms.txt](llms.txt) and [llms-full.txt](llms-full.txt): the llms.txt index of these docs, and the main docs in one file for a model's context. - [docs/CHEATSHEET.md](docs/CHEATSHEET.md): one page: the verdicts, the tiers, `bound`, the exit codes, every persona, live and monitor command. - [BENCHMARKS.md](BENCHMARKS.md): every number, with machine, load and `N`. - [USE-CASES.md](USE-CASES.md): what the measurements support, by problem shape and by industry. - [docs/probbit-ir-json.md](docs/probbit-ir-json.md): the `probbit-ir` JSON v1 wire format, instructions, resource controls, limits. - [docs/probbit-ir.schema.json](docs/probbit-ir.schema.json): a JSON Schema of that wire format (the page above is normative; a test keeps every object's fields equal to the parser's). - [docs/agents.md](docs/agents.md): calling probbit from a shell, Python, Node, PowerShell, MCP agents and a browser, and `probbit evaluate` after a decision model. - [docs/persona.md](docs/persona.md): `probbit persona`, the individuality layer: the persona file, the compilation, the stance and state documents, the canonical JSON, testing a character, bounded learning, `probbit live` and `probbit monitor` (§2.8, §5.7, §5.8), what a persona can not do; [examples/persona/](examples/persona/): three personas + goldens. - [python/](python/): `probbit.py`, a zero-dependency subprocess wrapper (with `evaluate`, the persona functions and a stdlib mock judge), its tests and three examples. - [playground/](playground/): one static page that runs probbit in a browser (`probbit-wasm`, built by `playground/build.sh`). [Try the puzzle: find the event that changed the decision.](playground/puzzle.html) - [bench/](bench/): the scripts behind BENCHMARKS §2, §5 and §8 (Python 3; the ILP baselines need `numpy` and `scipy >= 1.9`). - [CONTRIBUTING.md](CONTRIBUTING.md), [CHANGELOG.md](CHANGELOG.md). ## License Apache-2.0. --- # FILE: docs/persona.md Source: https://github.com/BitmapAsset/probbit/blob/main/docs/persona.md # probbit persona: the individuality layer (format 1) A **persona** gives an agent a stable individual temperament that lives outside the language model. It is a small file of traits, moods, habits and evidence rules. Every turn it compiles, with the agent's state and the turn's inputs, into **one probbit IR program**. probbit answers with the **stance** for that turn: a level for every trait with exact odds, the habits that were in force and the ones that changed the outcome, a refusal when the engine cannot vouch, a one-line explanation, and a short **stance line** (at most 40 tokens by default) that the host puts into whatever model writes the reply. The stance line is addressed to the model: the host puts it into the model's prompt for this turn (section 7), so the model reads it as its instructions for how to write; an agent can read it to know its own stance; it is not addressed to the end user. - **Same persona file + a different seed = a different, stable individual.** The seed fixes each individual's small, permanent offsets ("genes"); the same individual is recognisable on inputs it has never seen. - **Swap the model, keep the individual.** The stance is computed without any model in the loop, so it is the same for every model and every host, and it can be replayed byte for byte. - **A persona does not write text.** It decides *how* the text is written (warm or plain, one line or a breakdown, a joke or none, verify first, ask first). The model still writes every word. This document is normative for the file format, the compilation, the stance document and the state. It is implemented by `probbit persona` (probbit-cli/src/persona.rs and yaml.rs, no dependency) and served on every surface with the same documents: the CLI, `probbit mcp` (tools `probbit_persona_init`, `probbit_persona_turn`), the Python wrapper (`probbit.persona_init`, `persona_turn`, `persona_replay`) and the browser module (probbit-wasm ops 4 and 5). `probbit live` (section 5.7, live.rs) keeps an individual running on a clock, with learning (section 2.8) and a replayable log; `probbit monitor` (section 5.8, monitor.rs) draws its inner state live from that log. The example personas and their goldens are in [examples/persona/](../examples/persona/). An independent reference implementation of this format (Python) gives the same documents byte for byte on every example turn (section 5.3). ```sh probbit persona init examples/persona/tutor.yaml --seed 2 --out pip.json # an individual: genes from the seed probbit persona turn examples/persona/tutor.yaml --state pip.json --inputs '{"loss": true}' # this turn's stance; pip.json moves on ``` Contents: 1 Files · 2 Schema · 3 Compilation · 4 The stance document · 5 State, canonical JSON, replay, testing, live · 6 What a persona can NOT do · 7 Commands and surfaces · 8 A model-proposed stance (the `evaluate` bridge) · 9 Versioning. On one page (verdicts, tiers, `bound`, exit codes, every command): [CHEATSHEET.md](CHEATSHEET.md). ## 1. Files A persona is one document, written either in **JSON** or in the **YAML subset** below; both forms parse to the same object and have the same digest (`sha256` of the canonical JSON: keys sorted, no whitespace, UTF-8). **YAML subset.** Block mappings, block sequences (including `- key: value` items), one-line flow collections (`[a, b]`, `{a: 1, b: [x, y]}`), plain / single-quoted / double-quoted scalars (double-quoted ones take JSON's escapes), numbers, `true` / `false` / `null` / `~`, and `#` comments. Rejected with a line number, never guessed: tabs in indentation, anchors, aliases, tags, block scalars (`|`, `>`), flow collections spanning lines, duplicate keys, a second document, numbers that are not exact in a double (an integer beyond 2^53, `1e999`), and plain keys another YAML reader would not read as strings (`yes`, `no`, `on`, `off`, `y`, `n`, `true`, `false`, `null`, numbers: quote them). A file that uses only this subset means the same thing to every YAML 1.2 reader. A persona file whose name ends in `.json` is read as JSON (duplicate keys refused), any other as the subset. The document is **strict**: every field below is type-checked; an unknown field is an error at its path (e.g. `traits[2].sprad: unknown field`), as for every probbit document. Habit rules are checked when the file is read, as strictly as `probbit run` reads them, so a malformed rule fails at once and not only on the turns it is in force. The same holds for a turn's inputs and for the state: a bad value is one `{"error": {"code": "persona", "path", "message"}}` object and exit code 2, never a silent coercion. ## 2. Schema ```yaml probbit_persona: 1 # format version (required) identity: # required name: Rook # 1-64 chars: letters, digits, space, _ . - (genes are keyed by name + seed) version: 1.0.0 # a string seed: 1 # the default individual (integer >= 0); `init --seed N` makes another one summary: a cautious ops engineer # carried, never read by the engine traits: [...] # required, at least one (section 3) moods: [...] # optional couplings: [...] # optional soft links between traits / moods inputs: [...] # optional per-turn evidence history: [...] # optional features derived from past turns habits: [...] # optional hard rules agenda: {...} # optional ordered beats of a reply engine: {...} # optional engine settings line: {...} # optional stance-line settings learning: {...} # optional bounded learning from feedback (section 2.8) drives: {...} # optional goals with wanting, afterglow and expectation; the `pursue` output (section 2.9) reward_from: [human, env] # optional: the sources a reward may come from (section 2.10) comment: any text # ignored ``` Identifiers (`id` of traits, moods, inputs, history features, habits, agenda steps, and level names) match `[a-z][a-z0-9_]*` (at most 48 characters). Level names may not be YAML-reserved words (`yes`, `no`, `on`, `off`, `true`, `false`, `null`, `y`, `n`). ### 2.1 Traits and moods ```yaml - id: verbosity levels: [terse, short, full] # 2-7 ordered levels, lowest first prior: [0.5, 0.4, 0.1] # probabilities (normalised), or `logw: [...]` natural-log weights; default uniform spread: 0.5 # individuality: s.d. (nats) of each individual's ordinal shift; default 0 say: [1-2 sentences, a short paragraph, full detail] # phrase per level for the stance line ("" = say nothing) fallback: short # level used when there is no stance (default: the prior's most likely level) vouch: 0.5 # optional odds floor: below it the trait is "unsure" (default 0 = off) hold: short # optional: the level an unsure trait is held at (needs vouch > 0) ``` A **mood** has the same fields plus `inertia` (0 <= k < 1, default 0.5: how much of the previous turn's mood carries over) and `half_life_hours` (optional: idle time halves the carried mood). Traits are the stance; moods are slower internal variables that shape it through couplings. Trait and mood ids share one namespace. **The core trait set.** Any trait may be declared; the eight below are recommended so that personas, hosts and tools interoperate (the same ids and level names in every persona make individuals comparable across personas): | trait | levels (low → high) | what it sets in a reply | standard dimension it follows | |---|---|---|---| | `warmth` | cool, neutral, warm | pleasantries, encouragement | interpersonal warmth (agreeableness) | | `directness` | gentle, balanced, blunt | verdict first vs softened | politeness strategy / assertiveness | | `verbosity` | terse, short, full | length | quantity | | `humour` | none, light, playful | jokes | playfulness | | `formality` | casual, neutral, formal | register | formality of register | | `initiative` | follow, suggest, lead | answer only / propose / act | proactivity | | `caution` | bold, measured, careful | verification depth, risk talk | conscientiousness / risk appetite | | `curiosity` | focused, open, exploring | follow-up questions, alternatives | openness | Recommended moods: `valence` (down, even, up) and `arousal` (calm, steady, keyed): the two axes of the circumplex model of affect. Common custom traits: `emoji` (none, sparse, rich), `patience`. ### 2.2 Couplings (soft) ```yaml - {vars: [valence, humour], align: 0.8} # ordinal alignment: + w x c(a) x c(b) - vars: [caution, initiative] table: {careful: {lead: -0.8}, bold: {follow: -0.3}} # sparse log-weights, level of a -> level of b -> w ``` `c(level)` is the centred level index, from -1 (lowest) to +1 (highest). A positive `align` makes the two variables move together, a negative one opposite. Couplings are soft: they shape the odds; they never forbid anything. ### 2.3 Inputs (per-turn evidence) ```yaml - id: sentiment kind: level # flag | level | number levels: [negative, neutral, positive] default: neutral say: {negative: user upset, positive: user upbeat} # used in the explanation effects: # level -> target -> effect negative: {valence: -1.2, warmth: 0.4, humour: -0.8} - id: error kind: flag # effects apply when the flag is true reactivity_spread: 0.3 # individuality: each individual's gain on this input, exp(N(0, s^2)); default 0 effects: {caution: 1.0, verbosity: -0.6, valence: -0.6} - id: stakes kind: number # 0..max (default max 1; outside it: an error): effects scale linearly with the value effects: {caution: 1.6, humour: -1.0} ``` An **effect** on a trait or mood is a number (an ordinal shift: `w x c(level)`, so `+1.0` pushes toward the highest level), a list (one log-weight per level) or a mapping (`{level: log-weight}`). An effect on an agenda step (`step.`) is a number (positive = earlier) or per-slot log-weights. All weights are natural-log weights, so `+0.7` roughly doubles the odds. **Standard inputs** (recommended names, so one host can drive any persona; a persona ignores what it does not declare and lists it in the stance's `ignored`): | id | kind | meaning | |---|---|---| | `sentiment` | level: negative, neutral, positive | the user's tone this turn | | `error` | flag | the agent's previous action failed | | `loss` | flag | the user reports a loss or bad news | | `praise` / `criticism` | flag | the user praised / corrected the agent | | `stakes` | number 0..1 | how costly a mistake would be | | `time_pressure` | number 0..1 | how hurried the user is | | `claim_done` | flag | the draft reply is about to report a task as done | | `task` | level: chat, question, code, ops, explain (a persona may declare its own list) | what kind of turn this is | | `elapsed_hours` | number (reserved) | idle time since the previous turn; decays moods by their `half_life_hours` | ### 2.4 History features ```yaml - id: errors_in_row of: error # a flag input kind: streak # streak: consecutive turns with the flag, this one included; recent: count in the last `window` turns cap: 3 # the value saturates at cap (default 5) window: 5 # recent only (default 5) effects: {caution: 0.5, verbosity: -0.4} # per unit of the value ``` History features are inputs computed from the state; they can drive effects and habit conditions. ### 2.5 Habits (hard rules) A habit is a rule the stance obeys **by construction**: the engine only considers stances that satisfy every habit in force. ```yaml - id: no_jokes_on_loss when: {loss: true} # optional; all conditions must hold (flag: true/false; level input: a level or a list; # number or history feature: a number = at least, or {at_least, at_most}) then: {humour: [none]} # trait/mood -> allowed levels: # [levels] | {not: [levels]} | {at_most: L} | {at_least: L}, # L = a level, prev, prev-1 or prev+1 (the previous turn's level, clamped to the range) say: no jokes # phrase for the stance line priority: 0 # used only to resolve conflicts (section 5.5); default 0 - id: verify_before_reporting rules: # raw probbit IR rules over trait / mood ids, level names and agenda steps precedes: [{before: step.verify, after: step.report}] - id: one_flourish_at_a_time rules: linear: [{terms: [[humour, playful, 2], [humour, light, 1], [emoji, rich, 2], [emoji, sparse, 1]], limit: 3}] ``` `rules` takes the probbit IR constructs exactly as in a program: `caps` (with `members`), `implies`, `tables` (1-3 variables, `forbid` or `allow`), `precedes` (agenda steps), `linear` (whole-number weights) and `all_different`. Soft `pairs` belong in `couplings`. A habit without `when` is always in force. The persona's fallback stance must obey every unconditional habit (checked when the file is read). Examples of the three habits every persona author asks for: never joke when the user reports a loss (`when: {loss: true}`, `then: {humour: [none]}`); verify before claiming done (`when: {claim_done: true}`, `then: {caution: [careful]}`, or a `precedes` on agenda steps); after an error, shorter and more checks (`when: {error: true}`, `then: {verbosity: {at_most: prev-1}, caution: {at_least: prev+1}}`). ### 2.6 Agenda (optional) ```yaml agenda: steps: [acknowledge, diagnose, fix, verify, report] # 2-8 beats prefer: {acknowledge: 0.6, report: -0.8} # number = earlier (+) / later (-), or per-slot log-weights say_order: true # put "order: a > b > c" in the stance line (default true) ``` Each step becomes a variable `step.` over ordered slots `s0 .. s(n-1)`, all different (a permutation). Habits order steps with `precedes`. Keep the agenda free of couplings to traits: it is then its own component and costs nothing. ### 2.7 Engine and line ```yaml engine: op: decide # decide (exact tiers first, then the sampler + gate) | sample (sampler + gate only) sweeps: 2000 # fixed work per chain if the sampler runs (deterministic) polish_sweeps: 200 exact_limit: 2000000 chains: 4 twin: true # also solve the habit-free twin in the same program, to report which habits changed the stance values: positional # positional (level i -> l: small pair tables, fast) | semantic (level names: readable programs) on_conflict: fallback # fallback | yield: when habits contradict, the lower-priority one yields (section 5.5) line: max_tokens: 40 # estimated tokens (max of chars / 4 and 4/3 per word) order: [verbosity, caution, directness, warmth, humour, initiative, formality, curiosity] prefix: "Stance: " ``` ### 2.8 Learning (optional) ```yaml learning: from: [praise, criticism] # flag inputs: the reward, then (optional) the correction traits: [verbosity, humour] # the traits that learn (moods do not) rate: 0.5 # step size (natural-log weight per unit of credit) step_cap: 0.2 # the most one feedback turn moves a level's weight, before recentring total_cap: 1.0 # no learned weight ever leaves [-total_cap, +total_cap] ``` All five fields are required; 0 < step_cap <= total_cap <= 50 and 0 < rate <= 50. With this block the state keeps, per learned trait, one **learned weight** Δ per level (`learned`, all 0 at `init`) and the **credit** of the previous stance (`credit`): onehot(level) − odds for each learned trait that stance released, 0 for one it did not (status refused or fallback, an unreleased trait of a partial turn). A level that a habit or a hold forced has odds 1, so its credit is 0 as well. Feedback is about the previous reply. On a turn whose reward flag is on (sign +1) or whose correction flag is on (sign −1; both on: 0), each learned trait's weights move, before the turn is compiled: step(l) = clip(sign x rate x credit(l), -step_cap, +step_cap) step(l) = step(l) - mean over levels of step (recentred: the step moves weight between levels) Δ(l) = round(clip(Δ(l) + step(l), -total_cap, +total_cap), 6) With a `surprise` block (section 2.13) each learned trait's gain g joins the step and its cap, `step(l) = clip(sign x rate x g x credit(l), -step_cap x g, +step_cap x g)` (g is 1 without the block, so the step is the one above); with an optimism gene o (section 2.15) `rate` is `rate x e^(sign x o)`, and with a patience gene p a payment's half-life is `half_life_events x p`, clipped to [1, 10000]. A turn with no feedback, or feedback on a stance with zero credit, moves nothing. Δ is added to the trait's field (section 3, step 2), so the feedback turn's own stance already uses it. What learning can and cannot change: - It changes the learned traits' unary weights and nothing else, within ±total_cap per level. Genes, priors, couplings, moods, inputs and history effects are untouched. - It never changes a habit or a trait's allowed levels: habits are rules of the program (section 3, step 6), the learner's whole output is the Δ table, and every stance the engine returns obeys every habit in force whatever Δ is. Rule immunity is therefore by construction, not measured. - A persona without the block has no `learned` or `credit` field, and every document it produces is the same, byte for byte, as before the block existed. **Action-addressed feedback (`learning.credit`, optional).** Without it, feedback pays the previous stance. With it, the host can name the decision an outcome is about, so a late or out-of-order outcome credits the stance the host acted on: ```yaml learning: # ... the fields above, unchanged credit: max_open: 8 # decisions awaiting an outcome at once (1-64; default 8) half_life_events: 32 # eligibility halves per 32 events of delay (1-10000; default 32) ``` Three event keys are read (not compiled, so not in `ignored`) and come back in the stance's `inputs` as given: - `await: true`: the host will act on this turn's stance and report its outcome. The decision stays open with its credit and its stance's digest (`credit.stance` in the stance document). - `for: T` or `for: {turn: T, stance: "sha256:..."}`: this feedback pays the decision of turn T. The second form binds the paid stance's digest, so a restored state can never pay another decision under the same turn number (B-R6); with `for: T` the paid digest comes back in `credit.paid.stance` for the host to compare. - `cancel: [T, ...]`: no outcome will come for these open decisions. A cancel needs `src` env or human (never the individual). The credit step runs after the drive step and before compile, at turn t. Feedback is a learning flag on (or, with drives, a non-zero surprise term), with the sign above: expire each open n with t - n - 1 >= 4 x half_life_events (closed as expired) close each n in cancel (closed as cancelled) if feedback: paid = for if given, else t - 1 c = the open decision's credit, or the state's credit when paid = t - 1 is not open e = 0.5^((t - paid - 1) / half_life_events), 6 decimals (1 for t - 1) step(l) = e x clip(sign x rate x c(l), -step_cap, +step_cap), recentred and added as above close paid (closed as paid) -- compile and solve -- if await: open[t] = {credit, stance digest}; beyond max_open the oldest is closed (closed as evicted) Eligibility scales the capped step, so a stale outcome always moves less than a fresh one: 3 events late it keeps 0.937084 of the step, 10 late 0.805245, 32 late 0.5, 127 late 0.063869; 128 events after it was made a decision expires. At e = 1 the step is the one above exactly: feedback that names the previous stance, or names nothing while no other decision is open, learns what a persona without the block learns, Δ for Δ. Paying a stance a habit forced moves nothing and is marked `absorbed`. A control line closes every open decision (section 5.7). An event is **refused whole** (one `{"error"}` at its path, no turn, no state change; `persona turn` exits 2, `live` at the end of the run) when: | row | refused | path | |---|---|---| | B-R1 | feedback without `for` while a decision other than t − 1 is open (ambiguous) | `inputs.for` | | B-R2 | `for` naming a decision neither open nor t − 1: paid, cancelled, expired, evicted, never opened or still to come | `inputs.for` | | B-R3 | `for` on an event without feedback | `inputs.for` | | B-R4 | `cancel` naming a decision not open, the same turn twice, or the decision this event's feedback pays | `inputs.cancel[i]` | | B-R5 | `for` not a whole number or `{turn, stance}`; `cancel` not a list of at most `max_open` whole numbers; `await` not true or false; a cancel without an env or human `src` | `inputs.for`, `inputs.cancel`, `inputs.await`, `inputs.src` | | B-R6 | `for: {turn, stance}` whose digest is not that turn's stance | `inputs.for.stance` | Reward provenance (section 2.10) is checked before these rows. A stance's digest is the sha256 of its canonical JSON without `credit`, `state_digest` and `timing`. Without the block, `await`, `for` and `cancel` are undeclared inputs (listed in `ignored`; `live` says so once on stderr) and every document is as before. ### 2.9 Drives (optional) ```yaml drives: goals: # 2-7 goals: the levels of one more variable, `pursue` - id: chores # an identifier (not a YAML-reserved word); with the block, `pursue` is a reserved trait id say: clear the chores # the line's clause "pursue: chores: clear the chores" (default: the id, _ as spaces) interest: 1.0 # > 0 (default 1): prior weight, normalised over the goals like a trait's prior interest_spread: 0.3 # 0..5 (default 0): gene, s.d. (nats) of this individual's standing interest offset reactivity_spread: 0.3 # 0..3 (default 0): gene, gain exp(N(0, s^2)) on this goal's wanting evidence and afterglow starve_after: 12 # optional, 1..10000 turns: then a habit forces the goal (below) priority: 0 # -100..100 (default 0): that habit's priority - id: safety floor: 0.1 # optional, 0..0.5, all floors summed <= 0.5: pursue odds never below it while habits allow it wanting: {half_life_hours: 12, cap: 3, drain: 0.8, signals: {progress: 0.6, novelty: 0.5, cue: 0.4, setback: 0.5}} afterglow: {half_life_hours: 3, cap: 2} expectation: {rate: 0.5, half_life_hours: 48, pe_cap: 1.0, win_max: 2.0} pursue: {want: 1.0, glow: -0.5, deadline: 1.5, deadline_tau_hours: 24} effects: # effect maps as an input's (section 2.3), per unit of the aggregate wanting: {initiative: 0.5, verbosity: -0.3} # x the strongest wanting over the goals afterglow: {valence: 0.6, humour: 0.3} # x the strongest afterglow surprise: {valence: 0.4} # x the turn's prediction error (ordinal shifts; a negative one pushes back) learn_from_surprise: 0.5 # 0..1 (default 0): the prediction error joins the learning sign (section 2.8) ``` The block needs `goals`, each with an `id`; the goal fields' defaults are in the comments, `wanting`, `afterglow`, `expectation` and `pursue` show theirs, and `effects`, `floor` and `starve_after` have none. Bounds: half-lives, caps, `pe_cap`, `win_max` and `deadline_tau_hours` > 0; `drain` and `rate` 0..1; signal weights and `pursue` weights -50..50. **Goal signals** ride in the turn's inputs (or a live event) under `goals`, beside the ordinary inputs: `{"goals": {"chores": {"deadline_hours": 30}, "fun": {"cue": true, "win": 1.5}}, "praise": true, "elapsed_hours": 2}`. Per goal: `progress` 0..1, `novelty` 0..1 or true/false, `cue` true/false, `setback` 0..1, `win` 0..`win_max`, `deadline_hours` (a number >= 0 sets the countdown, `null` clears it). An unknown goal or signal is an error at its path. **The drive step.** Before the turn is compiled, for each goal in declared order (h = `elapsed_hours`, k = the goal's gain gene): 1. **Decay by clock time:** wanting W, afterglow A and expectation E each halve per their half-life (`x 0.5^(h / half_life)`); a set deadline counts down by h (not below 0). There is no per-turn inertia: a host that calls more often does not make a drive fade faster. 2. **A win of size m > 0:** the prediction error `d = clip(m - E, -pe_cap, +pe_cap)`; `E <- E + rate x (m - E)` (with an optimism gene, section 2.15, the rate above or below E); wanting is consumed, `W <- W x (1 - drain x min(m, 1))`; `A <- A + k x max(d, 0)`; a full win (m >= 1) clears the deadline. Without a win, d = 0. Repeated equal wins drive E toward m and d toward 0: they stop moving the individual. 3. **Wanting evidence:** `W <- W + k x (progress, novelty, cue, setback) . signals` (the weights above). 4. **Caps:** W in [0, cap], A in [0, cap], E in [0, win_max]; each value is rounded to 6 decimals. **What the stance reads.** The aggregates wanting = max over goals of W, afterglow = max of A and surprise = the summed d (clipped to ±pe_cap) act through `effects` exactly as number inputs do (trait effects in the field, mood effects in the mood evidence), and name themselves in `why` ("wanting", "afterglow", "a win above expectation", "a win below expectation"). `pursue`'s field per goal g (section 3, step 2b) adds `want x W_g + glow x A_g + deadline / (1 + deadline_hours_g / tau)` (the last term 0 while no deadline is set) to the prior, the interest gene, learned weights and any input or history effects a persona declares on `pursue` (an input `security` with `effects: {pursue: {safety: 1.0}}`). Couplings, `then: {pursue: [...]}` and rules over `pursue` use the existing grammar. **Habits over goals.** `when` takes `goal..deadline_hours`, `goal..want`, `goal..glow`, `goal..expect` and `since_pursued.` (turns since the goal was last pursued) as number conditions (a number = at least, or `{at_least, at_most}`); a deadline condition is false while no deadline is set. A must-do goal is a habit: `{id: chores_due, when: {goal.chores.deadline_hours: {at_most: 24}}, then: {pursue: [chores]}, priority: 2}`. `starve_after: N` adds the habit `starve_` (`when: {since_pursued.: {at_least: N}}`, `then: {pursue: []}`, the goal's priority) after the declared habits. After each turn, `since` is 0 for the goal a released stance pursues and grows by 1 for every other goal (for all of them on a refused or fallback turn). **Floors.** A goal's `floor` is not a rule (rules constrain stances, not odds): the compiler adds the smallest lift (to 1e-6, plus a 1e-5 margin) to the floor goal's field so that its odds stay at least the floor whatever the levels of `pursue`'s coupling partners, over the goals the habits in force allow (section 3, step 2b). That bound is sound when no multi-variable rule names `pursue`, so a persona with a floor and such a rule is refused when it is read. A habit outranks a floor: on a turn whose habits exclude the floor goal its odds are 0; `probbit persona lint` lists every habit that can exclude a floor goal. **Learning from surprise.** With a learning block (section 2.8) and `learn_from_surprise: kappa`, a turn's learning sign is `clip(sign + kappa x surprise, -1, 1)`: a win above expectation reinforces the stance before it, a win below expectation weakens it, an expected win moves nothing. With `pursue` in `learning.traits`, interests drift toward the goals whose wins surprise, never past ±total_cap. Learning still writes the Δ table and nothing else; W, A, E, deadlines and `since` are state, outside its reach. **Omission windows** (RFC 0002 A.4). A goal may declare `expect_by_hours: H` (> 0): after a released stance pursues the goal its window opens at H hours (if not already open); each event counts it down by its stamped hours in the decay (step 1); a win of the goal (m > 0) closes it in step 2; an event at which it reaches 0 without a win fires it once: `d = -min(E, pe_cap)` and `E <- E + rate x (0 - E)` (E after its decay; with an optimism gene the rate below E, section 2.15), the window closes and `why` says `an expected win did not come ()`. The omitted d joins the turn's summed surprise, so `effects.surprise` and valence act on it, and the optional `effects.omission` map, per unit of min(the omitted E, pe_cap) (the input `drv_omit`); it does not join the learning sign (it is about the stance that opened the window, and as feedback it would trip B-R1 whenever another decision is open). A goal a need binds must declare a window (section 2.12). The state keeps `drives.window` (per goal with a window: hours left, or null) and the stance's `drives` shows it. **Valence and liking** (RFC 0002 Design E, a simulation): ```yaml drives: valence: {mood: valence, expect: 0.2, surprise: 0.6} # replaces effects.surprise; 0 < expect < surprise, both required afterglow: {half_life_hours: 3, cap: 2, liking: 0.5, liking_spread: 0.3} # liking >= 0 (default 0); spread 0..3 ``` With `valence` the named mood's evidence is `expect x E` of the goal the previous stance pursued (its expectation after this event's decay, before its win: the input `drv_expect`, in [0, win_max]) plus `surprise x` the turn's summed prediction error (through `drv_surprise` and `drv_letdown`, as `effects.surprise` would): valence moves with expected value and with outcomes better or worse than expected, including expected wins that do not come. Both weights are declared (no default: the sources fix their signs and order only, Rutledge et al. 2014). The mood's inertia k plays the forgetting factor: at k = 0.61 an event five events back keeps 0.0845 of its push (half-life 1.402 events). `liking` adds a gain on the win itself to afterglow: `A <- A + k x (liking_g x m + max(d, 0))`, liking_g = liking x exp(liking_spread x z(name, seed, "liking")) (the gene `drives.genes.liking`, 4 decimals; spread 0 stores none). `liking: 0` is the afterglow of before. The block reserves the input ids `drv_want`, `drv_glow`, `drv_surprise`, `drv_letdown` and `drv_c` (the compiled form of the aggregates and goal conditions), `drv_expect` with `valence` and `drv_omit` with a window: inputs, habit conditions, properties and `learning.from` may not use them. A persona without the block gives every document byte for byte as before the block existed; `goals` in its inputs is listed in `ignored`. Without `valence`, `liking` and a window the documents are those of before. ### 2.10 Reward provenance (optional) ```yaml reward_from: [human, env] # every reward-bearing input: from a person or a sensor, never from the individual itself # or one list (or `any`) per reward-bearing input, naming each one: reward_from: {praise: [human], criticism: [human], goals.fun.win: [env], goals.safety.win: any} ``` A **reward-bearing input** is one that moves what an individual learns or how a goal pays off: the learning block's flags (`learning.from`, section 2.8) and each goal's `win` (section 2.9). An event may say who produced it with `src`: `human` or `human:` (a person), `env` or `env:` (a sensor: a test runner, a build, a monitor), `self` (the agent's own output, judged by the host) or `clock` (an idle tick); an id is 1-64 characters of `A-Z a-z 0-9 _ . - @ / :`. With `reward_from`, a turn whose reward-bearing input is on (a learning flag `true`, a goal's `win` above 0) needs its `src` to be one of the kinds listed for that input; otherwise the event is **refused whole**, as a bad event is (one `{"error"}` object at `inputs.src`, exit 2 for `persona turn` and at the end of a `live` run; no turn, no line, no state change): | the event | refused because | |---|---| | `{"praise": true, "src": "self"}` | a reward never comes from the individual itself | | `{"praise": true}` | a reward needs a source | | `{"goals": {"fun": {"win": 1}}, "src": "clock"}` | a source the persona does not list for it | | `{"praise": true, "src": "human:"}` | a malformed source | An event with no reward on is accepted from any source (`{"loss": true, "src": "self"}`: the agent reports its own failure). `src` is read, not compiled: it is not listed in `ignored`, it comes back in the stance's `inputs`, and the strand logs it with the other inputs, so `live verify` replays the decision. `any` (for one input, or for all) leaves that input's source unchecked. The list holds `human` and `env` only: `self` and `clock` are refused when the persona is read, and so is the key on a persona without a reward-bearing input; a mapping must name every reward-bearing input. `describe` lists the rule. What it guarantees, and what it does not. A refused event changes nothing, so a run equals the same run with its refused events removed, state and strand byte for byte (property P3, tested over 40 individuals x 300 random events mixing every source). The source is the host's word: provenance makes a host's claim a logged, replayed fact, it does not make it true. The host sets `src` in its own code (never from the model's text), and the labeller that sets `praise` or a `win` does not read the agent's own reply as its evidence of success. `fuzz` and `prove` search over stances, not sources: their events stand for events of an accepted source. A persona without the key reads `src` as any undeclared input (listed in `ignored`), and every document it produces is the same, byte for byte, as before the key existed. ### 2.11 Senses (optional) A sense is a reading the host measures (a tool failure, a latency, a message tempo, a silence) in declared units, from a named source. The engine keeps a learned **normal** per sense (mean, variance, count) and turns each reading into a capped z-score against it, which moves moods and traits as any input does; `why` names the normal a reading was judged against. Design notes and arithmetic: [docs/rfcs/0001-senses-and-reward.md](rfcs/0001-senses-and-reward.md). ```yaml senses: # 1-24 senses; with the block, `src` is read on every event - id: tool_fail # at most 32 characters; reserves the input ids sense_tool_fail_* kind: flag # flag (the normal is a rate) | number (then `range: [lo, hi]` is required) unit: event # 1-24 characters, in the digest: two units are two personas scale: linear # linear | log1p (the normal is kept on ln(1 + reading); a number with range low >= 0) src: [env] # env and/or human; default [env] prior: {mean: 0.05, var: 0.05} # required: the normal at init, anchor of both caps (on the scale) drift_cap: 0.25 # required: the mean stays within prior.mean ± drift_cap var_cap: 0.2 # default 4 × prior.var: the variance stays within [0, var_cap] half_life_events: 10 # default 10 (1-10000) zcap: 6 # default 6 (1-20) dead_zone: 0.5 # default 0.5 (0 <= dead_zone < zcap) var_floor: 0.01 # default 0.01 (> 0) say: tool failures # default: the id, _ as spaces effects: {caution: 0.8, valence: -0.4} # ordinal shift per unit of z beyond the dead zone; negative z pushes back gain_spread: 0.3 # gene: gain exp(N(0, s²)) on effects; default 0, range 0..3 (all three) threshold_spread: 0.3 # gene: offset N(0, s²) on dead_zone; default 0 habituation_spread: 0.3 # gene: multiplier exp(N(0, s²)) on half_life_events; default 0 ``` Why these defaults: a reading 10 events old counts half, so the normal follows a changed rate halfway within 10 readings; at `zcap` 6 a failure after 40 quiet readings scores 3.4× the same failure after 40 readings at rate 0.3 (1.7× at `zcap` 3); readings within half a standard deviation move nothing; `var_floor` keeps z finite when the variance collapses; at `var_cap` 4 × prior.var the spread at most doubles. `prior` and `drift_cap` have no default: no unit-free anchor is safe. **Refused when the persona is read** (exit 2, at the field's path): a taken `sense_` id (an input, history feature or goal whose id starts with a sense's `sense__`), an effect on an unknown trait or mood, `src` naming `self` or `clock`, `prior.mean` outside the scaled range, `prior.var` <= 0, `drift_cap` <= 0, `var_cap` < `prior.var`, `dead_zone` >= `zcap`, `log1p` on a flag or on a range low below 0, a cap with more than 6 decimals, an unknown field, a number sense without a range, two senses with one id, an empty block or more than 24 senses. **Events.** `{"senses": {"tool_fail": true, "tool_latency": {"v": 2.4, "src": "env:runner"}}, "src": "env:host"}`: a bare value takes the event's `src`, `{v, src}` carries its own. `senses` is read, not compiled: it is not listed in `ignored` and comes back in the stance's `inputs`. **The sense step**, after validation and before the drive step, per declared sense with a reading, in declared order (genes g_gain, g_thr, g_hab; each line rounded to 6 decimals): ``` x = the reading (a flag: 1 or 0), or ln(1 + reading) for log1p h = clip(half_life_events × g_hab, 1, 10000) a = 1 − 0.5^(1/h) z = clip((x − mean) / sqrt(var + var_floor), −zcap, +zcap) # against the past, before the update d = clip(dead_zone + g_thr, 0, zcap − 0.000001) zd = sign(z) × max(|z| − d, 0) mean' = clip(mean + a × (x − mean), prior.mean − drift_cap, prior.mean + drift_cap) var' = clip((1 − a) × (var + a × (x − mean)²), 0, var_cap) # with the mean before the update n' = min(n + 1, 1000000) ``` The turn adds g_gain × zd times each effect (an ordinal shift, section 2.3) to its target's field or mood evidence (section 3), as a number input does. It is lowered as drives' surprise is: two reserved number inputs, `sense__up` = max(zd, 0) carrying the effects and `sense__down` = max(−zd, 0) carrying them negated, both in [0, zcap], gain g_gain. Genes follow section 5.4's z: `round(exp(gain_spread × z(name, seed, "sense", id, "gain")), 4)`, `round(threshold_spread × z(…, "threshold"), 4)`, `round(exp(habituation_spread × z(…, "habituation")), 4)`; a spread of 0 means gain 1, offset 0, multiplier 1 and no stored gene. At the defaults (a flag with prior 0.05/0.05) the same failure every event gives zd 3.38, then 1.03 from reading 6 on: the push falls to 0.30× and stays there, because the drift cap keeps a chronic failure from becoming normal. **Rules the engine enforces.** | rule | what it means | |---|---| | S1 declared, in range | a reading for an undeclared sense, a number outside its range or a flag that is not true/false refuses the event at `inputs.senses.`; nothing is clipped | | S2 provenance | each reading's source kind must be in its `src`; a missing or malformed source, `self` and `clock` refuse the event whole, and a refused event changes nothing | | S3 the engine owns the normal | hosts send raw readings; `sense__*` inputs and a pre-judged z are refused; z, normals and genes come from logged readings only | | S4 anchored | a state is refused, even with a recomputed digest, if a mean leaves prior ± drift_cap, a variance leaves [0, var_cap], n is not a whole number up to 1e6, the sense set differs from the persona's or a gene misses its seed | | S5 no rule reads the normal | `when` takes `sense..raw` (true/false for a flag, declared units for a number) and `sense..z` (the z above, before dead zone and gain), never the mean or variance; a condition on a sense not read this event is false | | S6 no clock decay | normals move only on readings; silence, idle hours and control lines leave them | | S7 not through the agent's channel | the MCP turn tools refuse an event that carries readings (section 7) | A rule that must hold whatever the history reads `raw`: `prove` reads `sense__up/down` as boxes [0, zcap] × gain and each sense condition both ways, so a habit on `sense.tool_fail.raw` holds by construction for a rule with the same condition. `fuzz` adds readings: flags both ways, numbers on `--grid` × range, one reading repeated 1, 5 and 20 times, and the range ends alternating. **The stance and `why`.** The stance gains `senses` (per sense read: `raw`, `src`, `z`, `zd`, and the `mean` and `n` it was judged against) and `noticed` (the sense of the normal clause, or null). `why` becomes: the bound habits; the normal clause; up to two other live inputs (sense inputs excluded); the learned clause; the engine status if not ok. - **Normal clause**, at most one: the read sense with the largest |g_gain × zd| > 0 (ties: declared order), `tool failures far above its normal (z +6.0 over 40 readings)`: "far" at |z| >= 2, above or below by sign. Failing that, the read sense with the largest |z0| − |z| >= 1, z0 being the reading judged against the prior: `tool latency usual for it now (z +0.9 over 40 readings, +3.9 against its prior)`. - **Learned clause**, at most one, with a learning block and `line.why_past` (default true with senses, false without, so existing documents keep their bytes): the released learned trait with the largest |Δ| at its taken level, if >= 0.05, `learned: caution +0.31 on careful (0 rewards, 4 corrections)`. The counts are state: `learned_from`, per learned trait, the feedback turns that moved it, by sign. z prints with one signed decimal and Δ with two, rounded half to even from the stored value. What it guarantees, and what it does not. A persona without the block reads `senses` as any undeclared input (listed in `ignored`; `live` says so once on stderr) and every document it produces is the same, byte for byte, as before the block existed. A refused reading changes nothing (P3 holds with readings: 40 individuals × 300 random events mixing sources, readings, `self`, `clock` and out-of-range values). A source is the host's word: a forged `env` label passes every check; readings are logged and replayed, not shown true. ### 2.12 Needs (optional) A need is a number with a setpoint: use or time grows its deficit, and an event that reduces it pays a win sized by how much it reduced the drive. It changes which goal the stance pursues (RFC 0002 Design A; Keramati & Gutkin 2014). ```yaml needs: # needs a drives block; 1-8 items drive: {"n": 4, "m": 3} # required, n > m > 1 (quoted: the YAML reader refuses a bare n, YAML 1.1's false) pursue: {want: 2, cap: 3} # the pursue term: its weight, and |term| <= cap nats (defaults shown) success: {rate: 0.2, init: 1} # the bound goal's success rate s: its step per win or omission, its start (defaults shown) r_cap: 1 # required: |win| <= r_cap (at most drives.expectation.win_max) items: - id: context # at most 32 characters; reserves the input id need_context say: context budget # default: the id, _ as spaces unit: token_share # 1-24 characters cap: 1 # the deficit stays in [0, cap] weight: 1 # w > 0 per_hour: 0 # growth per stamped hour (>= 0) per_reading: {sense: tokens, rate: 1} # optional: growth per unit of a number sense's raw reading (section 2.11) satisfy_from: [env] # required: env and/or human amount: 0.6 # K: the typical satisfying amount, 0 < K <= cap (the forecast's) bind: compact # the goal its reduction pays; one need per goal; the goal needs expect_by_hours horizon_hours: 0 # tau, 0-72 (default 0) forecast_per_hour: 0.06 # the growth per hour the forecast assumes (default per_hour; >= 0) weight_spread: 0 # gene: multiplier exp(N(0, s^2)) on weight; 0..3, default 0 growth_spread: 0 # gene: multiplier exp(N(0, s^2)) on per_hour and per_reading.rate; 0..3, default 0 ``` **Amounts** ride in the event under `needs`, beside the ordinary inputs: `{"needs": {"context": {"amount": 0.6, "src": "env:compactor"}}, "src": "env:host"}`. A bare number takes the event's `src`; the source kind must be in the need's `satisfy_from` (never `self` or `clock`). `needs` is read, not compiled, and `inputs` echoes it as given. **The needs step** runs after the sense step and before the drive step, in two passes, every line rounded to 6 decimals (h = `elapsed_hours`; genes g_w and g_gr, 1 without a spread): ``` pass 1, every need in declared order: x = clip(x + g_gr x (per_hour x h + per_reading.rate x reading), 0, cap) D(x) = (sum_i w_i g_w,i x_i^n)^(1/m) # over all needs pass 2, each need satisfied this event by amount a, in declared order: r = clip(D(x) - D(x with x_j <- |x_j - a|), -r_cap, +r_cap); then x_j = |x_j - a|, clipped to [0, cap] ``` `reading` is the sense's raw value this event (0 when it is not read; never the normal). r > 0 is a `win` of size r for the bound goal, r < 0 a `setback` of min(-r, 1) (added to the host's, at most 1); both join the event's goal signals, so the drive step treats them as any win or setback (E, wanting, afterglow, the deadline, the window). n > m > 1 gives deprivation scaling: the same amount 0.2 pays 2.0229 times as much at deficit 0.8 as at 0.2, and an overshoot pays less than nothing (-0.117 at deficit 0): sign(r) = sign(x_j - a/2). The deficits after an event do not depend on the declared order; the wins depend on it only through D over the other needs, never in sign. **The pursue term.** For each need, with s the bound goal's success rate and K its `amount`: ``` x^ = x + g_gr x forecast_per_hour x horizon_hours # the forecast: a declared growth rate, not clipped at cap term = clip(want x s x (D(x^) - D(x^ with x^_j <- |x^_j - K|)), -cap, +cap) # cap = pursue.cap s <- s + rate x (1 - s) on a win of the goal, and k <- 0 s <- s x max(1 - k x rate, 0) when the goal's window fires, k <- k + 1 first # k: omissions since the last win (section 2.9) ``` Since 0.11.0 the forecast is not clipped at the cap: the deficit stays in [0, cap], but a need sitting at its cap still forecasts the growth its horizon brings, so the horizon also raises the term at the cap (before, x^ = x there and a horizon changed nothing once the deficit had reached it; bench/body n10). And omissions in a run cut deeper: the k-th unanswered pursuit in a row multiplies s by 1 - k x rate, so a run of 1/rate omissions (5 at rate 0.2) takes s to 0, while isolated omissions between wins still cost 1 - rate each (n11); a win ends the run. The state keeps the run as `needs.misses: {: k}` for goals with k > 0 (absent when every run is 0). It is lowered as the number input `need_` (box [-pursue.cap, +pursue.cap]) with effect `{pursue: {: 1}}` (section 3, step 2b). Pursuit is weighted by the goal's learned success, so unanswered pursuits extinguish themselves through the omission let-down; at horizon 0 the term is positive exactly when x > K/2, the deficits at which the satisfaction pays a positive win. A horizon > 0 lets the forecast pass K/2 before the deficit does (`lint` names it). Rules may read `need..deficit` as a number condition (a number = at least, or `{at_least, at_most}`), so a must-do habit can fire at a deficit: `{id: compact_when_full, when: {need.context.deficit: {at_least: 0.9}}, then: {pursue: [compact]}}`. A rule never reads `need_`. **Refused when the persona is read** (exit 2, at the field's path): `drive` without n > m > 1, a needs block without a drives block, no items or more than 8, two needs with one id, a taken `need_` id (N-R1); `bind` naming an unknown goal, or two needs bound to one goal (N-R2); `satisfy_from` missing, empty, or naming `self` or `clock` (N-R3); `per_reading` naming an undeclared or flag sense, a negative `per_hour` or `rate` (N-R4); `cap`, `weight`, `amount`, `want`, `pursue.cap` or `r_cap` <= 0, `amount` > `cap`, `r_cap` > `win_max`, `horizon_hours` outside [0, 72], `success.rate` outside (0, 1], `success.init` outside [0, 1], a number with more than 6 decimals (N-R5); a need whose bound goal has no `expect_by_hours`, or a window <= 0 hours (N-R6). **An event is refused whole** (one `{"error"}` at its path, no turn, no state change): an amount for an undeclared need, not a number > 0 or above the need's cap, or one need satisfied twice (N-E1, `inputs.needs.`); an amount whose source kind is not in `satisfy_from`: missing, malformed, `self`, `clock` (N-E2, `inputs.needs..src`); an amount and its bound goal's `win` in one event: one outcome, one payment (N-E3, `inputs.needs.`); a `need_*` or `drv_omit` input, or a deficit sent with an amount (N-E4: the engine owns them). **State and stance.** The state keeps `needs: {genes: {weight: {id: g}, growth: {id: g}}, deficit: {id: x}, success: {goal: s}}`: each deficit 0 and each s at `success.init` when the individual is made; genes `round(exp(weight_spread x z(name, seed, "need", id, "weight")), 4)` and the same with `"growth"` (spread 0 stores none). The stance gains `"needs": {"context": {"deficit": 0.55, "forecast": 0.61, "term": 1.109371}}` (the deficit after this event, the forecast and the term the field read), and `why` gains at most one need clause, for the pursued goal when a need binds it and its term is the largest positive one: `context budget deficit 0.55, above its setpoint`. What it guarantees, and what it does not. A persona without the block reads `needs` as any undeclared input (listed in `ignored`; `live` says so once on stderr) and every document it produces is the same, byte for byte, as before the block existed. A refused amount changes nothing (P3 holds with amounts: 40 individuals × 300 random events mixing sources, amounts, windows and payments). Amounts never come through the MCP tools (section 7). A source is the host's word: a forged `env` label passes every check; amounts are logged and replayed, not shown true. A need is arithmetic over logged numbers, not an inner state. #### Viability bounds, graded pressure and need valence (optional, RFC 0008; 0.11) Three keys, each additive: a persona without them gives every document byte for byte as before (the stance, the state, the strand, `describe`). A bound says where a need stops being viable; the couplings carry the distance to that bound into two moods with one weight each, with no thresholds to pick. All of it is arithmetic over the logged readings and amounts. ```yaml needs: pressure: {mood: pressure, gain: 2.0, shape: linear} # mood evidence += gain x (1 - margin_min); needs a bound valence: {mood: valence, tonic: 0.8, phasic: 0.6} # mood evidence += -tonic x D(x)/D(cap) + phasic x (D(x_before) - D(x_after)) items: - id: integrity per_reading: {sense: tool_fail, per_event: 0.08} # a flag sense: growth per true reading (a number sense takes rate) viable: {at_most: 0.8, events: 4} # the deficit bound x_b and the run of events beyond it that ends viability ``` **Bounds (RFC 0008 A).** After the needs step, per need with `viable`, every number rounded to 6 decimals: ``` margin = round(clip(1 - x / x_b, 0, 1), 6) # 1 at the setpoint, 0 at the bound beyond = beyond + 1 (at most events) if x > x_b, else 0 worst = the need with the smallest margin (ties: declared order); margin_min = its margin ``` When some need's `beyond` reaches its `events`, the state sets `viability: "lost"` once and keeps it; nothing else changes (the engine never stops a host). The state's `needs` gains `beyond: {id: n}` (per bounded need) and `viability: "viable" | "lost"`, only with a bound. Rules may read `need..margin` (a need with a bound) and `needs.margin_min` as number conditions (a number = at least, or `{at_least, at_most}`), as they read `need..deficit`. **Pressure (RFC 0008 B).** The named mood (any declared mood) takes `gain x (1 - margin_min)` as an ordinal shift each event, lowered as the reserved number input `need_pressure` (box [0, 1]). `shape` is `linear` (the one shape). **Need valence (RFC 0008 C).** The named mood takes `-tonic x D(x)/D(cap)` (lowered as `need_tonic`, box [0, 1]: how far the deficits are from their setpoints, D(cap) the drive with every need at its cap) and `phasic x (D(x before pass 1) - D(x after pass 2))` (lowered as `need_phasic`, box [-r_cap, r_cap]: this event's drive reduction, Keramati & Gutkin's homeostatic reward). It adds to `drives.valence` when both name the mood. A weight of 0 lowers no input. **Flag senses as growth (RFC 0008 D).** `per_reading` on a flag sense takes `per_event` (growth per true reading, times the growth gene); N-R4 reads "a number sense with `rate`, or a flag sense with `per_event`". **Stance and receipts (RFC 0008 E).** `needs.` gains `margin` and `beyond` (a need with a bound); the stance gains `viability: {worst, margin_min, status}`; `why` gains ` margin , nearest its bound` while the worst margin is under 0.25. **`block`**: with a bound, a coupling or `line.block: true`, the stance carries the text a host injects verbatim: every declared trait, then every mood, once each as `name level` (declared order, `; ` between), then with a needs block one line `viability: worst , margin ` (`viability: lost, worst ...` once lost; `viability: no bounds declared` without a bound). `line` is unchanged. `live verify` summarises `viability: {margin_min_seen, lost_at}` (the smallest margin_min after any event replayed, the event at which viability was lost or null), recomputed from the logged readings and amounts; `live verify --blocks FILE` writes each event's block (any persona, a block key or not). `monitor` draws a VIABILITY line (worst, margin_min, status) with one margin bar per bounded need, the worst highlighted. `prove` boxes `need_pressure`, `need_tonic` and `need_phasic` like any number input. **Refused when the persona is read** (exit 2, at the field's path): `viable.at_most` outside (0, cap] or with more than 6 decimals, `viable.events` not a whole number in 1..10000, an unknown key (V8-R1); `needs.pressure` without a need with `viable`, a `gain` <= 0 or above 50, a `shape` other than `linear`, a `mood` not declared; `needs.valence` with `tonic` or `phasic` outside [0, 50] or a mood not declared; `per_event` on a number sense or `rate` on a flag sense (N-R4); a need whose input id `need_` is `need_pressure`, `need_tonic` or `need_phasic` while the coupling that reserves it is declared; `need..margin` on a need without a bound; a rule on `need_pressure`, `need_tonic` or `need_phasic`. **An event is refused whole** when it sends `need_pressure`, `need_tonic` or `need_phasic` (N-E4: the engine computes them). What it guarantees, and what it does not. A margin is a declared bound applied to a logged deficit; "viability" is a status of that arithmetic, not a state of the host or of anything else. A source is the host's word, as for every reading. See docs/welfare-stance.md for what the words need, viability and damage mean here and what is not claimed. ### 2.13 Surprise (optional) After a run of surprising outcomes the learner takes larger steps, and smaller ones again once things settle (RFC 0002 Design B; Behrens et al. 2007; Nassar et al. 2010). ```yaml surprise: # needs a learning block; every field optional (the defaults shown) volatility_rate: 0.3 # a: the volatility average's weight per feedback, in (0, 1) v0: 0.45 # the volatility at which the gate opens, in [0, 1) r_ref: 4 # the run length at which the gain is 1 (>= 1) r_max: 20 # the cap on the run length (>= 1) g_max: 3 # the most the gate multiplies a step, in (1, 3] precision: false # true needs a senses block: each sense's effect gain x (1 - var / var_cap) ``` **The gate** runs inside the credit step (section 2.8), after the payment is settled and before the learner: for each learned trait whose paid credit has a taken level l* (credit > 0), when the feedback's sign is not 0, in this order (each stored number rounded to 6 decimals): mb = (plus(l*) - minus(l*)) / (plus(l*) + minus(l*) + 2) p_same = clip((1 + sign x mb) / 2, 0.01, 0.99) v = (1 - a) x v + a x (1 - p_same) Ω = clip((v - v0') / (1 - v0'), 0, 1) v0' = clip(v0 + the volatility gene, 0, 0.999999) counts = counts x (1 - Ω), then plus(l*) or minus(l*) += |sign| r = min((1 - Ω) x (r + 1) + Ω, r_max) g = clip((1 + Ω x r) / (r + 1) x (r_ref + 1), 1, g_max) and the learner's step carries g (section 2.8). A new learner's gate starts closed (since 0.11.0): v = 0, r = r_ref, so g = 1, the fixed rate, and its opening feedback at the defaults leaves (v, r, g) = (0.15, 5, 1). While Ω = 0, r only grows, so g stays at 1; g rises above 1 only after a gate step with Ω > 0, that is once a run of contrary feedback (a lesson reversed) has raised v past v0. Consistent feedback after that raises r and brings g back to 1 within a few turns. g never goes below 1, so the gate never learns slower than `rate` alone; `total_cap` is untouched, so the learned box `prove` uses is the same. The volatility average is slow on purpose: it needs a run of surprises, which a real change produces and noise rarely does. bench/body's bars s1-s3 measure the re-learning turns, the stable-phase movement and the caps (BENCHMARKS.md section 12). **Precision** (`precision: true`): each sense input's effect gain is multiplied by 1 − var / var_cap of its sense after this event's sense step (section 2.11), so a sense whose readings vary widely moves the stance less. It is off by default because it changes every sensed document. **State and stance.** The state keeps `surprise: {: {v, r, g, counts: [[plus, minus], ...]}}` per learned trait, one pair per level. The stance gains `"surprise": {"": g}` for the learned traits whose gain is above 1 (`{}` once every gate is closed), and `why` says `learning fast: feedback changed` while any gain is above 1. Refused when the persona is read (exit 2, at the field's path): a surprise block without a learning block, `precision: true` without a senses block (V-R1); `volatility_rate` outside (0, 1), `v0` outside [0, 1), `r_ref` or `r_max` below 1, `r_ref` above `r_max` (a run length capped below r_ref would keep g above 1 for good), `g_max` outside (1, 3] (V-R2). A state is refused, even with a recomputed digest, when v leaves [0, 1], r leaves [1, r_max], g leaves [1, g_max], a count is negative, the pairs do not match the trait's levels, or a number has more than 6 decimals. ### 2.14 Pace (optional) Average reward over the last hours sets the pace of the next actions; a single reward does not (RFC 0002 Design C; Niv et al. 2007; Beierholm et al. 2013). ```yaml pace: # needs reward_from (section 2.10) half_life_hours: 6 # the average's half-life in stamped hours (> 0) cap: 4 # the average stays in [0, cap] (> 0) r0: 1 # the average at which the effects are 0, in (0, cap) levels: [0.5, 1.5] # required: slow below the one, brisk above the other; 0 <= slow_below < brisk_above <= cap effects: {initiative: 0.5, verbosity: -0.3} # required: ordinal shift per unit of (average - r0), on traits or moods pace_spread: 0 # gene: a gain exp(N(0, s^2)) on the effects, 0 to 3 ``` **The pace step** runs after the drive step (h = `elapsed_hours`; r_t = the sum of this event's accepted wins, the needs' wins included, plus 1 when the learning block's reward flag is on; corrections add nothing): R = round(clip(R x 0.5^(h / half_life_hours) + (1 - 0.5^(1 / half_life_hours)) x r_t, 0, cap), 6) A new individual starts at R = 0. Stance t reads R after event t: the outcome of turn t's action, carried by a later event, moves only later stances. R enters the field as two number inputs, `pace_up` = max(R − r0, 0) (box [0, cap − r0]) and `pace_down` = max(r0 − R, 0) (box [0, r0]), carrying `effects` (negated for `pace_down`) times the gain g_pace = round(exp(pace_spread x z(name, seed, "pace", "gain")), 4). The two ids are reserved only with the block; the engine sets them (an event that sends one is refused at `inputs.pace_up`), and the stance's `inputs` leave them out. On hourly events this is a running average: a host acting a = base x (1 + κ x (R − r0)) times an hour, each action paying with probability p, settles at R* = p x base x (1 − κ x r0) / (1 − G) while the loop gain G = p x κ x base < 1 (1.2632, 1.5, 2.25 at G = 0.24, 0.60, 0.84) and runs to the cap that binds when G >= 1. The engine cannot see that loop (p is the world's), so `lint` reports the largest shift pace can put on the field, sum of |effects| x max(cap − r0, r0), and warns over 1; `describe` prints the closed form. **State and stance.** The state keeps `pace: {rbar}`. The stance gains `"pace": {"level": "brisk", "rbar": 1.683403}` (slow, steady or brisk by `levels`, for the host's scheduler), and `why` says `pace brisk: average reward 1.68 an hour` (R to 2 decimals) when the level is not steady. Refused when the persona is read: a pace block without `reward_from` (P-R1: no self-reward can move it); `cap` or `half_life_hours` <= 0, `r0` outside (0, cap), `levels` not ordered within [0, cap], a missing `levels` or `effects`, a `pace_spread` outside 0 to 3 (P-R2); an effect on `pursue` or on an unknown trait or mood, a declared input or a habit condition that takes `pace_up` or `pace_down` (P-R3). A state is refused, even with a recomputed digest, when R leaves [0, cap] or has more than 6 decimals. ### 2.15 Dispositions (optional) Each individual learns at its own rate from better- and worse-than-expected outcomes; the rates are genes (RFC 0002 Design D; Lefebvre et al. 2017; Frank et al. 2009). ```yaml dispositions: # each spread 0 to 3, default 0 (no gene) optimism_spread: 0.3 # o = s x z: rate x e^o above the expectation, rate x e^-o below (a learning or a drives block) patience_spread: 0 # p = exp(s x z): x credit.half_life_events and x deadline_tau_hours (a credit or a drives block) volatility_spread: 0 # an offset s x z on surprise.v0 (a surprise block) ``` The genes come from the persona hash family z(name, seed, "disposition", ): `optimism` = round(s x z, 4), `patience` = round(exp(s x z), 4), `volatility` = round(s x z, 4). They act here: section 2.8 step: rate x e^(sign x o), then the step_cap clip section 2.9 E: E <- E + α x (m - E), α = min(rate x e^o, 1) if m > E, min(rate x e^-o, 1) if m < E (the omission too) patience: half_life_events x p, clipped to [1, 10000] (expiry at 4 half-lives); deadline_tau_hours x p With wins b and a at equal odds, E settles at the expectile τb + (1 − τ)a, τ = α⁺ / (α⁺ + α⁻); on one stream an individual with o > 0 keeps E at or above its spread-0 twin's at every step (the update is increasing in E and α⁺, decreasing in α⁻). **State.** `dispositions: {optimism, patience, volatility}`, only the genes whose spread is above 0: a spread of 0 stores nothing, so a block with every spread 0 gives the bytes of the persona without it (digests aside). There is no `why` clause; `describe` prints the spreads and the genes at the persona's seed. Refused when the persona is read: a spread outside 0 to 3 (D-R1); `volatility_spread` without `surprise`, `optimism_spread` without a learning and without a drives block, `patience_spread` without a credit block and without a drives block (D-R2). A state whose genes miss its seed is refused. What the three blocks guarantee, and what they do not. A persona without them may use `pace_up` and `pace_down` as any ids, and every document it produces is the same, byte for byte, as before the blocks existed. Every new number is capped, rounded to 6 decimals (genes to 4) and recomputed by `live verify` from logged feedback, wins and stamped hours. The gate, the average and the genes are arithmetic over logged numbers, not inner states. ### 2.16 Bound (optional; RFC 0004) A certificate, not a behaviour: `bound` adds no state and no per-turn cost, and it stays out of the persona digest, so adding it to a persona with a recorded life changes no state, strand line or turn (RFC 0004, amendment A1). ```yaml bound: rest_hours: 200 # the rest probe: one event of this many idle hours, no inputs (default 30 x the longest half-life; 24 if none) max_epsilon: 0.5 # optional: `persona check` fails above it, or on an uncounted channel seeds: 0-9 # the individuals epsilon is the worst case over (default 0-99) count: [arousal] # optional: moods without half_life_hours to search instead of naming them uncounted ``` `probbit persona bound PERSONA [--seeds LIST] [--json]` prints, per learned trait, **epsilon**, the attaining level set and seed, the `ceiling` tanh(D / 4), `speed` (the built-in greedy persuader's feedback events to epsilon / 2, a witness), `speed_floor` (proved: no shorter sequence reaches epsilon / 2), the residue `r` of decayed channels, the enumerated history and every uncounted channel at its path. Exit 0 when every channel is searched, decayed or enumerated; 1 on an uncounted channel or an epsilon above `max_epsilon`; 2 on a refusal (BO-R1 to BO-R6: an unknown key, `max_epsilon` outside (0, 1), malformed `seeds`, no `learning` block, `rest_hours` outside (0, 1e6], a bad `count` entry). **The claim, exactly** (a theorem about the engine's arithmetic, labelled mechanism): no event sequence moves a learned trait's odds at rest (`rest_hours` idle, no inputs) by more than epsilon + r in total variation; epsilon is the exact maximum of that change over the learned-weight box ±`total_cap` (and the counted and enumerated channels), computed from this file by probing the box's vertices. It bounds the stance at rest, not the stance mid-conversation and not the words a model writes. It is not a safety gate and grants nothing. Above 12 searched coordinates in a trait's component it prints the coarse ceiling (sound, not tight), never sampled vertices. **History.** A `recent` feature whose effects reach the trait's component, or which a habit's `when` names, is enumerated by its values, not by stored sequences. The rest probe's own flag is false, so a feature reads min(`cap`, trues in the last `window` − 1 stored turns) and a `streak` reads 0; the stored turns of a flag are split at each of its features' window boundaries and every count of trues per segment is probed once per distinct tuple of capped values. Every tuple is reachable and nothing else of the stored turns enters the rest probe, so this is the exact maximum over histories (equal, in the test, to the search over every stored sequence). A chat persona's praise (window 4) and wins (window 6, cap 3) give 4 × 4 = 16 combinations instead of 256. Epsilon then includes what the history moves on its own (`ceiling` prints null). Limit: more than 4096 combinations per trait and seed prints the coarse value 1 (sound), and `--audit` refuses with `: more than 4096 enumerated history combinations: the audit needs an exact epsilon` (exit 2). In words for a reader: "Its learned character has a published limit: no sequence of messages can move the odds of any trait at rest by more than epsilon, a number computed from its file that anyone can recompute. The limit is arithmetic over declared numbers. It covers the stance at rest, not the words a model writes, and it is published with how fast the limit can be reached." **Audit.** `probbit persona bound PERSONA --audit STRAND` replays a recorded life and reports, per learned trait, the room used (resting TV between the individual and the same individual with its learned weights at 0, divided by epsilon at the strand's seed): the most at any event, at which event, now, and the distance to the ceiling now; exit 1 on an exceedance (impossible by the theorem; the audit shows it on the recorded life). The room used counts the learned weights only, at the individual's own history; epsilon is the whole resting change, history included, so on a trait an enumerated history reaches the room used is the learned share of that limit. **Erasure.** `probbit live control STRAND erase --by human:ID --reason TEXT` appends a chained control line that returns every learned weight to 0 (and closes the open credits, as every control line does); replay applies it, the next event's state digest records it, and the audit then reports a room used of 0. ### 2.17 Expectation and trajectory (optional; RFC 0009) Two additive keys, both off by default and both outside the persona digest (as `bound` is): a persona without them gives every document byte for byte as 0.11.0, and adding either to a persona with a recorded life leaves every state and stance digest as it was. Both have effects {}: they move nothing. ```yaml expectation: # optional; {} = the defaults half_life_events: 32 # the decayed combo counts' half-life, in events max_combos: 16 # mixture members at most, the empty combo included; the rest fold into it engine: trajectory: # optional k: 8 # steps, 2-16 beta0: 0.4 # first step's inverse temperature, (0, 1); linear to 1 ``` **Expectation.** On `probbit live` (the strand is the memory), per event, from the state before it: A_t = the event's program (`persona compile` of its inputs); E_t = the program with `elapsed_hours` only; Pi_t = the mixture over the canonical input combos seen so far (false, 0 and null dropped; a declared number input bucketed into the thirds of its range; `elapsed_hours` dropped), each member compiled from its combo's latest inputs with this event's hours (the event's own combo with its own inputs, so A_t is a member whenever its combo is), weights = exponentially decayed counts plus one pseudo-count on the empty combo. Mixture marginals are linear in the members' exact marginals, so every number is exact from at most M + 1 exact solves. The stance gains ```json "deviation": {"self": 2.18, "pred": 1.64, "input": 1.21, "cold": false, "z": 2.3, "registers": true} ``` `self` = sum over the non-twin variables of KL(A_i || E_i); `pred` = the same against Pi_i (nats); `input` = -ln of the mixture weight of the event's combo (its own member's, else the empty member's); `cold` = no non-time combo seen yet (`pred` is 0 then); `z` = (ln(1 + pred) - mean) / sqrt(var + 1e-4) against a learned normal (prior mean 0, var 0.04, the same half-life; read after the cold start, from the logged value); `registers` = z >= 1.5. The strand line carries the receipt `"deviation": {"input", "pred", "self"}` at 5 significant digits (with `"cold": true` while cold); `live verify` recomputes it ("the deviation differs" on an edit), `resume` folds the same memory from the logged lines, and the stance digest the line logs excludes `deviation`. Bound (by construction): per variable, `pred_i <= input` for a member combo and `pred_i <= self_i + input` otherwise. `persona turn` and `persona replay` carry no memory and add nothing. A strand with the block verifies from its header (`--from-checkpoint` refuses: the memory reads every event). The deviation is absent (`null`) when a program is not solved by the exact tier. **Trajectory.** p_beta, proportional to p^beta, of the event's program at the k betas, each solved by the exact tier on `Model::scaled(beta)`: no sampler, a pure function of the program. It is derived and never logged: `probbit monitor` shows a `TRAJECTORY` row (mean marginal entropy per step) and its JSON frame a `trajectory` key; `probbit persona trajectory PERSONA --state S [--inputs I]` (or `--program FILE` for any probbit-ir program) prints the steps. The joint entropy of p_beta is non-increasing in beta (dH/dbeta = -beta Var(log w)); a single variable's marginal entropy can rise (measured in RFC 0009). ### 2.18 Display glyphs (optional; since 0.13.0) Since probbit 0.13.0. probbit 0.12.1 and earlier refuse a persona that has it (`display: unknown field`, exit 2). One emoji per stance, computed by the engine from the stance document, never chosen by the model. Off by default and outside the persona digest (as `bound` and `expectation` are): a persona without `display` gives every document byte for byte as before, and adding the block to a persona with a recorded life leaves every state and stance digest as it was. It has effects {}: it moves nothing. ```yaml display: glyphs: moods: {valence: [down, even, up], pressure: [calm, steady, keyed]} # two mood ids: rows, then columns, in table order table: # one row per level of the first mood, one glyph per level of the second down: ["😔", "😕", "😣"] even: ["😌", "🙂", "😬"] up: ["😊", "😄", "🤩"] surprise: {glyph: "👀", z_at_least: 2.0} # optional modifier (default z_at_least 2.0) habits: {security_careful: "🔒"} # optional: habit id -> glyph, first listed in force wins ``` **The rule order** (first base match wins; at most one base glyph and one modifier): 1. the first habit listed under `habits` that is in the stance's `habits.active`. Active means the habit applies this turn; bound means the habit-free stance would have broken it and the engine held it, so a habit whose inputs already take the stance where it points is active and not bound, and its glyph still shows. An unconditional habit (no `when`) is active on every turn, so listing one here shows its glyph on every turn; 2. otherwise the table's cell at the two moods' levels: `table[][]`; 3. then the modifier: `surprise.glyph` appended when the stance carries `deviation` (an `expectation` block, section 2.17, on `probbit live`) with `registers: true` and `z >= z_at_least`. `persona turn` and `persona replay` carry no deviation, so they never add it. The stance document gains `"glyph": "🙂"` and `"glyph_why"`: `"mood valence=even pressure=steady"`, `"habit security_careful"`, or either followed by ` +surprise z=2.31` (z to 2 decimals). Both come last, after `block`; the digest a strand line logs as `stance` excludes them, as it excludes `deviation`. **The receipt.** On `probbit live` each event line carries `"glyph"`, and `live verify` recomputes it from the replayed document: an edited, added or removed glyph diverges at its line as "the glyph differs". The same rules give the same glyph in the stance document, the strand line, a host's footer and the monitor's header, so a mismatch between them is a host bug, not a different state. A strand written with the block is read by an engine without it as `unsupported by this engine: display` (not verified, not a divergence). **Read strictly** (`persona check`, `persona lint` and every command that loads the persona refuse it at its path): `moods` names two moods of the persona, each with its levels listed once; `table` has one row per level of the first mood, each a list with one glyph per level of the second; every glyph is one Unicode code point that shows on its own, optionally followed by U+FE0F (the emoji presentation selector, so `🛠️` and `❤️` pass) and by nothing else: the code point is not a control or a space, and no zero-width joiner, other variation selector, keycap mark, skin-tone modifier, regional indicator, tag or combining mark appears (so `👨‍💻` and `1️⃣` are refused; a terminal, a chat client and a device panel draw one glyph, and a base glyph and its modifier never fuse into a third); `habits` names habits of the persona; no key is repeated; `z_at_least` is a number >= 0. `persona lint` reports e.g. `display.glyphs.table.even[2]: one Unicode code point, optionally followed by U+FE0F (no ZWJ, skin-tone, flag, keycap, tag or other combining sequence)`. A host that counts a line's length in code points counts the selector as one more (`🛠️` is two, `🛠️👀` three). With the modifier the glyph is the base followed by the modifier, each one code point plus an optional selector, so a host splits them at the first code point (and its selector). What it is: a lookup from the stance's mood levels, active habits and deviation to a symbol, so a person reading the chat or the monitor sees the engine's state at a glance. What it is not: a measurement of anything beyond those fields. ## 3. Compilation: persona + state + inputs → one probbit IR program For turn t of an individual (seed s), with inputs x_t: 1. **Values.** `positional` (default): `l0 .. l(L-1)` (L = the largest number of levels) followed by the agenda slots; `semantic`: every level name in order of first appearance, then the slots. Both give the same distribution and stance (checked on every example). 2. **Trait variable** v, level l (allowed: its own levels): `h(v,l) = ln prior(l) + gene_v x c(l) + Δ_v(l) + sum over inputs i of gain_i x scale_i(x_t) x effect_i(v,l) + sum over history features f of min(value_f, cap_f) x effect_f(v,l)`, with `scale` = 1 for a true flag or the matching level, the value for a number input, 0 otherwise, and Δ_v the learned weights after this turn's feedback (section 2.8; 0 for a trait that does not learn). With a senses block (section 2.11) the sense step runs before this one and its lowering joins the inputs: `sense__up` and `sense__down` are number inputs with gain g_gain and the sense's effects (negated for `down`); a sense not read this turn adds nothing. 2b. **`pursue`** (a drives block, section 2.9): one more trait variable over the goal ids, after the declared traits. Its field per goal g is step 2's (prior from `interest`, learned weights if `pursue` learns, input and history effects on `pursue`) plus `interest gene_g + want x W_g + glow x A_g + deadline / (1 + D_g / tau)` (each term rounded to 6 decimals, W, A and the deadline D after this turn's drive step), summed left to right and rounded. **The floor lift** follows: for each floor goal s the habits in force allow, the smallest L_s >= 0 (plus a 1e-5 margin, rounded up to 1e-6) with `exp(h_s + L_s + lo_s) / (exp(h_s + L_s + lo_s) + sum over allowed g != s of exp(h_g + L_g + hi_g)) >= floor_s`, where lo_g / hi_g are the least / most `pursue`'s couplings can add to goal g over every level of each partner. Several floor goals are lifted together (each lift raises the others' sums; at most 60 rounds); the lifts join the field as the contribution `floor` and the field is rounded again. The aggregates (wanting, afterglow, surprise) enter as number inputs and the goal conditions as flag inputs of the turn; `starve_` habits follow the declared habits (step 6). With a needs block (section 2.12) each need's term enters as the number input `need_` (weight 1 on its bound goal, box [-pursue.cap, +pursue.cap]) and each `need..deficit` condition as a flag input; with `valence` the input `drv_expect` and with a window and `effects.omission` the input `drv_omit` (section 2.9) join the evidence. With a pace block (section 2.14) `pace_up` and `pace_down` join the inputs with gain g_pace; with `surprise.precision` each sense input's gain is multiplied by its precision factor (section 2.13). 3. **Mood variable** m: `h(m,l) = ln prior(l) + gene_m x c(l) + a_t(m,l)`, where the **mood accumulator** `a_t = k x d x a_(t-1) + (1 - k) x e_t`, e_t = the turn's input and history effects on m (with gains), k = `inertia`, d = 0.5 ^ (elapsed_hours / half_life_hours) (1 without them). An event's total impact over time is e_t whatever k is; inertia only spreads it out, so it smooths the mood without shifting the persona's average character. 4. **Agenda**: one variable per step over the slots, unary = `prefer` + step effects; `all_different` over the steps. 5. **Couplings** become `pairs` with a k x k `table` over the value alphabet. 6. **Habits in force** (conditions evaluated on x_t and the history): each `then` entry becomes a one-variable `tables` allow-list (`prev` resolved against the previous turn's level); `rules` are copied as written. Inactive habits are not in the program. 7. **Twin** (`engine.twin`): a copy of every trait, mood and step variable named `free.`, with the same unaries, couplings and agenda structure and **no habits**, in the same program (an independent component, solved separately). 8. Every weight is rounded to 6 decimals; the program is serialised in a fixed order. Its sha256 is the turn's `program` digest. **The steps of one event**, in order: validation and refusals (section 2.10, the senses' S1-S2, the credit rows, the needs' N-E1-N-E4); the sense step (section 2.11); the needs step (section 2.12: growth, then each satisfaction's win or setback); the drive step (section 2.9: decay and the windows' countdown, wins, an omission, wanting, caps; with an optimism gene the rate above or below E); the pace step (section 2.14); the credit step (section 2.8) with the surprise gate (section 2.13); the compile above; the solve, the stance and `why`; after the turn: `since`, a window opened for the goal a released stance pursues, `await`. Nothing runs between events: growth and decay use the next event's stamped hours. **The instruction is `probbit run --op decide` at fixed work** (`--seed S --sweeps N --polish-sweeps M --exact-limit X --chains C`), in process: the program is built as a document and handed to `probbit run`'s own parser and instruction (no text round trip, no child process). `probbit persona compile` prints it; `probbit run` with those flags answers it identically. - `run`, because a persona is a general constrained categorical program (levels, soft pairs, caps, implications, precedences, linear budgets). `decide` is the task-router front-end (tasks to workers under quotas) and cannot express per-level unaries, couplings or these rules without contortion. `evaluate` is for a judge's per-question probabilities plus rules: it is the bridge when a model proposes the stance (section 8), not the per-turn path. - `--op decide` answers exactly whenever the program is small or decomposable (every persona here: enumeration per component, about 0.2-0.7 ms of engine time) and falls back to the sampler and the diagnostics gate when it is not (a large, densely coupled character), which is where refusals come from. - The whole program is enumerated before its components when its raw space (the product of every variable's levels, the twin's included) is at most `exact_limit`. A drives block multiplies that space by G² (`pursue` and its twin, G goals), so a small persona can land there: the drives fixture of the tests (four 3-level traits, a 3-level mood, four goals) has 944,784 states and takes about 19 ms per turn, where without the block it has 59,049 (1.3 ms). Setting `engine.exact_limit` below the raw space (`exact_limit: 1000`) has the engine solve it per component instead: the same odds, 0.14 ms per turn (BENCHMARKS.md §9). - Fixed work makes the answer a pure function of the program and the seed even when the sampler runs. `polish_sweeps: 0` means no plan polish (never the wall-clock one, which would make a sampled turn depend on the machine's speed). - The turn seed: `S = first 4 bytes of sha256("probbit-persona/1|turn|||")`, masked to 31 bits. ## 4. The stance document (one per turn) ```json {"probbit_persona_turn": 1, "persona": {"name": "Rook", "version": "1.0.0", "digest": "sha256:..."}, "seed": 3, "turn": 5, "status": "ok", "engine": {"verdict": "exact", "tier": "components", "program": "sha256:..."}, "stance": {"verbosity": {"level": "terse", "p": 0.81, "odds": {"terse": 0.81, "short": 0.17, "full": 0.02}, "released": true}, "...": {}}, "mood": {"valence": {"level": "down", "p": 0.44, "odds": {"down": 0.44, "even": 0.53, "up": 0.03}, "released": true}}, "agenda": ["acknowledge", "diagnose", "fix", "verify", "report"], "habits": {"active": ["after_error_shorter", "high_stakes_careful"], "bound": ["after_error_shorter"], "violations": 0, "conflict": [], "yielded": []}, "unsure": [], "held": [], "escalate": null, "inputs": {"error": true, "stakes": 0.8, "errors_in_row": 2, "...": "..."}, "ignored": [], "line": "Stance: shorter than last time, check more; 1-2 sentences; verify before claiming anything; no jokes.", "line_tokens": 24, "why": "shorter than last time, check more (habit); last action failed -> caution up, verbosity down", "state_digest": "sha256:..."} ``` - `level` is the variable's value in the engine's joint plan (the most likely stance that obeys every habit); `odds` are its exact marginals (or gate-checked estimates on a sampled answer). A level can differ from its own most likely value because the plan must fit the other traits (the IR's "plan or odds" rule): act on `level`, read uncertainty from `odds`. - `habits.active`: habits in force this turn. `habits.bound`: the active habits the habit-free twin breaks, i.e. the ones that changed the stance. `violations` is always 0 when there is a stance (checked independently by the reference implementation on every turn). - `status`: - `ok`: the engine answered `exact` or `diagnostics_passed`. - `partial`: some variables did not pass the gate (`released: false`); their phrases are left out of the line. - `refused`: nothing is vouched for; the line carries only the habits in force (hard rules are always safe to state). - `fallback`: no stance at all (the engine proved the habits contradict on this turn, or failed): levels = the `fallback` levels, line = the non-conflicting habits or `neutral`, `escalate` says why. `habits.conflict` names a minimal set of contradicting habits (deletion filter: one extra engine call per active habit, only on such turns). - `unsure` / `held`: traits whose top odds are under their `vouch` floor; with a `hold` level they are re-solved clamped to it (a second engine call), so the stance stays consistent with every habit. - `line`: phrases by priority — (1) bound habits, (2) other conditional habits in force, (3) unsure notes, (4) traits off this individual's resting level, (5) traits at rest, (6) agenda order — cut from the lowest priority until it fits `max_tokens`. Unconditional habits that did not bind are not repeated (the trait phrases already carry them). - `why`: the bound habits, then the two strongest live inputs with the two traits each pushes hardest, by direction (`learner upset -> valence down, humour down`): a push is evidence, not the level the stance took. With a senses block the normal clause comes before the inputs and the learned clause after them (section 2.11). - With a senses block (section 2.11): `"senses": {"tool_fail": {"raw": true, "src": "env:runner", "z": 3.878359, "zd": 3.378359, "mean": 0.05, "n": 0}}` per sense read this turn (the `mean` and `n` it was judged against) and `"noticed": "tool_fail"` (the normal clause's sense, or null); `inputs` echoes `senses` as given. - With a credit block (section 2.8): `"credit": {"paid": {"turn": 3, "e": 0.937084, "sign": 1, "absorbed": false, "stance": "sha256:..."}, "closed": [{"turn": 3, "as": "paid"}], "open": [5, 6], "stance": "sha256:..."}`: the payment this turn made (null when none), the decisions it closed (by a control line, expired, cancelled, paid, evicted, in that order), the turns still open, and this stance's own digest (what `for: {turn, stance}` names); `inputs` echoes `await`, `for` and `cancel`. - `held`: the unsure traits held at their `hold` level; such a trait also carries `odds_unheld`, its odds before the hold. - With a drives block (section 2.9), `pursue` leaves `stance` for its own object and the turn's drive values follow it: `"pursue": {"goal": "fun", "p": 0.44, "odds": {"chores": 0.10, "craft": 0.26, "fun": 0.44, "safety": 0.20}, "released": true, "say": "play", "lift": {}}` (`lift`: the floor lift per goal on this turn, empty when none was needed) and `"drives": {"want", "glow", "expect", "surprise", "deadline", "since"}` (per goal, as this turn read them). The line clause `pursue: : ` ranks as a trait phrase, ahead of the other traits unless `line.order` places it; `inputs` echoes `goals` as given. With a window (section 2.9) `drives` adds `"window": {"report": 1.5}` per goal with `expect_by_hours` (hours left, or null). - With a needs block (section 2.12): `"needs": {"context": {"deficit": 0.55, "forecast": 0.61, "term": 1.109371}}` per need (after this event's needs step); `inputs` echoes `needs` as given. - With a pace block (section 2.14): `"pace": {"level": "steady", "rbar": 0.97}` (the average after this event's pace step); `inputs` leaves out `pace_up` and `pace_down`. - With a surprise block (section 2.13): `"surprise": {"": g}` for the learned traits whose gain is above 1 (`{}` when none is). - With `display.glyphs` (section 2.18): `"glyph"` and `"glyph_why"`, last. - Timing is never part of the stance: `--timing` adds a separate, non-canonical `timing` object (compile, engine and decode ms, engine calls, the 1-minute load average). ## 5. State, replay and determinism ### 5.1 State ```json {"probbit_persona_state": 1, "persona": {"name": "...", "version": "...", "digest": "sha256:..."}, "seed": 3, "turn": 5, "genes": {"shift": {"warmth": -0.2135, "...": 0}, "react": {"error": 1.1847}}, "mood": {"valence": [-0.31, 0.0, 0.12], "arousal": [0, 0, 0]}, # accumulators a_t (log-weights per level) "history": {"error": [false, true, true]}, # recent flag values (as long as the longest window / cap) "prev": {"verbosity": "short", "...": "...", "agenda": ["..."]}, # last turn's levels (for prev-relative habits) "rest": {"warmth": "cool", "...": "..."}, # the individual's resting stance (no evidence), set at init "learned": {"verbosity": [-0.12, -0.31, 0.43]}, # with a learning block (section 2.8): Δ per level "credit": {"verbosity": [-0.08, -0.36, 0.44]}, # with a learning block: the last stance's onehot - odds "drives": {"genes": {"interest": {"fun": 0.12}, "gain": {"fun": 1.08}}, # with a drives block (section 2.9), per goal: "want": {"fun": 0.61}, "glow": {"fun": 0}, "expect": {"fun": 0}, # wanting, afterglow, expectation, the last "surprise": {"fun": 0}, "deadline": {"fun": null}, "since": {"fun": 0}, # error, deadline hours, turns since, "window": {"fun": null}}, # and (expect_by_hours) the omission windows "needs": {"genes": {"weight": {}, "growth": {"contact": 1.0914}}, # with a needs block (section 2.12): genes, "deficit": {"context": 0.3, "contact": 0.12}, "success": {"compact": 1, "report": 0.8}}, # deficits, s per goal "surprise": {"verbosity": {"v": 0.15, "r": 2, "g": 1.666667, "counts": [[0, 1], [0, 0], [0, 0]]}}, # with surprise (2.13) "pace": {"rbar": 0.97}, # with a pace block (section 2.14) "dispositions": {"optimism": 0.1421, "patience": 1.0815}, # genes with a spread above 0 (2.15) "senses": {"genes": {"gain": {"tool_fail": 1.19}, "threshold": {"tool_fail": -0.17}, "habituation": {"tool_fail": 1.46}}, "norm": {"tool_fail": {"mean": 0.3, "var": 0.2, "n": 10}}}, # with a senses block (section 2.11): per sense "learned_from": {"caution": {"rewards": 3, "corrections": 2}}, # with learning and line.why_past: feedback turns by sign "open_credit": [{"turn": 5, "credit": {"verbosity": [0.1, -0.4, 0.3]}, "stance": "sha256:..."}], # with a credit block "prev_stance": "sha256:...", "control_closed": [], # (section 2.8): open decisions, digests, control closures "digest": "sha256:..."} ``` The state is plain JSON the host stores between turns (a file, a database row, a session field). A turn refuses a state whose persona digest differs from the persona file, whose own digest does not match, or whose genes do not match its seed, and (with a learning block) learned weights outside ±total_cap or credits outside ±1, (with a drives block) drive genes that do not match the seed, a drive value outside its box (wanting, afterglow and expectation within their caps, the error within ±pe_cap, deadlines 0 to 1e6 hours or null, `since` a whole number >= 0) or a goal set that differs from the persona's, and (with a senses block, rule S4) a mean outside prior ± drift_cap, a variance outside [0, var_cap], a count that is not a whole number up to 1e6, a sense set that differs from the persona's or sense genes that do not match the seed, even with a recomputed digest. Each normal starts at its prior with n = 0; a persona without the block refuses a state with a `senses` field. With a credit block (section 2.8) the state keeps `open_credit` (the decisions awaiting an outcome: at most `max_open`, turns increasing and before the state's turn, credits within ±1, a stance digest each), `prev_stance` (the previous stance's digest, null before any turn) and `control_closed` (decisions a control line closed, listed by the next stance); a persona without the block refuses a state with any of them. With a needs block the state is refused, even with a recomputed digest, when a deficit leaves [0, cap], a success rate leaves [0, 1], a window leaves [0, `expect_by_hours`] or null, the need set or the bound goals differ from the persona's, or a need gene misses its seed; a persona without the block refuses a state with a `needs` field. The fields of sections 2.13 to 2.15 are refused the same way, at their paths: a gate's v outside [0, 1], r outside [1, r_max], g outside [1, g_max], a negative count; R outside [0, cap]; any of them past 6 decimals; genes that miss the seed; a field whose block the persona lacks, or a block without its field. ### 5.2 Canonical JSON and the number rule Every document a persona produces, and every text it hashes, is printed canonically: object keys sorted by code point, no whitespace, UTF-8 (no `\u` escape except for control characters; `"`, `\`, `\b`, `\f`, `\n`, `\r`, `\t` escaped as in JSON), and every number by one rule: - a number whose value is a whole number of magnitude at most 2^53 prints as an integer: `1`, `-2`, `0` (so `1`, `1.0` and `1e0` are one number in every digest, and so are `0` and `-0.0`); - any other number prints as the shortest decimal that reads back to the same double (an exact tie between two such decimals goes to the even one), in plain notation when its decimal exponent e satisfies -5 < e < 16 (`0.43543`, `0.0001`, `123.25`) and in exponent notation otherwise (`1e-05`, `1.5e-07`, `1.2e+16`): Python's `repr` of a float. The persona digest is the sha256 of the persona document printed so; the state digest of the state without its `digest` field; `engine.program` of the program text, which takes the same number rule but keeps the compiler's key order. A host that rewrites a state with another JSON library (Python writes `0.0` where probbit wrote `0`) still has the same state. ### 5.3 Guarantees - A turn is a pure function of (persona file, state, inputs, engine version): the same four give the same stance and new state, byte for byte (canonical JSON). Replaying a script from `init` reproduces every stance. - The engine runs at fixed work, so this holds on the sampled tier too (the engine's documented fixed-work determinism). - Genes use the platform's `ln`, `sqrt` and `cos` and are rounded to 4 decimals; weights to 6. A last-place difference in a maths library could, in principle, flip a rounding boundary on another platform (not observed; not proven impossible). - The 0.5.0 documents equalled an independent reference implementation's (Python, written before this one, from this document) byte for byte on the example personas: their state0, all 20 workday stances and the final state (the 0.5.0 goldens), and on 5,400 further turns of random inputs over every input each persona declares, three seeds each, as written, with `values: semantic` and forced onto the sampler, held, yielded, refused and fallback turns included (measured on 2026-10-02). 0.6.0 changed two things that check did not cover: `why` names a direction (`humour down`) where it named a level, and the tutor has one habit more. Its goldens in examples/persona/golden/ are regenerated from this implementation; the parity claim is for 0.5.0. ### 5.4 Individuality and distance `gene_v = round(spread_v x z(name, seed, "shift", v), 4)` and `gain_i = round(exp(reactivity_spread_i x z(name, seed, "react", i)), 4)`, with `z` a standard normal from Box-Muller on the first 16 bytes of `sha256("probbit-persona/1|" + parts joined by "|")`. Genes depend on the name, the seed and the variable id only, so editing an unrelated part of a persona keeps its individuals. **Distance between two individuals** over a script (the same inputs for both): the mean, over turns and the traits both declare with the same levels, of the total-variation distance between their trait odds (0 = identical, 1 = disjoint); and the fraction of (turn, trait) where their levels differ. Identical persona + seed + script → 0 / 0. ### 5.5 Contradicting habits Two hard habits can contradict on some turns (for example "after an error, shorter than last time" against "at high stakes, at least a short paragraph" when the previous reply was already short). The engine then proves that no stance obeys them all (`infeasible`). With `on_conflict: fallback` the turn has no stance (status `fallback`, `habits.conflict` lists the contradicting habits, the line omits them). With `on_conflict: yield` the lowest-priority habit of the conflict yields (on a tie, the later-declared one), the turn is re-solved, and `habits.yielded` and `escalate` say so. **Lint.** `lint` searches every single conditional habit and every pair of them (with all unconditional ones and every previous level their `prev` restrictions read) for an empty stance, and reports each contradiction with its resolution. Run it before shipping a persona. **Exit codes.** `lint` exits 0 when every contradiction is resolved and, with rules (`--never RULE` / `--props FILE`, section 5.6), every rule is held by construction or proved; 1 when a contradiction is unresolved or a rule has a counterexample; 2 on a bad persona, rule or flag; 3 (since 0.8.2) when nothing is broken but some rule is **unproven**: `prove` left it `unknown` and the default `fuzz` search found no counterexample. Unproven is not broken, and it is not green either: the JSON's `ok` is false and `unproven` lists each such rule as `{id, rule, seeds_unknown, searched}` (`seeds_unknown`: the individuals the bound could not decide; `searched`: `individuals`, `scripts`, `depth`, `beam`, `grid`, `hours`), and stderr says "unproven, not broken". Exit 1 wins over 3. A CI gate that should accept unproven rules checks for exit 3 explicitly; up to 0.8.1 the same case exited 0 with `ok: true`. **Plan and mode.** A stance level is the joint plan's, so on coupled traits it can differ from the trait's own most likely level (section 4): the 0.5.0 tutor's seed 1, told the learner is upset, planned humour `playful` (odds 0.411) while `light` had 0.425, and valence `up` (0.386) while `even` had 0.457; the two are coupled (`align: 1.0`) and the plan took their high levels together. `lint` also probes every individual of `--seeds` (default 0-99): at rest, a quiet turn, then every declared input alone (numbers at their maximum). `warnings` lists each trait whose planned level differs from its marginal mode on a released, not unsure, probe turn, with the count and the earliest case (`example`). A warning is advice and leaves the exit code alone. The options, none of which changes decoding: a habit pins the level where it matters (what the tutor does now); a `vouch` floor with a `hold` holds the trait when its top odds are low; a host that wants each trait's own most likely level can read it from `odds`. ### 5.6 Testing a character A **character property** is a rule in habit syntax that a stance must never break, for example "never playful when the learner is upset": `{when: {sentiment: negative}, then: {humour: {at_most: light}}}`. It is checked on the stance a host gets (status `ok` or `partial`, released traits; `prev` restrictions read the previous turn's levels), never on the words a model writes. Two commands test it over a population of individuals (`--seeds 0-99`); `--never RULE` gives one rule (JSON or one line of YAML), `--props FILE` several (`{"props": [...]}`, each with an optional `id`). **`fuzz`** searches event scripts built from the persona's declared inputs (flags, levels, numbers on `--grid` x their max, idle hours on `--hours`). The grid (default 0, 0.5, 1) also gets, after the given points and each once, every threshold the rule's `when` and the habits' `when` read on a number input, as a fraction of that input's max (since 0.8.2): a rule in force only at stakes 0.7 to 0.8 is searched at 0.7 and 0.8; `search.grid` in the JSON lists the grid used. The search runs random scripts, then a beam guided by the exact odds (the next events that put the most odds on a level the rule forbids). Each individual's shortest counterexample is shrunk (drop events, then single inputs) and printed with the breaking turn's levels, odds, why and line, and the `replay` / `explain` commands that reproduce it. The search is Philox keyed by `--fuzz-seed` and the seed: the same inputs give the same report, byte for byte, on any number of threads. "None found" is evidence over the scripts searched, not a proof. **`prove`** gives each rule one verdict over the population: - `held by construction`: habits that are in force whenever the rule applies (their conditions follow from its own) imply it, for every previous level. With `on_conflict: yield` a habit counts here when no turn where the rule applies ever drops it (a scan of every cell replays the conflicts; which habit yields depends on the habits in force and the previous levels, not on genes or moods). - `proved for every event sequence`: for each individual, a bound covers every turn of every script and decides all of them. The mood accumulators stay in a box (per level, between the most negative and the most positive evidence one turn's inputs and history can add, 0 included), whatever the inputs, idle hours or `--no-inertia`. Learned weights (section 2.8) of traits linked to the rule's trait stay in their own box, ±total_cap per level, whatever the feedback. The rest of a turn falls into finitely many cells: flags and levels, each number at every threshold a habit or the rule reads and on each interval between them, history values that fit the turn's own flags, and the previous levels the restrictions in force read. In every cell the best stance the rule allows must beat the best one it forbids by more than the box, the number intervals and the 6-decimal rounding can move their scores. Moods linked to the rule's trait by no coupling or rule cannot move it and are left out. Held traits and habit conflicts are replayed as the turn resolves them. - `unknown`: the bound could not decide some individual. The report names the earliest such cell (inputs, history, previous levels) and the `fuzz` command that searches it. Unknown is not broken: run `fuzz`. Example: the tutor as it shipped in 0.5.0 (kept byte for byte as `probbit-cli/tests/fixtures/persona/tutor-0.5.0.yaml`) breaks "never playful when the learner is upset" for 67 of seeds 0-99: 37 on the single message `{"sentiment": "negative"}`, 27 when that message also carries praise, 3 on two messages; `prove` says unknown. 0.6.0's tutor adds one habit, `no_play_when_upset` (`when: {sentiment: negative}`, `then: {humour: {at_most: light}}`): `prove` says held by construction and `fuzz` finds nothing. The habit fixes the stance the host gets; whether a model writes jokes anyway is the model's (section 6). Limits. Both commands cover the stance, not the model's words (section 6). `prove` reads exact engine answers: a cell whose program leaves the exact tiers is `unknown`. `held by construction` relies on every plan the engine returns keeping every rule in force. A rule with raw `rules` (not `then`) is fuzzed but not proved. The bound is per cell with the moods at the edges of their box, so a soft rule whose margin is small stays `unknown` even when no script breaks it. **With a drives block** (section 2.9). A rule may name `pursue` in `then` and, in `when`, the goal conditions the persona's habits use (the same goal, signal and range). `fuzz` adds goal signals to its events: per goal a win on `--grid` x `win_max`, a cue, a deadline on each `--hours` value. `prove` reads the drives as boxes: the aggregates as number inputs over their whole range (wanting and afterglow 0 to their caps, the prediction error 0 to `pe_cap` each way), the goal conditions both ways, and, when `pursue` is in the rule's component, its drive terms (wanting, afterglow, deadline urgency, the floor lift at its most over that box) widen the bound. A goal floor is reported held by construction, with the habits that can exclude it (`floors` in the JSON). If a checked individual's goal-field bounds (including couplings) leave the normal exponential range, its floors are reported `unknown`: the log-space lift still runs, but the structural proof does not certify floating-point rounding over every turn in that range. On the drives fixture of the tests: "a chore due within 24 h -> pursue chores" is held by construction (its habit); "`since_pursued.chores` >= 12 -> pursue chores" is unknown, rightly, since `starve_safety` (priority 1) wins over `starve_chores` (priority 0) on a turn where both are due. **`lint --props FILE`** (or `--never RULE`) runs both for CI: `prove` on every rule, then `fuzz` (the default search) on the ones it leaves unknown; the lint document gets `props` (each rule's `prove` entry, with a `fuzz` entry when it was unknown) and `broken` and `unproven`; exit 1 when a contradiction is unresolved or a rule breaks, exit 3 when a rule stays unknown and the search finds no counterexample (section 5.5). Exit codes: `fuzz` 0 nothing found, 1 a counterexample; `prove` 0 every rule held or proved, 1 some rule unknown; both 2 bad input. JSON: `--json` (`probbit_persona_fuzz: 1`, `probbit_persona_prove: 1`); timing goes to stderr. **Source-restricted fixtures.** For a persona with `reward_from`, exported abstract counterexamples have no runnable `replay` or `explain` command unless a synthetic source is explicitly authorized with `--fixture-src env:synthetic` (or `human:synthetic`, where allowed). The exporter strictly replays that labelled script; it rejects incompatible source rules, records `fixture_provenance`, and never bypasses the production source check. With a senses block the labels cover readings too (a bare reading takes the event's synthetic `src`). **Senses in fuzz and prove** (section 2.11). `fuzz` adds readings to its events (nested in `senses`): flags both ways, numbers on `--grid` × the range, one reading repeated 1, 5 and 20 times, and the range ends alternating. `prove` reads each `sense__up/down` input as a box [0, zcap] × gain and each sense condition both ways, so a habit on `sense..raw` holds by construction for a rule with the same condition. A rule reads the sense conditions the persona's habits use. **Surprise and pace in fuzz and prove** (sections 2.13, 2.14). With a pace block `fuzz` adds reward bursts: each goal's top win and the reward flag, at elapsed 0, repeated 1, 5 and 20 times (the average runs to its cap); with a surprise block, feedback alternating between the reward and the correction flag over 20 events (the gate opens). The alphabet never carries `pace_up` or `pace_down` (the engine sets them). `prove` reads them as boxes [0, cap − r0] and [0, r0] times g_pace in their traits' and moods' boxes; the learned box is unchanged by `surprise` (g scales the step, never `total_cap`). **Needs in fuzz and prove** (section 2.12). `fuzz` adds amounts to its events (nested in `needs`) on `--grid` × (0, cap] per need, each amount repeated 1, 5 and 20 times (satisfactions past the deficit overshoot), and each window's length as idle hours over 6 events (a pursued goal's window expires); the hours alphabet gains the windows' lengths. An event that pays a need and its bound goal's own win at once is refused (N-E3) and skipped. `prove` reads each `need_` input as a box [-pursue.cap, +pursue.cap] on its bound goal, `drv_expect` as [0, win_max] and `drv_omit` as [0, pe_cap] in their moods' and traits' boxes, and each `need..deficit` condition both ways, so a habit on `need..deficit` holds by construction for a rule with the same condition (`examples/persona/body-props.json`). **Payments in fuzz** (section 2.8, with a credit block). `fuzz` adds `await` to its events and payments resolved against the state each event runs on: `for` the oldest open decision (late), the newest (reordered), the previous stance, a past turn no longer open (a duplicate, or a payment after a cancel or an expiry), a turn still to come (unknown), or an open decision bound to another stance's digest; `cancel` the oldest open decision or a turn still to come. A refused payment changes nothing, so the search goes on from the same state, and a reported counterexample's script holds only the events that ran, with every payment as a concrete turn, so it replays as printed. A persona without the block is searched exactly as before. The Python `persona_fuzz` and MCP fuzz tool accept `fixture_src` too. See the [host regression example](../examples/agent-harness/README.md). Synthetic observations are not production evidence. **Output formatting.** Single-document persona commands accept `--pretty` for stdout: init, turn, diff, lint, check, compile, describe, fuzz and prove (for fuzz/prove it implies `--json`). Written states and programs remain canonical; replay remains JSONL and explain remains text, so those commands reject `--pretty` with guidance. The `probbit_persona_turn` field is the document schema version, not its zero-based `turn` counter. ### 5.7 Live: a resident individual `probbit live` keeps one individual running: JSONL events in (one object of inputs per line, from `--events FILE` or stdin), one stance per event out (canonical JSON, the document of section 4). It adds three things to `persona turn`: a clock, learning from feedback between events (with a learning block, section 2.8), and a **strand**, a log the whole life replays from. ```sh probbit live examples/persona/tutor.yaml --seed 2 --strand pip.strand < events.jsonl # one stance per event; pip.strand logs it probbit live verify pip.strand # ok, or the earliest line that differs probbit live examples/persona/tutor.yaml --seed 2 --demo week --plain # a scripted week (below) ``` **Time.** With `--clock real` (the default) a monotonic clock starts with the run and stamps each event's `elapsed_hours`, the time since the previous event (for the opening event, since the start), quantised to 1e-6 h (3.6 ms): the value the turn uses is the value the strand logs. Under the real clock an event that carries its own `elapsed_hours` is refused. With `--clock fixed` each event carries its own `elapsed_hours` (default 0), so a run is a pure function of its events (tests, demos, replays). **What decays.** Moods, and with a drives block the drives (wanting, afterglow and expectation by their own half-lives, deadlines counting down: section 2.9), nothing else. Between two events each mood accumulator is multiplied by 0.5 ^ (elapsed_hours / half_life_hours) (section 3, step 3), so quiet hours bring a mood back to the individual's resting level. A test holds it to the closed form: an event, then quiet events after gaps g_1..g_n, leaves k^n x 0.5^((g_1 + ... + g_n) / half_life_hours) x a_1, to the 6 decimals the state keeps. **Goals.** An event of a persona with a drives block carries its goal signals under `goals` (section 2.9); the strand logs the inputs as given, `goals` included, and `verify` replays them. **What learns.** With a learning block, a turn whose reward or correction flag is on moves the learned weights Δ of the levels the previous stance took (section 2.8), never past ±total_cap per level. Without a block nothing learns, and every stance and state is the one `persona turn` gives. **What never changes.** Genes, priors, couplings, input effects, history features, habits and every trait's allowed levels. The learner's whole output is the Δ table, and habits are rules of every compiled program (section 3, step 6), so every stance obeys every habit in force whatever Δ is: rule immunity is architectural, not trained or measured into it, and `prove` (section 5.6) includes the learned box in its bound. Measured all the same (BENCHMARKS.md §8): an adversary that praises every joke and criticises every joke-free stance, the ones a failure forced included, over 10,000 turns of the tutor on each of seeds 0-9, broke no rule; on all 33,330 failure turns humour was `none`, while the learned humour weights sat at their cap from turn 8-26 on. **How far.** Learning saturates at its box: 200 and 2,000 feedback turns leave an individual at the same distance from its initial self, so `total_cap`, not the time in use, sets how far use can move an individual. On the tutor (learners seeds 0-19, compared with the 100 initial individuals of seeds 0-99 by the distance of section 5.4), total_cap 0.25 left every learner nearer its own initial self than any sibling; total_cap 1, more than the genes' spread on those traits (0.5-0.6), left 9 of 20 with a sibling as near or nearer. Keep total_cap well under the learned traits' spread when an individual must stay recognisably itself. Feedback credits every learned trait with the level it took, so a reward aimed at one trait also pushes the levels the others took (in the demo below, praise for short answers also raised the light joke the tutor was making): learn the traits the feedback judges. **The strand.** `--strand FILE` logs the life. Line 1 is the header, compact JSON: `{"probbit_strand":1,"engine":"probbit 0.13.0","persona":{"name","version","digest"},"seed":N,"state":{...},"document":{...}}`, with the initial state as canonical JSON (one individual has one header, whether it comes from `init` or from a state file) and the persona document in its own key order (the compiled program, and `engine.program` with it, follows the document's key order). Then one line per event, canonical JSON: `{"inputs":{...,"elapsed_hours":h},"n":k,"prev":"sha256:...","stance":"sha256:...", "state":"sha256:..."}`, that is the inputs as the turn used them (the stamped hours included), the sha256 of the line before (the header, for event 1), the sha256 of the stance's canonical JSON and the new state's digest (with `display.glyphs`, section 2.18, the line also carries `"glyph"`, which `verify` recomputes). Lines end in `\n`. A strand is never rewritten, just appended to: `--strand` on an existing strand continues it from `--state`, which must be the state after its last line (that line carries its digest) of a persona with the header's digest, and the run uses the header's document. A life continued over several runs is the strand of one run, byte for byte. **Verify.** `probbit live verify STRAND` reads nothing but the strand. It rebuilds the persona from the header's document (its digest must match), checks the initial state as any state is checked (section 5.1), rebuilds the header, replays every event on the fixed clock with its logged hours and compares, line by line, `prev`, `n`, the stance digest, the state digest and the bytes. It prints `{"ok":true,"events","persona","seed","engine","verified_by","final_state","last_line"}` and exits 0, or prints `{"ok":false,"line":n,"diverges":"..."}` for the earliest line that differs and exits 1: a changed input diverges at its own line (its stance or state no longer replays), a removed or reordered line at the next line's `prev`, a changed header at line 1. A copy with `\r\n` line endings (Windows text mode, an editor) verifies and can be continued: the hashes cover each line's text without its line ending, and a canonical JSON line never ends in `\r`. Verify replays with the binary that runs it; the header's `engine` names the version that wrote the strand (a development build writes its `-dev` version, so its strands are told from a release's), and `verified_by` (since 0.12.1) the version that replayed it, so a strand begun before an upgrade verifies under the new engine. Measured (BENCHMARKS.md §8, Apple M4): a 10,000-event strand of the tutor is 3,055,367 bytes and verifies in 3.3 s, about 3,000 events per second. **Unsupported is not diverges.** A newer engine's strand can carry what this engine does not know: a header key, a persona block or field (`senses` and `learning.credit` for 0.8.2; RFC 0002 part 2's `load` for this one), a state field, a line kind (a `rest` line) or a field on a known line. `verify` stops at the earliest such line, prints `{"engine":"","line":n,"ok":false,"strand_engine":"","unsupported":""}`, says `probbit: live verify: line n: unsupported by this engine: (written by ; this engine is ): not verified, and not a divergence` on stderr, and exits 5. Nothing from that line on was replayed: the strand is not verified, and not shown to differ either; verify it with the engine that wrote it. The field is named as the persona validator names it (`needs`, `learning.credit`), as `state.` in a state, and by its path in the line otherwise (`rest`, `inputs.senses.tool_fail.z`). What this engine knows is still checked: a value it does not accept in a field it knows diverges (a changed input, a changed header value), and the chain is checked past an unsupported line, so an edited, removed or reordered line diverges at the earliest line whose `prev` shows it (exit 1); `unsupported` is left only for a strand whose chain is whole. An edit that adds a field this engine does not know, with the chain rebuilt after it, reads as unsupported: exit 5 is not a pass either. A newer engine that computes a known field differently (another stance or state digest) cannot be told from an edit: such a strand diverges at the earliest line where it differs, and the header's `engine` names the engine to verify it with. The released 0.8.2 binary predates this: on a 0.9.0 strand it prints `{"diverges":"the persona document: senses: unknown field (allowed: ...)","line":1,"ok":false}` and exits 1 (`probbit-cli/tests/fixtures/strands/` keeps that output next to the strand). MCP's `probbit_live_verify` and Python's `live_verify` return the same document: like a divergence, it is an answer, not an error. `probbit check` counts an unsupported strand as one that fails the gate (exit 1) and prints `line n: unsupported by this engine: (written by )` for it, never FAIL; its `--json` entry adds `unsupported` and `strand_engine`. **Following a file.** `--events FILE --watch` follows the file as lines are appended (as `tail -f`): a line counts once its newline is written, and the run stops when the file is removed (tested on Linux and macOS). **The demo.** `probbit live PERSONA --demo week [--seed N] [--strand FILE] [--plain]` runs one individual through a scripted week on the fixed clock, events 1 h apart from 09:00 to 15:00 and quiet nights until 09:00. Campaign 1 praises every answer shorter than the longest level and criticises the longest until the learned verbosity weights reach their cap (day 3 at the latest); an hour later comes an upset (a failure, with a negative sentiment when the persona has one), then the night and a morning without feedback; campaign 2 praises every joke to the end of day 7, with a failure every third hour. A persona without a learning block gets the demo's, `{"from":["praise","criticism"],"traits":["verbosity","humour"],"rate":0.5,"step_cap":0.2,"total_cap":1}` (its opening line says so, and the strand records the document used). The script reads nothing but the stances, so a run is a pure function of persona, seed and version. On a colour terminal it draws bars on stderr, 1 s per hour with each night fast-forwarded in 2 s (about 55 s in all); otherwise, or with `--plain`, it prints one line per event on stdout without waiting. The tutor, seed 2, 0.7.0: 50 events over 150 h of clock; the learned verbosity bar at its cap on day 1 at 15:00 (event 7); valence `down` at the upset and `up`, this individual's resting level, after 17 quiet hours; P(joke) on the turns without a failure, mean per day, 0.57 on day 1, 0.78 on day 2 and 0.89 on each of days 3-7, with humour's learned weights ending at [-1, +1, -0.46] for none / light / playful; 13 failure turns, a joke on none of them; 0 rule breaks; a 21,361-byte strand that `live verify` replays. **One writer per strand.** `--strand FILE` takes the writer lock `FILE.lock` before it reads the strand and holds it to the end of the run: the lock file is created exclusively (written aside and hard-linked into place, so it is never seen half written) and holds the writer's process id and start time. A second writer finds it and exits 4 with `{"error":{"code":"locked",...}}` naming the holder's pid, and changes nothing. A lock whose process is gone is stale and is taken over; a lock that names no process, or a process id reused by another program, stays held until a person removes it. Before every append the writer checks the lock is still its own, so a lock removed or taken over under a running writer stops it before it writes. Two `live` runs started on one state and strand (the two-writer case of an earlier test round) now give exactly one exit 0 and one exit 4, and the strand verifies. `probbit_live_event` (MCP) and `live control` take the lock for their one append, `--demo week --strand` for its week. **Control lines: pause, resume, retire.** A person (or a sensor) stops an individual in its own log: ```sh probbit live control pip.strand pause --by human:owner --reason "checking the last answers" probbit live control pip.strand resume --by human:owner --reason "checked" probbit live control pip.strand retire --by human:owner --reason "end of the trial" ``` Each appends one control line under the writer lock, canonical JSON chained like the others: `{"at":"2026-10-08T22:00:00Z","by":"human:owner","control":"pause","prev":"sha256:...","reason":"..."}` (`at` is the time it was written, RFC 3339 UTC; `--at TEXT` sets it). `--by` is `human[:id]` or `env[:id]`, never the individual; the reason is 1-500 characters. Pause moves an active individual to paused, resume a paused one back, retire either to retired; nothing follows retire. While paused or retired every event is refused: one `{"error"}` object with code `paused` or `retired` per event, nothing written, exit 4 at the end of the run. **No credit crosses a control line**: the credit of the stance before it is cleared (section 2.8), so feedback after a resume credits no stance from before the pause, and learning cannot tie a reward to an interruption. With a credit block (section 2.8) a control line also closes every open decision: the next stance lists them as closed `control`, and a payment that names one is refused (B-R2), so no outcome is paid across an interruption either. Nothing else moves: moods, drives, learned weights, history and the turn count are the individual's as before, and the clock goes on (with `--clock fixed` the host's hours of the next event include the pause). The individual has no input that names a control line or unsets one: an event's `control` key is an undeclared input like any other, and the stance never mentions the status. Control lines are not an MCP tool: keep `live control` out of the agent's own reach. A run continues a strand that ends in control lines from `--state`, the state after the last event line (the one a run writes), and applies them: the status, and the cleared credit. `verify` replays control lines (a control line must be a valid move; a line after retire diverges) and adds `"controls": N, "status": "active" | "paused" | "retired"` to its summary when a strand has any. A control line is free text where it says why, so a changed reason shows at the next line's `prev`, as a removed line does. Measured on 50 individuals x 1,000 random events with 147 pause / resume pairs inserted at random (property P5): every run equals, stance for stance and state for state, the same events without control lines whose credit is cleared at the same points; 4 of the 50 end with other learned weights than the run without the pairs (the cleared credit's effect, at most 1.2 on one level). After retire, 10,000 random events are all refused and the strand and state do not change (P10). **Checkpoints.** Every K-th event (`--checkpoint-every K`, default 1,000; 0 = none) `live` appends a checkpoint line after the event line: `{"checkpoint":n,"prev":"sha256:...","stance":{...},"state":{...}}`, the event count, that event's stance document and the whole state after it, chained like the others. A full `verify` replays from the header and checks every checkpoint against the replay (its `"checkpoints": N` is in the summary); `verify --from-checkpoint` reads the header, checks the last checkpoint against the event line before it (prev, the stance digest, the state digest, the state read as any state is) and replays only what follows (`"from_checkpoint": n`); the lines before it are trusted, so run a full `verify` to check them. **Senses in live** (section 2.11). An event carries `senses` as `persona turn` does; the strand logs each reading as used, with its source, and `verify` recomputes the normals from the logged readings: its summary adds `"normals": {"": {"mean", "n", "max_move_50"}}`, the normal now and the largest move of its mean within 50 events (the monitor alerts above drift_cap / 2). A persona without the block lists `senses` in `ignored`, as before, and `live` says so once on stderr, at the earliest event that carries it: `probbit: live: events[i]: this persona has no senses block: ...`. The documents and the strand are unchanged. **Surprise, pace and dispositions in live** (sections 2.13 to 2.15). `verify` recomputes the average, each gate's v, r, g and counts and the genes from the logged feedback, wins and stamped hours: its summary adds `"pace": {"rbar", "max"}` (the average now and the largest after any event replayed) and `"surprise": {"": {"g_max_seen"}}` (the largest gain after any event replayed). A life split across runs equals one run, byte for byte: all three live in the state. **Needs in live** (section 2.12). An event carries `needs` as `persona turn` does; the strand logs each amount as given, with its source, and `verify` recomputes the deficits, the success rates and the windows from the logged amounts, readings and stamped hours: its summary adds `"needs": {"": {"deficit", "at_cap_hours"}}`, the deficit now and the stamped hours of the events that found it at its cap (it sat there since the event before; the monitor alerts while it does). A persona without the block lists `needs` in `ignored`, as before, and `live` says so once on stderr, at the earliest event that carries it: `probbit: live: events[i]: this persona has no needs block: ...`. Nothing runs between events, so a later rest line (RFC 0002 part 2) is its own strand line, replayed as a control line is. **Credit in live** (section 2.8). Events carry `await`, `for` and `cancel` as `persona turn` does, and the strand logs them as given; `verify` recomputes the open decisions with everything else and adds `"open": [turns]` (the decisions still awaiting an outcome) to its summary. `live verify STRAND --actions FILE` also reads the host's action log, one `{"turn": T, "action": {"": ""}}` per line, and compares each action with the level its turn's stance released: the summary adds `"actions": {"checked": N, "differs": null}`, or the earliest turn (then line) where the host acted otherwise, as `{"turn", "line", "trait", "action", "released", "why"}` (another level, a trait the stance did not release, or no such turn), and exits 1. A trait the persona does not have is an input error (exit 2); `--actions` replays from the header, so it does not combine with `--from-checkpoint`. A persona without a credit block lists `await`, `for` and `cancel` in `ignored`, as before, and `live` says so once on stderr: `probbit: live: events[i]: this persona has no credit block: ...`. `monitor` starts at the last checkpoint and draws its event at once. A strand that ends in a checkpoint line is continued from the state it carries. Before continuation or a control append, the stored chain and lifecycle are checked from the header: a checkpoint cannot hide a retired status, an invalid transition or a broken preceding hash link. This structural scan costs time proportional to the existing log; it does not recompute earlier inference. Full `verify` is still necessary to check that inference. Hashes alone do not authenticate a writer or detect a wholly rewritten log without a trusted external anchor. Historical measurement before this hardening, on an Apple M4: a 10,000-event strand of a drives persona (4 goals, a floor, learning; 3,126,444 bytes, 10 checkpoint lines) opens in `monitor --once` from its last checkpoint in 0.005 s (load 3.3), where a full `verify` takes 192 s. A replay from a checkpoint costs what the events after it cost: 500 events past the last one took 26 s to draw (load 6-13, about 50 ms an event for this persona), so K bounds the wait; a smaller K costs a few KB per checkpoint line. A strand without checkpoint or control lines (shorter than K events, or `--checkpoint-every 0`) is the same format as in 0.8.0. New strand headers record the current engine version, so their chain hashes differ; existing 0.8.0 strands remain replayable. Binaries before 0.8.0 do not read checkpoint or control lines. Exit codes: `live` 0 when every event got its stance; 2 for a bad persona, state or flag, and after a bad event (each bad event prints one `{"error"}` object on stdout, changes nothing and the run goes on); 4 when the writer lock is held by another writer (nothing read or written) and after events refused by a paused or retired individual. `verify` 0 when every line replays, 1 at a line that differs, 5 at a line with a field or line kind this engine does not know (unsupported, above: not verified, and not a divergence), 2 for a bad flag or an unreadable file. `control` 0 when the line is appended, 2 for a bad field or an invalid move, 4 when the lock is held or the individual is retired. ### 5.8 Monitor: watch an individual's inner state `probbit monitor STRAND` draws an individual's declared state as live terminal bars, or as a compact browser Activity Monitor. It replays the strand with the rules of `live verify` (the two share one replay), recomputing every stance document from the strand alone: it needs no other log, works for any strand on any machine, and says "replay verified" while every line checks, or names the line that differs. ```sh probbit monitor pip.strand --follow # bars in the terminal, redrawn as the strand grows probbit monitor pip.strand --open # a channel table and inspector at http://127.0.0.1:PORT/, in the browser probbit monitor --demo --open # the tutor's week, paced, for someone without a strand yet probbit monitor --demo drives --open # eight synthetic events with goals and drives ``` **What it shows.** The latest event (or a selected historical event in the browser). In the terminal, a header: persona and version, seed, the individual's digest, the event number, the hours since the event before, the replay badge; with a glyph (section 2.18) the glyph after the persona's name and, on the line under it, a GLYPH row with `glyph_why` (in plain ASCII the row keeps the rule and drops the glyph). The stance: one row per trait, its levels as segments sized by their odds, the level taken in colour and the phrase it says. The moods, with a sparkline of the last 50 events. The inputs: the event's inputs as the turn used them (flags lit, numbers as bars, levels by name, the clock) and the history features against their caps. The senses, with a senses block (section 2.11): one row per sense, its normal as a tick over the range, this event's reading, z and n (a sense not read keeps its normal), and an alert when a normal moved over drift_cap / 2 within 50 events. The habits in force, the ones that bound in colour, and the violations counter (0 by construction, section 2.5). The learned deltas per level as centred bars within ±total_cap (with a learning block, section 2.8), and with a credit block a CREDIT row: the open decisions with the eligibility a payment at the next event would get, this event's payment (turn, e, sign, absorbed) and the decisions it closed. With a needs block a NEEDS row per need: its deficit as a bar over [0, cap], its term and its goal's window (hours left or closed), and an alert while a deficit sits at its cap. With a pace block a PACE row: the average as a dot over [0, cap] with an r0 tick, and the level; with a surprise block a GAIN row per learned trait: its gain g (1 when closed). The drives, when a document carries `pursue` or `drives`: documents without them draw no row, so a strand of a later engine lights them up. The stance line, and why. **The panel.** Under the rows, a PANEL block (and in the browser an Instrument panel of dials) gives every field one line of fixed words: `