The problem is not only choosing a model
When I started using AI agents, the tempting approach was to let the agent work anywhere: run commands, edit files, send notifications, or call external APIs directly.
That is fine for experiments. As the workflow grew, the same problems appeared repeatedly:
- conversational context became mixed with business logic;
- important commands lived only in prompts;
- results were hard to audit;
- schedulers accumulated implementation details;
- external mutations had no consistent approval and verification path.
So I created karina-workspace: a dedicated repository that acts as Hermes' execution plane.
The principle is simple: Hermes decides and communicates; the workspace executes operations through inspectable contracts.
Separating the conversational brain from execution
Hermes remains the main interface. I use it through Telegram and the CLI to understand requests, retain context, select skills, and route work.
The workspace does not replace Hermes. It exposes deterministic capabilities through a single CLI named kw.
With this boundary, Hermes does not need to know how to write a transaction, run a watcher, or deliver a notification. It calls a capability with a defined input, output, and lifecycle.
The repository as a work contract
The important parts of the workspace look like this:
src/kw/kernel/ # shared lifecycle primitives
src/kw/capabilities/<name>/ # vertical domain slices
src/kw/adapters/ # concrete adapters
src/kw/composition.py # built-in handler wiring
config/jobs.toml # canonical schedules
config/projects.toml # project registry
agent/skills/<name>/SKILL.md # thin conversational routers
docs/ # architecture and runbook
Several rules are intentionally strict:
kernel/cannot import concrete capabilities or adapters.- Domain logic lives in vertical slices, not in a giant CLI module.
composition.pyis the single place where concrete implementations are wired.- Credentials, tokens, chat IDs, and runtime data never enter the repository.
- The Hermes profile contains identity, environment, scheduler launchers, and thin skills only.
These rules make the repository easier for both humans and agents to understand. When I add a domain, I know where to look for its capability, adapter, configuration, and tests.
Why route everything through a CLI?
I chose a CLI because it provides a simple contract that humans, Hermes, and schedulers can all use. Every command can be called programmatically, and results are available as JSON.
For example:
.venv/bin/kw doctor --json
.venv/bin/kw capability list --json
.venv/bin/kw job list --json
The output does not need to be prose. Another program can inspect ok, consume data, and react to error consistently.
The project registry also prevents Hermes from guessing repository locations:
.venv/bin/kw project list --json
.venv/bin/kw project show achmadya-dev/achmadya.dev --json
.venv/bin/kw project preflight achmadya-dev/achmadya.dev --json
.venv/bin/kw project sync achmadya-dev/achmadya.dev --json
preflight checks the path, remote, branch, dirty state, and divergence before coding begins. sync only fetches and fast-forward pulls when the state is safe. If the repository is dirty, detached, conflicted, or diverged, the command stops instead of forcing git reset --hard.
External writes need a lifecycle
Read-only operations are relatively straightforward. The dangerous part is mutation: creating an event, writing a transaction, opening an issue, committing, or pushing to a remote.
I use this lifecycle:
plan payload
→ persist frozen payload + hash
→ explicit approval
→ execute the same payload
→ read-back verification
→ persist result and status
The important property is that the payload is not rebuilt from conversational context after approval. The exact approved payload is what gets executed.
An API acknowledgement is not always enough evidence. When a resource can be read back, the adapter verifies it through a read operation. A success status therefore means more than “the request was sent”.
The scheduler only triggers jobs
It is easy to create many cron entries, each carrying its own business logic. I replaced that pattern with one generic Hermes scheduler that reads the workspace job manifest.
Hermes scheduler
→ kw job run <job-id> --json
→ JobRuntime
→ capability handler
→ persisted run result
Canonical schedules live in config/jobs.toml, while concrete handlers are registered in the composition root. The scheduler does not need to know how a watcher fetches data or evaluates results.
Several jobs use this path, including employment, freelance, investing, finance import, and server health watchers. The manual command is the same path used by the scheduler:
.venv/bin/kw job run employment.watch --json
.venv/bin/kw job latest employment.watch --json
This lets me test jobs outside Hermes and inspect the latest result without opening a conversation transcript.
Producers do not send Telegram directly
Notifications are split into two phases:
watcher
→ evaluate
→ enqueue_once into a durable outbox
→ delivery drain
→ Telegram
A producer only enqueues a delivery with an idempotency key. The delivery job handles formatting, retries, and dead-letter state.
This solves several practical problems:
- a watcher does not fail completely because Telegram is unavailable;
- the same message is not accidentally sent multiple times;
- failed deliveries can be retried explicitly;
- watcher results and delivery results can be audited separately.
Telegram routes and credentials live in a private, Git-ignored overlay. The repository stores the schema and route names, not secret values.
Skills stay thin and repository-owned
I do not put all logic into Hermes skills. A skill acts as a conversational router: it explains when to use a capability, which command is relevant, and which constraints apply.
The implementation remains in src/kw/. This matters because a skill is not a security boundary and is not a substitute for tests. It can help Hermes select a command, but the workspace still validates and enforces behavior.
The Hermes profile loads agent/skills/ as an external directory. Skills can therefore be reviewed, tested, and versioned with the workspace code. I avoid profile-local duplicates because a duplicate can take precedence and make behavior difficult to trace.
What this gives me
A dedicated workspace does not make an agent automatically intelligent. It makes the problem manageable:
- conversational decisions are separated from execution;
- commands have testable JSON contracts;
- risky operations have approval and verification;
- the scheduler triggers canonical jobs only;
- notifications have an outbox and retry path;
- project coding has a registry and preflight;
- skills remain thin operational documentation instead of hidden business logic.
For me, this is a practical way to turn Hermes from a chatbot that can call tools into a work system with boundaries, state, and evidence.
The workspace is still evolving, but the central principle remains: the agent can be flexible when understanding the goal, while execution must have a deterministic and verifiable contract.