SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
35.5 KB

# 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 khaelorkhaelor. 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/<project-hash>/. 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<ModelEvent>; 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<ModelEvent>) 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_results — 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_ids "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