Build Aldena
KINDAreplaces $99/mosaves $1,188/yrback to the verdict
A local multi-agent room whose hard problems are containing work that was written by a model from text you did not write, making an approval gate that actually blocks, and producing a run record somebody who is not an engineer can read.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why The container is the only real boundary in this design, and the daemon's own privileges are part of that calculation.
Get it Install Docker Engine and read its security documentation on rootless mode and user namespace remapping before your first run. open ↗
Verify
docker run --rm alpine id prints a non-root mapping, or rootless mode is confirmed active - installfree
Why Runs the CLI, the orchestrator and the local page.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v24 or higher - API keypay per call; measure one real run before planning a day of them
Why Every agent turn is a call, and a multi-agent run makes many of them, which is why the cost ceiling is in phase five rather than in a wish list.
Get it Create a key and set a provider-side spend limit.
Verify
One manager turn returns a tool call against your schema - API keyfree
Why It lives in the CLI process and never enters the container, and the scopes are what make that separation meaningful rather than decorative.
Get it Create a fine-grained token limited to the single repository, with contents and pull request write and nothing else.
Verify
The token can open a pull request and cannot merge one, tested by trying - have readyfree
Why The first few runs will do something you did not intend, and the right time to find that out is on a checkout you can throw away.
Get it A scratch clone, or a branch you do not mind losing.
Verify
A run produces a branch and a pull request you can close without regret
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- agents(id, name, role, instructions_ref, model_id, created_at) · seeded from roles.json and editable there - runs(id, request, started_at, finished_at, state, container_id, branch, pr_url, cost_minor) - messages(id, run_id, from_agent, to_agent, kind, body, depth, at) · every hand-off and every reply, so a run reads back a fortnight later - tool_calls(id, run_id, agent_id, tool, arguments_json, policy, state, decided_by, decided_at, exit_code, output_ref) - approvals(tool_call_id, prompt_shown, decision, at) · what the human was actually shown, not what the agent claimed it wanted - memory(id, scope, agent_id, line, added_at, run_id) · scope is project or agent, and the table is capped by row count rather than by hope - The approvals table stores the rendered command as the human saw it, because an approval that recorded only an intention proves nothing about what ran.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
MODEL_API_KEYsecret | required | sk-...redacted | The model. Never passed into the container. |
REPO_TOKENsecret | required | github_pat_...redacted | Held by the CLI process only. If this appears inside a container, phase five has failed. |
DATABASE_PATH | required | ./data/runs.db | Runs, messages, tool calls and approvals. This is the record you would show somebody. |
SANDBOX_IMAGE | required | node:24-bookworm | Pinned by digest in production use, because an image that moves is a sandbox that changed. |
PROJECT_PATH | required | /home/you/code/project | The single directory mounted into the container. Exactly one. |
MAX_DEPTH | required | 5 | Chain depth cap. A run that hits it stops in a named state. |
MAX_FANOUT | required | 3 | Agents the manager may delegate to per turn. |
RUN_COST_CEILING | required | 4.00 | A run stops here and tears down its container. |
The build, in order
A room with one door
Only the manager takes your request, hand-offs are bounded, and every message is a row.
Roles get tuned constantly and a database row is a bad editor.
Files
roles.jsonsrc/agents/seed.tsA delegation loop is a paid loop.
Files
src/run/limits.tstests/limits.test.tsThe run record is the product for anyone who is not watching live.
Files
src/store/messages.ts
done when · tick each as it passeswatch out- Agents that can call each other freely produce a loop where two of them agree to keep refining, and because each turn is a paid model call the first sign is a bill rather than a hang. Cap fan-out per turn and depth per chain in code, count them in the database, and stop the run with a clear state rather than a crash when either cap is reached.
One container, one mount, no socket
Every file edit and shell command happens inside a container that can reach the checkout and nothing else.
Every extra mount is a directory an agent can rewrite.
Files
src/sandbox/container.tsThe socket inside a container is the host.
Files
tests/no-socket.test.tsRoot-owned files on your host are the symptom, not the problem.
Files
docs/rootless.mdsrc/sandbox/user.ts
done when · tick each as it passeswatch out- Running the container as root with the checkout bind-mounted leaves every file the agents touched owned by root on your host, so your next ordinary git operation fails with permission errors on files you appear to own, and the usual fix people reach for is running the whole thing with elevated privileges, which removes the last of the boundary. Run as a matching non-root user inside, or turn on user namespace remapping, and check ownership on the host after a run.
A denied tool is an absent tool
The policy file decides what each agent can call, and denial means the tool never appears in the schema.
Absence is the only restriction that cannot be argued with.
Files
tools.jsonsrc/tools/schema.tstests/denied-absent.test.tsAn approval of a command nobody rendered proves nothing.
Files
src/tools/approve.tsRejecting one command should not waste the other four agents' work.
Files
src/run/refusal.ts
done when · tick each as it passeswatch out- Putting the restrictions in the system prompt and filtering the results afterwards means the model still sees the tool, still tries it, and any text it read along the way can argue it into trying harder, while your filter only catches the shapes you anticipated. Build the schema from the policy so the tool does not exist for that agent, and treat a filter as a second line rather than the first.
Memory that does not grow forever
Two small files carry context between runs and neither can quietly become a novel.
The prompt budget is real and this is where it leaks.
Files
src/memory/cap.tstests/memory.test.tsForgetting is a decision worth recording.
Files
src/memory/write.tsA note file is the easiest place for a key to end up.
Files
src/memory/scan.ts
done when · tick each as it passeswatch out- An unbounded memory file grows until it is the largest thing in every prompt, at which point you are paying to resend a thousand lines of last month's opinions on every single turn and the model is weighting them as heavily as your actual request. Cap the line count, make the agent choose what to delete before it can add, and keep the removals in the database so you can see what it decided to forget.
A pull request and a run somebody can readproduct builder
A run ends with a branch and a pull request, a readable record, and a cost that cannot run away.
A credential in the context window is a credential you published.
Files
src/delivery/git.tsThe final act belongs to the process a person is running.
Files
src/delivery/pr.tsThis is what you show whoever asked for the work.
Files
src/web/run.ejssrc/cli/report.ts
done when · tick each as it passeswatch out- Handing the repository token to the agents so they can push means the credential is inside the same context window as whatever text they read this morning, and the smallest version of that going wrong is a force push to the default branch. Keep the token in the CLI process outside the container, give the agents no git remote access at all, and let the branch and pull request be something that happens after the agents have stopped.
That is the whole plan for Aldena. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A provisioned server per project.
- OAuth connectors into Jira, Linear, Slack and the rest.
- Multi-user accounts and seat billing.
- Any path by which an agent can merge.
- a machine per project that someone else provisions, patches and bills by the hour
- connected GitHub, Bitbucket, Jira, Linear, Notion, Slack, Drive, Gmail, Sentry and Vercel, and the upkeep behind those tokens
- memory that outlives the run, private per agent and shared per project
- a screen a non-engineer can watch the run in and approve from
- one prepaid balance metering every model and every server hour
- A read-only agent that reviews the diff with no filesystem tools at all, which is a cheap second opinion
- Per-run network allow lists in the container, once you know which hosts a build genuinely needs
- A weekly summary of denied and rejected tool calls, which is the most honest measure of whether the policy is right
Need the files? The project pack on the verdict page hands your agent the whole brief · more automation.