The contract · The lw CLI

The lw CLI

The 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:

  1. Prove the thing works in five minutes, on their machine, in their game, without

playing for two hours to see one grudge form.

  1. Answer "why did it just do that?" in seconds, from a terminal, without reading a

JSONL file by hand.

  1. Keep the licence and the flywheel honest — a subscription that lapses, a receipt

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.


01 Shape

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:

DoorAudience
pipx install <wheel url> / standalone binaryengine-agnostic, the default
npx @canonopy/cliweb / Three.js / Babylon devs
Unity + Godot editor buttons shelling out to lwthe 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.


02 lw trigger — the reason the CLI pays for itself

Stripe'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.

The trigger table

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.

CommandWhat it actually doesDoorStatus
lw trigger theft --char hulda --by player --value 40writes a first-person journal line, gate-classed theftperception (§⁠08)Wraps pipeline/perceive.py
lw trigger attack --char faendal --by banditditto, class attackperception (§⁠08)Wraps
lw trigger debt --char lucan --by playerditto, class debt — a serious classperception (§⁠09)Wraps
lw trigger gift --char faendal --by playerditto, class kindnessperception (§⁠08)Wraps
lw trigger amends --char lucan --by playerditto, class amends — stands a plan downperception (§⁠08)Wraps
lw trigger slight --char hulda --by drifterthe accumulating class — fire it 3× to watch a grudge build from nothing graveperception (§⁠08)Wraps
lw trigger need hunger --char adriannethe need line; needs only accumulateneeds (§⁠09)Wraps
lw trigger order "follow me" --char miraa command-lane order with a two-minute leaseorders (§⁠11)Wraps command_compose
lw trigger threat --char frodnararmed-close on the feed — the rung that outranks everythingfeed (§⁠16)Wraps devcmd.py combat
lw trigger cast --char lucan --role ambushera scene leaseArchitect (§⁠12)Wraps world_mcp.cast_role
lw trigger say --char lucan "..."put a line in their mouthArchitect (§⁠12)Wraps
lw trigger --raw --char lucan "made away with the contents of my till"your own sentence, straight to the gateperception (§⁠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.

What a trigger prints

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.

Composition

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.


03 Errors

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.

Three tiers, and they must not be confused

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

Tier 4 — Silent starvation (LW4xxx), the dangerous class

This 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.

Exit codes

CodeMeaning
0success / proven / certified
1assertion failed (lw assert)
2an uncaught class exists (lw prove — matches prove_never.py today)
3manifest loads but this adapter cannot execute some verbs (matches verb_manifest --check)
4config error — manifest did not load
5licence invalid or lapsed
7brain not running / could not reach the broker

Keep 2 and 3 where they already are. CI scripts in the wild will depend on them.

One rule for every error

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.


04 lw watch — the flagship

lw 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.


05 Inspecting a mind

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.


06 Verification

These already exist as scripts and need a front door, not a rewrite.

CommandWrapsGates
lw manifest check <file>verb_manifest.py --checkload errors, unexecutable verbs
lw manifest exportverb_manifest.py --exportvocabulary JSON for C#/GDScript adapters
lw prove <manifest>prove_never.pythe §⁠18 publish-time never-proof
lw certifycertify.py, certify_full.py, certify_motor.py, certify_world.pythe whole thing
lw battery <name>plan_battery, planning_battery, obedience_battery, forgiveness_battery, manifest_battery, believability_battery, generalization_batteryone dimension
lw doctornew + eyes_test.pymachine, 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.


07 Licensing

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.htmlPOST /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):

piecewhere it runshow you get it
reflex student, event gate + Qwen embedder, motor experts, ACT / ADVANCE / HOLD / late-bindlocal, in your build, offlineweights fetched on lw licence activate, versioned (sdk/artifacts.json)
WAKE / COMPOSE / RANK — the deliberator and the plan ranker's weights, and the flywheelhosted, next to each otherevery 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.


08 The flywheel, and how data gets back

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.

LaneRetrainsPayloadWhat comes back to you
studentthe reflex brain (student_s1-*.pt)the tuple the student was unsure about + the verdictweights, local, yours to ship
gatethe event gate (event_gate_qwen3.pt)deliberator_escalation records the gate classed other — sentences it could not readweights, local, yours to ship
rankerthe plan ranker (hosted)deliberator_escalation / plan_escalation / authored, richest label winningnothing 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.


09 Agent-ready

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 (Shipssdk/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 (Shipssdk/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.


10 What the CLI must never contain

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.


11 What already exists

Most of this is a front door over working code. The mapping, so the build is scoped honestly:

CommandExists today asWork
lw dev, lw statuspipeline/lw_launcher.py (--mock, --status)rename + polish
lw dev --mockpipeline/skyrim_mock.py, gta_mock.pywire through
lw trigger <class>pipeline/perceive.pyflags + chain output
lw trigger threat/worldpipeline/devcmd.pygeneralise beyond Skyrim
lw trigger orderpipeline/command_compose.pyfront door
lw trigger cast/saypipeline/world_mcp.pyfront door
lw manifest check/exportpipeline/verb_manifest.py --check/--exporterror formatting
lw provepipeline/prove_never.pyfront door only
lw certifycertify*.py (4 files)one entry point
lw battery7 battery scriptsone entry point
lw why, lw watchreceipts in ~/lw-mining/decisions/new — the renderer
lw replaypipeline/golden_replay.pyfront door
lw sync, lw escalationspipeline/escalation_archive.pyfront door + consent UX
lw mcppipeline/livingworld_mcp.pyfront door
lw servepipeline/ws_server.py+ REST layer
lw doctorpipeline/eyes_test.py + newnew
lw assertnew — ~200 lines
lw licence, lw trainnew + hosted service
lw initnew

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.


12 Build order

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.


13 Non-goals

interface with its own bugs.

licence 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.