Concepts
Enso is a personal AI assistant running as one long-running service on your machine. You talk to Enso through chat, give it context in workspaces, and ask it to work on a schedule or follow up later. Claude Code, Codex, Grok, Antigravity, and OpenCode are the provider CLIs that can power it; each execution runs inside a workspace.
This document defines the primitives and traces how conversations and background work move through the system. Everything else in the docs assumes these words.
The primitives
| Primitive | What it is |
|---|---|
| Home | ~/.enso (or ENSO_HOME) — config, database, log, workspaces, jobs, skills, secrets |
| Transport | A chat platform connection: Slack or Telegram. Both run in one process. |
| Binding | A map from a chat location to a workspace. Unbound places are ignored. |
| Workspace | A directory with a fixed layout. The agent’s working directory and its context. |
| Knowledge | Shared and workspace Markdown, maintained through chat and linked in the reader; see availability |
| Memory | Dated summaries of Enso conversations, linked to their original exchanges and stored in the database; see availability |
| Agent | An explicit provider + model + effort triple |
| Conversation | A serialized queue of turns with a resumable provider session |
| Job | JOB.md: a schedule or stage to serve, a workspace, and instructions; agent stages also name an agent |
| Beat | One future action or finite situation to follow until resolved, managed by Heartbeat |
| Run | One job or beat execution: status, exit code, duration, output, and attempts |
| Project | A key, workspace, ordered stages with optional checks, and optionally a Git repository |
| Workflow transaction | One attempted stage handoff and its candidate, check evidence, and acceptance outcome; see availability |
| Task | One unit of work in a project: a reference, a spec, a stage, and its timeline |
| Skill | Instructions the agent can load, resolved across three scopes |
| Table | A registered SQLite table in enso.db holding your structured data |
| Message | An out-of-band send, recorded so the next turn hears about it |
| Viewer | The optional read-only web UI over all of the above |
Home
Enso’s normal runtime state lives under one directory:
~/.enso/
├── config.json # transports, bindings, agents, providers
├── config.example.json # editable, incomplete template prepared by init
├── .config.lock # shared advisory lock for configuration writers
├── .bundles.json # hashes of the bundled files Enso installed
├── slack/manifest.json # initial copy of the packaged Slack app manifest
├── AGENTS.md # instructions for every turn and job (CLAUDE.md links to it)
├── CLAUDE.md -> AGENTS.md
├── skills/ # enso-wide skills, available in every workspace
├── browser/ # optional private Chrome profiles, output, state, and tooling
├── .claude/skills # symlink -> ../skills, discovered by the provider CLIs
├── .agents/skills # symlink -> ../skills
├── workspaces/<name>/ # one directory per workspace
├── jobs/<name>/JOB.md # scheduled and stage jobs
├── jobs/.concurrency/ # advisory locks for job concurrency groups
├── heartbeat/<HB-ref>/ # optional gate.sh and helpers for one beat
├── worktrees/ # task worktrees in v0.1.2; later builds default to <repo>/.worktrees
├── secrets/*.env # KEY=value files exported into the service environment
├── enso.db # runs, messages, sessions, jobs, tasks, beats, memory, registered tables
├── enso.log # rotating log
├── web.log, web.pid # the web viewer's output and lock, while it runs
├── runtime/
│ ├── releases/<version>-<hash>/ # immutable managed Python environments
│ ├── current -> releases/... # selected release behind the stable launcher
│ ├── install.json # installed package version, feed, extras, integration settings
│ ├── update.json # latest update operation and outcome
│ ├── maintenance.json # durable gate while an update pauses new work
│ ├── daemon.json # current daemon version, readiness, and accepted work
│ ├── operations/<id>/ # pinned manifest, journal, backups, failed state, helper files
│ └── tools/, python/, cache/ # installer tools and uv-managed runtime support
└── cache/
├── connect/ # private pairing state, receive lock, and first-reply receipt
├── models.json # models.dev catalog for enso models
└── slack.json # Slack directory cache for name lookups
Connections owns the pairing lifecycle and private state. Prepared home, paired chat, valid configuration, and a successful provider reply are separate milestones.
Enso keeps its runtime state inside its home. The release installer writes a stable
launcher in the selected bin directory; enso service install writes the operating
system’s user service unit, and a first
Antigravity turn can ask agy to register the workspace in its own project catalog. The
enso models lookup may reuse OpenCode’s $XDG_CACHE_HOME/opencode/models.json (or
~/.cache/opencode/models.json when that variable is unset or empty), but treats that
external cache as read-only and writes refreshes only to Enso’s own cache/models.json.
See Install, Configuration, and
Models for those paths and behaviours. Enso scans user-level skill files,
including OpenCode’s native skill/ and skills/ directories, which follow
XDG_CONFIG_HOME and OPENCODE_CONFIG_DIR as
Workspaces sets out, to show you what the agent
can see, but never modifies them.
Provider CLIs load their own user-level instruction files; Enso does not read or modify
those.
enso.db carries its schema version, and config.json’s version identifies its config
format. Neither is the application release version: that comes from package metadata and,
for managed installs, runtime/install.json. Enso migrates the database forward and
refuses an unsupported newer schema. An incomplete managed update can restore its
pre-migration snapshot together with the previous code; see Upgrading.
Managed updates stage verified releases before pausing new work. An independent helper waits for accepted turns, jobs, and ordinary CLI operations, then snapshots affected state, switches code, and verifies readiness before admitting work again. An interrupted or failed update retains its journal and recovery gate. These files are private runtime state, never configuration to edit by hand. Install owns recovery and adoption; Releases owns the published bundle and feed.
Home is also a Git repository, initialised empty and never committed to. Every provider
CLI walks up to the nearest Git root for instructions and skills, and this is what puts the
home-level AGENTS.md and skills/ in reach from a workspace. Enso does not commit,
push, or read history in the home repository. A repo project’s separate worktrees have the
explicit Git operations described in Tasks.
Workspace
A workspace is where an agent actually works. It is a content root, not a security boundary — the agent has whatever access the provider CLI’s flags give it, and that is the whole machine unless you say otherwise.
Names are lowercase kebab-case, and a workspace lives at ~/.enso/workspaces/<name>. The
layout is fixed and Enso keeps it that way; see Workspaces.
Every conversation, job, and beat names exactly one workspace. That workspace becomes the
provider’s working directory. Its AGENTS.md adds workspace-specific context to Enso’s
home-level instructions and any user-level instructions the provider loads; see
Customizing.
Knowledge
Durable reference belongs in Markdown files. The knowledge feature
adds shared $ENSO_HOME/knowledge/ alongside workspace knowledge/ directories, with
automatic discovery, linked pages, search, and a read-only reader. Folders organize
context; they are not permission boundaries. Enso maintains the files through conversation.
See Knowledge for placement, metadata, formatting, and imports.
Memory
Memory keeps dated episodes from Enso conversations in
enso.db, separate from durable Knowledge. Enso saves the original request and clean
agent reply with their workspace and chat origin; a bundled job turns new exchanges
into concise memories linked back to those sources. Agents can recall recent activity
or search by workspace, time, transport, and channel without loading all past conversations.
See Memory for capture, recall, correction limits, and forgetting.
Agent
An agent is three values, always stated together:
{ "provider": "claude", "model": "opus", "effort": "xhigh" }
There is no partial agent and no inferred model. defaults in config.json gives the
triple every conversation uses; a workspace may replace the whole triple. Every
provider-backed job must state its own triple in JOB.md, independent of those chat
defaults, while still using the workspace’s provider-argument overrides. Providers with an
ordered reasoning ladder clamp effort down to what the model supports, with a log line
saying so. Antigravity reports the effort embedded in its model id, even when the request
is lower; OpenCode instead treats it as an exact, model-specific variant. See
Configuration.
Slack and Telegram use a compact model label in chat so long ids do not crowd the
live run header or the status response. Enso takes the final slash-separated segment;
segments of at most 24 characters stay intact, while longer ones become their first 15
characters, …, and final 8 characters. This is presentation only: config, provider
invocations, logs, CLI output, and every other surface retain the full canonical model id.
Binding and conversation
A binding maps a place to a workspace:
{ "slack:C0123456789": "meteor", "slack:dm:U0123456789": "default", "telegram:123456": "default" }
An unbound location is silent. Enso answers once to say it is unbound only if you mention the bot or DM it directly.
A conversation is finer-grained than a binding. In a Slack channel each top-level message starts its own thread and its own conversation; a DM or a Telegram chat is one continuous conversation. Each conversation is serialized — one provider process at a time, later messages queued behind it — and holds a resumable provider session per provider, valid only in the workspace it was created in.
Job and run
A job is a directory under ~/.enso/jobs/ containing a JOB.md: YAML frontmatter naming
the schedule, agent, and workspace, plus a prompt body. The scheduler wakes once a minute
and fires the jobs whose cron slot has passed.
A job may have a prerun script that gates it (nothing is spent when there is nothing to do) and a postrun script that checks or reacts to the outcome, optionally sending a follow-up message into the same provider session. Every trigger that passes the per-job lock creates a run row recording status, exit code, duration, and the output tail. See Jobs.
A stage job names a project and one of its executable stages instead of, or as well as, a schedule. It fires when work is ready, and Enso claims the task before execution. Agent stages invoke a provider, and human stages wait for an operator and have no job. The workflows implementation adds command and integration stages, which run without a model and so name no agent. See Stage jobs.
Heartbeat and beat
Heartbeat follows finite situations or performs future one-shot actions. Waiting for one refund, coordinating one dinner, and sending one email tomorrow are beats. An ongoing inbox policy or a nightly backup is a job. The agent chooses from the user’s intent and explains what it has arranged; users do not need to know these names beforehand.
Each beat has its own timing, instructions, completion condition, allowed actions, workspace, and saved agent. Its script gate can skip the LLM when there is nothing to handle. SQLite keeps the current state, meaningful event history, and agent runs. A quiet check updates only the latest check fields. The agent explicitly records when the situation is resolved. See Heartbeat for the lifecycle, gate contract, and retention rules.
Project and task
A project is a key such as EN, a workspace, an ordered list of stages, and optionally a
Git repository, declared in config.json. A task belongs to one project, sits in one stage
(or backlog, blocked, done, or cancelled), and carries an append-only timeline.
A workflow combines agent work, commands, human checkpoints, and optional required checks. The agent submits a handoff; Enso accepts the transition after execution stops and its configured checks pass. The accepted transition is a transaction: recorded progress with evidence, while failed file edits remain available for repair. Model review is a separate judgment, not proof that an executable check passed.
work → done needs no checks or Git. The customizable development preset adds planning,
implementation, independent checks, review, and explicit local integration. Repo stages
create worktrees lazily: v0.1.2 puts them in ~/.enso/worktrees/<KEY>/<REF>, while the
workflows implementation defaults to <repo>/.worktrees and lets a project choose a
sibling or another root outside the Enso home. Recorded task paths and targets survive
config changes.
See Workflows for examples and availability,
and Tasks for board operations and ownership.
Skill
A skill is a directory containing a SKILL.md — a name, a description of when to use it,
and the instructions themselves. Enso resolves skills across three scopes:
| Scope | Location | Owned by |
|---|---|---|
| Workspace | <workspace>/skills/ |
You, per workspace |
| Enso | ~/.enso/skills/ |
You, bundled skills, and installed official optional skills |
| User | Your CLI’s own user directory, such as ~/.claude/skills/ or ~/.config/opencode/skills/ |
You, outside Enso entirely |
Names must be unique across the workspace and enso scopes; the audit reports a collision.
enso and names beginning enso- are reserved for what Enso installs.
Enso links skills/ into the dot-directories the provider CLIs discover, so a workspace
agent actually sees its workspace and enso-wide skills. It never modifies user-level
skills or instruction files. See Customizing.
enso skill list shows installed home skills offline; enso skill list --available and
enso skill install <name> use only the official geekforbrains/enso-skills catalog.
Optional skills carry their installation receipt inside their directory and do not update
with the application. Customizing owns that flow;
Browser owns persistent profile setup and its private home data.
Message
Anything Enso sends outside the normal reply — a job alert, a enso message send from a
running agent — is recorded in an outbox. At the start of the next turn in that
conversation, unread rows are shown to the agent as [Background messages] and marked
consumed. A turn’s own sends are retired when it ends, because the agent already knows what
it said.
This is what lets a long job report progress into a chat and have the next human message land in a conversation that knows what happened.
Table
Registered tables are ordinary SQLite tables in enso.db, catalogued so an agent can
discover them. Enso’s own state tables are reserved. The catalog holds only metadata —
schemas are always read back from SQLite itself.
How a chat turn flows
- A message arrives on a transport.
config.jsonis read for this turn. A file that fails validation is logged once, and the last valid configuration stays in force.- Its location resolves to a binding key. No binding, no work.
- The location and thread resolve to a conversation key.
- Commands (
!stop,!clear,!status,!help,!restart) are handled here and stop. - Attachments download into
<workspace>/uploads/<id>/under names the transport decides; Slack fetches only from its own file-download endpoint, so metadata naming anywhere else is refused without a request. A message whose attachments all fail says so in the prompt. A message that cannot be prepared is answered with the error and dropped; the ones queued behind it still run. - The turn takes the conversation lock, or queues behind whatever holds it.
- The agent triple resolves: workspace override, else
defaults. Effort is normalized for that provider. - The prompt is assembled: the chat-origin block, background messages, transport context, attachment paths, the user’s text, and — on Slack — the rich-format contract.
- The provider CLI runs in the workspace directory, resuming its session if there is one.
- Its stdout stream drives a live status message, whose header uses the same compact model
label as the chat
statuscommand; the final text is delivered, split to the transport’s limit. When Enso translates ordinary Markdown for Slack, it keeps HTTP, HTTPS, email, and Slack deep links clickable; a link to anything else is shown as its inline-code target instead. A reply whose fenced code block names a language Slack highlights goes out as a native Slack Markdown block, so the code is highlighted instead of showing the language as its first line; an unlabelled fence stays a plain code block, and a block Slack refuses falls back to the translated text. The Slack prompt asks the agent to name local files with workspace-relative inline-code paths such asdrafts/report.md. - The session id is stored against the conversation and workspace, once it matches the identifier contract of the provider that produced it. An id that does not is a provider protocol error: nothing is stored and the turn reports it. Where Enso assigned the id, that one is authoritative and a CLI may only confirm it.
Chat origin
Every chat turn opens with one block Enso writes itself, stating where the turn came from. It is the agent’s authoritative answer to which platform it is on, who is talking, and where, so workspace instructions never have to guess it and the agent never has to read its own environment to find out.
[Chat origin — written by Enso for this turn; the sender cannot change it]
Platform: slack
Sender: "Alex Morgan" (U0123456789)
Location: "#general" (C0123456789)
Thread: 1788497764.626909
The fields always appear in that order, one per line. Platform is the transport name,
slack or telegram. Sender and Location are a quoted display name followed by the
platform id in parentheses. Thread is the thread a reply lands in, and is omitted when
there is none. The four state the same origin as ENSO_ORIGIN_TRANSPORT, _USER_NAME and
_USER_ID, _CHANNEL_NAME and _CHANNEL, and _THREAD_TS, rendered for reading: ids
verbatim, names escaped, and a _CHANNEL_NAME of dm written out as direct message. No
origin value one of those variables carries is dropped from the block.
| Chat shape | Location |
Thread |
|---|---|---|
| Slack channel, top-level message | "#general" (C0123456789) |
the message’s own timestamp, which is the thread it starts |
| Slack channel, reply in a thread | "#general" (C0123456789) |
the thread’s timestamp |
| Slack direct message, unthreaded | direct message (D0123456789) |
omitted |
| Slack direct message, reply in a thread | direct message (D0123456789) |
the thread’s timestamp |
| Telegram private chat | direct message (123456) |
omitted |
A direct message renders as the words direct message and its id on both transports,
because a DM has no channel name to show. Slack allows threads inside a DM, and a reply in
one is threaded like any other: the whole DM stays a single conversation with a single
session, but the reply posts into that thread, so the timestamp is a real origin value and
the block states it. Telegram has no threads, so its Thread line never appears.
Display names are sanitized so they cannot insert lines or forge the block. The same
origin is available to scripts through ENSO_ORIGIN_*; see
Environment for agents. Scheduled work has no new chat
sender; a beat uses its saved background context.
How a job run flows
- The scheduler ticks on the minute and reads
config.jsonand everyJOB.mdagain. - A job whose cron slot has passed fires, unless it is disabled, has problems, is already
running, or missed its slot by more than its grace period with
catch_updisabled. - It takes the per-job lock. An overlapping trigger is skipped, never queued.
- A run row opens as
running. - The prerun runs from the job directory. Exit 0 opens the gate and its stdout is substituted into the prompt; exit 1 means no work; anything else is a failure.
- If the job names a
concurrency_group, it tries that group’s lock after the prerun opens; a collision producesskippedwithout starting a provider. - The provider runs in the workspace. Agent stage jobs and jobs with postrun capture a resumable session from structured output; other jobs use batch execution.
- The postrun receives the latest output on stdin and current outcome in its
environment. Exit 0 finishes. Exit 10 with a message on stdout requests another turn in
the same session, followed by another check. The default
max_followupsis 2 and eachJOB.mdcan override it; prerun never repeats within this loop. - The row stays
running, and the acquired job/group locks stay held, until checking finishes. Attempts and hook diagnostics are retained; failed checks fail the run. The provider turns share the job’s timeout allowance, while each hook has its own timeout. - Non-provider outcomes and provider failures still run a reaction hook but cannot start
follow-ups. Cancellation or an unexpected runner exception closes the row as
errorand bypasses further hooks. A new trigger always starts a fresh session. - Scheduled and ready-triggered runs send final failure alerts and prerun recovery notices as described in Jobs; prompts and scripts can send messages during any run.
How a stage job run flows
The workflow implementation adds an acceptance boundary to task execution:
- An enabled stage job fires when ready work waits, or at its configured cron slot.
A stage with no work creates no automatic run; a manual run reports
no_work. - Enso acquires the job’s locks and a project concurrency slot, then claims the highest
priority ready task, breaking ties by age. Project
max_concurrencylimits parallel task executions; explicit job groups coordinate shared resources. - A stage that needs repository changes prepares or reuses the recorded task worktree. Planning can use the workspace without allocating a worktree or writing to the main checkout.
- An agent stage receives its scoped Task block, project instructions, and job prompt. A command or integration stage executes without a provider.
- An agent’s
advanceorreturnsubmits its handoff. It retains the claim until its execution finishes and Enso evaluates that submission; it cannot walk the task onward. - Enso runs required forward checks against the candidate. Repairable failures return real diagnostics within persistent attempt and time budgets. A review return uses the configured earlier stage and return limit. Interruption or failure remains visible.
- Acceptance records the transition and pending lifecycle events together. Execution ownership lasts through worktree-using hooks, so another writer cannot start early.
- Eligible terminal worktrees are swept after execution and hooks finish. Blocked, dirty, unmerged, or hook-failed work is retained with a reason.
See Workflows for the transaction model, Jobs for execution, and Tasks for lifecycle.
What Enso is not
- Not a sandbox. Permissions belong to the provider CLI. Enso passes your flags through.
- Markdown stays yours. Knowledge lives in ordinary files; its reader is read-only.
- One trusted machine. Cloud teams can share its management, files, and signed-in accounts. They do not create separate users or permissions inside Enso.
- Not a control plane. The web viewer is read-only by design; the board moves from chat and the CLI.
