# KHAELOR Architecture Decisions > Phase 0 output. Each record: **Decision** / **Evidence from references** / **Trade-offs accepted** / **V1 scope note**. Evidence cites the four analysis documents in `docs/research/`. Where reference evidence pushes against a decision, a clearly-marked **⚠ Concern** is recorded rather than silently changing the decision. --- ## ADR-1 — Language & Runtime: TypeScript, Node ≥ 20, npm distribution **Decision.** TypeScript in strict mode, targeting Node ≥ 20, shipped as `npm install -g khaelor` → `khaelor`. No Bun hard-dependency. No Electron, no web UI. **Evidence.** Hermes is the cautionary tale: choosing Python first for a terminal product forced an 18.7K-line prompt_toolkit REPL, then a full parallel React/Ink TypeScript TUI, then a Python↔Node JSON-RPC bridge process — "KHAELOR starting TypeScript-native skips that entire tax" (HERMES §8.2). Hermes and OpenHands both drown in thread/lock machinery (`FIFOLock`, `_step_holds_state_lock`, compression commit fences, issue-numbered workarounds #3485/#3053) that "evaporates in a single-threaded TS event loop with structured async" (OPENHANDS §9). OpenCode validates the TS/terminal pairing end to end. OpenHands' discriminated-union serialization framework is free in TypeScript tagged unions. **Trade-offs accepted.** We forgo Bun's `bun build --compile` single-binary startup story — OpenCode inlines the models catalog into the compiled binary and disables dotenv autoload to protect cold start (OPENCODE §9.11). On plain Node we must earn startup speed with OpenCode's *other* discipline: a codified lazy-import rule and lazy thunks for heavy modules. Bun-compile remains a later distribution optimization, never a runtime dependency. **V1 scope note.** One process, one package. Rust native components deferred until a measured bottleneck exists (CLAUDE.md §5). --- ## ADR-2 — Kernel: tiny, state-derived agent loop **Decision.** The kernel is a small loop that **re-derives "what next" from recorded session state each iteration**: build context → stream model → record events → execute tool calls → record observations → repeat. Exit conditions derive from state, not in-memory flags. The kernel holds no business logic; context, tools, permissions, and persistence are services around it. The stream-event reducer is a separate component from the loop. **Evidence.** mini-SWE is the existence proof: a 190-line `DefaultAgent` with eight state fields scores >74% on SWE-bench Verified — "any kernel larger than a few hundred lines is carrying non-kernel work" (MINI §Executive answer). OpenCode's `SessionPrompt.runLoop` shows the production-grade version: each iteration re-reads persisted messages; compaction and subtasks are *persisted parts* popped as tasks; "a crashed process can resume mid-conversation because nothing lives only in loop-local variables"; concurrent prompts join the running loop instead of double-driving it (OPENCODE §4.2). OpenCode separates `SessionProcessor` (pure stream reducer) from `runLoop` — "that *is* the small kernel." The counter-evidence is unanimous: Hermes' 7,740-line loop with inline recovery regions and OpenHands' `Agent.step()` buried under MCP/critics/hooks/vision fallbacks are the same god-object failure from two codebases. **Trade-offs accepted.** State-derivation costs a per-iteration projection read; in practice the kernel consults an in-memory projection maintained by the same event reducer that writes the log (rebuilding from the JSONL only on resume), so the property preserved is "derivable from recorded state," not "re-read disk every iteration." mini's exceptions-carrying-messages pattern is adopted as typed control-flow results caught at one loop boundary. Doom-loop detection (OpenCode: 3 byte-identical consecutive tool calls → ask) rides along as a cheap kernel-adjacent check. **V1 scope note.** Kernel state: history handle, model client, tool runtime, budget counters, run status — mini's "if a field isn't consulted by the loop itself, it belongs to a service." --- ## ADR-3 — Sessions: event-sourced, append-only JSONL **Decision.** One append-only JSONL event log per session under `~/.khaelor/sessions//`. State, UI, and LLM context are projections; resume = replay. No event-tree branching in V1, but the event schema leaves room for a future `parent_id`. **Evidence.** OpenHands proves event-log-as-source-of-truth: append-only per-event JSON, conversation state / LLM view / UI all projections, "resume and replay are trivial-by-construction," idempotent init that skips when a SystemPromptEvent already exists (OPENHANDS §2.3, WELL #1). OpenCode proves the same spine over SQLite: durable typed events + atomic projectors, "persistence and streaming are literally the same write" (OPENCODE §2.3). Both storage backends were examined and rejected for V1: OpenHands' file-per-event costs an inode per event plus index-scan/lock-file machinery; OpenCode's SQLite works but adds a DB dependency — and Hermes shows the armor tax that follows (WAL fallback detection, macOS fsync barriers, schema self-repair across 10,888 lines, HERMES §9). Atomic line appends to one file per session capture the durability property (Hermes' incremental mid-turn flushes; mini's `finally`-block save every step) with neither cost. OpenHands' event *tree* works but its own analysis documents the sentinel/legacy-fallback debt (`ROOT_PARENT_ID`, `head_is_empty`, bug #4057) — "ship a linear log; keep `parent_id` cheap future-proofing" (OPENHANDS NOT-COPY #7). OpenHands' resume contract (`AgentBase.verify`: tools add-only, model swappable) is adopted. **Trade-offs accepted.** No SQL queries over history — session search/pagination must scan or maintain rebuildable sidecar indices. Derived metadata (title, token totals, cost) must always be recomputable from the log; OpenCode's incremental-counter trick is reproduced as a projection cache, never as separate truth. Version events from day one (OpenCode's `{durable, version}` on event definitions) — never ship a v1/v2 dual architecture (OpenCode's most visible debt). **⚠ Concern.** OpenCode's projectors commit event + projection **in the same transaction**; with JSONL we get atomicity only for the log line itself. Accepted consequence: the log is the *sole* truth and every projection must tolerate being stale/rebuilt. Very long sessions also make full replay on resume O(session); if measured to matter, add periodic snapshot events (a natural fit — the compaction checkpoint of ADR-6 already is one), not a database. --- ## ADR-4 — Event Bus: in-process, typed, durable/ephemeral split **Decision.** An in-process typed event bus carrying the CLAUDE.md §7 vocabulary. Durable events are appended to the session log; ephemeral events (text/thinking/tool-input deltas) flow to the TUI only. Deltas are coalesced ~16 ms into batched renders. **Evidence.** OpenCode's event system is the engine's spine: typed pub/sub with durable definitions feeding both projections and SSE clients; the client coalesces SSE events "in a 16 ms window and applied inside Solid's `batch()` — one render per frame regardless of event rate" (OPENCODE §3.3, §9). Hermes' TUI gateway independently converged on the same design: ~40 typed event kinds with streaming deltas coalesced on a 33 ms timer (HERMES §8.1) — a convergent finding, so it is treated as settled. The durable/ephemeral split follows from OpenCode's model, where deltas are `updatePartDelta` events distinct from persisted part upserts. mini-SWE marks the boundary of doing without: "the moment streaming exists, messages-as-the-only-log stops working" (MINI §4). OpenHands supplies the anti-pattern to exclude: `visualize` Rich properties welded onto domain events — rendering lives in the TUI layer, keyed by event type, never on the event. **Trade-offs accepted.** Ephemeral deltas are lost on crash mid-block; acceptable because the completed block is recorded durably when the stream event closes it (OpenCode behaves identically). The bus is in-process only (see ADR-17). **V1 scope note.** Events carry a session id from day one — the cheap seam ADR-16 depends on. --- ## ADR-5 — Streaming: first-class from day one **Decision.** Anthropic SDK streaming is the substrate. The kernel consumes `AsyncIterable`; the TUI renders deltas. Non-streaming is the degenerate case, never the default. **Evidence.** OpenHands is the named anti-pattern: `on_token` callbacks pass raw litellm chunks, silently degrade to non-streaming, and events exist only post-completion — "the architecture is request/response at heart — unacceptable for a terminal UI where streaming is the product" (OPENHANDS POORLY #1). mini-SWE blocks on `litellm.completion` behind a spinner — fine when nobody watches, disqualifying for KHAELOR (MINI Divergence #1: "the single biggest structural divergence"). OpenCode shows the target: provider delta → part-delta event → single-node repaint, 60 fps during full-speed token streams (OPENCODE §9.1). **Trade-offs accepted.** Streaming forces the cancellation and steering design of ADR-11 to exist up front; there is no cheap synchronous fallback path to hide behind. **V1 scope note.** `ModelStarted · TextDelta · ThinkingDelta · ToolCallStarted · ToolInputDelta · ModelFinished` are all present in the first Anthropic integration (Phase 3). --- ## ADR-6 — Context Engine: Hermes' verbs, OpenHands' mechanics, token triggers, structured checkpoint **Decision.** The Context Engine exposes Hermes' interface shape — `select_context` / `compress` / `on_turn_complete` / `prune_tool_results_only` — with OpenHands' compaction-as-event mechanics: a `ContextCompacted` event recorded in the log and deterministically re-applied on replay, cutting only at indices where Anthropic tool_use/tool_result pairing survives. Triggers are token-based from **real API usage numbers**. The summary payload is KHAELOR's structured YAML checkpoint (CLAUDE.md §12). Tool-result pruning runs first (cheap, deterministic, no LLM); summarization second. **Evidence.** Hermes' `ContextEngine` ABC is "the best-factored component in the codebase" — selection ("this turn belongs to a different context") explicitly orthogonal to compression ("context is too long"), observation (`on_turn_complete`) and cheap pruning as separate verbs, all fed by `update_from_response(usage)` (HERMES §3.3). Its compressor algorithm skeleton (prune tool results → protect head → protect token-budgeted tail → summarize middle on an aux model → iteratively update) is adopted, minus the cross-process lock machinery a single-process V1 doesn't need (HERMES ADOPT #3). OpenHands supplies the structural insight "worth stealing wholesale": a `Condensation` is an event in the log; the View re-applies it deterministically; `manipulation_indices` guarantee API-safe cuts; condensation doubles as the recovery path for context-window errors — dual proactive/reactive triggers (OPENHANDS §8). OpenHands' weakness is named and corrected: it triggers on **event count** (240), not tokens (OPENHANDS POORLY #7); OpenCode demonstrates the correct budget arithmetic — real usage tokens vs. usable window minus a reserved compaction buffer, plus a separate cheaper `prune()` protecting the newest 40K tokens of tool output (OPENCODE §7). mini-SWE's cliff (context overflow = fatal abort) is the failure mode this ADR exists to prevent. **Trade-offs accepted.** A structured YAML checkpoint is more prescriptive than OpenCode/OpenHands' free-text summaries — it may occasionally fit a session awkwardly, but it makes `/context` inspectable and preserves the fields that free-text summaries destroy (failed_attempts, running_processes, decisions). Preserve important raw evidence alongside the checkpoint when summarization would destroy it. **V1 scope note.** `select_context` ships as a pass-through hook in V1 (no retrieval/topic routing yet); the verb exists so repository intelligence can plug in without interface change. --- ## ADR-7 — Prompt Caching: byte-stability as a design invariant **Decision.** Prompt-cache byte-stability is a system-wide invariant, not an optimization: the system prompt is built once per session in stable tiers; history is never rewritten (compaction, recorded as an event, is the sole sanctioned break); volatile per-turn context is injected only into the API-copy of the current message; Anthropic `cache_control` breakpoints are planned deliberately. **Evidence.** This is Hermes' strongest lesson, stated as its first governing invariant ("per-conversation prompt caching is sacred") and implemented everywhere: the three-tier system prompt whose docstring reads "Hermes never re-renders parts of this string mid-session — that's the only way to keep upstream prompt caches warm"; the `api_content` sidecar replaying byte-exact historical sends; memory injected as a frozen snapshot; per-turn ephemeral context confined to the API copy of the current user message (HERMES §3.1–3.2, WELL #1). OpenHands independently converged: static cacheable system block vs. dynamic uncached block, "should NOT be included in the cached system prompt to enable cross-conversation cache sharing" (OPENHANDS §1.5). OpenCode applies ephemeral cacheControl breakpoints plus a session-scoped promptCacheKey (OPENCODE §7, §9.9). Even 164-line mini-SWE auto-enables cache control for Anthropic-looking models (MINI §3.1). Four for four. **Trade-offs accepted.** Anything volatile (timestamps, git status, running-process lists) must live in the dynamic tier or the current-message injection — never interleaved in history. This constrains how repository context is delivered and must be enforced by tests, since a single careless mutation silently destroys the economics. **V1 scope note.** Cache read/write tokens surface in `/cost` from real usage fields, making cache health observable (Absolute Rule #4). --- ## ADR-8 — Tools: seven primitives, replacer-cascade edit, model-facing process manager **Decision.** Exactly `read / write / edit / grep / glob / bash / process`. Few parameters (≤5) with rich descriptions. `edit` is a replacer cascade of matching strategies (OpenCode's nine-strategy cascade as the reference) with OpenHands-quality failure messages (line-number hints, "maybe you meant", post-edit snippet). Long output: head/tail truncation with spill-to-file paths returned to the model. `bash` and `process` are strictly separated; the process manager is **model-facing** (`process.start/list/read/write/stop`). **Evidence.** Tool-count calibration: Hermes' ~93 tools required a `tool_search` bridge to cope with its own surface (HERMES POORLY #4); mini-SWE's single bash tool scores >74% but is "brutal for interactive use — a one-character `sed` mistake silently corrupts files, no diffs, no ambiguity detection" (MINI §4) — CLAUDE.md's ~7 primitives is where the evidence lands. Schema discipline: OpenCode's `edit` has 4 params with guidance in description text files (OPENCODE §6.1). The edit cascade: nine strategies (exact → line-trimmed → block-anchor Levenshtein → whitespace-normalized → indentation-flexible → escape-normalized → trimmed-boundary → context-aware → multi-occurrence) with uniqueness and disproportionate-match guards, CRLF/BOM preservation, and repair-prose errors — "adopt wholesale" (OPENCODE §6.2, ADOPT #3); layered with OpenHands' failure UX: multiple-occurrence errors citing line numbers, "Maybe you meant {cwd/path}?", post-edit snippet so the model self-verifies without a re-read, per-file undo history (OPENHANDS §4.2). Output economics are a Hermes/OpenCode/OpenHands convergent finding: head/tail truncation with explicit omission markers and full output spilled to a path the model can `read`/`grep` (HERMES §6.3, OPENCODE §6.1, OPENHANDS §4.3). The process manager: Hermes' `terminal`+`process` pair "validates KHAELOR's §10 design almost line for line" — rolling buffers, poll/log pagination, crash checkpoints with PID-reuse guards, a hard foreground-timeout ceiling that *redirects* long commands to background (HERMES §6.1–6.2, ADOPT #8); OpenCode's biggest tool-level gap is precisely this omission ("do not inherit the omission", OPENCODE NOT-COPY #10); OpenHands' soft-timeout `exit_code=-1` convention is explicitly rejected as the primary mechanism (OPENHANDS NOT-COPY #9). Adopt mini's process-group-kill hygiene inside it (MINI Divergence #4). **Trade-offs accepted.** Seven tools means no `git` tool in V1 (git flows through `bash` under permission rules; re-evaluate per CLAUDE.md §10). A model-facing process manager adds schema weight and permission surface that OpenCode chose to avoid — accepted, because dev-server workflows are a stated differentiator. **V1 scope note.** LSP-diagnostics feedback in edit results (OpenCode) is a strong idea deferred beyond V1; the spill-file directory lives under `~/.khaelor/` with size caps. --- ## ADR-9 — Permissions: capability rules, last-match-wins, persisted grants **Decision.** Capability-based permissions (`file.read`, `file.write.project`, `process.execute`, `filesystem.outsideProject`, `network.access`, `git.modify`) with `allow / ask / deny`; last-match-wins wildcard rules. Bash commands are parsed to generate precise "always allow `git push *`" suggestions — conservative shell-word parsing in V1, tree-sitter later. "Always allow" decisions are **persisted** to project config. Elegant inline permission panel per CLAUDE.md §13. **Evidence.** OpenCode's evaluator is the elegance benchmark: `{permission, pattern, action}` triples, "evaluation = last matching rule wins with wildcard matching… a 4-line `findLast`", capability-ish keys (`write` folds into `edit`), and deny rules deriving tool visibility so modes are pure policy (OPENCODE §8.1, WELL #3). Its arity dictionary turns approvals into human-meaningful generalizations — "the difference between a permission system users tolerate and one they like" (OPENCODE §8.2, ADOPT #5). Its named flaw is fixed here: v1 `always` approvals are in-memory per session, so "users re-approve across restarts" — KHAELOR persists scoped grants from day one (OPENCODE POORLY #4, NOT-COPY #7). From Hermes: a small unbypassable hardline deny floor beneath the rule system, matched against de-obfuscated variants, "silence is not consent" timeout semantics, and refusing always-allow entries containing shell operators (HERMES §6.5, ADOPT #10) — while rejecting its regex-armory-as-primary-defense posture (HERMES NOT-COPY #9). From OpenHands: pending approval represented as persisted unexecuted action events (approvals survive restarts for free), rejection reasons fed back to the model as observations, read-only short-circuit; its LLM self-assessed risk is rejected as the gate — "deterministic capability rules first" (OPENHANDS §6, ADOPT #5, NOT-COPY #10). mini confirms the evaluator itself can be ~10 lines (MINI §5). **Trade-offs accepted.** Conservative shell-word parsing will under-generalize on compound commands (pipelines, `&&` chains, subshells) — V1 mitigates by refusing to suggest "always" patterns for commands containing shell operators (Hermes' guard) and falling back to exact-command approval. **⚠ Concern:** OpenCode's evidence shows tree-sitter parsing also powers `external_directory` escape detection on filesystem verbs; shell-word parsing is weaker here, so V1's `filesystem.outsideProject` checks are best-effort on complex commands. Tree-sitter is the planned upgrade, not a maybe. **V1 scope note.** Rejection-with-feedback (denial text delivered to the model as steering, OpenCode's `CorrectedError`) ships in V1 — it converts denials from dead ends into course corrections. --- ## ADR-10 — Model Layer: one `ModelClient` over the official Anthropic SDK **Decision.** A single `ModelClient` interface (`stream(request): AsyncIterable`) over the official Anthropic SDK. Typed error taxonomy (retryable / fatal / context-overflow), retries with backoff, cancellation via AbortSignal propagation, cost/token accounting **only** from real API usage fields. No litellm-style universal adapter, no provider framework. **Evidence.** Provider generality is the quantified villain of every reference: Hermes' "provider sprawl is the single largest complexity driver" — credential pools, failover chains, per-provider sanitizers scattered through the loop (HERMES §10.4); OpenHands' 2,300-line litellm wrapper with four near-duplicate call paths — "precisely what `ModelClient` must *not* become" (OPENHANDS §7); OpenCode's model-family prompt files and transform matrices (OPENCODE POORLY #7). The transferable ideas are exactly three, present in all references: (1) a typed exception taxonomy the kernel branches on (OpenHands' `LLMContextWindowExceedError` et al. mapped to distinct kernel reactions; Hermes' `ClassifiedError{retryable, should_compress, …}` consumed as hints); (2) honest accounting — cache read/write tokens from real usage metadata, aggregated per session (OPENHANDS §7 Telemetry; mini's "hard failure if cost cannot be computed"); (3) retry with backoff at the model layer, `finish_reason`-aware error messages (MINI §5). Internal message/event types model Anthropic semantics directly — no OpenAI-dict lingua franca (Hermes' leakiest abstraction, HERMES NOT-COPY #2). **Trade-offs accepted.** Adding a second provider later means real work at this boundary. Accepted deliberately: `ModelClient` exists for clean architecture, not multi-provider readiness (CLAUDE.md §6). The auxiliary-model seam (Hermes ADOPT #14) is kept: one interface, two configured Anthropic model IDs — compaction summaries route to a cheaper model. **V1 scope note.** Context-overflow errors route to the Context Engine (reactive compaction trigger, ADR-6); never retried blindly. --- ## ADR-11 — Interruption & Steering **Decision.** `Esc` cancels: the model stream is aborted, running tools cancelled, and dangling `tool_use` blocks closed with synthetic cancelled `tool_result`s — history is protocol-valid at all times. Steering: user messages typed mid-run are queued and injected at safe boundaries (appended after tool results, never breaking role alternation), displayed as `Queued instruction`. **Evidence.** Convergent across the two mature implementations. Hermes: `interrupt()` force-closes sockets and cancelled tools get synthetic `[Tool execution cancelled …]` results with correct `tool_call_id`s "so history stays valid"; `steer()` drains at exactly two seams — pre-API-call (appended to the last tool message, "since injecting a user message would break role alternation") and post-tool-batch (HERMES §2.7, WELL #4). OpenCode: `Effect.onInterrupt` marks the assistant message aborted, a 250 ms grace for in-flight tools, and interrupted tool parts converted to "[Tool execution was interrupted]" results "so Anthropic never sees a dangling `tool_use`" (OPENCODE §4.2, ADOPT #10). mini contributes the UX seed — Ctrl-C becomes a steering comment — while its own analysis notes that with streaming and background processes this becomes "a genuinely new design, not an extension of mini's" (MINI Divergence #10). Cancellation propagates as an AbortSignal tree (ADR-10), not thread flags (Hermes' `_interrupt_requested` checked at scattered points is the pattern to avoid). **Trade-offs accepted.** Steering injected only at tool-result boundaries means a long uninterrupted text stream cannot be steered until it completes or is cancelled — the price of never corrupting alternation. Interruption must not kill `process`-managed background processes (they are explicitly long-lived); only in-flight tool calls are cancelled. **V1 scope note.** Queued steering ships in V1 (CLAUDE.md §23); `redirect` (Hermes' third verb — rewrite the active objective) is noted but not V1. --- ## ADR-12 — Completion: verification-on-stop gate **Decision.** When the model stops with code changed this turn and no fresh verification evidence exists, the kernel nudges it (synthetic message carrying detected verify commands) to run relevant checks — max 2 attempts — before accepting completion. Completion is internally represented as a `CompletionEvidence` record (CLAUDE.md §17). **Evidence.** Hermes proved this design in production: `verification_stop.py` checks whether code files changed this turn lack fresh passing evidence, injects an evidence-bearing nudge with detected verify commands, caps at `max_attempts=2`, filters documentation-only changes, and **preserves the withheld candidate answer** so budget exhaustion returns it rather than losing it — "the closest existing implementation of KHAELOR's `CompletionEvidence` idea… it works by nudging the model with evidence, not by trusting the model's confidence" (HERMES §2.8, WELL #3, ADOPT #4). mini-SWE supplies the negative proof: its magic-string completion (`COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` sniffed from stdout) is "spoofable by any command output" — completion must be a structured signal the kernel verifies before it believes (MINI Divergence #7). **Trade-offs accepted.** OpenHands takes a different path — an explicit `FinishTool` call, optionally vetoed by a critic (OPENHANDS §1.4). KHAELOR follows Hermes' stop-without-tool-calls + gate instead: it needs no extra tool schema and the gate supplies the rigor. Noted honestly: an explicit finish tool makes intent unambiguous; if end-without-tools proves ambiguous in dogfooding, a finish signal can be added without kernel changes. Only checks relevant to the repository run — never blind full test suites (CLAUDE.md §17). **V1 scope note.** The withheld-answer preservation detail is in scope — it prevents the gate from ever destroying a model response. --- ## ADR-13 — Workspace: four methods, local only **Decision.** The `Workspace` interface from CLAUDE.md §9 (`cwd / readFile / writeFile / exec`); `LocalWorkspace` is the only V1 implementation. Tools depend on `Workspace`, never on Node fs/process globals directly. **Evidence.** mini-SWE demonstrates the leverage of a thin seam: a 3-method `Environment` protocol lets local↔Docker↔Singularity swap in ~100–150 lines each "with zero kernel changes" (MINI §2). OpenHands demonstrates the failure to avoid: it *has* `BaseWorkspace`, but tools bypass it and open files directly — sandboxing then requires relocating the whole agent behind a FastAPI server with ~25 routers (OPENHANDS §3). Hermes has the seam for terminal execution only; file tools touch the local FS everywhere — "the world is not one seam" (HERMES POORLY #6). The lesson recorded in the OpenHands analysis: keep the interface thin so the future remote strategy is "run KHAELOR's core remotely," not "proxy every syscall" (OPENHANDS §3). **Trade-offs accepted.** Four methods will not cover everything tools want (globbing, stat, watch); those live in tool-side helpers *parameterized by* the Workspace rather than fattening the interface prematurely. Docker/SSH explicitly not implemented (CLAUDE.md §9). **V1 scope note.** A lint/test guard should flag direct `node:fs`/`child_process` imports outside `workspace/` and approved shared modules. --- ## ADR-14 — TUI: framework decided by a Phase 1 prototype spike; techniques fixed now **Decision.** The TUI framework is the one decision requiring a Phase 1 prototype spike. Candidates: (a) React + Ink, (b) SolidJS + `@opentui` (OpenCode's engine — **verify Node-without-Bun compatibility**), (c) a minimal custom ANSI renderer with settled-block streaming. Recorded decision criteria: flicker-free streaming; stable input line during streams; <16 ms input latency; Node-only compatibility; markdown + syntax-highlight + diff quality; memory in long sessions; terminal selection/copy preserved. Adopted regardless of framework: settled-block incremental markdown streaming, ~16 ms delta coalescing into batched renders, a bounded live-render region, width-responsive status bar, virtualized or capped scrollback **without** OpenCode's hard 100-message UX cliff. **Evidence.** The techniques are convergent, framework-independent findings. Hermes `ui-tui/`: `StreamScanState` freezes settled top-level markdown blocks and re-parses only the live tail ("explicitly avoiding O(blocks²) re-tokenization"); hard live-region caps (16K chars / 240 lines — unbounded trails OOM-killed Node, issue #34095); virtualized scrollback; width-responsive status segments with pre-reserved spinner width; 33 ms coalescing (HERMES §8.2, ADOPT #12). OpenCode: delta-only part updates → fine-grained single-node repaints; 16 ms coalescing inside `batch()`; 60 fps under full token streams; terminal-reality hardening (kitty keyboard, palette-derived theme, selection-safe dialogs, win32 FFI fixes) (OPENCODE §3, §9). OpenCode's 100-message cap is explicitly rejected: "long sessions silently drop scrollback from the UI" (OPENCODE POORLY #9, NOT-COPY #6). Framework risk is real on both sides: stock Ink required Hermes to vendor a ~100-file fork (ScrollBox, mouse, selection); opentui is developed inside a Bun monorepo with patched deps, so Node-without-Bun compatibility is unproven — hence a spike, not a bet. OpenCode's composer (extmark-backed structured parts, frecency mentions, collapsed pastes, `$EDITOR` round-trip) is the reference standard for the Phase 2 composer regardless of framework (OPENCODE §3.5). **Trade-offs accepted.** A spike costs Phase 1 time; the alternative — discovering flicker, input-latency, or Bun-dependency problems in Phase 2 — costs more. If both ecosystems fail the criteria, option (c) is viable precisely because the adopted techniques (settled blocks + bounded live region) are what make a custom renderer tractable. **V1 scope note.** One exceptional default theme; `NO_COLOR` and monochrome-meaningful symbols from the start (CLAUDE.md §19). --- ## ADR-15 — Git Awareness: baseline capture and attribution; shadow git deferred **Decision.** Record baseline git state (branch, dirty files, diff hash) at session start and before the first edit; attribute changes (KHAELOR's vs. pre-existing); never auto-commit. OpenCode-style shadow-git snapshots are noted as a strong post-V1 candidate and deferred. **Evidence.** OpenCode's shadow repository is the standout mechanism: a separate git dir against the real work tree, `objects/info/alternates` + copied index making snapshots "near-free even on huge repos", tree hashes recorded per step, powering revert/unrevert and the diff viewer, fully invisible to the user's `git status` (OPENCODE §5.3, WELL #6, ADOPT #7). Nothing comparable exists in the other references (Hermes detects changed files only to feed verification; mini has nothing) — which also shows agents function correctly with baseline-recording alone. CLAUDE.md §16's guarantees (never assume a diff belongs to KHAELOR; protect user work) are satisfied by baseline capture + attribution without the shadow-repo machinery. **Trade-offs accepted.** Without snapshots there is no `revert`/`unrevert` in V1 — undo relies on the user's own git hygiene plus the edit tool's per-file history (ADR-8). Deferring is a scope decision, not a quality judgment; the design keeps snapshot hashes representable as events so shadow git can slot in later. **V1 scope note.** Baseline events are durable log entries, so attribution survives resume. --- ## ADR-16 — Subagents / Memory / Skills / MCP: not V1, seams reserved **Decision.** None of subagents, memory, skills, or MCP ship in V1. The event log and kernel service boundaries leave room: events carry a session id; the tool registry is data-driven. No premature abstraction beyond that. **Evidence.** mini-SWE is the existence proof that the entire layer is unnecessary for strong coding performance today: no planner, no subagents, no memory — >74% SWE-bench Verified ("strong evidence this whole layer is accidental complexity at current model capability", MINI §5). The cost side is equally documented: Hermes' memory/skills complex is ~10K+ lines (background review, curator, hub, sync, guard, provenance — HERMES NOT-COPY #6); OpenHands' six extension systems interleaved with the core are exactly what bloated `Agent.step()` and `LocalConversation` (OPENHANDS §9). The seams that make later addition cheap are already validated: OpenCode's subagents are just child *sessions* with derived permissions — trivially expressible once events carry session ids (OPENCODE §4.3); Hermes' background-review lessons (aux model, cache-warm replay, cancel on new live turn) are recorded for the future memory system (HERMES NOT-COPY #6). **Trade-offs accepted.** Some tasks would benefit from an `explore`-style read-only subagent (OpenCode) in V1; declined to protect scope. Fresh-context child sessions (both references) will be the model when it comes — with Hermes' "no parent context" weakness noted for correction. **V1 scope note.** Matches CLAUDE.md §23's NOT-V1 list exactly. --- ## ADR-17 — Anti-scope: no multi-provider, no daemon split, no plugins, no Docker/SSH **Decision.** Reaffirmed exclusions: no multi-provider machinery; no daemon/server split (in-process event bus — OpenCode's worker/RPC split noted as unnecessary for a V1 single client); no plugin system; no Docker/SSH. **Evidence.** Each exclusion is priced by a reference. Multi-provider: Hermes' largest complexity driver (HERMES §10.4); OpenHands' 2,300-line adapter (OPENHANDS POORLY #5). Server split: OpenCode runs client/server *in one process* via worker RPC — proof the layering matters, not the socket; its own analysis concludes "keeping the kernel behind an internal typed API/event boundary preserves KHAELOR's clean layering without daemon complexity" (OPENCODE §2.1, ADOPT #12). OpenHands' agent-server (~25 FastAPI routers, PyInstaller specs) is "pure liability for a terminal-native tool" (OPENHANDS §9). Plugins: OpenHands' six extension systems and OpenCode's plugin-slot sidebar are both flagged premature-for-V1 in their analyses. Docker/SSH: Hermes and mini both show environments bolt onto a thin exec seam later (ADR-13 preserves it). **Trade-offs accepted.** **⚠ Concern (recorded honestly):** OpenCode's worker split delivers a real property KHAELOR gives up — "a render-thread stall never blocks tool execution and vice versa" (OPENCODE §2.1). In one Node process, heavy rendering and tool I/O share the event loop. Mitigations: the bounded live-render region and 16 ms coalescing (ADR-14) keep render work small; the internal bus/API boundary is kept clean so moving the engine into a `worker_thread` later is a packaging change, not a rewrite. If Phase 8 latency measurements show contention, that is the sanctioned escape hatch — measure first (CLAUDE.md §18). **V1 scope note.** `khaelor serve` / remote attach / IDE clients are all out; the event bus vocabulary is the only "API". --- ## Summary of flagged concerns | ADR | Concern | |---|---| | ADR-3 | JSONL loses OpenCode's transactional event+projection atomicity; projections must be rebuildable, and long-session replay may need snapshot events. | | ADR-9 | V1 shell-word parsing is weaker than OpenCode's tree-sitter for compound commands and `external_directory` escape detection; mitigated by operator-guard + exact-command fallback; tree-sitter is a planned upgrade. | | ADR-12 | OpenHands' explicit `FinishTool` contradicts stop-inference; Hermes' production evidence supports the chosen gate, but a finish signal remains addable if dogfooding shows ambiguity. | | ADR-14 | `@opentui` Node-without-Bun compatibility unproven; stock Ink historically required forking — hence the mandated spike. | | ADR-17 | Single-process design forfeits OpenCode's UI/engine thread isolation; clean bus boundary keeps a later `worker_thread` move cheap. | --- *Author: Simon-Pierre Boucher · contact@spboucher.ai*