ajna
A terminal coding agent in pure Python. No third-party packages, no build step, no virtualenv. Cold start is about 180 ms.
It runs on free models too: a repair layer absorbs the mistakes small models
make, so gpt-oss:20b on Ollama's free tier completes tasks that otherwise
need a frontier model. Bring your own key for any provider, or run fully local.
Demo
ajna reads the code, finds the bug, and fixes it on its own. See it run on a throwaway sample project — nothing in your own files is touched:
python scripts/demo.py
Install
Clone the repository — there is nothing else to download, and no build step:
git clone https://github.com/AbhiLabs421/Ajna.git
cd Ajna
Install it as a console script (needs only Python 3.8+):
pip install -e .
That puts an ajna command on your PATH. Prefer not to install? Add the
cloned folder to your PATH and run the ajna launcher directly — on Windows
use ajna.cmd, on macOS or Linux symlink ajna.sh as ajna somewhere on your
PATH.
Configure
ajna --setup
This asks for a provider, an API key, and a model, verifies the model answers,
and writes ~/.ajna/config.json. The key is stored in plain text there, so
treat that file as a secret; AJNA_API_KEY in the environment overrides it.
Providers: ollama (Ollama Cloud), ollama-local, openai, anthropic,
groq, together, openrouter. Anything OpenAI-compatible works via
--provider openai --base-url <url>.
Use
ajna # interactive session
ajna "why does the build fail?" # interactive, with a first prompt
ajna -p "summarise src/parser.py" # one-shot, prints and exits
git diff | ajna -p "review this" # reads a piped prompt
Useful flags:
| flag | meaning |
|---|---|
-m, --model |
model for this run |
-c, --continue |
resume the last session in this directory |
-r, --resume [id] |
resume a specific session, or pick from a list |
--plan |
read-only: the agent may look but not change anything |
--yolo |
skip every permission prompt |
-C, --cwd DIR |
run against another directory |
--shell NAME |
powershell, cmd, bash, sh, zsh, or auto |
Inside a session, /help lists the slash commands: /model, /models,
/clear, /compact, /cost, /tools, /undo, /changes, /review,
/mcp,
/shell, /plan, /auto, /ask, /init, /sessions, /resume, /cwd,
/config, /exit.
Choosing a shell
The first interactive run asks which shell commands should go through, and remembers the answer:
Which shell should ajna run commands in?
Only the one you pick is used.
1) powershell PowerShell
2) cmd Windows cmd.exe
3) bash bash
4) auto follow this machine's default (powershell)
Pick cmd and every command runs through cmd.exe — no PowerShell, ever. The
choice also goes into the system prompt along with that shell's syntax rules,
so the model writes dir and %VAR% for cmd instead of PowerShell cmdlets.
Change it later with /shell, or per run with --shell cmd.
Tools
| tool | what it does |
|---|---|
read |
line-numbered file read with offset/limit |
write |
full-file write; refuses to clobber a file it has not read |
edit |
exact-string replace, unique match enforced, shows a diff |
ls |
directory tree, skipping .git, node_modules, and friends |
glob |
find files by pattern, newest first |
grep |
regex search across files, fanned out over a thread pool |
shell |
run a command in the shell you chose (see below) |
todo |
track a multi-step plan |
fetch |
retrieve a URL as text |
agent |
delegate a job to a sub-agent with its own context |
Independent read-only tools in the same turn run in parallel.
Sub-agents
agent runs a child agent with its own message history and a restricted
toolset. Its tool output never enters the main conversation — only its closing
report does. Reading nine files to answer one question costs the main thread a
paragraph instead of nine files, which matters most when the model's useful
context is short.
Three roles: explore (read-only research), build (scoped implementation),
review (read-only critique, may run tests). Several launched in one turn run
in parallel. Sub-agents cannot spawn sub-agents.
Undo
Every file is snapshotted before it is changed. /changes lists the turns that
touched files; /undo reverts the last one, /undo #2 a specific one. Undo
works per turn, not per edit, because that is the unit people think in. It
does not touch git, so it works outside a repository and never disturbs staged
work. Snapshots live in ~/.ajna/checkpoints/ and are pruned automatically.
The model is not told about a revert — say what you undid, or it will assume its earlier changes are still in place.
MCP servers
Configure Model Context Protocol servers in ~/.ajna/config.json:
{
"mcp_servers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_TOKEN": "..."}
}
}
}
Their tools are discovered at startup and registered as <server>_<tool>;
/mcp lists them. Every remote call asks permission, since a server can do
anything. With no servers configured this costs nothing at startup.
Making small models work
A frontier model calls the tools it was given. Smaller models drift, and the harness absorbs it rather than failing the turn:
- Tool-name routing.
exec,bash,str_replace,read_file,searchand about sixty other invented names map onto the real tool. - Argument aliasing.
file_path,old_string,target_file,queryand friends are renamed to what the schema expects; string"50"and"true"are coerced to the right types; unknown arguments are dropped instead of raising. - Diff-marker guard. Replacement text whose lines start with
+/-is rejected before it reaches the file — a real corruption seen in testing. - Syntax check after every write. Python, JSON, XML and TOML are parsed after the change. A write that leaves a file unparseable comes back as an error, not a note: the same warning appended to a successful "Created logstat.py (97 lines)" was read as done, and one run shipped a file with an IndentationError.
- Deletion notice. If an edit removes a function or class that existed before, the result says which — small models replace when asked to add.
- Regex fallback.
grep "total("is not valid regex; it is searched for literally rather than costing a turn.
Measured effect: the same failing-test task that took gpt-oss:120b 81
seconds and left a corrupted file before this layer existed now completes on
gpt-oss:20b — the smallest model available — in 15 seconds, correctly.
Safety
- Writes, shell commands, and network fetches ask before running. Answer
ato save a rule likeshell:git *for next time; rules live in the config file. - Read-only commands (
git status,ls, …) never prompt. - Destructive commands (recursive force-delete, disk formatting, force push,
piping a download into a shell) are refused outright, even under
--yolo. --plandenies every mutation, which is the safe way to let it explore an unfamiliar repository.- Content from files, command output, and fetched pages is treated as data. The system prompt tells the agent to surface instructions found there rather than follow them.
Project context
Drop an AJNA.md at the repository root and it is loaded into every session.
/init writes a first draft by exploring the codebase. AGENTS.md,
CLAUDE.md, and .cursorrules are picked up too, so an existing repository
usually needs no setup.
Context management
Conversations are compacted automatically at 75% of the model's context
window: older turns are summarised by the small model and replaced, while the
recent tail is kept verbatim. /compact forces it early, /cost shows where
you stand.
Sessions are saved to ~/.ajna/sessions/ after every turn, so closing the
terminal does not lose the thread.
Tests
python -m unittest discover -s tests -v
Benchmark
Claims about an agent are worth what their measurement is worth, so there is one:
python bench/run.py -m gpt-oss:120b --repeat 2
Ten tasks, each run in a fresh temporary directory with --yolo -p, each
scored by running code rather than by reading what the model said: fix a bug
the tests catch, wire a feature end to end, rename across files, search and
cite, add tests without deleting the existing ones, edit JSON precisely,
remove duplication without changing behaviour, count files with the shell,
repair a file that does not parse, and answer a question without touching
anything.
That last one matters as much as the rest — an agent that edits when you only asked a question is a liability.
Layout
ajna/
cli.py argument parsing, REPL, slash commands
agent.py the tool loop, parallel dispatch, sub-agents, compaction
provider.py Ollama / OpenAI / Anthropic streaming over urllib
repair.py absorbs small-model mistakes before they cost a turn
checkpoint.py per-turn file snapshots behind /undo
mcp.py MCP client, JSON-RPC over stdio
tools/ read write edit ls glob grep shell todo fetch agent
prompt.py system prompt and project-context discovery
perm.py permission gate and rule matching
session.py session persistence
config.py layered configuration
ui.py colour, spinner, streaming markdown, diffs
Measured results
The benchmark is what settles arguments about whether a change helped. Ten tasks, two runs each, on the two models the free Ollama Cloud tier allows:
| build | gpt-oss:120b | gpt-oss:20b | samples each |
|---|---|---|---|
| v1 — no completeness check | 80% | 90% | 10 (noisy) |
| v2 — "go and verify your work" | 95% | 65% | 20 |
| v3 — harness checks, facts only | 90% | 80% | 20 |
v2 is the row worth staring at. Telling the model to go and verify lifted the larger model and wrecked the smaller one: read as an instruction to keep working, a weak model edits files that were already correct — across those runs it dropped a key from a JSON file, produced JSON that no longer parsed, and deleted a test it had been told to keep. Mean time per task doubled, from 16s to 33s.
So v3 inverts it. The harness runs the checks itself — does every changed file still parse, is any removed name still referenced somewhere — and speaks only when it has a specific fact to report. When the checks come back clean the turn simply ends, with nothing said.
Read the numbers honestly. v2 → v3 on the small model, 65% → 80% at equal sampling, is a real gain from the redesign. Against v1 it is not: v1's figures come from a single run of each task, and 85% aggregate for v1 versus 85% for v3 is well inside the noise those sample sizes carry. What v3 buys that shows clearly is consistency — the spread between the two models narrows from 30 points to 10, which is what you want from scaffolding that is supposed to be model-agnostic.
The remaining v3 failures are precision failures — three of the four are the model not following an explicit instruction it was given and warned about, not the harness losing information. That is a model ceiling, and no amount of scaffolding moves it.
The general lesson: scaffolding that asks a model to do more is not free, and only measurement tells you which way it cut.
Comparing against a frontier model
bench/human_run.py lays out the same ten tasks for an agent that is not
ajna, and scores the result with the same verifiers:
python bench/human_run.py setup /tmp/run # writes the tasks, prints the prompts
# ... another agent does the work ...
python bench/human_run.py verify /tmp/run # same scoring as ajna gets
Claude Opus 5, driven through the Claude Code harness, scored 10/10 on it.
That number should be read with more suspicion than it will invite:
- The scorer wrote the tasks. Opus authored these verifiers and knew exactly what each one checks. That is contamination, and it is the single biggest reason to discount the result.
- It is a different harness. This is Opus inside Claude Code, not Opus inside ajna. The pair is what was measured, not the model. Opus driven through ajna would be a separate, unmeasured number.
- It had already seen the failure modes. These exact tasks had been run against gpt-oss twice in the same session, so the traps were known.
- n = 1 per task, the same small-sample weakness called out above.
So it is not a clean win over anything. What it does establish is narrower and still useful: every one of these tasks is solvable, and the ajna failures that remain are the model missing an instruction it was given, not the harness losing information. A clean comparison needs Opus run through ajna, on tasks nobody involved has seen:
python bench/run.py --provider anthropic -m claude-opus-4-5 --repeat 2
That has not been run — this machine has no Anthropic key.
Building a whole project from a spec
trial/ holds a harder test than the task benchmark: a spec, and three
independent implementations of it.
trial/SPEC.md describes logstat, a Common Log Format analyser with
filters, deterministic tie-breaking, a JSON contract and defined exit codes.
trial/acceptance.py checks 34 properties of any implementation purely
through its command line. Both were written before any code existed.
Spec compliance. Opus 5, ajna with gpt-oss:120b, and ajna with gpt-oss:20b each scored 34/34 on the first attempt. On this axis the model genuinely does not matter.
Behaviour past the spec. trial/differential.py feeds every
implementation the same malformed input and reports where they disagree. Six
cases diverged. Four were real defects in the generated code: an unanchored
regex that accepted corrupt lines, a negative byte count that reduced the
total, a crash printing non-ASCII under a redirect, and a hard failure on one
undecodable byte.
Two of those are Windows encoding traps — the same class of bug that ajna
itself shipped with until the benchmark caught it. That is knowledge the
harness has and the model does not, so it now goes in the system prompt as
exact code to copy. Stating it as a judgement instead ("decide what should
happen on bad bytes") made things worse: a small model applied half of it,
adding encoding= without errors=, converting a working case into a crash.
Concrete beat advisory, measurably.
The two remaining divergences — whether a quoted request that breaks out of its quotes is malformed, and whether a negative size is — are judgement calls the model has to make. No harness check decides those.
Run-to-run reliability, which is the honest weak point. Six runs of the same prompt with gpt-oss:20b scored 34, 34, 32, 33, 27 and 0 out of 34. The zero shipped a file with an IndentationError. ajna's syntax guard had already detected it and said so — but inside an otherwise successful "Created logstat.py (97 lines)", which reads as done. A write that leaves a file unparseable is now an error result, not a footnote. Four runs since: 34, 32, 33, 33. Small sample, and the claim is correspondingly small: the catastrophic mode is gone, the typical run is unchanged.
Scoring note: acceptance.py reports out of all 34 checks even when a run
dies early. It used to report only the checks it reached, which scored a run
that crashed immediately as 0/1 rather than 0/34.
Metadata
Release files for ajna-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ajna_cli-0.1.0.tar.gz | 76.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ajna_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 145.2 kB
Release files / ajna_cli-0.1.0.tar.gz
| Download URL | ajna_cli-0.1.0.tar.gz |
|---|---|
| Size | 76.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fff8421cfec983a2fcebb2c46d372b3d1c572c2a69556397920724faa87222a9
|
|
BLAKE2b-256 checksum How to use checksums |
b2fbd6f86d3f64ff5f6dff6468739b27216585e57b795cbbeb7efdc9109364a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.2
|
Release files / ajna_cli-0.1.0-py3-none-any.whl
| Download URL | ajna_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 69.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ca561f003a5ac6bf98e6dfe3c7deb0b1389c730379b73d73c64579a296bd3b61
|
|
BLAKE2b-256 checksum How to use checksums |
4d01471d83c8bcc0664f47ced148122d89fa812e76e7e59ac0e2096555719e83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.2
|
Security for ajna-cli 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.