Writes its own skills

An assistant that writes its own skills when it runs out of them.

It runs on your machine, does real work on it, and when a task needs something it cannot do, it says so, asks, and compiles the missing piece. Each one checked inside a container before it is allowed near your files.

A real session

It asks before it builds, and shows its work

jumabek — a real session

The idea

Every skill is a separate program

JumaBek does not link its abilities in. It starts them as processes and writes a line of JSON to their standard input.

core {"id":1,"method":"execute","params":"{\"method\":\"execute_command\",\"args\":\"ls\"}"}
skill {"id":1,"payload":{"Output":{"Text":"file1.txt\nfile2.txt"}}}
Any language

A skill is whatever speaks the protocol — and that now includes the ones JumaBek writes itself, in Rust, Python or Node.

Nothing to rebuild

Adding a skill means dropping a binary in a folder. The agent itself is never recompiled.

Crashes stay local

A skill that hangs is killed and restarted on the next call. It cannot take the agent down.

Lazy by default

Descriptions are cached, so twenty installed skills cost one millisecond at startup instead of seven hundred.

Self-improvement

It notices what is missing, and asks

Nobody has to tell it to write a skill. It works down a list first — is there already one, would a single shell command do, would a throwaway script do — and only then proposes building something. A minute of compiling has to earn its place. The language is its choice too: Rust when the skill will live for months, Python or Node when the library it needs only exists there.

the loop
chunks arrive from the model rust · python · node

└─ laid out on disk source + manifest + the protocol helper

└─ built in the language's own image 2 cpu · 2g ram ·
  network on

└─ exercised in a container 0.5 cpu · 256m · no
  network · read-only

└─ every declared method called a phantom method
  fails the build

├─ fails → errors go back to the model, attempts
  are counted
└─ passes → built natively, snapshot taken,
  loaded into this session

The validator does not read the code. It starts the skill and talks to it: does it answer, does it name itself correctly, does it survive a bad method and malformed arguments — and then it calls every method the skill claimed to have. A method that answers like one that does not exist fails the build, because declaring a method and forgetting to wire it up is the mistake a model actually makes. A skill that stops answering never reaches your disk.

Intelligence

It is not one model, and it knows which one it needs

Turning a light on and writing a skill are not the same kind of thinking, and paying for them at the same rate is a choice nobody makes on purpose. Three models sit behind three levels. JumaBek moves between them itself, and the core moves it when the reason is measurable rather than a matter of taste.

low

One call and done: turn the light on, note that someone will be late, read a chat. Anything obvious once you have looked.

medium

The default. Several steps, a search and a summary, files, skills chained together, ordinary conversation.

high

Writing a skill. Reading an error nobody understands. Anything that already failed one level down.

a level moving on its own
> write me a skill that reads my
  RSS feeds

· intelligence cc/claude-opus-4-8 · writing a skill always runs at
  the highest level
· rss_parser · built and validated
· intelligence cc/claude-sonnet-4-6 · back to the default level for a
  new task

Writing a skill goes to the top level before the first line of code, and that is the core's decision, not the model's — a cheap model has no way of telling that the skill it just wrote is bad, and the cost of being wrong is a binary that lives on your machine for months. Two unreadable answers in a row, or a build that keeps failing, move it up as well. Work with nobody at the keyboard — a scheduled job, something that arrived through the inbox — starts at the bottom.

Down is free, up costs a sentence

The model drops a level whenever it likes. Going up needs a reason written into the action, and that reason is kept. Explaining yourself every time is a small tax, and it is the right one.

Every task starts again

The level resets when a new task begins, so one hard afternoon does not leave the expensive model running for the rest of the week.

A wrong level is not the task's fault

When a turn fails and the core moves up to retry it, the task's iteration budget is not charged. Otherwise choosing the cheaper level would quietly make it worse.

Counted, not assumed

Every answered turn records the level it ran on, and every move records why. Whether switching earns its keep is a question with an answer, not an opinion.

Name all three under [llm.intelligence] and it is on. Name none and nothing changes — one model, exactly as before.

Each level can also point at its own endpoint instead of sharing one — a local Ollama for low, Ollama Cloud or anything else for medium and high — and its own protocol: OpenAI-shaped (/v1/chat/completions) or Anthropic-shaped (/v1/messages, which Ollama and the real Anthropic API both speak). Where the protocol allows it, JumaBek asks the endpoint for JSON matching the exact action schema instead of trusting the model to remember the formatting rules — falling back to reading a plain-text reply wherever it does not.

