Brain Developer Guide: Registry Access and Engine Upgrades
Omnislash engine npm packages (@omnislash-ai/*) are published to GitHub Packages (private). The base Docker image (ghcr.io/omnislash-ai/engine) is public. A GitHub PAT with read:packages scope is needed for the npm packages; Docker image pulls require no auth.
New here? Do getting-started.md first — it covers the same one-time token setup inline and takes you all the way to a first agent answer. This document is the reference for the parts that outlive that first run: where the token lives, how omnislash upgrade works, Dockerfile customization, and CI/CD.
Setup
1. Get a registry access token
Ask the engine maintainer for an access token: a GitHub PAT with the read:packages scope. Docker images (ghcr.io/omnislash-ai/engine) are public and require no auth.
You do not need a GitHub account or access to the engine repository. The token is all you need. (When the engine packages move to public npm this step disappears entirely — nothing else in this document changes, because the registry is a single engine-side constant every artifact derives from.)
2. Put the token in your own ~/.npmrc — nowhere else
One-time setup, on your machine:
printf '%s\n%s\n' \
'@omnislash-ai:registry=https://npm.pkg.github.com' \
'//npm.pkg.github.com/:_authToken=YOUR_TOKEN' >> ~/.npmrc
Replace YOUR_TOKEN with the token from step 1. The token lives on your machine, never in the project repo.
Multiple scoped registries? Global
~/.npmrcsupports any number of scopes. The token is bound to the registry URL — if multiple scopes use the same registry (e.g. GitHub Packages), one token covers all of them.
3. Project .npmrc — three lines, no token
Your brain commits this, and omnislash init writes it for you (enforced by omnislash doctor — see contract.md § 1):
auto-install-peers=true
strict-peer-dependencies=true
@omnislash-ai:registry=https://npm.pkg.github.com
There is deliberately no _authToken line, and adding one back breaks your installs. A project .npmrc outranks ~/.npmrc, and pnpm expands an unset ${NODE_AUTH_TOKEN} to an EMPTY token which it then uses — so the placeholder shadows the real credential in your ~/.npmrc and the request goes out with no Authorization header at all. The symptom is intermittent, which is what made it expensive: pnpm short-circuits the metadata fetch when the target version is already in its store, so installs that need no fresh registry data appear to work while a not-yet-cached version 401s.
With the line gone, a plain pnpm install from a brain directory picks up your ~/.npmrc like any other project, and no NODE_AUTH_TOKEN=$(awk …) prefix is needed anywhere.
The other two carriers are automatic:
- CI — the engine-generated
.github/workflows/deploy.ymlandci.ymlconfigureactions/setup-nodewithregistry-url+scope, which writes a scoped npm userconfig from theNODE_AUTH_TOKENsecret. This is the same mechanism the engine uses to publish itself. - Children the CLI spawns —
omnislash initandomnislash upgraderesolve a token (environment first, then~/.npmrc) and hand theirpnpm installa process-local, owner-only npm userconfig viaNPM_CONFIG_USERCONFIG, removed on exit.
Upgrading a brain scaffolded before this change? Delete the
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}line from its.npmrcby hand — nothing rewrites an existing brain’s.npmrc, andomnislash doctoronly checks that the required lines are present.
4. Install packages
cd my-brain
pnpm install
You should see the packages resolved from npm.pkg.github.com:
@omnislash-ai/config [version]
@omnislash-ai/cli [version]
5. Docker (base image)
The engine Docker image is private, and the same token reads it. omnislash start builds your brain image FROM ghcr.io/omnislash-ai/engine:x.y.z, so log Docker in once:
printf '%s' '<token>' | docker login ghcr.io -u <your-github-username> --password-stdin
CI needs nothing extra: the generated deploy.yml pulls the base image with the OMNISLASH_PAT secret (§ GitHub configuration).
Token Renewal
GitHub classic tokens expire. When yours does:
pnpm installfails with 401 Unauthorized- Ask the engine maintainer for a new token
- Replace the old token in
~/.npmrc(//npm.pkg.github.com/:_authToken=ghp_NEW_TOKEN) and re-run thedocker login ghcr.iofrom § 5 - In CI, rotate the
OMNISLASH_PATrepo secret the generated workflows read
Updating Engine
cd my-brain
omnislash upgrade # latest version
omnislash upgrade 0.6.0 # specific version
This rewrites the version pin of every @omnislash-ai/* package in your peerDependencies to the exact X.Y.Z (discovered dynamically — currently cli, config, infra, pipeline-sdk, types; exact so nothing but omnislash upgrade can move the installed engine), runs pnpm install, and regenerates the Dockerfile, .dockerignore, and .github/workflows/{deploy,ci}.yml to match the new engine version. It also syncs the brain-facing framework docs into a committed docs/engine/ (version-matched, overwrite-on-upgrade like the Dockerfile; opt out with a docs/engine.override sentinel) — and keeps a short engine block in your root CLAUDE.md (created if missing) and in AGENTS.md when that is a file of its own, not a symlink: between <!-- BEGIN:omnislash-engine --> and <!-- END:omnislash-engine --> it imports @docs/engine/README.md and says how to upgrade and diagnose, so an agent working in the brain finds them. Only the text between the markers is rewritten; with no markers the block is appended; write your own instructions outside them. The latest release’s copy is also online at omnislash.ai/docs. Likewise it refreshes the compose-override examples in docker/ (compose.prod.yml.example, compose.start.yml.example) so you always have an up-to-date, copy-pasteable starting point for compose overrides — engine-owned *.yml.example, overwrite-on-upgrade, never touching your live docker/compose.{mode}.yml; opt out with a docker/.infra-examples.override sentinel. It also delivers the engine’s system skills into skills/engine/ — a mirrored, engine-owned directory (overwritten on every upgrade, no lockfile: the directory is the manifest). They show up in omnislash skills list with origin engine. To change one, omnislash skills fork <name> copies it into your own skills/ where it overrides the engine’s by name and stops being touched; to switch one off, omnislash skills disable <name> (it records the decision in skills/.settings.json, and — because the image bake gates on that file — regenerates your Dockerfile too, so commit both); to decline the whole corpus, drop a skills/.engine-skills.override sentinel. A skill of yours that already uses a delivered name is no longer an error — it simply wins. Any failure (contract violation, registry/auth error, failed install, failed regeneration) exits non-zero with the reason and a hint; the command is idempotent, so fix the cause and re-run — it resumes where it stopped and converges every artifact onto the target version. If the working tree only contains the upgrade-owned paths (package.json, pnpm-lock.yaml, Dockerfile, .dockerignore, .github/workflows/{deploy,ci}.yml, docs/engine/, docker/*.yml.example, skills/engine/, upgrade/, and the engine block of CLAUDE.md / AGENTS.md), the command offers to commit and push for you (interactive terminals only — a non-interactive run never touches git history and prints the manual step instead). Otherwise commit manually, and push only as upgrading.md says (a push deploys).
Need those artifacts rewritten without changing versions — a hand-mangled Dockerfile, a half-deleted docs/engine/, anything where omnislash upgrade sees matching version stamps and reports “Already on vX”? omnislash regen regenerates every engine-owned artifact (Dockerfile, .dockerignore, .github/workflows/{deploy,ci}.yml, docs/engine/, docker/*.yml.example, engine system skills in skills/, constitution/engine-capabilities.json, the engine block in CLAUDE.md / AGENTS.md) from the installed engine, unconditionally — only your explicit .override sentinels are honored.
The upgrade is an agent’s task, and the command is only its first step. After the regeneration, omnislash upgrade writes upgrade/TASK.md: the steps the releases you crossed need in this brain, which an agent follows to the end (checks against production, one commit per entry, the gates, the deploy, the after-deploy steps). With a task written, the auto-commit commits without pushing, because a push deploys. What the command does versus what the agent does, the entry format, resuming and --from, the deploy’s downtime and what to do when it fails: upgrading.md. Nothing upgrades the brain from CI: there is no upgrade workflow.
If you use ENGINE_VERSION in GitHub Actions variables instead of pinning in the Dockerfile, update the variable in Settings → Variables → Actions.
Compose and monitoring configs are always generated from CLI templates — pnpm update brings new templates, next deploy applies them automatically.
Renamed engine names
omnislash upgrade rewrites the files it owns; it never rewrites yours. When the engine renames a name your files spell — a capability in a role’s capabilities:, a blueprint task’s requiredCapabilities or a grants: block, a tool in constitution/engine-tools.json, toolLoading.eager, a prompt or a skill, a system role an agent: { role: … } points at, an env var in your secret store — your copy keeps the old spelling until you change it. Run omnislash doctor after every upgrade, before deploying: it names each one with the file it sits in and what it became (Capability names, Tool names, Blueprints, Agent roles, Brain entities, Env vars). Renames are rejected, never resolved — no part of the engine maps an old name in the table below to its new one; the upgrade task walks you through each one your brain spells (upgrading.md); the table below stays as reference:
- An old tool name in
engine-tools.jsonortoolLoading.eagerfails the load — the brain does not boot until the file says the new name (the error names it). A name no tool answers to at all — neither an engine tool nor one your brain registers — fails the boot the same way, with the nearest real name. - An old tool name in a prompt or a skill points an agent at a tool that no longer exists; the call fails as an unknown tool.
- An old capability name in your tool code (
providesCapabilities) fails the boot — the tool’s registration names the new name. Inconstitution/engine-capabilities.jsonit fails the load the same way, butomnislash upgraderewrites that file for you (it is engine-reconciled), carrying yourrisk/hitlonto the new name. - An old capability name anywhere else fails nothing at load: a role runs without the tool, a grant request is refused naming the new name, and a blueprint fails the cron tick that reaches it (
schedule_overviewand the dashboard show that plan as invalid, with the same reason). Grants, pinned agent capabilities, open grant requests and live runs the engine stored are rewritten by the upgrade’s migration.
The MCP server exposes tools under their registry names, so a Claude Desktop / Code allow-list (mcp__<server>__<tool>) that names an old tool needs the new one.
| Was | Now | Since | Where it bites |
|---|---|---|---|
capability state:read | runtime:read | 1.6.0 | role capabilities:, blueprints, grants: |
capability improvement:read | loop:read | 1.16.0 | same |
capability improvement:execute | loop:analyze | 1.16.0 | same |
capability improvement:supervise | loop:supervise + loop:hold (the hold split off, hitl: false) | 1.16.0 | same |
capability improvement:auto_accept | loop:auto_accept (admin-only) | 1.16.0 | admin grants |
role system:browser-operator | system:browser | 1.17.0 | agent: { role: … }; a brain copy role-browser-operator.md becomes role-browser.md |
role system:root-assistant | system:chief (glossary.md) | 1.19.0 | a brain copy role-root-assistant.md is no longer read — move it to role-chief.md; since 1.22.0 a reference to the old id does not resolve either — rename it (the upgrade’s migration moves the agent rows) |
role system:dashboard-guide | system:editor-assistant (the dashboard editors’ assistant) | 1.22.0 | a brain copy role-dashboard-guide.md is no longer read — move it to role-editor-assistant.md (it now speaks to one draft, not the dashboard: see the engine’s); agent: { role: … } and agent rows on the old id do not resolve — rename the references, and re-point the rows by hand (omnislash upgrade does not): UPDATE omnislash.agents SET role = 'system:editor-assistant' WHERE group_key = 'editor-chat' AND role = 'system:dashboard-guide' |
defineProject({ rootAgent }) | { chief } | 1.19.0 | the old key fails the load, naming the new one |
chief persona root-chat (the default chief.name) | chief | 1.22.0 | with bots omitted, the engine’s one bot is named after chief.name: a bots.root-chat: key in constitution/access.yaml names no bot and fails the load — rename it bots.chief:; drop an explicit chief: { name: 'root-chat' } (the upgrade’s migration renames the chat agent rows) |
loop module ctx.runKind | ctx.targetKind (scenario · heartbeat · role) | 1.19.0 | a <id>.loop.ts reading it in window() / compare() gets undefined; doctor does not flag it |
loop targetKind: 'workflow' / 'pipeline' / 'chat' | 'scenario' (both engines) / 'role' | 1.19.0 | a loop_* tool call in a prompt or skill fails the tool’s input validation; a pipeline that spells it must spell the new kind; doctor does not flag either |
config subpath @omnislash-ai/config/custom-agents | @omnislash-ai/config/roles (loadAllRoles, getRoleById, RoleDefinition, …) | 1.19.0 | brain code that imports it fails to resolve the import; doctor does not flag it |
env MCP_OPERATOR_TOKEN / MCP_OWNER_TOKEN | MCP_TOKEN | 1.18.0 | since 1.22.0 the old names are not read: the MCP bearer door stays closed until MCP_TOKEN is set |
env TELEGRAM_GUEST_IDS | removed — identities: in constitution/access.yaml (chat-access.md) | 1.17.0 | a set value fails the boot |
tool respond_to_agent_question | agent_answer (agent_escalate hands a question to a person) | 1.17.0 | role prompts and skills that name the tool |
tool state_overview | runtime_overview | 1.6.0 | role prompts and skills that name the tool |
capability artifacts:publish | artifact:publish | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability chats:read | access:read | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability chats:write | access:write | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability chats:widen | access:widen | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability chats:admin | access:admin | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability chat_episode:write | episode:write | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability database:read | db:read | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability database:write | db:write | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability integrations:execute | integration:execute | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability contacts:read | contact:read | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
capability contacts:write | contact:write | 1.19.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
tool agent_run | agent_call | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool agent_update_config | agent_update | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool read_file | file_read | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool write_file | file_write | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool edit_file | file_edit | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool memory_get | memory_read | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool describe_tables | db_schema | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool ingest_event | event_ingest | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool load_skill | skill_get | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool read_skill_resource | skill_read_resource | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool notify_send | notification_send | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool publish_artifact | artifact_publish | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool unpublish_artifact | artifact_unpublish | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool request_permissions_grant | hitl_request_grant | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool request_integration_connect | hitl_request_connection | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool blueprint_read | blueprint_get | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool blueprint_workflow | scenario_plan | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool plan_scenario | scenario_plan | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool execute_workflow | scenario_start | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool execute_pipeline | scenario_start | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_pipelines | pipeline_list | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool eval_run | eval_start | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool chat_episode_close | agent_close_episode (was episode_close until 1.24.0) | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool schedule_write | scenario_update | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool workflow_failure_diagnosis | run_diagnosis | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool patch_blueprint_snapshot | run_update_blueprint | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool queue_add | queue_create_job | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool queue_read | queue_get | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool watch_resource | watch_create | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool unwatch_resource | watch_delete | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool request_approval | hitl_request_approval | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool loop_analyze_now | loop_analyze | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool loop_idea_post | loop_create_idea | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool loop_idea_withdraw | loop_cancel_idea | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool schedule_prompt | schedule_create_prompt | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_calendar_events | calendar_list_events | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_calendars | calendar_list | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool create_calendar_event | calendar_create_event | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool update_calendar_event | calendar_update_event | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool delete_calendar_event | calendar_delete_event | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_emails | email_list | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool read_email | email_get | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool send_email | email_send | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool search_drive | drive_search | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_drive_files | drive_list_files | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool read_drive_file | drive_read_file | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_task_lists | tasks_list_lists | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_tasks | tasks_list | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool create_task | tasks_create | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool update_task | tasks_update | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool complete_task | tasks_complete | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool delete_task | tasks_delete | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool extract_pdf | document_read (also reads Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, HTML and text) | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool process_image | image_process | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool generate_image | image_generate | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool transcribe_audio | audio_transcribe | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool get_sitemap | web_sitemap | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool parse_webpage | web_parse | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool google_maps_search | maps_search | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool google_maps_reviews | maps_reviews | 1.19.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool cancel_workflow | run_cancel (workflow and pipeline runs, input { id }) | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool cancel_pipeline | run_cancel | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool retry_workflow | run_retry (input { id }) | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool pause_pipeline | run_pause (workflow and pipeline runs) | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool resume_pipeline | run_resume (workflow and pipeline runs) | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool kill_pipeline | run_kill | 1.20.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
capability pipeline:kill | run:kill; cancel / pause / resume / retry / recover of any run take the new run:execute (pipeline:execute and workflow:execute kept starting runs until 1.23.0) | 1.20.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
tool list_runs | run_list (every kind of run — workflow, pipeline, tick, fire, eval, cycle step; filters entity / feature / kind / status / window) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool list_pipeline_runs | run_list (kind: ['pipeline']) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool workflow_run_status | run_get (input { id } — a run id or slug) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool pipeline_run_status | run_get (input { id }; the pipeline detail is under pipeline) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
capability workflow:read | run:read + eval:read (run_list, run_get, run_diagnosis · eval_get, eval_list); eval_start moves from workflow:execute to the new eval:execute | 1.21.0 | role capabilities:, blueprints, grants:, tool code providesCapabilities |
tool chats_overview | access_overview (input { bot? }; the frame per bot with every tier’s own entry; access_identities lists who is in a chat) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool chats_explain | access_explain (chat: 'dm' | tg:chat:<id>, sender or senderChat) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool chats_set | access_update (typed edits, one tier as YAML in rule, or the whole file; returns { sha, diff, consequences }) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool chats_widen | access_widen (same input as access_update) | 1.21.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
file constitution/chats.yaml | constitution/access.yaml | 1.21.0 | nothing reads the old file: with no access.yaml every bot serves only its owners — git mv it (1.21.0 alone moved it itself) |
MANIFEST dashboard.dailyBrief.refreshHours | brief.schedule (HH:MM or disabled) — the daily brief is an engine tick into the chief’s DM, no longer a dashboard visit; brief.instructions overrides what the chief is asked to write | 1.21.0 | the manifest load fails on the unrecognized key dashboard.dailyBrief |
@omnislash-ai/config/bots-env defaultBotId() / defaultBotSurface(chatId) / defaultBotSurfaceOrNull(chatId) / resolveDefaultHitlSurface(); @omnislash-ai/config/scenarios resolveScenarioSurface(scenario) | chiefBotId(config) / chiefBotSurface(config, chatId) / chiefBotSurfaceOrNull(config, chatId) / resolveDefaultHitlSurface(config) / resolveScenarioSurface(scenario, config) — the chief’s bot, not the bot on TELEGRAM_BOT_TOKEN (config: getProjectConfig() from @omnislash-ai/config/project-store) | 1.22.0 | brain code that imports a removed name fails to resolve the import; a call to the two kept names without config fails type-check; doctor does not flag either |
event hook:executed | hook:enqueued (the hook’s agent was handed the prompt; it may still be working) | 1.22.0 | a hook whose event: names the old event never fires; doctor Hook events names it |
newId / deriveId from @omnislash-ai/types/ids | @omnislash-ai/types/id-mint (ids keeps ID_PREFIXES, IdKind, stripIdPrefix) | 1.22.0 | brain code that imports them fails to resolve the import; doctor does not flag it |
tool health_feature | health_list — every feature in one call (feature narrows to one); entity rows carry their trigger and ten newest runs (outcomes) | 1.23.0 | engine-tools.json, toolLoading.eager, prompts, skills |
tool workflow_start, pipeline_start | scenario_start — one start for either engine: id (the scenario) + input + idempotencyKey; pipeline only service (an auxiliary <id>-<suffix> service the scenario’s own *.pipeline.ts registers — pipeline_start’s pipelineId when it was not the scenario’s primary), handler, key. Answers run: { id, kind, existing } or inputRequested | 1.23.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool schedule_update | scenario_update — { id, patch: { schedule, enabled, … }, dryRun, expectedSha? }, under feature:write (not schedule:write: a role that held the old tool through schedule:write needs feature:write); a schedule is written in the scheduler’s grammar — no wording resolved by a model | 1.23.0 | engine-tools.json, toolLoading.eager, prompts, skills, role capabilities: |
tool blueprint_create | scenario_plan with dryRun: true and brief (a goal) or id (a scenario) — the plan comes back, nothing is saved; a commit (id) queues the scenario’s planning | 1.23.0 | engine-tools.json, toolLoading.eager, prompts, skills |
tool blueprint_get, blueprint_list, scenario_plan (new inputs) | blueprint_get({ scenario } | { run }), blueprint_list({}) (every workflow scenario, its plan’s state), scenario_plan({ id, dryRun }) | 1.23.0 | prompts, skills |
capability workflow:execute, pipeline:execute | run:execute (scenario_start) | 1.23.0 | role capabilities:, blueprints’ requiredCapabilities, grants:, tool code providesCapabilities — stored grants are not migrated: re-grant |
tool agent_status | agent_get (its snapshot under current; addressed by { id } or { name }) | 1.24.0 | engine-tools.json, toolLoading.eager, prompts, skills |
tool agent_get, agent_history, agent_tail (inputs) | agent_get({ id } | { name }); agent_history({ id, limit, cursor }) (messages_count → limit ≤ 100, no name); agent_tail({ id, … }) (no name) — resolve a name with agent_get first | 1.24.0 | prompts, skills, brain code calling the tools |
tool episode_close | agent_close_episode ({} still closes the turn’s own episode; { id } names an agent — an agent only itself) | 1.24.0 | engine-tools.json, toolLoading.eager, prompts, skills, tool code |
tool agent_stop, agent_delete (inputs) | { id } or { ids } (no agent_id, no name, no all — resolve a name with agent_get first) | 1.24.0 | prompts, skills, brain code calling the tools |
tool agent_update (inputs) | { id, patch: { role, capabilities, systemPrompt, modelRef, repo, runIn, groupKey, displayName } } (no agent_id / name addressing); a widening patch is refused → agent_widen (role:widen); patch.name (a rename) is refused from an agent | 1.24.0 | prompts, skills, brain code calling the tools |
tool watch_create, watch_list (inputs) | optional fields omitted, never null; watch_list’s agent → agentId, runId added | 1.24.0 | prompts, skills, brain code calling the tools |
tool schedule_forecast (inputs), run rows’ errorClass | schedule_forecast({ ahead }) (was window; 6h / 24h / 48h / 7d); a run row’s error.class (error = { class, head } or null) on run_list / run_get / run_trace / run_compare | 1.24.0 | prompts, skills, brain code calling the tools |
tool eval_get (inputs), eval_start (answer) | eval_get({ runId } | { id }) (no runKind), answering { evaluation, snapshot }; eval_start answers { evaluationId } (no queued / runId / jobId) | 1.24.0 | prompts, skills, brain code calling the tools |
tool hitl_list (inputs) | status / kind / producer take lists (status: "closed" → ["answered", "cancelled"]); pipelineRunId → runId (any execution); responderId → responder; an old cursor is refused | 1.24.0 | prompts, skills, brain code calling the tools |
tool hitl_list (window) | a window holds the closed requests decided in it (respondedAt) and every pending one — no longer the requests opened in it | 1.24.0 | prompts, skills, brain code calling the tool with a window |
tool loop_set_autonomy | loop_widen — same input (targetKind, targetId, knob from the oversight list, op, value) plus dryRun; capability loop:autonomy unchanged | 1.24.0 | engine-tools.json, toolLoading.eager, prompts, skills |
tool loop_join, loop_set, loop_widen, loop_hold, loop_release, loop_create_idea, loop_policy (answers) | loop_join / loop_set / loop_widen take dryRun and answer files (diffs); a refused hold, release or idea arrives as a refusal (precondition) instead of a text failure; loop_policy adds sections, lockGroups, proposerInstructions, calibration, loopCode, config | 1.24.0 | prompts, skills, brain code calling the tools |
tool access_people | access_identities — input person → identity; answer view.people → view.identities, scope: 'person' / person → scope: 'identity' / identity; access_update / access_widen edit { op: 'person', person } → { op: 'identity', identity } and their answer’s people → identities | 1.24.0 | engine-tools.json, toolLoading.eager, prompts, skills, brain code calling the tools |
constitution/access.yaml key people: | identities: (a person or a chat that speaks — chat-access.md) | 1.24.0 | a file still keyed people fails to load (unknown key): the bots serve only the env owners until it is renamed |
omnislash regen (and every upgrade) reconciles constitution/engine-capabilities.json for you; the rows above are the files it does not touch.
Escape hatch: omnislash upgrade itself broken
omnislash upgrade is run from the brain’s node_modules/.bin/omnislash — i.e. the currently installed CLI version. If that version has a regression in the upgrade flow (e.g. v0.142.1 had an empty-_authToken bug that 401’d pnpm install mid-upgrade), it cannot fix itself. Use the CLI-independent bootstrap to break the cycle:
# Bumps every @omnislash-ai/* peer to the latest registry version. Does NOT
# touch Dockerfile / .github/workflows/deploy.yml — re-run `omnislash upgrade`
# after this completes (the newly installed CLI regenerates those).
pnpm up "@omnislash-ai/*@latest"
# Now the fresh CLI is installed; re-run upgrade to regenerate Docker artifacts
# and write upgrade/TASK.md. Do not commit in between: the task is planned from
# the pin your last commit carries (if you did, add `--from <old version>`).
omnislash upgrade
A bare pnpm is enough here: the brain’s committed .npmrc carries no auth line, so your ~/.npmrc token applies to a raw pnpm call exactly as it does to one the CLI spawns. (Older brains that still commit _authToken=${NODE_AUTH_TOKEN} need that line deleted first — see § Setup step 3.)
Dockerfile Customization
The engine generates a standard Dockerfile in your brain root during omnislash upgrade (or first omnislash build). This file is always overwritten on upgrade — do not edit it manually.
When you need customizations
If the standard Dockerfile doesn’t cover your needs (extra system packages, custom build steps, multi-stage builds), create a Dockerfile.override in your brain root:
cp Dockerfile Dockerfile.override
# Edit Dockerfile.override with your changes
Build priority: Dockerfile.override > Dockerfile. If the override exists, omnislash build and omnislash start use it instead of the generated one.
The generated Dockerfile always reflects what the current engine version expects — use it as a reference when writing your override.
CI with override
If your brain uses Dockerfile.override, update your CI workflow:
# .github/workflows/deploy.yml — build-push-action
file: Dockerfile.override
If you use the standard generated Dockerfile, no CI changes are needed (it uses Dockerfile by default).
.dockerignore
A standard .dockerignore is regenerated alongside the Dockerfile on every omnislash build, omnislash start, and omnislash upgrade (ensureDockerignore always rewrites it). Treat it like the generated Dockerfile — do not edit manually; any local changes will be lost on the next build. If you need extra ignore entries, add them to your Dockerfile.override build context instead.
CI/CD
Gates on every change (ci.yml + deploy.yml)
omnislash upgrade / omnislash regen write .github/workflows/ci.yml. Its one job, gates, installs your dependencies with the same registry auth deploy.yml uses, then runs your package.json scripts — type-check, lint, test — each as its own named step. Nothing about your domain is encoded in the file: add a gate by adding a script.
Before them runs one engine-owned step, Upgrade task: while upgrade/TASK.md has an unticked before-deploy or in-repo entry, it fails and lists them, so an engine upgrade never deploys ahead of its own checks (upgrading.md). After your scripts comes another engine-owned step, Pipeline modules (omnislash doctor --check pipelines): it imports every enabled *.pipeline.ts under the discovery rules, without a server, and fails on any module that would not be served, listing every broken file. It is not optional and not counted as one of your scripts.
A brain that declares none of the three fails the workflow’s first step, on purpose: a CI that runs nothing and reports green is worse than no CI. If the upgrade that brought you this file turned your repo red, that is the message — add the scripts ("type-check": "tsc --noEmit", "lint": "biome check .", "test": "vitest run", whichever you actually want) and the gate goes green. upgrade will not add them for you: what a brain checks is the brain author’s call.
ci.yml runs on pull requests into main, on a daily schedule and on manual dispatch — not on push. Pushes to main are gated by Deploy itself: deploy.yml’s first job is the same gates job (one renderer, so the PR gate and the deploy gate cannot drift), and its deploy job needs both the gates and the image build of the same run. So the commit that was gated is the commit that is deployed, a manual deploy dispatch is gated too, and nothing waits on ci.yml. Deploy skips a push that only moved runtime-only paths (**.md, memory/**, your brainSync.runtimeOnly directories) — your brain-sync daemon commits the G-set every few minutes, and nothing there can fail a gate; anything else, including the features/ code in the G-set, is gated before it is deployed. The schedule is the net for what no push can trigger: main going red because a dependency or a registry moved under it.
A red gate blocks the deploy. The host stays on the previous version and the Deploy Drift alert reports that main carries code production is not running — fix main (there is no skip-gates switch). A gate does not hold back brain-sync’s hot reload of an edited *.pipeline.ts (contract.md § 2.1).
Your tests — and your pipeline modules — must not need a .env. These are unit gates on a clean runner: no containers, no network, and deliberately no production secrets. If a module reads env.*, stub it the way the engine stubs its own — a test.env block in vitest.config.ts:
export default defineConfig({
test: {
include: ['features/**/*.test.ts'],
env: { TELEGRAM_BOT_TOKEN: 'test-token', OPENAI_API_KEY: 'sk-test' },
},
});
Locally this changes nothing (vitest loads your .env first); on a runner it is the difference between a green gate and a schema-validation failure that has nothing to do with the change under test.
The Pipeline modules step imports your pipeline modules on the same secret-less runner. A module that reads env.* at import time cannot be checked there and is reported as a warning (the gate stays green, the module is not checked) — move env reads into the handlers. So is a pipeline scenario whose .scenario.md the runner could not read (e.g. frontmatter that validates only once a ${VAR} resolves from the deployment env): its modules were never imported. The step imports modules the way the pipelines service does (under tsx), so TypeScript the service runs is never a false red here.
Take the file over by creating .github/workflows/ci.yml.override — the same sentinel the other generated workflows honour. omnislash doctor then asks your version what it needs instead of the generated one. An owned ci.yml is yours: it no longer follows the engine’s triggers or steps. An owned deploy.yml (deploy.yml.override) does not get the gates job either — re-base it on the generated file (omnislash doctor → Owned workflows names the drift) or your deploys stay ungated.
Infisical setup
Before configuring CI, set up secrets management in Infisical:
- Create a project (e.g.
my-project) - Add runtime secrets to the
prodenvironment.
Four variables are hard-required for every brain — OPENAI_API_KEY, WEBHOOK_SECRET, CONNECTIONS_TOKEN_ENC_KEY, CONNECTIONS_STATE_HMAC_KEY — and the Telegram vars join them only when the config declares a bot persona: TELEGRAM_STATUS_CHAT_ID, TELEGRAM_ALLOWED_IDS and each bot’s tokenEnv (TELEGRAM_BOT_TOKEN for the roster synthesized when bots is omitted). The authoritative copy of that list is not this table: it is your own brain’s .env.example, generated from the live engine env schema, where an uncommented NAME= line marks a variable this brain must supply.
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY | yes | OpenAI API key. Memory embeddings hardcode the openai provider, so an Anthropic or OpenRouter key is not a substitute |
WEBHOOK_SECRET | yes | Shared secret authenticating incoming webhook requests. Generated by omnislash init — copy the value out of your local .env |
CONNECTIONS_TOKEN_ENC_KEY | yes | AES-GCM key (base64, 32 bytes) encrypting stored OAuth tokens. Generated by omnislash init — copy your local value; a different key here orphans every token already stored |
CONNECTIONS_STATE_HMAC_KEY | yes | HMAC key (base64, 32 bytes) signing the OAuth state parameter. Generated by omnislash init — copy your local value |
TELEGRAM_BOT_TOKEN | if a bot names it | From BotFather. Required when a bot’s tokenEnv names it — the roster synthesized when bots is omitted does; other bots read their own tokenEnv. Carries no meaning of its own: system messages — Grafana’s alerts included — ride the chief’s bot, whatever var holds its token |
TELEGRAM_STATUS_CHAT_ID | if Telegram | Chat for status/approval messages |
TELEGRAM_ALLOWED_IDS | if Telegram | Owners — owners of every bot (every command, control buttons, HITL approvals): 123456789,987654321. Numeric user ids only — a @username can be re-registered by someone else, so it fails validation; message @userinfobot for yours. Co-owners and everyone else: constitution/access.yaml (chat-access.md) |
TELEGRAM_HITL_CHAT_ID | no | Dedicated chat for HITL approvals; when empty, headless approvals go to the primary owner’s DM (first TELEGRAM_ALLOWED_IDS id) |
POSTGRES_PASSWORD | yes* | Managed postgres password (skip if using external DATABASE_URL) |
REDIS_PASSWORD | yes* | Managed redis password (skip if using external REDIS_URL) |
CLAUDE_API_KEY | no | Anthropic API key |
OPENROUTER_API_KEY | no | OpenRouter API key |
AI_GATEWAY_API_KEY | no | Vercel AI Gateway key — enables the vercel: API provider and the <harness>:vercel:<model> CLI router |
PUBLIC_DOMAIN | no | Domain for auto-TLS (e.g. yourdomain.com) — enables domain mode |
ACME_EMAIL | if domain mode | Email for Let’s Encrypt cert expiry warnings |
ADMIN_USER | if domain mode | Admin basic auth username for dashboards (e.g. admin) |
ADMIN_PASS_HASH | if domain mode | Admin bcrypt hash (see domain mode below) |
NC_ADMIN_EMAIL | if NocoDB | NocoDB admin email |
NC_ADMIN_PASSWORD | if NocoDB | NocoDB admin password |
SENTRY_DSN | no | Error tracking |
GRAFANA_PASSWORD | no | Grafana admin (default: admin) |
LOG_LEVEL | no | Default: info |
For database and Redis, choose managed (container) or external (BYOI):
- Managed postgres: set
POSTGRES_PASSWORD— do NOT setDATABASE_URL - External postgres (Supabase, Neon): set
DATABASE_URL=postgresql://... - Managed redis: set
REDIS_PASSWORD— do NOT setREDIS_URL - External redis: set
REDIS_URL=redis://...
Domain mode (optional): if you have a domain, set PUBLIC_DOMAIN to enable auto-TLS via Caddy + Let’s Encrypt. This exposes services on subdomains (bot., app., grafana., bullboard., webhooks., artifacts., nocodb., n8n., restate., mcp.) with automatic HTTPS, one vhost per service you actually run — see deploy/Caddyfile.domain for the canonical list of vhosts.
Prerequisites:
- Create DNS A records pointing each enabled subdomain to the server IP. A single wildcard record (
*.yourdomain.com → server-ip) is the simplest setup and covers any future subdomain Caddy adds. - Open ports 80 + 443 on the server (
ufw allow 80/tcp && ufw allow 443/tcp)
Required env vars for domain mode:
PUBLIC_DOMAIN=yourdomain.com
ACME_EMAIL=admin@yourdomain.com
TELEGRAM_WEBHOOK_URL=https://bot.yourdomain.com
TELEGRAM_WEBHOOK_SECRET=<random-string>
Admin dashboards (app., grafana., bullboard., nocodb., restate.) become publicly reachable in domain mode, so Caddy enforces basic auth on every one of them — Grafana included, even though it has its own login: an unauthenticated Grafana vhost pays memory per anonymous request and a credential scanner can OOM-kill it. Generate a bcrypt hash and add credentials:
docker run caddy:2-alpine caddy hash-password --plaintext 'your-password'
ADMIN_USER=admin
ADMIN_PASS_HASH=$2a$14$... # output from caddy hash-password
Without domain mode, services are accessible only via localhost/IP (self-signed certs for webhooks). See operations.md § Webhook TLS for details.
- Create a Universal Auth machine identity with access to
prod— you’ll getclient-idandclient-secret
Server: Infisical CLI setup
Install the CLI and store machine identity credentials on the production server. This is a one-time setup — CI deploys will SSH in and use these credentials to export .env from Infisical on every deploy.
ssh <user>@<server-ip>
# The release tarball is the only channel with anything to check the bytes
# against: the apt/deb repo is outdated, and npm's package downloads this same
# tarball from a preinstall script that verifies nothing. The release's own
# `checksums.txt` is unsigned and comes from the same origin as the tarball, so
# it catches a corrupted transfer and nothing else — the digests below are the
# ones the engine pins in `bootstrap.sh` §0, reviewed and in git.
V=0.43.128
A=$(dpkg --print-architecture) # amd64 or arm64
case "$A" in
amd64) SHA=a3f460be321ad46fefba99cba883bcc601d0f18b02849d2d30ae9b398a8d99dc ;;
arm64) SHA=e4fffc20191135c539ab0b086a7a5e8b8bee08db3defde172aa380c768210628 ;;
*) echo "no pinned digest for $A" >&2 ;;
esac
curl -fsSL -O "https://github.com/Infisical/cli/releases/download/v${V}/cli_${V}_linux_${A}.tar.gz"
echo "${SHA} cli_${V}_linux_${A}.tar.gz" | sha256sum -c -
tar -xzf "cli_${V}_linux_${A}.tar.gz" infisical
sudo install -m 0755 infisical /usr/local/bin/
# Verify
infisical --version
Store the machine identity credentials (from step 3 above):
cat > ~/.infisical-credentials << 'EOF'
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id>
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret>
EOF
chmod 600 ~/.infisical-credentials
Verify it works — this is exactly what CI does on each deploy:
# Load credentials
set -a && source ~/.infisical-credentials && set +a
# Authenticate (returns a JWT token)
INFISICAL_TOKEN=$(infisical login \
--method=universal-auth \
--client-id=$INFISICAL_UNIVERSAL_AUTH_CLIENT_ID \
--client-secret=$INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET \
--domain=https://eu.infisical.com \
--silent --plain)
# Export secrets as .env (dry run — prints to stdout)
infisical export \
--token=$INFISICAL_TOKEN \
--domain=https://eu.infisical.com \
--projectId=<your-project-id> \
--env=prod \
--format=dotenv
You should see your secrets printed as KEY=VALUE pairs. If this works, server setup is complete — CI will do the same thing automatically on each deploy.
Updating the CLI:
omnislash upgradeto an engine version whose pin is newer, then re-runomnislash init-server— the step is version-guarded, so it reinstalls. By hand: repeat the block above with the newer version and the digest that engine version pins.
GitHub Actions secrets and variables
Add these to your brain repository (Settings → Secrets and variables → Actions). Secrets go under Repository secrets, variables go under Repository variables (not Environment).
| Name | Type | Required | Description |
|---|---|---|---|
OMNISLASH_PAT | secret | yes | GitHub classic PAT with read:packages scope — for the @omnislash-ai/* npm packages and the engine base image |
DEPLOY_KEY | secret | yes | SSH private key (ed25519) for server access — it logs in as the app user, which is in the host docker group, so treat it as a root credential on the production host |
DEPLOY_HOST | variable | yes | user@server-ip (empty = skip deploy job). Letters, digits, @ . _ -, no leading - — the deploy job rejects anything else up front |
DEPLOY_DIR | variable | yes | Deploy path on server (e.g. /opt/my-project). Letters, digits, / . _ ~ - — the value reaches a shell on the server, so the deploy job rejects anything else up front |
DEPLOY_HOST_KEY | variable | yes | The server’s ed25519 host public key (ssh-ed25519 AAAA…, from /etc/ssh/ssh_host_ed25519_key.pub, no host name in front). The deploy job pins the server to it instead of scanning — omnislash init-server sets it |
INFISICAL_CLIENT_ID | secret | yes | Infisical machine identity client ID |
INFISICAL_CLIENT_SECRET | secret | yes | Infisical machine identity client secret |
INFISICAL_PROJECT_SLUG | variable | yes | Infisical project slug |
INFISICAL_PROJECT_ID | variable | yes | Infisical project ID (for CLI export) |
INFISICAL_ENV_SLUG | variable | no | Infisical environment (default: prod) |
ENGINE_VERSION | variable | no | Engine base image tag (default: latest) |
INFISICAL_DOMAIN | variable | no | Infisical instance URL (default: https://eu.infisical.com) |
GITHUB_TOKEN is automatic — used for GHCR login (push brain image). It cannot read the engine base image: package Actions access admits only repositories of the org that owns the package, and your brain lives in your own namespace. So the build job logs in a second time with OMNISLASH_PAT, scoped to omnislash-ai/engine pulls, and the push keeps GITHUB_TOKEN.
For the deploy workflow (.github/workflows/deploy.yml) and the full CI/CD flow, see operations.md § Brain CI/CD.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
ERR_PNPM_FETCH_401 on pnpm install | Token expired or missing | Ask maintainer for a new token, update ~/.npmrc |
ERR_PNPM_FETCH_404 | Token doesn’t have access to packages | Ask maintainer to verify the token |
| Packages resolve but types don’t match | Version mismatch between npm packages and base image | Ensure package.json versions match FROM tag in Dockerfile |
docker build fails with denied / 403 on ghcr.io/omnislash-ai/engine | Docker not logged into GHCR, or logged in with an expired token or one without access to the engine image | Re-run the docker login ghcr.io from § 5 with a current token; in CI, rotate OMNISLASH_PAT |
Manual upgrade (when CLI is broken)
If omnislash upgrade doesn’t work (e.g., the installed CLI is broken or won’t start), copy-paste this block as-is:
cd my-brain
pnpm update \
@omnislash-ai/cli@latest \
@omnislash-ai/config@latest \
@omnislash-ai/infra@latest \
@omnislash-ai/pipeline-sdk@latest \
@omnislash-ai/types@latest
V=$(node -e "console.log(require('./node_modules/@omnislash-ai/cli/package.json').version)")
# Patch the base image version in Dockerfile (generated or override)
sed -i "s|ghcr.io/omnislash-ai/engine:[^ ]*|ghcr.io/omnislash-ai/engine:$V|g" Dockerfile
git add package.json pnpm-lock.yaml Dockerfile
git commit -m "chore: update engine to v$V"
# No push yet: a push deploys, and the agent task comes first.
If pnpm update fails with 401 — fix ~/.npmrc first (see token renewal above).
Then, once a CLI works, run
omnislash upgrade --from <the version you were on>: it regenerates theDockerfileproperly and writes the agent task for the range, which you follow before pushing (upgrading.md).
Production troubleshooting
If a service behaves differently in production (crashes, OOM, wrong resource limits, missing env vars), you can fix it with a compose override — no engine changes needed.
Place docker/compose.prod.yml in your brain root with only the delta you want to change. CLI deep-merges it on top of engine templates. Objects are deep-merged, scalars and arrays are replaced.
omnislash upgrade ships a ready-to-copy docker/compose.prod.yml.example (and compose.start.yml.example) next to where the real file goes — cp docker/compose.prod.yml.example docker/compose.prod.yml, then uncomment the blocks you need (deep-merge tuning, x-override: replace take-over, a new sidecar). These *.yml.example files are engine-owned and refreshed on each upgrade; your live docker/compose.{mode}.yml is never touched.
# docker/compose.prod.yml
services:
worker:
deploy:
replicas: 5
resources:
limits:
memory: 2G
bot:
environment:
LOG_LEVEL: debug
Common overrides:
| Problem | Override |
|---|---|
A service’s memory or CPU ceiling (an OOM, the Container CPU At Limit alert, a host of another size) | Its server-size knob in the production env first — WORKER_MEMORY=2G, DASHBOARD_CPUS=2.0, every one in operations.md § Server size; this override for a service without one: services.<name>.deploy.resources.limits.cpus: "2.0" — a ceiling, not a reservation: an idle service costs nothing under it |
| Need more workers | WORKER_REPLICAS=5, or services.worker.deploy.replicas: 5 |
| Debug a service | services.<name>.environment.LOG_LEVEL: debug |
| Add a sidecar (metrics, proxy) | Add a new service under services: |
Workflow:
# 1. Generate production compose locally to see the full structure
omnislash compose --prod
# 2. Inspect what engine generates
cat .omnislash/docker-compose.prod.yml
# 3. Create docker/compose.prod.yml with only your delta
# 4. Re-generate to verify — override is applied
omnislash compose --prod
cat .omnislash/docker-compose.prod.yml # should include your changes
# 5. Commit and push — CI will apply the same override
git add docker/compose.prod.yml && git commit && git push
CLI logs Applied brain override: docker/compose.prod.yml when an override is detected.
NocoDB: Browsing Database Tables
NocoDB provides an Airtable-like web UI for browsing your database. It runs automatically as part of the stack.
Access
| Mode | URL |
|---|---|
| Domain mode | https://nocodb.<PUBLIC_DOMAIN> |
| No domain | http://<server-ip>:8080 |
Local dev (omnislash start) | http://localhost:8080 |
Login with the NC_ADMIN_EMAIL / NC_ADMIN_PASSWORD credentials from your Infisical secrets.
Database Schemas
Engine and brain use the same PostgreSQL database but different schemas:
| Schema | Owner | Tables |
|---|---|---|
omnislash | Engine | agents, agent_conversations, conversation_events, workflow_runs, workflow_task_runs, capability_grants, hitl_interactions, pipeline_runs, heartbeat_runs, etc. |
public | Brain (your project) | Your custom tables (e.g. customers, orders) |
nocodb_meta | NocoDB | Internal metadata (~100 nc_* tables) — don’t touch |
Auto-configured Data Sources
The nocodb-init container creates two read-only NocoDB data sources on first boot:
- Engine →
omnislashschema (engine tables, schema-readonly) - Project →
publicschema (your brain tables, schema-readonly)
Both sources auto-discover tables via NocoDB meta-sync. Once a migration has created new tables, they appear in NocoDB after the next meta-sync (Settings → Data Sources → Sync Metadata).
Adding a Data Source Manually (UI)
If the auto-init didn’t run or you need to re-add a source:
- Open NocoDB → click base name (top-left) → Settings (gear icon)
- Go to Data Sources → + New Source
- Fill in:
- Source Name:
EngineorProject - Database Type: PostgreSQL
- Host:
postgres(Docker service name) or your external DB host - Port:
5432 - Username:
omnislash(or yourPOSTGRES_USER) - Password: your
POSTGRES_PASSWORD - Database:
omnislash(or yourPOSTGRES_DB) - Schema:
omnislashfor engine tables,publicfor brain tables
- Source Name:
- Click Test Connection → Save
- Go to Data Sources → click the sync icon (↻) next to the new source to discover tables
Known Limitations
- Reserved table names: NocoDB silently skips tables named
conversationsandgrantsduring introspection. Engine tables are already renamed to avoid this (agent_conversations,capability_grants). If your brain tables don’t appear, try renaming them. - Schema-readonly: Both auto-configured sources have
is_schema_readonly: true— you can view and edit data but cannot alter table structure through NocoDB. - Meta-sync for new tables: After a migration adds tables, open NocoDB → Settings → Data Sources → click sync icon (↻) to discover them.
Adding Brain Tables
Brain projects define their own tables in the public PostgreSQL schema. Convention-based — no config needed: declare them in db/schema.ts, generate a migration, commit it, and the next deploy applies it.
// db/schema.ts — import pg-core primitives from the engine, not from drizzle-orm directly
import { pgTable, text, timestamp, uuid } from '@omnislash-ai/infra/drizzle';
export const customers = pgTable('customers', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
email: text('email'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
omnislash db generate --name add_customers # writes db/migrations/NNNN_add_customers.sql
# review the SQL, commit db/migrations/ (the .sql AND meta/), push
Usage in brain tools:
import { db, eq } from '@omnislash-ai/infra/drizzle';
import { customers } from '../db/schema.ts';
const rows = await db.select().from(customers).where(eq(customers.name, 'test'));
Full rules — foreign keys into engine tables, renames, what never to edit: database.md.
For the Engine Maintainer
Grant npm access (PAT)
To grant a brain-dev access to npm packages without exposing engine source:
- Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic)
- Generate a token with only
read:packagesscope - Set expiration (recommended: 90 days — set a reminder to renew)
- Share the token with the brain-dev (securely — e.g., via a password manager or encrypted channel)
This token is tied to your GitHub account. It grants read access to all packages you own, but not to any repositories. Brain-dev cannot see engine source code.
When the token expires, generate a new one and share it again.
GHCR base image
The engine Docker image (ghcr.io/omnislash-ai/engine) is public. No additional setup needed — brain CI pulls it without auth via GITHUB_TOKEN.
Quick Reference
Starting from nothing is getting-started.md; this is the day-2 card.
# One-time registry access (your machine, once — never in a project file)
printf '%s\n%s\n' \
'@omnislash-ai:registry=https://npm.pkg.github.com' \
'//npm.pkg.github.com/:_authToken=TOKEN' >> ~/.npmrc
# New brain, from zero to a running stack
pnpm dlx @omnislash-ai/cli init my-brain
cd my-brain && omnislash start
# Update engine
omnislash upgrade # or: omnislash upgrade 0.199.0
# then follow upgrade/TASK.md to the end (upgrading.md)
# Diagnose anything
omnislash doctor # --fix applies what it can
omnislash doctor --check <id> # one check by its id (llm-providers, upgrade-in-progress, …)
omnislash doctor --json # results as one JSON array of { id, name, status, message, hint, fix }
# After token expiration
# 1. Ask maintainer for a new token
# 2. Replace token in ~/.npmrc
# 3. pnpm install