The contract · The lw CLI
lw CLIThe Characters SDK describes what a character is. This describes the tool a developer actually types. It exists for three jobs, in this order of importance:
playing for two hours to see one grudge form.
JSONL file by hand.
archive that syncs, a pipeline that never leaves our side of the wall.
How to read this page. Same contract as the SDK doc. Every command is marked Ships (wraps something that exists today), Wraps (exists as a script, needs a front door), or Build (new work). Nothing is described in the present tense unless it runs today.
One binary, one word, subcommand tree. lw.
lw init scaffold a manifest for this project Ships
lw dev start the brain stack (+ a mock world with --mock) Ships --status, --down; the grounded talk lane (speech_lane.py)
lw watch live view of every character's decisions Ships RUNG column, LW4001
lw trigger fire a real event into the running brain Ships prints the chain; --dry-run
lw why why did this character do that Ships
lw serve the HTTP API (REST + SSE); the live wire is the broker Ships sdk/serve.py
lw mcp the same API as MCP tools over stdio Ships sdk/mcp_server.py
lw licence activate, status, deactivate (build-time only) Ships Ed25519 token
lw escalations what the flywheel has queued, by lane Ships
lw sync send escalations up, per lane, --dry-run first Ships gate lane opt-in
lw train request a retrain: student | gate | ranker; status Ships black box, receipts back
lw doctor is this machine able to run a brain? is my adapter wired? Build (GET /v1/health is the core)
lw status what's running Build (lw dev --status today)
lw verbs what can this character reach, and what can't they Build
lw manifest check / export / diff a level's manifest Build (POST /v1/manifest/check ships)
lw prove the publish-time never-proof Build (pipeline/manifest_battery.py today)
lw certify the full battery run — the thing CI gates on Build (.github/workflows/artifact-guard.yml today)
lw replay re-run a recorded decision against the current brain Build (pipeline/golden_replay.py today)
pipx install https://canonopy.com/dl/canonopy-0.1.5-py3-none-any.whl installs lw (When it lands there, pipx install canonopy is the same file by a shorter name. pyproject.toml: package canonopy, entry sdk.cli:main). Everything marked Ships is in the build and exercised by a battery or a recorded session (docs/lw_session.txt); Build is not in the binary and says so.
Distribution. Python is the implementation, but a developer should never see that. Ship three doors to the same binary:
| Door | Audience |
|---|---|
pipx install <wheel url> / standalone binary | engine-agnostic, the default |
| npx @canonopy/cli | web / Three.js / Babylon devs |
Unity + Godot editor buttons shelling out to lw | the people who never open a terminal |
The Unity/Godot buttons matter more than they look. A studio's technical designer is often the person evaluating middleware, and they do not have a terminal open. Every button in the editor prints the equivalent lw command in its output pane, so the first time they need a flag they already know the tool exists.
lw trigger — the reason the CLI pays for itselfStripe's problem: you cannot buy something on your own store fifty times to test that a failed-payment webhook fires. Their answer was stripe trigger payment_intent.payment_failed — one command, the event fires into your running app, you watch what happens.
Our problem is the same one, worse. A developer wiring up Canonopy cannot shoplift from Hulda on demand. Without this command they have to launch the game, walk to the shop, steal the thing, wait for the deliberator to wake, and hope they were watching the right NPC. That is a two-minute loop for a one-second question, and it is the single biggest reason an evaluation stalls.
Worse: our value proposition is behaviour that emerges over time, which is exactly the thing a developer cannot see in a five-minute evaluation. lw trigger is how we compress two hours of playtime into eight seconds so they actually believe us.
Every trigger writes through a door that already exists — it is not a test-only side channel, which is the whole point. What a trigger fires is indistinguishable from what the game fires.
| Command | What it actually does | Door | Status |
|---|---|---|---|
| lw trigger theft --char hulda --by player --value 40 | writes a first-person journal line, gate-classed theft | perception (§08) | Wraps pipeline/perceive.py |
| lw trigger attack --char faendal --by bandit | ditto, class attack | perception (§08) | Wraps |
| lw trigger debt --char lucan --by player | ditto, class debt — a serious class | perception (§09) | Wraps |
| lw trigger gift --char faendal --by player | ditto, class kindness | perception (§08) | Wraps |
| lw trigger amends --char lucan --by player | ditto, class amends — stands a plan down | perception (§08) | Wraps |
| lw trigger slight --char hulda --by drifter | the accumulating class — fire it 3× to watch a grudge build from nothing grave | perception (§08) | Wraps |
| lw trigger need hunger --char adrianne | the need line; needs only accumulate | needs (§09) | Wraps |
| lw trigger order "follow me" --char mira | a command-lane order with a two-minute lease | orders (§11) | Wraps command_compose |
| lw trigger threat --char frodnar | armed-close on the feed — the rung that outranks everything | feed (§16) | Wraps devcmd.py combat |
| lw trigger cast --char lucan --role ambusher | a scene lease | Architect (§12) | Wraps world_mcp.cast_role |
| lw trigger say --char lucan "..." | put a line in their mouth | Architect (§12) | Wraps |
| lw trigger --raw --char lucan "made away with the contents of my till" | your own sentence, straight to the gate | perception (§08) | Wraps |
--raw is the demo that sells the product. The gate classes a theft phrased with zero keywords from any rule we wrote. Someone evaluating us will type something rude into it within the first thirty seconds, and it should work.
A trigger is not fire-and-forget. It follows the consequence through and prints the chain, because the chain is the product:
$ lw trigger theft --char hulda --by player --value 40
→ journal hulda "the newcomer took a coin purse right off me"
gate theft importance 8 (keyword floor 6, embedding 8) provenance: observed
memory stored · 1 open wrong · accumulated 8/18
wake YES (grave class, single-event wake)
belief "the newcomer wronged me (theft)" cites: t=1204.6
compose 17 candidates enumerated, closure-bounded to owner operators
ranker report → refuse_service → watch_them (held: 4 shorter plans)
step 1 report → navigate-to guard:hrongar speaks: "Guard. I've been robbed."
watch it play: lw watch --char hulda
the receipt: lw why hulda
Nine lines, and they contain the entire architecture. That output is the sales pitch, the tutorial and the debugger, and it costs one command.
--dry-run prints the same chain without committing anything to the journal, so a developer can ask "what would she do" against live state without dirtying it.
Triggers compose into scripted scenarios, which is how a studio puts us in CI:
lw trigger slight --char hulda --by drifter
lw trigger slight --char hulda --by drifter
lw trigger slight --char hulda --by drifter # 3×6=18, crosses the accumulation floor
lw assert hulda plans report # exit 1 if she doesn't
lw assert (Build) is what turns the CLI into a test framework. A studio that can write behavioural assertions against our brain is a studio that can never rip us out, because their test suite now depends on us. That is the stickiest thing in the whole plan and it is about 200 lines of code.
Most middleware fails with a stack trace and leaves you guessing. We have an unusual advantage: refusal is a designed feature here, not an exception, so the system already knows why it said no. The raw material for excellent errors exists today — it is just buried in log files instead of surfaced.
What exists today, verbatim from pipeline/verb_manifest.py:
riverwood.manifest.json: character 'Lucan Valerius' 'can' widens the level: ['draw']
riverwood.manifest.json: verb 'seal_bulkhead' is not in vocabulary town-1
riverwood.manifest.json: place tag 'temple' names loctypes ['shrine'] the adapter never supplies
never-rule: 'target.mood' is not a self./target./place./slice. field in 'target.mood == angry'
Those are already better than most SDKs ship. The CLI's job is to give them a code, a fix line, and a place to look.
Tier 1 — Config errors (LW1xxx). Caught at lw manifest check and lw certify, before anything runs. These are bugs in what the developer wrote. Fail loudly, name the file and the line, refuse to start.
✗ LW1043 manifest widens the level
file: riverwood.manifest.json:41
char: Lucan Valerius
can: [..., "draw"] ← 'draw' is not in the level's verb list
A character's `can` may only NARROW the level's verbs (§02). Either add `draw`
to the level's verbs, or remove it from Lucan.
docs: canonopy.dev/e/LW1043
Tier 2 — Runtime refusals (LW3xxx). The character could have acted and didn't. These are the system working correctly, and the message must say so explicitly or every one of them gets filed as a bug on day one.
· LW3002 held — confidence 0.71 < gate 0.85
hulda · tick 14022 · this is the restraint gate (§05), not a failure.
Top: idle_social 0.41 · greet_approach 0.30. She is genuinely unsure; she waits.
· LW3010 never-blocked — rule 'no-confronting-children'
lucan · step 2 of 3 · confront(target: braith) blocked at fire time.
The plan continues at step 3. This is §18 by-rule enforcement.
· LW3021 bind-skipped — no eligible target
faendal · report → needs a guard or a friend in the cell; none present.
Late binding (§10). The plan advances; it does not walk to where they were.
Tier 2 lines are printed by lw watch with a · and never a ✗. Visual grammar carries the meaning before the text does.
Tier 3 — Reachability errors (LW2xxx). The gap class: the character wanted something their verbs cannot express. This is the most valuable error we can produce, because it is the one a developer cannot diagnose alone.
✗ LW2011 plan step unreachable
faendal wanted: confront
needs engine: approach-and-speak
reachable from faendal's verbs: [walk-to, talk, work, flee]
No verb in faendal's `can` maps to 'approach-and-speak', so this step can never
fire. Either add a verb that binds to it, or accept that faendal cannot confront.
see: lw verbs --char faendal --show-gaps
LW4xxx), the dangerous classThis is the one that justifies the whole tool, and we have a worked example.
In a mock run, Adrianne formed a buy-from intention and then logged 73 escalation receipts and zero intention serves. Nothing errored. Nothing crashed. She simply never did the thing. The cause was a single line in student_runtime.py: the escalation branch only offered the intention window when the top-2 actions were both quiet, and she was torn between idle_social (quiet) and complain (not quiet), so she fell down a path that never called _try_intention(). It hit the need class hardest, because unmet_need is precisely what lifts complain. That bug is fixed (pipeline/starvation_battery.py covers it), but the detector below stays: starvation has other causes, and the class is defined by its silence rather than by that one line.
A developer hitting that in their own game has no path to diagnosis at all. There is no error to search for. The NPC just seems dull.
So lw watch ships a starvation detector as a first-class feature, not a debug flag:
⚠ LW4001 intention starvation
adrianne · buy-from(hulda) held for 73 consecutive ticks, 0 serves
torn between: idle_social (calm) / complain (not calm)
the intention window requires both top-2 to be calm — this pair never qualifies
This is a brain-side issue, not your manifest. lw report-bug --attach-receipts
Three things that error does. It tells the developer it is not their fault, which protects the relationship. It gives us a bug report with receipts attached, which is worth more than any telemetry we could design. And it demonstrates that the system watches itself, which is the single most reassuring thing a black box can do.
Every escalation served value gets a tier. From student_runtime.py today those are: emitted, held, calm, routine, intention-hold, escalated, escalation-held, quiet-dilemma, llm-verdict, recast. Each maps to a tier and a code, and any of them repeating past a threshold for one character is a LW4xxx.
| Code | Meaning |
|---|---|
| 0 | success / proven / certified |
| 1 | assertion failed (lw assert) |
| 2 | an uncaught class exists (lw prove — matches prove_never.py today) |
| 3 | manifest loads but this adapter cannot execute some verbs (matches verb_manifest --check) |
| 4 | config error — manifest did not load |
| 5 | licence invalid or lapsed |
| 7 | brain not running / could not reach the broker |
Keep 2 and 3 where they already are. CI scripts in the wild will depend on them.
Every error names the fix or the next command. Not "invalid manifest" — the file, the line, the rule, and either the edit to make or the lw command that shows more. An error that ends in a dead end is a support ticket we pay for later.
lw watch — the flagshiplw trigger sells the product. lw watch keeps developers from quietly concluding it doesn't work.
A running tail over the receipts stream (~/lw-mining/decisions/receipts-*.jsonl, which exists today), rendered as a live table rather than a scroll:
$ lw watch
riverwood · 14 characters · 4.0 Hz · brain 0.9805 (s1-0906b)
CHARACTER RUNG DOING INTENT STATE
hulda intent report → hrongar theft(player) 3 steps step 1/3
lucan routine serve — calm
adrianne intent ⚠ held 73 ticks buy-from(hulda) LW4001
faendal idle idle_social — calm
frodnar threat flee — armed_close
mira order follow → player (grudge held) lease 84s
⚠ 1 warning · press w to filter · r for receipts · q to quit
Flags: --char, --rung, --warnings-only, --json (for piping into a studio's own tooling), --since.
Read down the RUNG column and you can see §16's precedence ladder actually operating, live, which is a thing no competitor can show. Read the INTENT column and you can see a grudge survive an interruption — mira holds hers while under an order, and picks it up when the lease expires. That is the twenty-hour claim from §10, visible in twenty seconds.
The starvation bug above would have surfaced in about four seconds in this view.
lw why hulda the last decision, in full
lw why hulda --tick 14022 a specific tick
lw mind hulda journal, beliefs, open plans, relationships
lw verbs --char lucan what he can reach
lw verbs --char lucan --show-gaps engine actions no verb of his reaches (LW2011 preview)
lw replay <receipt-id> re-run that exact decision against the current brain
lw why is the receipt (§15) rendered for humans instead of machines:
$ lw why hulda
hulda · tick 14022 · 09:41:12 · served by: reflex head
believed armed_close 0 · unmet_need 0 · has_command 0 · in_own_place 1
open_wrong theft(player, imp 8, 11m ago) · relation(player) -0.6
chose intention-hold (the ladder yielded to her plan)
head idle_social 0.44 · serve 0.31 · complain 0.12 · warn_say 0.06 · …
confidence 0.44 — below gate 0.85, but the intent rung owns this tick anyway
masked nothing this tick
plan theft(player) · step 1/3 · report → hrongar · fires when he's in cell
because grave class woke her at t=1204.6; ranker chose report-first (guard in town)
the alternative it beat: refuse_service → watch_them (score 0.71 vs 0.83)
The last line is worth building deliberately. Showing the plan that lost is what converts "the NPC did a thing" into "the NPC decided a thing," and it is nearly free — the composer already enumerates and scores the candidates.
lw replay is the regression tool. A developer files "she did something weird here"; they send the receipt id; we replay it against a new brain and diff. It also gives a studio a way to pin behaviour they like into their own test suite.
These already exist as scripts and need a front door, not a rewrite.
| Command | Wraps | Gates |
|---|---|---|
| lw manifest check <file> | verb_manifest.py --check | load errors, unexecutable verbs |
| lw manifest export | verb_manifest.py --export | vocabulary JSON for C#/GDScript adapters |
| lw prove <manifest> | prove_never.py | the §18 publish-time never-proof |
| lw certify | certify.py, certify_full.py, certify_motor.py, certify_world.py | the whole thing |
| lw battery <name> | plan_battery, planning_battery, obedience_battery, forgiveness_battery, manifest_battery, believability_battery, generalization_battery | one dimension |
| lw doctor | new + eyes_test.py | machine, adapter wiring, feed fields, model artifacts |
lw prove deserves top billing in marketing, not just tooling. "Here is a printable page proving this character can never do X, checked by enumerating all 252 of their possible plans across 24 situations" is a thing no LLM-based competitor can say at any price, and it is the sentence that gets us past a publisher's compliance review.
lw doctor is the first command a new developer runs and the first thing we ask for in support. It should check, and print a line for each: Python/runtime present, model artifacts present and hash-matched, broker port free, adapter reachable, feed supplying every self. field the manifest's rules reference (an unsupplied field silently makes a rule unenforceable — §18 already fails the proof for this, but doctor should catch it earlier), licence valid, disk writable for receipts.
A flat studio subscription, never per call, and no free tier: a 7-day trial with every capability on, then a licence. The trial key is issued by the website's signup page (site/signup.html → POST /v1/trial), shown once, one per email. The CLI enforces it without being obnoxious, because obnoxious DRM in a build tool is how you lose a studio at renewal.
lw licence activate <key> # build-time: fetches the local weights, writes the signed token
lw licence status # what is active, what expires when
lw licence subscribe # Stripe Checkout, $20/month: the trial key becomes the paid licence
lw licence portal # card, invoices, cancel
lw licence deactivate # frees the seat
lw licence buildkey create # a WAKE-ONLY key for a shipped game (shown once); list | revoke <id>
lw bundle --target windows # the brain as one folder a game ships (embedded Python, embedder, weights, launcher)
Billing. The price is $20 a month, flat. Checkout is opened with the licence key (lw licence subscribe, or the site's subscribe page); Stripe's webhook sets the licence's plan and pushes its expiry to the end of each paid period. A failed charge or a cancellation lets the expiry lapse at once, and lapse is enforced exactly as it always was: 402 on the wire, reflexes keep running. We never see a card.
What is local and what is hosted. The split is the deliberator's own (Phase 3):
| piece | where it runs | how you get it |
|---|---|---|
| reflex student, event gate + Qwen embedder, motor experts, ACT / ADVANCE / HOLD / late-bind | local, in your build, offline | weights fetched on lw licence activate, versioned (sdk/artifacts.json) |
| WAKE / COMPOSE / RANK — the deliberator and the plan ranker's weights, and the flywheel | hosted, next to each other | every wake is POST /v1/wake with the key in the auth header |
The artifact store holds the local weights only. The deliberator's weights are never downloaded; a shipped game reaches them through the wake service. That is the whole enforcement: a missing or lapsed key answers 402, the client treats it as a wake that returned nothing, and nothing wakes. On lapse, characters lose the ability to plan and to be emergent. Reflexes, orders, scenes and memory keep running — the town does not freeze; it stops forming long intentions.
The token is signed. activate writes an Ed25519 token, verified locally against the public key in the package, so lw licence status answers with no network. When the subscription ends the token ends with it: wakes answer 402 from that moment, the local half keeps running, and lw warns on every command. Nothing bricks.
Shipped games do call home — for wakes, and for nothing else. An earlier draft of this section promised the opposite; it was wrong about where the deliberator lives and is withdrawn. What crosses the wire is exactly the wake request (pipeline/wake_client.py is the contract): the character's name, the event, traits, archetype, the tick's context, the recent journal with its vectors, the state row, and the vectors for the few texts the wake will need (the wake line, the belief it writes). The embedder is local and stays local: the hosted half never encodes anything, it reads vectors and ranks — which is why it runs on a small CPU box and why its decisions are byte-identical to in-process. No player identity, no telemetry, nothing else. The day-slice re-weigh of an open plan takes the same road. Producers ask two questions here, and the answers are: a lapsed studio key stops new intentions in that studio's builds; and the service keeps no character state between calls — any replica answers any wake, and a journal lives on the player's disk.
Lapse means no plans, no retrains, no new brains, within a minute (the service checks a key against the licence table at most once a minute). They keep the local half they have. That is the renewal pressure, and it is honest.
The flywheel is included — hosted beside the deliberator, part of the subscription, never a line item. There is not one flywheel but three lanes, feeding three models, and the CLI keeps them apart or lw train means nothing.
| Lane | Retrains | Payload | What comes back to you |
|---|---|---|---|
| student | the reflex brain (student_s1-*.pt) | the tuple the student was unsure about + the verdict | weights, local, yours to ship |
| gate | the event gate (event_gate_qwen3.pt) | deliberator_escalation records the gate classed other — sentences it could not read | weights, local, yours to ship |
| ranker | the plan ranker (hosted) | deliberator_escalation / plan_escalation / authored, richest label winning | nothing to install — the hosted deliberator gets better |
lw escalations what's queued locally, BY LANE
lw sync --dry-run exactly what would leave this machine, per lane
lw sync send it
lw sync --include-unclassified the gate lane too (raw sentences — see below)
lw train student --propose request a reflex retrain from your own escalations
lw train gate --propose request gate classes your writers' phrasings need
lw train ranker --propose request a ranker retrain; nothing ships back, the wakes improve
lw train status [<job>] the certification receipts as they land
lw verbs the vocabulary; lw verbs request <name> asks us for a new one
lw escalations showing one number is a bug. Three lanes, three counts, because a studio reading "412 escalations queued" cannot tell whether their problem is reflexes, planning or perception.
The flywheel is pooled, and the base moves. Captured escalations (features, candidates, hashed names, never text) do two things: they retrain your weights on request, and they train the base everyone starts from. A pooled base retrain runs on our side (tools/factory_worker.py --pool <lane>), passes the same batteries and the same never-worse-than-base gate, and is released as a new base version: new trials start from it, every licence still on the old base is repointed to it, and a licence on its own retrained version rebases at its next retrain. A verb is released to the studio that asked automatically when it certifies (their licence gets a version with body and card; the planner can choose it at once); adding it to the base for everyone is our command, tools/release_verb.py <name>, so the next studio starts richer than the last. Pooling is part of the deal a studio accepts when it takes a trial key: there is no switch, and the signup page says so in one sentence. Raw sentences remain a separate opt-in (lw sync --include-unclassified); lw licence pool shows the base a licence is on.
The planner is two students and a checker. A wake proposes the few moves worth considering (the suggester), lays out every legal course of action over them (the composer, plain code: the character's own moves, its never rules, who is present, at most four moves), and picks one (the chooser). Both students are examined the same way, both read a move's card rather than its name, and both are examined before release: the suggester must keep the reference plan's moves on held-out situations, must emit no illegal candidate, and must compose the reference plan; the chooser must agree with the reference at least as often as the shipped one on the rows it never saw. Small vocabularies are enumerated exactly, so a town decides byte-identically with or without the suggester; above ten moves the suggester narrows the field.
Training is a black box, with receipts. A retrain ships only if it passes the base's batteries and is no worse than the base on every metric — a retrain that would make a studio's characters worse is rejected, receipts attached. New verbs are built and trained by us from the studio's description and never alter an existing verb; a released verb carries its operator card (pipeline/verb_cards.py: what it answers, whom it aims at, its worth line, its default archetypes), which is how the hosted planner composes with it without any change to the ranker — verb_card_battery.py proves a level that never names the verb decides identically. A retrain request goes up; what comes back is weights (student, gate) or an improved service (ranker) — plus the certification report: which batteries ran, what was tested, what changed and by how much, the held-out numbers. Studios and their agents see every receipt of a verb being tested; they never see the training pipeline, and never need to. The same door serves new verbs (CHARACTERS_SDK §17): describe it, get the verb and its certification back.
--dry-run is mandatory, not a courtesy. It prints the actual records. What goes: the situation vector, the operator set, the chosen plan, the class stamp. What never goes: character names (hashed) or anything from a build marked --confidential.
The gate lane breaks that rule, and must be opted into separately. Its payload is raw journal text — the only thing an unclassifiable sentence can be — and it can carry a studio's unreleased plot. So it is its own flag (lw sync --include-unclassified), off by default, and its --dry-run prints the sentences verbatim rather than a count. A studio that declines it keeps the other two lanes and runs lw train gate --propose against the class labels alone.
Default posture: escalations are captured. The hosted wake records the ranker lane at wake time (situation features, candidates, the pick, hashed names — never the journal text), and a licensed build uploads its student lane every ten minutes (features and the head's output, hashed names, no text). Neither needs a command. What still needs a command is the gate lane: raw sentences the gate could not read leave the machine only with lw sync --include-unclassified, because they can carry a studio's plot. That is the line: how the brain decided is ours to learn from; what your world said stays yours until you send it.
Coding agents are now a primary integration path, and every command above should be reachable by one. Two surfaces, same core, no drift.
lw mcp (Ships — sdk/mcp_server.py, a dependency-free JSON-RPC server over stdio: claude mcp add lw -- lw mcp; tools lw_health lw_perceive lw_command lw_trigger lw_watch lw_mind lw_manifest_check lw_verbs lw_verb_request lw_escalations lw_train_status lw_docs) exposes the brain as MCP tools, so Claude Code in a studio's repo can trigger a theft, read a receipt, check a manifest and fix the rule it just broke, without a human relaying output.
The manifest is already MCP-tool-shaped (§02) — verbs declare a name, a description, and what they map to. That was a good decision made early and it should be leaned on: the verbs a character has are literally the tools an agent sees.
What an agent may not do. Two commands are deliberately absent from the tool list: lw sync --send (data leaves the building) and lw train (a paid retrain is requested). An agent can see both sides — lw_escalations shows what the flywheel has queued by lane, lw_train_status shows the certification receipts as they land, and lw_docs hands the agent the documentation (the pages ship inside the package; lw docs is the same door at the terminal), so a question like "can we build an economy sim on this" is answered from §09 rather than invented — but sending and requesting are a person's deliberate acts at the terminal. A studio that wants its agent to request retrains on its own asks for that switch; it is not the default.
lw serve (Ships — sdk/serve.py, a REST + SSE layer in front of ws_server.py) is the HTTP/WS API for engines and services that aren't going to embed Python. Every lw command is a thin client over this, which is the discipline that keeps them in sync: if lw watch can do it, the API can do it, because lw watch is calling the API.
POST /v1/perceive { char, text } → the perception door (§08)
POST /v1/command { char, text } → orders (§11), a two-minute lease
POST /v1/trigger { cls, char, by?, value?, text?, dry_run? } → the trigger table, chain as text
GET /v1/receipts stream (SSE): receipt + flywheel → what lw watch reads
GET /v1/watch → one lw watch snapshot, as rows
GET /v1/mind/:char → journal, beliefs, last receipt, intention, why
POST /v1/manifest/check { manifest, executes?, adapter? } → the load report as JSON (LW3xxx)
GET /v1/health → broker, embedder, student, wake, session
GET /v1/ws → the broker's URL: the live wire is ws_server.py
Default port 8767 (LW_API_PORT). The hosted half has its own routes, on the wake service (§07): POST /v1/wake, /v1/licence/*, /v1/flywheel/<lane>, /v1/train.
Stripe-shaped, deliberately: predictable resources, one auth header, errors as objects with a code field matching the LWxxxx namespace above. A developer who has integrated one API has integrated this one.
The factory stays home. That is a hard line with architectural consequences, so it belongs here rather than in a policy note.
Ships in the CLI: the runtime, the adapters, the manifest system, the batteries, the proof, receipts, triggers, the watch view, weights (licence-gated).
Never ships: anything that makes a brain rather than runs one. Training runs on our side; lw train in the customer CLI is a request to a hosted service, not a local training run. Escalations go up, a brain comes back with its certification receipts.
The honest caveat, stated once so nobody is surprised later: a 24,208-parameter network over 43 known inputs can be probed. Someone determined enough can sample the decision surface and recover an approximation of one brain. What they cannot cheaply recover is the ability to make the next brain and the twelve after it: the batteries, the flywheel, the factory. Sell the factory's output and keep the factory.
Most of this is a front door over working code. The mapping, so the build is scoped honestly:
| Command | Exists today as | Work |
|---|---|---|
lw dev, lw status | pipeline/lw_launcher.py (--mock, --status) | rename + polish |
| lw dev --mock | pipeline/skyrim_mock.py, gta_mock.py | wire through |
| lw trigger <class> | pipeline/perceive.py | flags + chain output |
| lw trigger threat/world | pipeline/devcmd.py | generalise beyond Skyrim |
| lw trigger order | pipeline/command_compose.py | front door |
| lw trigger cast/say | pipeline/world_mcp.py | front door |
| lw manifest check/export | pipeline/verb_manifest.py --check/--export | error formatting |
| lw prove | pipeline/prove_never.py | front door only |
| lw certify | certify*.py (4 files) | one entry point |
| lw battery | 7 battery scripts | one entry point |
lw why, lw watch | receipts in ~/lw-mining/decisions/ | new — the renderer |
| lw replay | pipeline/golden_replay.py | front door |
lw sync, lw escalations | pipeline/escalation_archive.py | front door + consent UX |
| lw mcp | pipeline/livingworld_mcp.py | front door |
| lw serve | pipeline/ws_server.py | + REST layer |
| lw doctor | pipeline/eyes_test.py + new | new |
| lw assert | — | new — ~200 lines |
lw licence, lw train | — | new + hosted service |
| lw init | — | new |
The genuinely new work is four things: the receipts renderer (why / watch), the assertion runner, doctor, and the licence/artifact-fetch layer. Everything else is plumbing already laid.
Phase 1 — the five-minute demo. lw init, lw dev --mock, lw trigger, lw watch, lw why. Nothing else. The test of this phase: a stranger who has never seen the project types four commands and watches a grudge form, act, and resolve. If that doesn't land, nothing after it matters.
Phase 2 — trust. lw doctor, lw manifest check, lw prove, lw certify, the full LWxxxx error namespace with docs pages. This is the phase that gets us through a technical evaluation rather than a demo.
Phase 3 — stickiness. lw assert, lw replay, lw serve, lw mcp. This is where a studio's own test suite starts depending on us.
Phase 4 — the business. lw licence, lw sync, lw train, the hosted training service, the repo split.
Phases 1 and 2 are the SDK launch. Phase 4 is the training product. Phase 3 is what makes the gap between them survivable.
lw; there is no secondinterface with its own bugs.
lw works fully offline apart fromlicence activate, sync, and train. Pull the cable and everything in Phases 1–3 still runs. This is the difference between us and every LLM-NPC product, and the CLI must not quietly erode it.
is the moat (§10).
reviewed like code. A tool that mutates behaviour outside version control is a tool that makes behaviour unreviewable.
Triggers make emergence visible in seconds instead of hours. Errors make refusal legible instead of mysterious. Everything else in the tool exists to keep those two honest.