Safety

Five things stand between generated code and your machine

An agent that writes its own code runs code that did not exist a minute ago. These are the guards, and each exists because of something that actually went wrong.

Enforced by the core

Dangerous commands stop

Recursive deletes, disk formatting, shutdown, a download piped into a shell — all need your word, whether or not the model thought to ask. Told to skip the confirmation, a model skips it. So the core asks instead.

Before installation

New code is exercised first

No network, read-only filesystem, capped CPU and memory, every capability dropped. Code that hangs, crashes or reaches for the network is caught there.

Reversible

Every install is snapshotted

Rolling back removes a skill that did not exist at that point, rather than merely restoring the files that did. And the rollback itself is snapshotted first.

Intersection, never union

Rights only narrow sideways

When one agent asks another to do something, what the second may do is the intersection of both, never the union. Needing more means asking upward, under a ceiling in config.toml that no decision at runtime can raise — and every answer, yes or no, is written down against whoever asked.

Nothing outlives its parent

Skills cannot leak processes

Each runs inside a group that dies as a unit, so a shell command it started does not keep running after it is gone — even if the agent itself is killed.

Memory

It remembers, and knows what it has forgotten

Everything said is kept in SQLite. The current session is always in context; older ones are searched only when the model asks, through a full-text index that stems both English and Russian — so file finds files, and a word you typed months ago in either language still finds the answer.

recall across sessions
> what did I ask you about
  earlier?

· memory · asked question request task file folder

Yesterday you asked how many files were in the current folder — I counted six.
Before that you had me look for doc.txt and open it from Documents.

When a conversation outgrows the context window, the oldest exchanges are dropped in whole task groups — never half of one, which would leave a result with no matching command — and replaced by a marker telling the model what it can still recall.

Facts it chose to keep sit in front of it. Writing a key again replaces what was there, so a fact that changed stops being two facts that disagree; when a key honestly holds two values it says so and both stay. A fact can be pinned so it is always present, or scoped to a language or a project so one project's details do not bleed into another. Turn on retrieval and the rest are picked by meaning instead of all being loaded every turn — by a model that runs on your machine and sends nothing anywhere.

Cooperation

Several of them, and a record of what they decided

A copy can be given a role: a short brief, a list of skills, and what it may do. Its rights are then the parent's narrowed by the role's — never the union. Without that rule an agent with no shell access reaches a shell by asking a peer, and nothing in the record shows a permission was ever granted.

jumabek agents
┌ agents (2) ─────────────────┐┌ board · g-4f21 ─────────────────────┐
│> 8ac71 researcher  running  ││#1 finding  8ac71 -> everyone   open │
│    skill · searxng_search   ││the 0.4.2 release has the same bug   │
│  b3d90 writer      waiting  ││    #2 task  8ac71 -> writer    open │
│    asking permission        ││    turn that into two paragraphs    │
└─────────────────────────────┘└─────────────────────────────────────┘
┌ g-4f21 · work out why payments time out ────────────────────────────┐
│████████████                    12 of 40 shared iterations           │
└─────────────────────────────────────────────────────────────────────┘

Everything spawned from one task shares a group: one goal, one board, and one pot of iterations for all of them together. The shared pot is not decoration — three agents each comfortably under their own limit will otherwise pass work between themselves indefinitely. The board is the single record: a conclusion that was not written there did not happen.

Cost

What a turn cost, not what it probably cost

The number printed while it works is a local estimate, made before the request goes out so it can decide what fits. What the provider counted is a different number, kept separately and labelled as such — against a Claude model on Cyrillic the two differ by about ten percent, which is a tokenizer that does not belong to that model.

jumabek tokens
cc/claude-opus-4-6 · openai
     3 turn(s) · 61959 in · 63 out — counted by the provider
     56220 in — guessed locally before sending
     caching: 58864 read, 0 written, over 3 of 3 turn(s)

The standing part of the context is marked for caching and everything that changes each turn sits after the mark; put it on the wrong side and every turn is a miss. In a real conversation about ninety-five percent of the input comes back from cache. A provider that reports nothing about caching is recorded as silent rather than as a miss — those are different facts, and only one of them is a problem.

Background

