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 ~/.npmrc supports 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.yml and ci.yml configure actions/setup-node with registry-url + scope, which writes a scoped npm userconfig from the NODE_AUTH_TOKEN secret. This is the same mechanism the engine uses to publish itself.
  • Children the CLI spawns — omnislash init and omnislash upgrade resolve a token (environment first, then ~/.npmrc) and hand their pnpm install a process-local, owner-only npm userconfig via NPM_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 .npmrc by hand — nothing rewrites an existing brain’s .npmrc, and omnislash doctor only 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:

  1. pnpm install fails with 401 Unauthorized
  2. Ask the engine maintainer for a new token
  3. Replace the old token in ~/.npmrc (//npm.pkg.github.com/:_authToken=ghp_NEW_TOKEN) and re-run the docker login ghcr.io from § 5
  4. In CI, rotate the OMNISLASH_PAT repo 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.json or toolLoading.eager fails 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. In constitution/engine-capabilities.json it fails the load the same way, but omnislash upgrade rewrites that file for you (it is engine-reconciled), carrying your risk / hitl onto 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_overview and 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.

WasNowSinceWhere it bites
capability state:readruntime:read1.6.0role capabilities:, blueprints, grants:
capability improvement:readloop:read1.16.0same
capability improvement:executeloop:analyze1.16.0same
capability improvement:superviseloop:supervise + loop:hold (the hold split off, hitl: false)1.16.0same
capability improvement:auto_acceptloop:auto_accept (admin-only)1.16.0admin grants
role system:browser-operatorsystem:browser1.17.0agent: { role: … }; a brain copy role-browser-operator.md becomes role-browser.md
role system:root-assistantsystem:chief (glossary.md)1.19.0a 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-guidesystem:editor-assistant (the dashboard editors’ assistant)1.22.0a 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.0the old key fails the load, naming the new one
chief persona root-chat (the default chief.name)chief1.22.0with 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.runKindctx.targetKind (scenario · heartbeat · role)1.19.0a <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.0a 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.0brain code that imports it fails to resolve the import; doctor does not flag it
env MCP_OPERATOR_TOKEN / MCP_OWNER_TOKENMCP_TOKEN1.18.0since 1.22.0 the old names are not read: the MCP bearer door stays closed until MCP_TOKEN is set
env TELEGRAM_GUEST_IDSremoved — identities: in constitution/access.yaml (chat-access.md)1.17.0a set value fails the boot
tool respond_to_agent_questionagent_answer (agent_escalate hands a question to a person)1.17.0role prompts and skills that name the tool
tool state_overviewruntime_overview1.6.0role prompts and skills that name the tool
capability artifacts:publishartifact:publish1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability chats:readaccess:read1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability chats:writeaccess:write1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability chats:widenaccess:widen1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability chats:adminaccess:admin1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability chat_episode:writeepisode:write1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability database:readdb:read1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability database:writedb:write1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability integrations:executeintegration:execute1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability contacts:readcontact:read1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
capability contacts:writecontact:write1.19.0role capabilities:, blueprints, grants:, tool code providesCapabilities
tool agent_runagent_call1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool agent_update_configagent_update1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool read_filefile_read1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool write_filefile_write1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool edit_filefile_edit1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool memory_getmemory_read1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool describe_tablesdb_schema1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool ingest_eventevent_ingest1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool load_skillskill_get1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool read_skill_resourceskill_read_resource1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool notify_sendnotification_send1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool publish_artifactartifact_publish1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool unpublish_artifactartifact_unpublish1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool request_permissions_granthitl_request_grant1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool request_integration_connecthitl_request_connection1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool blueprint_readblueprint_get1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool blueprint_workflowscenario_plan1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool plan_scenarioscenario_plan1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool execute_workflowscenario_start1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool execute_pipelinescenario_start1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_pipelinespipeline_list1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool eval_runeval_start1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool chat_episode_closeagent_close_episode (was episode_close until 1.24.0)1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool schedule_writescenario_update1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool workflow_failure_diagnosisrun_diagnosis1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool patch_blueprint_snapshotrun_update_blueprint1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool queue_addqueue_create_job1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool queue_readqueue_get1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool watch_resourcewatch_create1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool unwatch_resourcewatch_delete1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool request_approvalhitl_request_approval1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool loop_analyze_nowloop_analyze1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool loop_idea_postloop_create_idea1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool loop_idea_withdrawloop_cancel_idea1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool schedule_promptschedule_create_prompt1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_calendar_eventscalendar_list_events1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_calendarscalendar_list1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool create_calendar_eventcalendar_create_event1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool update_calendar_eventcalendar_update_event1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool delete_calendar_eventcalendar_delete_event1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_emailsemail_list1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool read_emailemail_get1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool send_emailemail_send1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool search_drivedrive_search1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_drive_filesdrive_list_files1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool read_drive_filedrive_read_file1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_task_liststasks_list_lists1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_taskstasks_list1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool create_tasktasks_create1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool update_tasktasks_update1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool complete_tasktasks_complete1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool delete_tasktasks_delete1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool extract_pdfdocument_read (also reads Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, HTML and text)1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool process_imageimage_process1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool generate_imageimage_generate1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool transcribe_audioaudio_transcribe1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool get_sitemapweb_sitemap1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool parse_webpageweb_parse1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool google_maps_searchmaps_search1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool google_maps_reviewsmaps_reviews1.19.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool cancel_workflowrun_cancel (workflow and pipeline runs, input { id })1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool cancel_pipelinerun_cancel1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool retry_workflowrun_retry (input { id })1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool pause_pipelinerun_pause (workflow and pipeline runs)1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool resume_pipelinerun_resume (workflow and pipeline runs)1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool kill_pipelinerun_kill1.20.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
capability pipeline:killrun: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.0role capabilities:, blueprints, grants:, tool code providesCapabilities
tool list_runsrun_list (every kind of run — workflow, pipeline, tick, fire, eval, cycle step; filters entity / feature / kind / status / window)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool list_pipeline_runsrun_list (kind: ['pipeline'])1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool workflow_run_statusrun_get (input { id } — a run id or slug)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool pipeline_run_statusrun_get (input { id }; the pipeline detail is under pipeline)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
capability workflow:readrun:read + eval:read (run_list, run_get, run_diagnosis · eval_get, eval_list); eval_start moves from workflow:execute to the new eval:execute1.21.0role capabilities:, blueprints, grants:, tool code providesCapabilities
tool chats_overviewaccess_overview (input { bot? }; the frame per bot with every tier’s own entry; access_identities lists who is in a chat)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool chats_explainaccess_explain (chat: 'dm' | tg:chat:<id>, sender or senderChat)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool chats_setaccess_update (typed edits, one tier as YAML in rule, or the whole file; returns { sha, diff, consequences })1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool chats_widenaccess_widen (same input as access_update)1.21.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
file constitution/chats.yamlconstitution/access.yaml1.21.0nothing 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.refreshHoursbrief.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 write1.21.0the 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.0brain 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:executedhook:enqueued (the hook’s agent was handed the prompt; it may still be working)1.22.0a 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.0brain code that imports them fails to resolve the import; doctor does not flag it
tool health_featurehealth_list — every feature in one call (feature narrows to one); entity rows carry their trigger and ten newest runs (outcomes)1.23.0engine-tools.json, toolLoading.eager, prompts, skills
tool workflow_start, pipeline_startscenario_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 inputRequested1.23.0engine-tools.json, toolLoading.eager, prompts, skills, tool code
tool schedule_updatescenario_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 model1.23.0engine-tools.json, toolLoading.eager, prompts, skills, role capabilities:
tool blueprint_createscenario_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 planning1.23.0engine-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.0prompts, skills
capability workflow:execute, pipeline:executerun:execute (scenario_start)1.23.0role capabilities:, blueprints’ requiredCapabilities, grants:, tool code providesCapabilities — stored grants are not migrated: re-grant
tool agent_statusagent_get (its snapshot under current; addressed by { id } or { name })1.24.0engine-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 first1.24.0prompts, skills, brain code calling the tools
tool episode_closeagent_close_episode ({} still closes the turn’s own episode; { id } names an agent — an agent only itself)1.24.0engine-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.0prompts, 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 agent1.24.0prompts, skills, brain code calling the tools
tool watch_create, watch_list (inputs)optional fields omitted, never null; watch_list’s agent → agentId, runId added1.24.0prompts, skills, brain code calling the tools
tool schedule_forecast (inputs), run rows’ errorClassschedule_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_compare1.24.0prompts, 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.0prompts, 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 refused1.24.0prompts, 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 it1.24.0prompts, skills, brain code calling the tool with a window
tool loop_set_autonomyloop_widen — same input (targetKind, targetId, knob from the oversight list, op, value) plus dryRun; capability loop:autonomy unchanged1.24.0engine-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, config1.24.0prompts, skills, brain code calling the tools
tool access_peopleaccess_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 → identities1.24.0engine-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.0a 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:

  1. Create a project (e.g. my-project)
  2. Add runtime secrets to the prod environment.

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.

VariableRequiredDescription
OPENAI_API_KEYyesOpenAI API key. Memory embeddings hardcode the openai provider, so an Anthropic or OpenRouter key is not a substitute
WEBHOOK_SECRETyesShared secret authenticating incoming webhook requests. Generated by omnislash init — copy the value out of your local .env
CONNECTIONS_TOKEN_ENC_KEYyesAES-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_KEYyesHMAC key (base64, 32 bytes) signing the OAuth state parameter. Generated by omnislash init — copy your local value
TELEGRAM_BOT_TOKENif a bot names itFrom 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_IDif TelegramChat for status/approval messages
TELEGRAM_ALLOWED_IDSif TelegramOwners — 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_IDnoDedicated chat for HITL approvals; when empty, headless approvals go to the primary owner’s DM (first TELEGRAM_ALLOWED_IDS id)
POSTGRES_PASSWORDyes*Managed postgres password (skip if using external DATABASE_URL)
REDIS_PASSWORDyes*Managed redis password (skip if using external REDIS_URL)
CLAUDE_API_KEYnoAnthropic API key
OPENROUTER_API_KEYnoOpenRouter API key
AI_GATEWAY_API_KEYnoVercel AI Gateway key — enables the vercel: API provider and the <harness>:vercel:<model> CLI router
PUBLIC_DOMAINnoDomain for auto-TLS (e.g. yourdomain.com) — enables domain mode
ACME_EMAILif domain modeEmail for Let’s Encrypt cert expiry warnings
ADMIN_USERif domain modeAdmin basic auth username for dashboards (e.g. admin)
ADMIN_PASS_HASHif domain modeAdmin bcrypt hash (see domain mode below)
NC_ADMIN_EMAILif NocoDBNocoDB admin email
NC_ADMIN_PASSWORDif NocoDBNocoDB admin password
SENTRY_DSNnoError tracking
GRAFANA_PASSWORDnoGrafana admin (default: admin)
LOG_LEVELnoDefault: info

For database and Redis, choose managed (container) or external (BYOI):

  • Managed postgres: set POSTGRES_PASSWORD — do NOT set DATABASE_URL
  • External postgres (Supabase, Neon): set DATABASE_URL=postgresql://...
  • Managed redis: set REDIS_PASSWORD — do NOT set REDIS_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.

  1. Create a Universal Auth machine identity with access to prod — you’ll get client-id and client-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 upgrade to an engine version whose pin is newer, then re-run omnislash 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).

