alan-code
v0.1.4
Published
Alan coding harness — local coding agent CLI and TUI
Readme
alan-code
Standalone Alan coding harness: the Alan engine as the agent runtime (local server + @opencode-ai/sdk), plus a host that launches Alan's own terminal UI.
The interactive UI is @alan-ai/tui (packages/alan-tui) — Alan's Ink/React TUI, spawned as a
child process so Ink gets the real TTY, compiled builds can ship a sibling alan-tui binary, and
TUI restarts do not take down the engine. Feature parity against OpenCode's TUI is tracked in
packages/alan-tui/PARITY.md; read upstream behavior from the gitignored OpenCode clone at
notes/oss-clis/clones/opencode/packages/tui (refresh with bash notes/oss-clis/clone.sh, pinned
to v1.18.32).
Runtime is Bun >= 1.3 for the harness itself; @alan-ai/tui runs on Bun or Node 20+.
Architecture
| Concern | Approach |
| ----------------------------- | ----------------------------------------------------------------- |
| Agent loop / tools / sessions | Alan engine via local server + SDK |
| Product chrome | Alan TUI (@alan-ai/tui) — home / transcript / composer / footer |
| Auth / models | Alan PAT + catalog when available |
| Permissions | Ask-before-edits in the Alan footer → engine permission replies |
Install (npm)
The only supported install channel is npm:
npm install -g alan-code
alanalan is a compiled binary: no Bun and no Node are needed to run it. The published layout
is a small wrapper plus one package per platform:
alan-code wrapper — installs the matching native executable
@alan-ai-hq/alan-code-darwin-arm64 the executable for that platform
@alan-ai-hq/alan-code-linux-x64
@alan-ai-hq/alan-code-windows-x64
…npm reads os/cpu/libc on each platform package and installs only the matching one, so the
others are never downloaded. During installation, the wrapper puts the matching native executable
at its own command path. Launching then needs no interpreter.
The harness and the TUI ship inside that one executable. The TUI is started by re-execing it with
--alan-tui-mode, so it still gets its own process and the real TTY.
The install also pulls opencode-ai, which supplies the Alan engine (~137MB unpacked). That is
expected, and it is larger than the CLI itself.
Bun >= 1.3 is only needed to build the CLI or run it from source, not to use an installed one.
Shipped builds bake the production API endpoint, so no configuration is needed after install.
Running from source instead targets the local API at http://localhost:8400. Point either at a
different backend with ALAN_API_URL (or ALAN_HARNESS_API_URL), which always wins — that is also
how you run an installed binary against a local API.
To build an artifact that defaults elsewhere — a staging CLI, for example — use the build-time
override (note _DEFAULT, deliberately distinct from the runtime ALAN_API_URL):
ALAN_API_URL_DEFAULT=https://staging-api.tryalan.ai pnpm --filter alan-code buildA non-production build prints a loud warning, and the runtime ALAN_API_URL still overrides the
baked value.
Auto-update
The npm-installed CLI checks the registry on startup and installs a newer version in the background:
- Applies on the next restart — a running process keeps the code it already loaded, which is exactly the semantics an npm-only install gives you for free.
- Installs any newer stable release (not just patches). Check interval is 1h.
- Only self-installs when running from
node_modules/alan-code; a checkout or the monorepo never overwrites itself (it prints the update command instead). - Never blocks or fails a run: the check is one registry GET and the install is detached.
| Env | Effect |
| --- | --- |
| ALAN_HARNESS_AUTO_UPDATE=0 | Disable the update check entirely |
| NO_UPDATE_NOTIFIER=1 | Same, for ecosystem familiarity |
| ALAN_HARNESS_REGISTRY | Override the registry (defaults to npm's) |
| CI=1 | Skipped automatically |
alan --version prints the running version; state lives in
<agent-dir>/update-state.json.
Install / run (monorepo)
Bun >= 1.3 is required for the harness process itself.
pnpm install
# Engine platform binary comes from optional `opencode-*` packages via the opencode-ai npm package.
# Alan harness resolves it and shims `opencode` onto PATH (pnpm may skip opencode-ai postinstall).
# If needed: `cd node_modules/opencode-ai && node postinstall.mjs` (approve builds) or rely on the shim.
# Builds two binaries per target: `bin/alan-code` and `bin/alan-tui`.
pnpm --filter "alan-code" build
pnpm --filter "./packages/harness" start
pnpm --filter "./packages/harness" start -- --harness-help
pnpm --filter "./packages/harness" start -- --mode print "list files in cwd"Task routing
Use --route to select an enabled Alan catalog model from the task description:
alan --route --mode print "fix the failing parser test"
alan --routePrint mode routes before starting the engine. The TUI routes the first prompt in each session and
keeps that model for follow-ups. Print mode names the selected model on stderr, and the TUI shows
it in the footer. --model and a manual TUI model choice take precedence. The Alan
API calls Jev through TypeSafe's official System One endpoint; the TypeSafe key stays on the server. If
the decision is unavailable or invalid, the catalog default is used. Set ALAN_HARNESS_DEBUG=1
to see the fallback reason and routing latency in print mode.
Routing is opt-in. It trades a small decision call for cheaper/faster routine models and stronger
models on risky tasks. Recheck live model prices and task outcomes before claiming savings against
the current default; the route evaluation script is scripts/eval-task-router.ts.
For real coding-task descriptions, run bun packages/harness/scripts/eval-task-router-public.ts
from the repository root with local dev API credentials. It draws a deterministic 60-case sample
from SWE-bench Verified, checks
the pinned dataset revision, and writes JSON and HTML under test-output/reports/task-router/.
The report breaks selected tiers, fallbacks, and router latency down by human difficulty. Those
human fix-time labels do not establish which model is correct or whether it completed the task;
the 20-case hand-labelled script remains the routing regression check.
To compare a public run against published per-issue agent outcomes, run
bun packages/harness/scripts/score-task-router-public.ts test-output/reports/task-router/<run>.json.
This checks pinned SWE-Router results from two DeepSeek V4 Flash and two Claude Opus 4.7 runs
per issue. The output is a separate .outcomes.json file. These older models and their agent
harness are not the three current router candidates, so the scores are proxy signals, not
current-model accuracy. See notes/task-router-calibration.md for the interpretation and prompt
iteration gates.
Interactive Alan TUI
pnpm --filter "./packages/harness" start
# optional model override
pnpm --filter "./packages/harness" start -- --model alan/deepseek-v4.1-flashHome screen: logo, workspace/branch, numbered starters, prompt input, footer. Session screen: transcript (tools/diffs), follow-up input, ask-before-edits footer.
Session modes (Tab on an empty composer, or /mode):
- build — ask before edits (default).
- plan — planning only; edits blocked at the engine.
- auto — engine
permission: "allow"(dangerous; same idea as OpenCode--auto/--yolo). Also auto-replies any remaining permission asks. - Tab / Shift+Tab cycle auto / plan / build;
/permissions auto|plan|buildsets a mode explicitly. ALAN_HARNESS_AUTO_ALLOW_EDITS=1starts in auto for automation / print mode.
Dev loop (reload the TUI on save)
pnpm --filter alan-code dev:watchThis is the dev run with ALAN_HARNESS_TUI_WATCH=1: the host spawns the TUI with bun --watch, so
saving a .tsx under packages/alan-tui/src re-renders within a moment and nothing needs re-running.
The engine is a separate process the host owns, so a save costs a re-render and nothing else —
sessions, a turn still streaming and telemetry all survive; only TUI-local state (draft, scroll
position, route) resets, so you land back on the home screen (ctrl+p rejoins a session).
Two things to know:
- Use
dev/dev:watch, notstart.scripts/run.shprefers a prebuiltdist/binary, which runs the sibling compiledalan-tuiand ignores source edits entirely — and--watchdoes not apply to it. - Do not reach for
bun --hot. It re-runs the entry without unmounting React, sorender()runs a second time and two Ink instances share stdin/stdout — stale state in one, new code in the other.
System prompt (baseline kernel)
The harness uses Alan's product kernel (prompts/baseline.md) as the native build and plan
agent prompt. This replaces OpenCode's product identity while keeping its dynamic runtime context:
- available tools (+ snippets)
- runtime guidelines
- project context (
AGENTS.md, etc.) - skills
- cwd / platform / date
- any append-system-prompt text
| Env | Effect |
| --------------------------------------------- | ------------------------------------------------------------------------- |
| (default) | Use prompts/baseline.md (baseline-v3, Alan kernel) + runtime sections |
| ALAN_HARNESS_SYSTEM_PROMPT=0 or off | Disable Alan kernel (engine default prompt only) |
| ALAN_HARNESS_SYSTEM_PROMPT_FILE=/path/to.md | Use a custom Alan instruction file |
Research notes and iteration method: notes/system-prompt-research.md. Eval: scripts/eval-system-prompt.mjs.
Skills, web, task, MCP
| Surface | Default | Notes |
| -------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Skills | On (discovery) | Agent Skills (SKILL.md) discovered by the engine from .claude/skills, .cursor/skills, .agents/skills, and the harness agent dir. |
| web_fetch | On (engine builtin) | URL fetch → text. |
| web_search | On (Alan proxy) | Custom OpenCode tool web_search (not websearch — engine hides that id for Alan) → POST /public/harness/v1/websearch → Parallel. Needs Alan login / ALAN_API_TOKEN. Metered: each search debits the same monthly allowance as token usage, and is capped per user (4 concurrent, 30/min, 300/day). The search mode is a server constant (fast) — the client cannot pick it, so behavior and cost cannot drift between what was tested and what runs. Refusals carry retryable/retryAfterSeconds; a short retryable wait is retried inline. Disable with ALAN_HARNESS_WEBSEARCH=0. |
| task | On (engine builtin) | Nested agent sessions are provided by OpenCode. |
| MCP | On (runtime + Alan entry) | Credential-free alan remote entry auto-registered from the Alan API base (<api>/mcp, OAuth at connect like desktop sync); extra servers come from OpenCode config. ALAN_HARNESS_MCP=0 or profile.mcp=false disables automatic Alan registration. A user-defined alan entry is never overwritten. |
Research write-up: notes/skills-mcp-subagents-web-research.md (historical; see banner at top).
Config
Looked up from cwd (first hit wins):
alan-code.json.alan-code/config.json
Example (examples/alan-code.json):
{
"profile": "default",
"profiles": {
"default": {
"description": "Alan defaults"
},
"no-mcp": {
"description": "Disable automatic Alan MCP registration",
"mcp": false
}
}
}Profiles currently control only automatic Alan MCP registration. Models use --model or
ALAN_HARNESS_MODEL; tools and permissions are owned by the engine.
Auth (Alan backend)
alan authenticates to Alan — there is no multi-provider /login catalog in the CLI.
- Team CLI credentials live in Alan (team settings).
- Prefer Alan PAT / device-code OAuth (
/login,--login); catalog models come from the Alan API. - Local overrides:
ALAN_API_TOKEN/ALAN_HARNESS_TOKEN, plusALAN_HARNESS_API_URLwhen needed.
The harness:
- Stores harness credentials under
~/.alan-code/pi-agent/by default (legacy directory name). Override withPI_CODING_AGENT_DIRif set. - Exposes Alan catalog model ids only (
--list-models,--model <id>). Provider is not a user concept. /loginopens Alan device-code OAuth;/logout/--logoutclears the stored Alan credential (and notes when env tokens still authenticate).- Does not scope
--models alan/*; the catalog is already Alan-only.
Enable/disable models in the harness_models table (API serves enabled rows).
Alan MCP
The harness auto-registers a credential-free alan remote entry (<api>/mcp)
into its engine config, mirroring desktop sync. Authentication stays with the
engine host (OAuth at connect time; /mcps dialog in the TUI) — the harness
PAT is never sent to /mcp. Opt out with ALAN_HARNESS_MCP=0 or
profile.mcp=false; a user-defined alan entry in engine config is never
overwritten. Headless print mode cannot complete browser OAuth, so Alan tools
stay unavailable there until the engine holds a prior grant.
Plan mode, question, and todos
- Search tools: the engine provides its native coding tools.
- Plan mode:
/planor--planenables read-only exploration. The agent writes a durable markdown plan withplan_writeunder.alan/plans/, then asks for approval before editing. - Question:
questionasks the user a multiple-choice clarifying question (interactive UI). - Todos:
todowriteand/todoscome from the engine and Alan TUI.
Dependency
Pinned engine: opencode-ai + @opencode-ai/sdk (see package.json). Platform binaries come from
the opencode-ai optional packages / postinstall shim (see Install / run above).
Assumptions / stubs
- Ask-before-edits: Alan TUI footer ↔ engine permission replies (not a separate host protocol).
- Fuzzy edit / sandbox: not implemented; engine coding tools run as configured.
- Taste / compaction product policy: not implemented.
- If OpenCode SDK/server contracts drift, adapt
src/engine/+ TUI client; keep auth unit-tested without spinning the full agent loop.
Package API
import {
resolveHarnessConfig,
runPrintSession,
startEngineRuntime,
} from "alan-code";