It keeps working after you close the terminal

A job outlives the prompt: a reminder, a recurring check, a folder being watched. Jobs live in SQLite and come back after a restart — most of what makes a reminder worth setting at all.

a job being agreed to
> every weekday at nine, summarise the
  top HN headlines

permission MEDIUM run 'morning headlines' in the
background

When: cron 0 0 9 * * 1-5
It does: Fetch the top Hacker News headlines and summarise them
It may use: rss_parser

It runs unattended and will not be able to ask you anything.

allow? [y/N] y
allowed

· job 1 · morning headlines · cron 0 0 9 * * 1-5

Everything else here asks at the moment it matters. A job cannot — there is nobody at the prompt at three in the morning. So the rights are settled first, and the question leads with them rather than with the task. Anything outside that list is refused and lands in the job's report; it cannot ask, and it cannot delegate its way around the limit either.

in 3h

A reminder. Fires once, then the job is finished and stops being checked.

every 30m

Minimum ten seconds, and the first run waits out one interval rather than firing the moment you create it.

cron 0 9 * * 1-5

Five fields, the way people actually write cron. A malformed line is refused when the job is created, not at three in the morning.

watch ~/Downloads

Runs when something there appears, changes or disappears. The first look only learns what is already present.

Delegation

Some work is worth doing but not worth reading

Scanning forty log files fills a context window with output whose only useful part is the conclusion. So JumaBek hands the work to a copy of itself. The copy starts empty — the prompt, the skills, and one standalone instruction. It cannot see the conversation it came from, which is the entire point.

handed off, and the prompt comes straight back
> what is failing in the logs under
  C:/logs?

Handing that off.
· subagent · read every .log under C:/logs and list the error
  codes
Handed off; I am free again.

> meanwhile, what is my disk at?

· the agent you spawned for 'read every .log' finished in 12s and
  reported: three codes account for everything — E_TIMEOUT 41 times,
  all against the payments host, E_AUTH 9 in one burst at 04:12, one
  E_DISK.

Spawning returns at once. The copy works while the conversation carries on, and its summary — never its transcript — arrives on a later turn, often after you have already been answered. Nobody is at the keyboard on its side, so it cannot ask you anything: give it work it can finish alone. Nesting stops at two levels, below which a tree is almost always a task that failed to decompose.

Voice

And when it cannot hear you, it says so

A microphone that goes unheard is the worst kind of failure: nothing happens, and there is nothing to look at. So the level is something you can watch, measured against the threshold it has to beat.

jumabek mic
  0 |                        | needs 50   quiet
 39 |######                  | needs 50
141 |####################### | needs 50   VOICE
 93 |###############         | needs 50   VOICE

loudest frame:        146
noise floor settled:   19
complete utterances:    1

The microphone works and speech is being detected.

The threshold falls over the first second as the noise floor settles, so a quiet room ends up more sensitive than a loud one. A sentence has to clear the line for half a second to count, and closes after nine hundred milliseconds of silence — which is why the check waits for you to stop talking rather than cutting at the clock.

While speaking is the one time the microphone is deliberately deaf. It reopens only once the synthesiser process has exited, plus a short tail for the room — the reason JumaBek never answers itself.

The inbox

A door, so a skill can speak first

Skills answer when called. That is fine until one is holding something live — a chat connection, a folder, a webhook — and has something to say. So the core listens on 127.0.0.1, and anything on the machine holding a token can push work in.

a skill knocking
$ curl -H "Authorization: Bearer $TOKEN" \
-d '{"source":"telegram","kind":"notify","text":"John: i will be there in an hour"}' \
localhost:20129/notify

{"status":"queued from telegram"}

· inbox · telegram · John: i will be there in an hour

No polling anywhere. The skill holds the connection Telegram already pushes over, and knocks the moment something lands. /ask works the same way but returns the answer over that connection — which is how a bot JumaBek writes for itself gets its replies back.

Each source and person is a running conversation, threaded together and surviving a restart, so "как в прошлый раз" from a Telegram chat means the last time in that chat. That thread is separate from what you type in the terminal: the terminal sees the inbox turns, the inbox side sees only its own.

Loopback only

The address is a constant, not a setting, with a test that fails if it changes. A port that runs tasks on your machine is a shell; one on the network is somebody else's shell.

A token each

One per caller, so a leaked one is revoked without touching the rest. Anything under 24 characters is ignored and said out loud rather than quietly dropped.