NameTypeRequiredDescription
OMNISLASH_PATsecretyesGitHub classic PAT with read:packages scope — for the @omnislash-ai/* npm packages and the engine base image
DEPLOY_KEYsecretyesSSH 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_HOSTvariableyesuser@server-ip (empty = skip deploy job). Letters, digits, @ . _ -, no leading - — the deploy job rejects anything else up front
DEPLOY_DIRvariableyesDeploy 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_KEYvariableyesThe 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_IDsecretyesInfisical machine identity client ID
INFISICAL_CLIENT_SECRETsecretyesInfisical machine identity client secret
INFISICAL_PROJECT_SLUGvariableyesInfisical project slug
INFISICAL_PROJECT_IDvariableyesInfisical project ID (for CLI export)
INFISICAL_ENV_SLUGvariablenoInfisical environment (default: prod)
ENGINE_VERSIONvariablenoEngine base image tag (default: latest)
INFISICAL_DOMAINvariablenoInfisical 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

ErrorCauseFix
ERR_PNPM_FETCH_401 on pnpm installToken expired or missingAsk maintainer for a new token, update ~/.npmrc
ERR_PNPM_FETCH_404Token doesn’t have access to packagesAsk maintainer to verify the token
Packages resolve but types don’t matchVersion mismatch between npm packages and base imageEnsure package.json versions match FROM tag in Dockerfile
docker build fails with denied / 403 on ghcr.io/omnislash-ai/engineDocker not logged into GHCR, or logged in with an expired token or one without access to the engine imageRe-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 the Dockerfile properly 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:

ProblemOverride
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 workersWORKER_REPLICAS=5, or services.worker.deploy.replicas: 5
Debug a serviceservices.<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

ModeURL
Domain modehttps://nocodb.<PUBLIC_DOMAIN>
No domainhttp://<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:

SchemaOwnerTables
omnislashEngineagents, agent_conversations, conversation_events, workflow_runs, workflow_task_runs, capability_grants, hitl_interactions, pipeline_runs, heartbeat_runs, etc.
publicBrain (your project)Your custom tables (e.g. customers, orders)
nocodb_metaNocoDBInternal 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:

  1. Engine → omnislash schema (engine tables, schema-readonly)
  2. Project → public schema (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:

  1. Open NocoDB → click base name (top-left) → Settings (gear icon)
  2. Go to Data Sources → + New Source
  3. Fill in:
    • Source Name: Engine or Project
    • Database Type: PostgreSQL
    • Host: postgres (Docker service name) or your external DB host
    • Port: 5432
    • Username: omnislash (or your POSTGRES_USER)
    • Password: your POSTGRES_PASSWORD
    • Database: omnislash (or your POSTGRES_DB)
    • Schema: omnislash for engine tables, public for brain tables
  4. Click Test Connection → Save
  5. 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 conversations and grants during 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:

  1. Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic)
  2. Generate a token with only read:packages scope
  3. Set expiration (recommended: 90 days — set a reminder to renew)
  4. 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

Screens from Sixty Seconds Studio, a fictional demo studio, running on the real dashboard.