Harness parity — LLeMbas CLI ⇄ LLeMbas
LLeMbas CLI (TypeScript) and LLeMbas
(Python) are two independent projects that share one harness design: the
same tools, the same permission floor, the same prompt texts, the same model
quirks and metadata, and one protocol between them. Each implements it natively;
neither ever executes the other. The specification is this repository's
harness/ (see its README.md); the CLI embeds it at build time
(src/harness.ts), LLeMbas vendors a pinned, hash-checked copy
(scripts/fetch_harness.py, scripts/harness.lock.json, into
src/lembas/harness_spec/).
This page maps every shared concept to its file in both, and says where they
still differ. Read it before touching either harness, and keep it true — a
wrong row is how one side silently stops matching the other. Paths: CLI under
src/ unless they start with harness/, scripts/ or tests/; LLeMbas under
src/lembas/ unless they start with scripts/ or tests/.
The rule
- A harness change (tools, permissions, prompts, quirks, the loop, the library, the link) goes into the spec first, then into both implementations — or is recorded as an open item against the other project.
- A bug found in one is searched for in the other, and the result — including "not present" — is recorded with the fix.
- Every such fix brings a conformance case in
harness/conformance/, which both test suites run. - An LLeMbas administrator's prompt overrides are a local setting, not a difference.
The spec itself
| Concept | LLeMbas CLI | LLeMbas |
|---|---|---|
| Where the spec lives | harness/ (source of truth), embedded by harness.ts |
harness_spec/ (vendored), read by services/harness_spec.py |
| Pinning and updating | harness/VERSION |
scripts/fetch_harness.py --update vX.Y.Z (a CLI tag), scripts/harness.lock.json |
| Spec version reported | HARNESS_VERSION (harness.ts), sent as harness_spec in initialize and the link's hello (acp/agent.ts, acp/link.ts) |
/.well-known/lembas.json (api/devices.py), /admin/updates (api/admin_updates.py) |
| Conformance runner | tests/conformance.ts (areas → functions), tests/conformance.test.ts; bun run harness record (scripts/harness.ts) |
tests/test_harness_spec.py (AREAS, and PENDING with reasons) |
| Tool definitions kept equal to the spec | tests/harness.test.ts |
tests/test_harness_spec.py (DEVICE_TOOLS, TOOLS_PENDING) |
Conformance areas
| Area | LLeMbas CLI | LLeMbas |
|---|---|---|
hardline |
permission/hardline.ts hardlineCommand |
services/agent/shell.py hardline_command |
split |
permission/bash.ts splitCommand |
services/agent/shell.py split_command |
plain |
permission/hardline.ts plainCommands |
services/agent/shell.py plain_commands |
arity |
permission/arity.ts prefix |
services/agent/shell.py prefix |
always |
permission/evaluate.ts alwaysPatterns |
services/agent/policy.py always_patterns |
permission |
permission/evaluate.ts evaluate |
pending — LLeMbas's modes are its own table (services/agent/policy.py POLICY) |
chunk |
library/chunks.ts split |
services/library/chunks.py split |
think |
provider/think.ts ThinkSplitter |
services/reasoning.py ReasoningSplitter |
effort |
provider/effort.ts effortRefused, advertisedEfforts |
services/generation.py _effort_was_refused, _advertised_efforts |
edit |
tool/replace.ts |
not run — the file tools are the CLI's |
unidiff |
tool/unidiff.ts |
not run — the file tools are the CLI's |
envelope |
tool/patch.ts |
not run — the file tools are the CLI's |
Tools
Scopes come from each harness/tools/<name>.json: shared (both implement it),
cli (the CLI's alone). LLeMbas runs no agent of its own — an agent chat runs on
a linked CLI — so the execution tools are cli.
| Tool (spec name) | Scope | LLeMbas CLI | LLeMbas | Differs |
|---|---|---|---|---|
ask_user |
shared | tool/question.ts; card tui/components/question.tsx |
ask_user in services/tools.py, services/interaction.py |
LLeMbas's own schema and card; same name |
task |
shared | tool/task.ts, app.ts spawn, project/agents.ts |
subagent_run in services/subagent.py |
name; LLeMbas's helper always on the parent's model |
web_search |
shared | tool/web.ts, search/ |
web_search in services/tools.py, services/search/ |
LLeMbas's own description |
web_fetch |
shared | tool/web.ts, search/fetch.ts |
fetch in services/tools.py, services/fetch.py |
name |
memory |
shared | tool/memory.ts, memory/store.ts |
memory_add, memory_forget in services/tools.py, services/library/memories.py |
shape |
notes_search · note_view · note_manage |
shared | tool/library.ts, library/store.ts |
notes_search, notes_get, notes_create, notes_edit, notes_delete in services/tools.py, services/library/notes.py |
names |
skills_list · skill_view · skill_manage |
shared | tool/skills.ts, skill/index.ts |
skill_get, skill_create, skill_edit in services/tools.py, services/library/skills.py; the index is in the prompt |
names; no list tool |
knowledge_search · knowledge_get |
shared | tool/library.ts, library/store.ts |
services/tools.py, services/library/documents.py, retrieval.py |
LLeMbas's own descriptions; same names |
read · write · list |
cli | tool/read.ts, tool/write.ts, tool/search.ts |
— | by design |
edit · multiedit |
cli | tool/edit.ts, tool/multiedit.ts, tool/replace.ts |
— | by design |
apply_patch |
cli | tool/apply_patch.ts, tool/patch.ts, tool/unidiff.ts |
— | by design |
glob · grep |
cli | tool/search.ts |
— | by design |
bash · bash_output · bash_list · bash_kill |
cli | tool/bash.ts, tool/jobs.ts |
— | by design |
todo |
cli | tool/todo.ts; tui/components/todos.tsx |
drawn from the device's events above the composer | — |
plan_submit |
cli | tool/plan_exit.ts; tui/components/plancard.tsx |
the plan card for a device chat (capability plan) |
— |
tasks · decisions |
cli | tool/project_tools.ts, project/board.ts |
— | by design |
session_search · settings · view_image |
cli | tool/session_search.ts, tool/settings.ts, tool/view_image.ts |
— | by design |
| Former names | tool/names.ts reads aliases.cli and argument_aliases |
its tools still carry the names in aliases.llembas |
see Open items | |
How a call is drawn (block) |
tui/state.ts, tui/components/messages.tsx |
services/tool_blocks.py |
— | |
| LLeMbas only | report_*, schedule_*, image_generate, scratch_write, custom HTTP tools, remote MCP |
web features, by design |
Permissions
| Concept | LLeMbas CLI | LLeMbas |
|---|---|---|
Modes manual edit auto plan |
harness/permission/modes.json; permission/evaluate.ts; config/schema.ts (unrestricted read as auto) |
services/agent/policy.py (the device's mode is clamped by the device) |
| Hardline floor, protected paths | harness/permission/hardline.json; permission/hardline.ts |
services/agent/shell.py, services/agent/policy.py refusal |
| Judging a command line | permission/bash.ts, permission/evaluate.ts (each command and its plain spelling) |
services/agent/shell.py |
| "Always allow" | harness/permission/arity.json; permission/arity.ts, evaluate.ts |
services/agent/policy.py always_patterns |
| Default rules | harness/permission/defaults.json; DEFAULT_RULES in evaluate.ts |
pending (see the permission area) |
| Approving | once, edit then once, session, project, deny, deny with a reason — tui/components/permission.tsx; over ACP session/request_permission |
the approval card on a device chat; first answer on either side wins |
| Device limits | the global remote: block → Limits in acp/agent.ts |
shown, never set |
Loop, providers, prompts
| Concept | LLeMbas CLI | LLeMbas | Differs |
|---|---|---|---|
| The loop: step backstop, budgets, wrap-up, parallel calls | harness/loop.json; session/engine.ts |
services/generation.py (_run, _wrap_up), services/settings_store.py |
budgets unset by default in the CLI |
| Capacity rule | session/capacity.ts |
services/subagent.py, the connection's flags (db/models/connection.py) |
— |
| Dialects | provider/openai-chat.ts, responses.ts, anthropic.ts, gemini.ts, ollama.ts |
services/llm/openai_client.py, anthropic_client.py |
LLeMbas: OpenAI chat and Anthropic, by design |
| Quirks of real servers | harness/quirks.json; provider/common.ts, sse.ts, openai-chat.ts |
services/llm/openai_client.py, services/generation.py |
— |
| Reasoning effort | provider/effort.ts, provider/learned.ts |
services/chat.py, services/generation.py |
— |
| Model metadata | harness/models/schema.json; config/schema.ts, lembas/webui.ts reads it from /v1/models |
served at /v1/models (api/v1/openai.py) from the model rows |
— |
| Prompt texts | harness/prompts/ (index.json: which are shared), prompt/assemble.ts |
services/prompts.py fragments, services/harness.py |
see below |
tasks/continue.md |
session/commands.ts, session/engine.ts |
harness_spec.prompt("tasks/continue.md") in api/chats.py, services/generation.py, web/templating.py |
identical |
| Personality presets | harness/prompts/personality/; prompt/personality.ts |
services/personalization.py (reads the vendored presets) |
identical |
| Compaction | prune, then summarise, prompt put back (session/engine.ts) |
summary as a user + assistant turn, turns kept collapsed (services/compaction.py) |
shape |
| Library: chunking, search, fusion | library/chunks.ts, library/store.ts |
services/library/chunks.py, retrieval.py |
— |
Prompt texts. harness/prompts/index.json records, for each shared text,
which LLeMbas fragment it is and whether LLeMbas's default is word for word the
same. Identical today: tasks/continue.md and the six personality/*.md.
Shared but still differing in LLeMbas: system/default.md (LLeMbas's core.*
fragments are separate, editable pieces), family/*.md, modes/*.md,
blocks/memory.md, blocks/skills.md, tasks/compact.md. The CLI's alone:
system/identity.md, blocks/skills-empty.md, blocks/agents-*.md,
tasks/review.md, tasks/changelog.md, tasks/init.md.
Connecting
| Concept | LLeMbas CLI | LLeMbas |
|---|---|---|
| The protocol, written down | harness/acp.md, checked against the code by tests/harness.test.ts |
vendored with the spec |
| ACP role | agent — acp/agent.ts |
client — services/device_link.py |
| JSON-RPC framing | acp/rpc.ts |
device_link.Link |
| The link | acp/link.ts (dials out), service.ts |
api/devices.py device_link_socket (/api/devices/link) |
_lembas protocol versions |
acp/agent.ts LEMBAS_PROTOCOL = 2; says 2 only to an instance whose discovery lists it |
device_link.PROTOCOL = 2, PROTOCOLS = (1, 2), ACP_VERSION = 1 |
| Discovery | lembas/client.ts reads protocols, at login and every dial |
/.well-known/lembas.json: protocol: 1, protocols: [1, 2] |
Capabilities (files, commands, attachments, ask, plan, model, turns, shell) |
offered in initialize's _meta.lembas.capabilities |
used only when listed |
| One session, both sides | acp/hub.ts, acp/share.ts |
announced sessions become chats; Chat.device_session_id |
| Turn ids | acp/turns.ts |
messages keep the turn id; reattach asks _lembas/session/status |
| A web prompt made into a typed one | acp/prompt.ts (/name via session/commands.ts, @path via project/attach.ts) |
the composer's @ and / lists, uploads as image / resource blocks |
| Deletes not yet acknowledged | acp/pending.ts |
kept and resent until answered |
| Terminal shell marks | acp/shellrc.ts (bash, zsh, fish) |
services/agent/terminal.py, services/agent/shell_marks.py (OSC 133 / OSC 7) |
| Device login (RFC 8628) | lembas/client.ts, lembas/login.ts |
services/devices.py, api/devices.py (/device) |
| The account's models | lembas/webui.ts, provider/refs.ts (<provider>/<model>) |
api/v1/openai.py |
| Personalization and usage | lembas/personal.ts |
api/v1/me.py (/v1/me/personalization, /v1/usage) |
| Library over MCP | library: lembas → mcp/index.ts LIBRARY_SERVER |
api/mcp_server.py (/mcp) |
| Search, fetch and speech through the instance | search/webui.ts, voice/speech.ts |
api/v1/services.py |
Open items
- Shared tools still in LLeMbas's own shape —
ask_user(own schema),task(subagent_run),web_fetch(fetch), and the library tools (memory_*,notes_*,skill_*, noskills_list).tests/test_harness_spec.pynames each inTOOLS_PENDING. - The
permissionconformance area — LLeMbas's mode table is its own; the spec'smodes.jsonand default rules are to be adopted. Commands themselves only ever run on a linked CLI, under the CLI's evaluator and the device's limits, so this affects what LLeMbas shows, not what runs. - Prompt texts — beyond the seven identical ones, the shared texts differ from LLeMbas's fragments by what only a coding agent needs; a shared base with variables, as the tool descriptions have, is the next step.
Known gaps, kept deliberately
- Dialects in LLeMbas. OpenAI chat and Anthropic cover its endpoints; the other three are not required for parity.
- Background jobs, snapshots and undo are the CLI's: they happen on the machine where the work is.