A pure-Python AI coding agent for your terminal, powered by DeepSeek V4.
Subagent-First · Context is Currency · Terminal-native
g023 Code reads your project, searches it, searches the web, runs commands, fetches pages, and looks at images — while telling you exactly what it is doing and what it just cost. Data-heavy work is delegated by default: a subagent reads the file in an isolated context and what comes back to the orchestrator is a structural summary plus a symbol → line-range map. When the summary is not enough, the orchestrator asks for an exact line range and gets verbatim source — so "summary first, raw on request" is the design, not "never raw".
That trade is a real trade, and it is worth being precise about which half of it is measured. The call counts below are asserted by the test suite. Whether delegation is cheaper overall is not: it depends on how many turns the file's text would otherwise have sat in context, which no test here can know. See break-even for the arithmetic and its assumptions.
It talks to DeepSeek exclusively through the Responses API, which is what makes DeepSeek's own native web search a first-class tool of the loop rather than a search provider bolted on the side.
Startup banner, live tool trace, and a permission prompt before anything runs a command. Captured on 1.3.3 from sanitised paths — see media/README.md.
- Quick start
- What you see while it runs
- Configuration
- Architecture
- What delegation costs
- Drift signals
- Slash commands
- Verbosity, context & cost
- Web search (native)
- Vision (via Ollama)
- Fetching web pages
- Design principles
- Tests
- Extending
- License
Requirements: Python 3.11 or newer, and a DeepSeek API key.
Drop the folder anywhere you like and run the installer inside it:
cd /path/to/g023-code
./installer.shWindows
cd C:\path\to\g023-code
installer.batIt finds a Python 3.11+, builds a .venv in the folder, installs only the
dependencies you are actually missing, asks once for your API key, writes a
default config.json, and puts g023 on your PATH. Every step checks what is
already true first, so running it again is safe — it is also how you repair a
half-finished setup or add the optional extras later.
Installer options
| Linux / macOS | Windows | Effect |
|---|---|---|
--yes |
/y |
Non-interactive; take the recommended default everywhere |
--key sk-… |
/key sk-… |
Write the key into K.dat instead of prompting ($DEEPSEEK_API_KEY is used if set) |
--with-optional / --no-optional |
/optional / /nooptional |
Decide the extras (vision preprocessing, browser-grade fetching) up front |
--no-venv |
— | Install into the current interpreter instead of a venv |
--no-path |
/nopath |
Do not touch ~/.local/bin, your shell rc, or the user PATH |
--uninstall |
/uninstall |
Remove the launcher shim and PATH entry (the folder, .venv, K.dat and config.json stay) |
Then launch from whatever project you want it to work on:
cd ~/some/project
g023Doing it by hand instead is four commands — pip install -r requirements.txt,
your key on the first line of K.dat, then /path/to/g023-code/g023.sh (or
g023.bat) from a project folder. The launchers prefer .venv/ in the install
folder when one exists, and set two environment variables for you:
| Variable | Meaning |
|---|---|
G023_HOME |
The installation folder — where K.dat and config.json live |
G023_PROJECT_ROOT |
The directory you launched from — your project |
A per-project .g023/ folder holds the SQLite cache, your input history, and —
since v1.3.3 — backups/: every WriteFile overwrite snapshots the previous
version there (mirroring the file tree, newest few kept per file) so a bad
write is one copy away.
prompt_toolkit is listed as a dependency but is not strictly required: with it
you get tab completion, persistent history, and a live status bar pinned to the
bottom of the terminal. Without it the input line falls back to a plain prompt
and everything else behaves identically.
The point is to never wonder what it is doing, or what it just cost.
Tool traces say what happened (708 lines, 9 classes, cached), not what was
sent. The status line after every turn carries the numbers that only mean
something together — context used, cache hit rate, cost of this turn, cost
of the session — because on V4 Flash a cached input token is billed at $0.007/M
off-peak against $0.22/M for an uncached one, and both double during surge
hours (peak times are 09:00–12:00 & 14:00–18:00 Beijing time). The hit/miss
gap is DeepSeek's price sheet, not something this harness achieves; what the
harness does is avoid breaking the prefix so more tokens fall on the cheaper
side.
Costs shown are computed locally from the token counts the API returns and the
price table in SPEC.md §3.2. They are an estimate of your bill, not
a reading of it.
Anything you type that does not start with / is a prompt. Everything else is
a slash command.
/settings shows every setting in one place and marks which ones persist;
/tools shows what the agent may do and what it has done this turn. Both are
editable from inside the session, and the ·saved keys are written to
config.json next to K.dat — the global default that follows you across
projects — or, if the project has one, to the project's .g023/config.json.
Settings live in two layers:
- Global —
config.jsonnext toK.dat. Applies to every project. - Project —
.g023/config.jsonin the project folder. When present it overrides the global file for this project only.
/config show both files and which scope is active
/config global promote the current settings to the global default
(new projects inherit them)
/config save project pin the current settings to this project
/config save save to the active scope
/config clear drop this project's override; global applies again
So you can tune settings while inside a project, then run /config global to
make exactly those settings the default for every future project — even when a
project file would otherwise mask them.
Defaults out of the box: Flash for the orchestrator and subagents, thinking on at high effort, verbosity low, auto-compaction on at 85% of the window, acting tools set to ask, and vision disabled. Nothing that writes, runs, or leaves your machine happens without a prompt.
Everything — orchestrator, subagents, compaction — goes to a single endpoint:
POST /responses. That is a deliberate choice, not an implementation detail. It
is the only DeepSeek endpoint that exposes the server-side web_search tool, so
using it everywhere is what lets web search be part of the loop.
The Responses API does not take a list of chat messages. It takes items — and the model's own output items go back into the next request verbatim:
messageitems (your input; the model's narration and its final answer)reasoningitems — echoed back unmodified so the model keeps its own chainweb_search_callitems — the server's record of a search it ranfunction_call/function_call_outputpairs, one per tool call
Two rules fall out of that, and every part of the program respects them. A
function_call with no matching output is a hard 400 in either direction, so
compaction, truncation and error recovery all repair pairing before the next
request. And items are echoed byte-identical, because rewriting them both breaks
the prefix cache and desyncs the model's reasoning.
- Orchestrator (
deepseek-v4-flash) keeps only high-level reasoning, tool schemas, and compact summaries. - Subagents handle data-heavy work in isolated contexts:
FileReader→ structural metadata, a symbol → line-range map, and a 2–4 sentence summary, cached by content hashSearcher→ metadata-first grep results, pure local Python — no API callExplore/Plan→ isolated reasoning with thinking modeVision→ an Ollama daemon, local or remote (DeepSeek V4 Flash is text-only)
- Native
web_searchruns server-side inside the model's turn — no client executor, no provider chain - SQLite cache → a repeated read of unchanged bytes is a local lookup and costs no API call
- Prefix-cache-friendly: instructions and tool definitions stay static
- Thinking mode with a
reasoning_effortdial (low / high / max, or off — which maps toreasoning: {"effort": "none"}, the only thing/responsesactually honours)
One model. deepseek-v4-flash is the only model sent to the API, in every
role. /model refuses deepseek-v4-pro because calls to it on /responses
returned an error when this was last checked, and refusing with that reason
attached beats handing you a model every call fails on. If DeepSeek has since
enabled it, that refusal is stale — it is a hardcoded list in config.py, not a
capability probe. Vision is the only other exception, and it does not use the
DeepSeek API at all.
The presentation and command layers are deliberately separate from the agent:
| Module | Responsibility |
|---|---|
commands.py |
The command catalogue as data — one source of truth shared by /help, tab completion, and "did you mean". A command cannot be completable but undocumented. |
ui.py |
Theme, glyphs, gauges, and the compact renderings of tool calls and their results. Falls back to ASCII on terminals that cannot draw box characters. |
prompt.py |
The input line and the shared numbered picker. Degrades cleanly without prompt_toolkit. |
ollama_client.py |
Host resolution, reachability probes, model discovery, and vision inference against any daemon. |
See SPEC.md for the full specification.
Delegating a read is not free — it is a second API call, and sometimes it is the wrong trade. Here is what is actually true, separated by how well it is known.
Measured. These call counts are asserted in tests/test_call_accounting.py,
so they are checked rather than remembered:
| Operation | API calls |
|---|---|
SearchContent, any size |
0 — pure local regex |
ReadFile, Python under 12k chars, no focus |
0 — summarised from the local AST |
ReadFile, same bytes again (cache hit) |
0 |
ReadFile with start_line/end_line |
0 — returns verbatim source |
ReadFile with raw=true |
0 — returns the whole file verbatim (the FileReader never needs the API for this) |
ReadFile, first read of a non-Python file, a file over 12k chars, or any focused read |
1 |
Arithmetic, given assumptions. A 40 KB file is roughly 10k tokens. Inlined into the orchestrator, it is re-sent on every subsequent turn — cheaply, if it stays in the cached prefix, at $0.007/M (regular rate). Summarising it costs one Flash call (~10k in, ~300 out ≈ $0.0024 at regular miss rates) and leaves ~300 tokens in context instead of 10k. Which wins depends on how many turns follow and whether the prefix survives them, and those depend on your session. Delegation is not unconditionally cheaper; it is unconditionally smaller, and smaller context is what keeps the model on task and the window from filling.
Not measured. Nobody has run a controlled A/B of end-to-end session cost. The flag to do it exists:
G023_READFILE_RAW=1 g023 # ReadFile returns raw content, no subagentWith it set, whole-file reads return the file's bytes and cost zero API calls —
the baseline a plain agent loop would have. Run the same task script with and
without it and compare /cost. Until someone does that on their own codebase,
the honest claim is the one above: fewer tokens in the window, an extra call to
get there.
When the summary is not enough. The known weakness of summary-first is that a summary can silently drop the one detail that mattered, and nothing detects that. What exists instead is a cheap way out: every Python summary carries
"symbols": {"Orchestrator.run_turn": [412, 498], "parse_reply": [199, 262]}with line numbers taken from the AST (decorators included), not from the model.
Escalating is then one targeted ReadFile with those two numbers, which returns
verbatim text and costs no API call. That turns "the summary was too thin" from
a re-read of the whole file into a precise second look. It does not turn it into
something the orchestrator notices automatically — that detector does not exist.
For editing, a summary is never enough. A summary omits code bodies, and
WriteFile replaces the entire file — so a write reconstructed from a summary
silently truncates the file. That is exactly how config.py got truncated once.
Two safeguards now cover it:
ReadFile raw=truereturns the whole file verbatim (capped at 200k chars), so the orchestrator can hold the exact bytes it is about to replace. The tool schema and the system prompt both say to use it before every edit.- Every
WriteFileoverwrite first snapshots the previous version to.g023/backups/<path-to-file>/<timestamp>-<hash>.bak, keeping the newest few per file, so even a bad write is one copy away.
The client does not validate responses on purpose: an unknown field has to reach
the model without a client release. The price is that a renamed field fails
quietly — output_text returns "", the model looks silent, and nothing
raises. Silent degradation, not a crash, is the realistic worst case here.
/signals (alias /drift) shows the three cheapest observations that would move
first:
| Signal | What it catches | What it cannot tell you |
|---|---|---|
Unknown item type in a response |
An additive API change, at the earliest moment it is visible | Whether it matters |
Empty output with no incomplete_reason |
What a renamed content field looks like from outside | Whether the model simply said nothing |
| Prefix hit rate diffed against previous days | Anything that started breaking the prefix | Whether the cause is your prompt, the tool list, or the server |
The hit rate is stored per day in .g023/cache.db and compared against the
trailing baseline, so the comparison survives restarts. It needs two prior days
before it will call anything a baseline, and says so until then. /cache clear
does not erase it — it is a record of what happened, not cached data.
None of these is a diagnosis. A model behaviour change, a schema change, and drift in your own prompts all present identically from here: same call, worse output, no error. These signals make the change visible and date it; they do not separate the causes.
Commands that take a fixed set of options open a picker when typed bare, so
/vision is as usable as /vision qwen3.5:2b — there is nothing to memorise.
/help <command> explains any one of them in full.
Session
| Command | Action |
|---|---|
/help [command] |
List commands, or explain one in detail |
/status |
One-screen dashboard: model, context, cost, vision, cache |
/clear |
Reset the conversation and usage counters |
/exit |
Quit |
/config [status|save|global|project|clear] (alias /cfg) |
Show or manage the global vs per-project config scopes |
Model
| Command | Action |
|---|---|
/model [flash] |
Show or set the orchestrator model — flash is the only one the Responses API serves |
/thinking [low|high|max|off] |
Set reasoning effort, or turn thinking off |
/verbose [low|mid|high] |
How much detail to print while working |
/steps [n|off|default] |
Cap the tool steps one turn may take before it gives up (default 255; off for no cap, with progress printed every 10 steps) |
/max-tokens [n|off|default] |
Cap how long one reply may be (default off — no budget is sent, so the model's own maximum is the only ceiling) |
Context & cost
| Command | Action |
|---|---|
/compact [focus] |
Summarise the history (micro = free local pass, auto on|off) |
/context |
Break down what is occupying the context window, by role and size |
/cost |
Token usage and spend, split by cache hit/miss, with a per-turn sparkline |
/settings [save|reset] |
Every setting, marking which persist; save or restore defaults |
/signals (alias /drift) |
Prefix hit rate against its own history, unknown item types, unexplained empty responses |
Tools
| Command | Action |
|---|---|
/tools |
List tools, their permission level, and how often each ran |
/tools <tool> allow|ask|block |
Change one tool's permission |
/fetch <url> |
Fetch a URL yourself, with the cache/fresh prompt |
/fetch status |
Report how closely fetches imitate a real browser |
/cache [stats|web|clear] |
Inspect and purge the local SQLite caches |
Vision
| Command | Action |
|---|---|
/ollama |
Status: host, version, latency, installed model count |
/ollama host <addr> |
Point vision at another machine (tested before saving) |
/ollama host default |
Fall back to OLLAMA_HOST, then localhost |
/ollama models |
List installed models, sizes, quantisation, vision capability |
/ollama test [model] |
Run a real inference — proves the whole path, not just the port |
/ollama ps |
What the daemon currently holds in VRAM |
/vision |
Interactive picker of installed vision models |
/vision <model> · /vision off |
Enable a specific model, or disable |
/surge |
DeepSeek surge pricing — status, notify, auto-pause around peak |
/or |
OpenRouter vision — key, models, rate-limit tier, usage |
Work
| Command | Action |
|---|---|
/goal <text> |
State a high-level objective; runs one turn at max reasoning effort with no step cap |
/verbose has three levels, persisted to config.json:
| Level | Shows |
|---|---|
low (default) |
The answer, plus a one-line trace of every tool call |
mid |
↑ plus each tool's outcome and duration, and the per-turn token/cost line |
high |
↑ plus the orchestrator's reasoning excerpts and per-iteration token counts |
/context answers the question /cost cannot: what is filling the window.
If tool results dominate, /compact micro clears the stale ones for free; if
it is real conversation, /compact summarises it with the cheap model in an
isolated context. The command tells you which of the two applies. Compaction
also runs automatically once the context passes compact_threshold (85%) —
turn that off with /compact auto off.
Every API call — orchestrator and subagents — reports its token usage to a
single tracker, so /cost and /settings show the cache-hit / cache-miss /
output split per model and price it from the SPEC §3.2 table.
Web search is DeepSeek's own, not a search API wired in behind it. There is
no provider chain, no key to supply, no rate limit to nurse, and no
/websearch command — because there is nothing on this side to configure.
The entire integration is one line in tools/schemas.py:
WEB_SEARCH_TOOL = {"type": "web_search"}That marker is appended to the tool list. No name, no parameters, no executor. The server takes it from there.
The search happens inside the model's turn, on DeepSeek's infrastructure, before the response comes back. g023 never sees a tool call for it and never returns a result for it. What arrives instead is a record of what the server did, which shows up in the trace like any other tool:
● searching the web…
✓ web_search deepseek responses api web_search tool, deepseek v4 pricing
✓ web_search read platform.deepseek.com/docs/…
A few consequences are worth stating plainly:
- It never asks permission. There is no moment at which g023 could
interpose a prompt — the search is over by the time the response exists. So
web_searchhas no entry in/tools. Giving the model the tool is the decision; each individual search is not. Everything that does leave your machine under g023's own control —FetchUrl— still asks every time. - It can search several times per turn, following up on what it finds.
- Findings persist. The server's
web_search_callitems are carried in history like everything else, so what it learned survives across tool round-trips, across turns, and across/compact. - Slow searches are visible. The server can be out on the web for a while
without emitting anything, so g023 draws a live
searching the web…line rather than letting it read as a hang.
Just ask — what changed in the DeepSeek Responses API this month? — and it
searches when it decides it needs to.
DeepSeek V4 Flash is text-only, so image analysis is delegated to a vision
backend. Three are supported, toggled with /vision backend:
| Backend | Key | Default model | Notes |
|---|---|---|---|
deepseek |
K.dat |
deepseek-v4-flash-vision-exp |
DeepSeek's own multimodal model; billed at text prices. Default. |
openrouter |
OK.dat |
google/gemma-4-26b-a4b-it:free |
Models from ./OR_VISION.json, rotated on rate limits. |
ollama |
— | local daemon | A local/remote Ollama vision model (/ollama host points at it). |
Vision is disabled by default — enable it once and the choice persists.
/vision # configure the active backend (or pick an Ollama model)
/vision backend deepseek # DeepSeek's deepseek-v4-flash-vision-exp (K.dat key)
/vision backend openrouter # OpenRouter (OK.dat key)
/vision backend ollama # local/remote daemon
/vision endpoint responses # deepseek wire format: responses | chat_completions
/vision off # disable (the default)
/vision status # show the current setting
- DeepSeek backend —
deepseek-v4-flash-vision-expis an experimental multimodal model that runs on the sameK.datkey and bills at exactly the token rates ofdeepseek-v4-flash. The wire format is chosen per endpoint:/responses(the program's native format, default) or/chat/completions. - OpenRouter backend — the key lives in
OK.datnext toK.dat. Put a JSON array of model ids in./OR_VISION.json(project folder) to set the failover list; when one model returns HTTP 429 the next is tried. Free:freemodels are capped at 50 requests/24h for accounts under $10 of lifetime credits, and up to 1000/24h once you've bought $10+ credits — tell g023 which tier you're on with/or tier free|credits. - Ollama backend — only models the daemon reports as
vision-capable are listed; sizes are shown so you can stay inside your VRAM budget. The daemon may be local or on another machine (/ollama host). - While vision is disabled the
AnalyzeImagetool is not even offered to the orchestrator, so it never proposes a call it cannot fulfil. - Images are downscaled to 1024 px on the longest edge (needs
pillow; skipped if not installed) and answers are cached by image hash + question + backend + model, so re-asking the same question about the same image is a local SQLite lookup rather than another inference.
Once enabled, just ask: analyze screenshot.png — what's the error?
/or status: key, tier, model list, today's usage
/or model <id> set the vision model (or `default` to reset)
/or models reload ./OR_VISION.json
/or tier free|credits 50/24h vs 1000/24h ceiling
/or usage requests used today vs the ceiling
DeepSeek moved to tidal pricing on 2026-08-17: peak hours are 09:00–12:00 and
14:00–18:00 Beijing time (UTC+8), off-peak is exactly half of peak. g023
converts the windows to your local clock, shows every time in both
AM/PM and 24-hour format, and can notify you and/or pause usage around them.
/surge status also counts down — how many minutes to the next surge and
how long that surge will last — and shows the regular vs surge (2×) prices
for each model, straight from ds_pricing.json / ds_times.json.
/surge status: current state, local peak windows (AM/PM + 24h),
countdown to the next surge and its duration, prices
/surge notify on|off banner when surge pricing starts/ends (default on)
/surge on pause usage from 5 min before peak until 5 min after it
ends, then resume automatically
/surge buffer <min> widen the pause (default 5 min each side)
While a pause window is active the CLI waits it out instead of reading input, so
no requests are sent at double price; Ctrl-C bypasses it for that pass. The
toolbar shows SURGE 2× while peak hours are active.
The daemon does not have to be local — the usual reason to move it is that the GPU is somewhere else.
/ollama host 192.168.1.50 # bare IP: :11434 is appended for you
/ollama host gpu-box:11434
/ollama host https://ollama.example.com # behind a TLS proxy, port left alone
/ollama host default # back to OLLAMA_HOST, then localhost
Addresses are forgiving: a missing scheme becomes http://, a missing port
becomes :11434. The host is tested before it is saved, and if it does not
answer you are told why — connection refused reads differently from a timeout —
and asked whether to save it anyway.
Precedence, highest first: the vision_host setting (what /ollama host
writes) → the OLLAMA_HOST environment variable → http://localhost:11434.
/ollama always shows which one is in effect.
Three commands make a remote setup diagnosable rather than a guessing game:
| Command | Answers |
|---|---|
/ollama |
Is anything listening, what version, how far away (latency), how many models |
/ollama models |
What that machine actually has — names, sizes, quantisation, vision capability |
/ollama test |
A real inference round-trip: g023 sends a generated image and checks the answer. This proves the whole path — encoding, transport, and the model genuinely being image-capable — rather than just that the port is open. |
Warning
For a remote daemon to accept connections it must be bound to the network
(OLLAMA_HOST=0.0.0.0:11434 ollama serve) and the port must be open.
Ollama has no authentication. Anyone who can reach that port can use the
GPU and read whatever is sent to it — keep it on a trusted network, behind an
SSH tunnel (ssh -L 11434:localhost:11434 gpu-box, then leave the host at
default), or behind an authenticating reverse proxy.
Distinct from web search: that runs on DeepSeek's side and
you never see the request. FetchUrl is g023 reaching out from your machine,
to a URL you or the model named — so it is held to a different standard.
FetchUrl reads a page and hands back readable text rather than raw HTML, in
keeping with the context budget. Two things make it different from a plain
requests.get.
It asks first, every time. A fetch leaves your machine and touches someone
else's server, so it is never on the allow list. When a cached copy exists the
prompt is a three-way choice rather than yes/no:
Permission required to fetch https://example.com/pricing
engine: curl_cffi · requested mode: auto
Cached copy available — fetched 12m ago, HTTP 200, 48,210 chars
Use cached copy, fetch fresh, or deny? [c/f/d] (c):
Your answer overrides whatever the model asked for. The model can also request
a mode itself — auto (cache if younger than max_age), fresh, or cache
(never touch the network) — and cache mode is the cheap way to re-read a page
at a larger max_chars without a second request.
It presents itself as an ordinary browser. Requests carry a real browser's
headers in the order that browser sends them, negotiate HTTP/2, and keep
cookies per domain in .g023/cookies.json so a second visit looks like a
returning user rather than a stranger. Requests to the same host are spaced out
instead of fired back-to-back. Nothing identifies g023.
The weak point is TLS: Python's handshake has a recognisable fingerprint, and a
UA claiming Chrome over a Python TLS stack is a contradiction that fingerprint
checks notice. Installing curl_cffi fixes it properly — it reproduces Chrome's
actual handshake:
pip install curl_cffi # then: /fetch status| headers | HTTP/2 | TLS (JA4) | |
|---|---|---|---|
curl_cffi |
Chrome order | yes | t13d1516h2… — matches Chrome |
httpx + h2 |
Chrome order | yes | generic Python |
httpx alone |
Chrome order | no | generic Python |
/fetch status always tells you which of these you are on, and the JA4 column
reports what curl_cffi impersonates rather than a measurement — point /fetch https://tls.peet.ws/api/all at it to see your own fingerprint on the wire.
What is not claimed: that this gets you past any particular bot defence. Those systems weigh behaviour, IP reputation and history alongside the fingerprint, and they change. A handshake that matches Chrome removes one obvious tell; it is not a guarantee about any site, and any site may block you anyway.
Note
There is no JavaScript engine. Pages that build their body client-side
return the shell. Use a server-side check like tls.peet.ws/api/all to see
what your requests actually look like on the wire.
- Context is currency — keep raw files and search dumps out of the orchestrator unless they were asked for by line range.
- Subagents are the treasury — heavy I/O happens in isolated, minimal contexts, and the escalation path back to verbatim source stays open.
- Cache everything cacheable — file hashes, vision Q&A, fetched pages.
- Do it off-API where it is free — search the filesystem locally, see with a local GPU, and spend tokens only on reasoning.
- Thinking mode is a dial, not always-on max.
- Say what happened, not what was sent — and always say what it cost.
- Claim only what is checked. Numbers in this documentation are either asserted by a test, quoted from a published price sheet with attribution, or labelled as an estimate. Where something is not measured, it says so.
pip install pytest && python3 -m pytest tests/ -qNo plugins beyond pytest — async tests run through a small hook in
tests/conftest.py, and every test points G023_PROJECT_ROOT at a temporary
directory, so a run never reads your real cache and never touches the network.
| File | What it holds to account |
|---|---|
test_call_accounting.py |
The API-call table above, counted against a stub client |
test_file_reader.py |
Symbol ranges are exact and in-bounds; truncation is declared; local AST facts outrank the model's |
test_drift_signals.py |
Each signal fires on the shape that matters and stays quiet on ordinary turns; hit-rate history survives restarts |
test_history_integrity.py |
Tool-call pairing survives rollback and repair; output items are echoed back byte-identical |
test_usage_accounting.py |
Cost arithmetic, both usage spellings, worst-case assumption when the split is unreported, command/handler contract |
What these do not cover: anything requiring the real API, end-to-end session cost, or whether a summary was good enough for the question asked.
- New slash command — add a
Command(...)toCOMMANDSincommands.pyand a matchingcmd_*method onCLI. Help text, tab completion, and "did you mean" pick it up automatically;check_handlers()fails at startup if the method is missing, so the two halves cannot drift apart. - New tool — add a schema in
tools/schemas.pyplus an executor or a subagent route. Give it an entry inui.TOOL_STYLESand a case inui.describe_call/describe_resultso its trace reads like the others. - Vision backends —
ollama_client.pyandsubagents/vision.py; the router dispatchesAnalyzeImagethere. - Permissions —
permission_default(allow|ask|block) sets the starting level for the tools that act:Bash,WriteFile,Agent,AnalyzeImage. Read-only tools stayallow, andFetchUrlstill asks unless the default isblock. Per-tool overrides live intools/registry.pyand/tools.
Environment overrides:
| Variable | Effect |
|---|---|
G023_HOME |
Installation folder — where K.dat and config.json live |
G023_PROJECT_ROOT |
The project being worked on |
OLLAMA_HOST |
Vision daemon, when vision_host is unset |
G023_ASCII=1 |
Force plain-ASCII output on terminals that mangle box drawing |
G023_READFILE_RAW=1 |
ReadFile returns raw content instead of delegating — the A/B baseline described in What delegation costs |
G023_WRITE_BACKUP=0 |
Disable the automatic .g023/backups/ snapshot before WriteFile overwrites |
G023_WRITE_BACKUP_KEEP=5 |
How many backup versions to retain per file (minimum 1) |
MIT — built for the g023 workflow.