Rights, not requests

The grant belongs to the token, never to the message. A caller cannot widen its own permissions by asking for more.

Granted, not taken

The agent can ask for a key for a skill it wrote. The core generates it, writes it and hands it over — the model never sees the token.

Install

One command, then a key

windows — powershell
PS> irm https://raw.githubusercontent.com/jumabek-dev/jumabek/main/install.ps1 | iex
linux, macos
$ curl -fsSL https://raw.githubusercontent.com/jumabek-dev/jumabek/main/install.sh | bash

The installer puts everything under one directory, adds it to your PATH, and never overwrites a config you have already edited. It names the kinds of endpoint that work and installs none of them for you.

What it needs

Dependency Without it
An OpenAI- or Anthropic-compatible endpoint nothing works
Rust, Python or Node it can only write skills in the ones you have
Docker new skills are refused, because they cannot be checked first
ffmpeg voice is unavailable; typing still works

jumabek doctor reports all of it, and says what each gap costs you:

jumabek doctor
ok  home ~/.jumabek
ok  config ~/.jumabek/config.toml
ok  API key found
ok  LLM http://localhost:11434/v1 · qwen3.5:4b
ok  intelligence one model for everything
ok  Rust cargo 1.96.0 — skills can be written in Rust
ok  Python Python 3.12.4 — skills can be written in Python
WARN Node node not found
JumaBek runs, and can still write skills in the other languages
WARN Docker docker is installed but the engine is not running
new skills are checked in a container before they touch your
machine; without it building them is refused
ok  skills 2 installed: shell_executor, rss_parser

7 ok, 2 warning(s), 0 failure(s)
JumaBek will run; the warnings above disable parts of it

Writing skills

Or write one yourself

One file, one trait. Build it, drop the binary in the skills folder, and it is there next start — the same path the agent uses for its own.

word_count/src/main.rs
use jumabek_sdk::{MethodInfo, ModuleMetadata};
use jumabek_sdk::{SkillError, SkillModule, SkillOutput};

struct WordCount { metadata: ModuleMetadata }

#[async_trait::async_trait]
impl SkillModule for WordCount {
    fn get_metadata(&self) -> &ModuleMetadata {
        &self.metadata
    }

    fn health_check(&self) -> bool { true }

    fn available_methods(&self) -> Vec<MethodInfo> {
        vec![MethodInfo {
            method: "count".to_string(),
            description: "Count the words in a text".to_string(),
            args_description: "The text to count".to_string(),
        }]
    }

    async fn execute(
        &self,
        method: &str,
        args: &str,
    ) -> Result<SkillOutput, SkillError> {
        match method {
            "count" => {
                let n = args.split_whitespace().count();
                Ok(SkillOutput::Text(n.to_string()))
            }
            other => Err(SkillError::NotFound(
                format!("unknown method '{}'", other),
            )),
        }
    }
}

A skill runs with a stripped environment — it cannot see the agent's own credentials. Its own settings arrive as environment variables, and reach that skill only.

config [skills.weather] city = "Almaty" JUMABEK_SKILL_CITY
secrets [skills.weather] api_key = "..." JUMABEK_SKILL_API_KEY

Honest limitations

What it does not do

Every project has these. Most pages leave them out.

The container is a check, not a jail

It catches broken and misbehaving code before installation. It does not protect against a malicious build script in a dependency, because the binary that finally gets installed is compiled natively afterwards. That is why the config section is called preflight and not sandbox.

A local model connects; that is not the same as working

The client sends model, messages and stream, and reads choices[0].message.content. That much has been checked against Ollama, Ollama Cloud and OmniRoute, key or no key. What a small local model does not get you for free is the agent itself: every turn has to come back as one JSON object in a fixed action format, and a 4B model will miss it often. Local is a real option for routine work — treat "it connects" and "it can drive the loop" as separate questions.

Voice is only half proven

Capture has met real hardware: the device is found, the stream arrives, and speech is detected against the noise floor on a USB headset. What has not been exercised end to end is the rest of the round trip — transcription through to a spoken answer. The thresholds are tuned to one room and one microphone; jumabek mic will tell you how yours compares.

Parallelism helps across skills, not within one

Two calls to the same skill share one connection and one working directory, so they are deliberately serialised. Independent calls to different skills do run together.