| name | minds-dev-workflow |
| description | End-to-end dev workflow for the minds app stack -- first-time bring-up, every-startup vendor/mngr sync, and the iteration loop against a running Docker agent. Use this when starting or restarting the dev Electron app, or after changing any minds component (mngr, the system interface, the FCT template). |
Minds Dev Workflow
This skill covers the full minds dev cycle: standing up an FCT worktree, syncing the live mngr code into that worktree's vendor/mngr/, activating a per-developer dev env, starting the dev Electron app, and iterating against a running Docker agent. Use it whenever you're about to start the dev app (the vendor/mngr sync needs to happen every startup) or after editing any component (mngr, the system_interface, the FCT template).
Architecture Overview
The minds stack has four components that need to stay in sync:
- minds desktop client (
apps/minds/) -- Electron app + FastAPI backend that runs locally, proxies to agent web servers
- system_interface (lives in
forever-claude-template/apps/system_interface/, distributed as the system-interface CLI) -- FastAPI + web UI that runs INSIDE the agent's Docker container as a background service
- mngr core (
libs/mngr/) -- the agent management CLI
- forever-claude-template -- the template repo that defines the Docker container (Dockerfile, services.toml, skills, scripts)
The template contains a vendor/mngr/ directory (a snapshot of the mngr repo). During development, we sidestep that snapshot by rsyncing the local mngr working tree directly into a parallel-named branch of an FCT worktree under .external_worktrees/forever-claude-template/.
How changes propagate
local mngr repo --> FCT worktree's vendor/mngr/ --> Docker container's /code/
(under .external_worktrees/) (via rsync over SSH)
The desktop client runs on the host (via Electron). The system interface + mngr run inside the container. The vendor/mngr/ sync is what makes the dev loop work end-to-end.
Critical: the vendor/mngr/ sync must happen BEFORE every Create
When you click "Create" in the desktop client with a LOCAL-Docker provider, the desktop client (apps/minds/.../agent_creator.py) takes whatever's currently in the FCT worktree (including vendor/mngr/), shallow-clones it to a temp dir, rsyncs the worktree's working dir over the clone (so uncommitted FCT-side changes propagate), and ships the result into /code/ in the Docker container. mngr inside the container is uv tool install -e'd from /code/vendor/mngr/.
The desktop client does NOT auto-sync live mngr code into the worktree. If vendor/mngr/ is stale relative to your live mngr working tree, the Docker container's mngr will be stale too -- and (depending on what you've changed) mngr create inside the container may reject your .mngr/settings.toml with errors like Unknown fields in agent_types.claude: [...]. The bootstrap's chat-agent-create step then fails, and you'll see an empty workspace with "No conversation data" in the chat panel.
just minds-start (the all-in-one recipe described below) does this sync for you on every invocation. Use it rather than running pnpm start directly when you're testing local mngr changes.
Quick start (first time and every time)
cd apps/minds && pnpm install && cd ../..
git -C ~/project/forever-claude-template worktree add \
-b "$(git rev-parse --abbrev-ref HEAD)" \
"$PWD/.external_worktrees/forever-claude-template" \
josh/start-minds
eval "$(uv run minds env activate --create <your-user>-dev)"
uv run minds env deploy
eval "$(uv run minds env activate <your-user>-dev)"
just minds-start
That's it. After the create-form is filled in and you've created an agent, see Iterating on a running agent for the inner loop.
If you want to run against prod / staging instead of a personal dev env, use eval "$(uv run minds env activate production)" (or ... activate staging) and then just minds-start. Do not run minds env deploy against production / staging without coordinating with the rest of the team -- that pushes Vault secrets to Modal and re-deploys the live tier; the unified deploy CLI requires --yes-i-mean-production / --yes-i-mean-staging as a safety bar.
What just minds-start does
- Verifies a minds env is activated in the shell (refuses with a helpful error if not).
- Verifies the FCT worktree exists at
.external_worktrees/forever-claude-template/ and bails with a helpful error if not.
- Rsyncs the live mngr working tree into the FCT worktree's
vendor/mngr/ using the same exclusions as the pool-bake's --mngr-source path (.git, __pycache__, .venv, node_modules, etc.). Uncommitted changes are included; nothing is committed in the FCT worktree.
- Launches Electron with the right
MINDS_WORKSPACE_* env vars so the create-form auto-fills "repository", "name", and "branch".
Iterating on a running agent
After making changes to any component (mngr, the template's system_interface, the template, etc.), sync them into a running agent's container:
eval "$(uv run minds env activate <your-user>-dev)"
apps/minds/scripts/propagate_changes \
--user root --host 127.0.0.1 --port <SSH_PORT> \
--key <SSH_KEY_PATH>
This:
- Verifies a minds env is activated in the shell (refuses without it).
- Rsyncs the mngr repo into the FCT worktree's
vendor/mngr/ (same step just minds-start does, idempotent)
- Stops the agent (
mngr stop)
- Rsyncs the full template (with updated vendor/mngr/) into
/code/ in the container
- Rebuilds the system interface frontend (
npm run build via SSH)
- Starts the agent (
mngr start)
- Stops and restarts the Electron desktop client (clean SIGTERM shutdown)
The whole cycle takes about 5-10 seconds.
For local (non-container) agents:
eval "$(uv run minds env activate <your-user>-dev)"
apps/minds/scripts/propagate_changes --target /path/to/agent/workdir
Find the Docker container's SSH port and key
The port is randomly assigned by Docker per agent. The container name is <MNGR_PREFIX><agent-name>-host (set by your activated env's MNGR_PREFIX):
eval "$(uv run minds env activate <your-user>-dev)"
docker ps --format '{{.Names}} {{.Ports}}' | grep "${MNGR_PREFIX}mindtest"
The SSH key for a minds Docker agent lives under the activated env's MNGR_HOST_DIR:
eval "$(uv run minds env activate <your-user>-dev)"
find "${MNGR_HOST_DIR}/profiles" -path "*/docker/*/keys/docker_ssh_key"
Do NOT use a key from ~/.mngr/profiles/... -- that belongs to non-minds mngr agents and will silently fail with "Permission denied (publickey)". Likewise do NOT use a key from a different activated env (each env has its own profile dir).
Reference
Just recipes that touch this stack
| Recipe | Purpose |
|---|
just minds-start | Preferred dev entry point. Sync live mngr -> FCT vendor/mngr, then launch the Electron app. Requires an activated minds env in the shell. |
just minds-stop | Kill the desktop client started in this worktree by just minds-start. |
just minds-build | Build the desktop client distributable via todesktop (slow, only for releases). |
apps/minds/scripts/propagate_changes ... | Sync changes into a running container without restarting the Electron app from scratch. See "Iterating on a running agent". Requires an activated env. |
mngr imbue_cloud admin pool create --mngr-source <monorepo-root> ... | Bake a Vultr pool host. --mngr-source rsyncs the monorepo into the FCT vendor/mngr/ for the duration of the bake. (For pool hosts only -- has no effect on Docker mode.) Requires an activated env. |
just deploy [--yes-i-mean-<tier>] | Run minds env deploy on the activated env. For dev envs: provisions Modal env / Neon / SuperTokens + deploys both Modal apps + writes ~/.minds-<env>/{client.toml,secrets.toml}. For tier deploys: pushes Vault secrets to Modal + deploys both Modal apps, no local state written. |
just sync-vendor-mngr <fct-path> | One-shot: snapshot mngr HEAD into FCT's vendor/mngr/ via git archive and commit in FCT. Use for "release" syncs, not dev iteration (it commits and only carries committed mngr content). |
Env vars just minds-start sets
MINDS_ROOT_NAME / MNGR_HOST_DIR / MNGR_PREFIX / MINDS_CLIENT_CONFIG_PATH come from minds env activate <name> in your shell -- minds-start requires them to be set and refuses otherwise. Beyond those:
| Variable | Purpose | Default |
|---|
MINDS_WORKSPACE_GIT_URL | Template repo path/URL for the create-form | <repo>/.external_worktrees/forever-claude-template/ if it exists, else ~/project/forever-claude-template |
MINDS_WORKSPACE_NAME | Default agent name in the create-form | mindtest (override with agent_name=...) |
MINDS_WORKSPACE_BRANCH | Default git branch for the template | The FCT path's current branch (matches your mngr branch when you set up the worktree on a parallel-named branch) |
The desktop client reads these in apps/minds/imbue/minds/desktop_client/templates.py.
Clean shutdown
The Electron app shuts down cleanly via this chain:
- Electron window close ->
before-quit handler -> backend.js shutdown() -> SIGTERM to uv run
uv run forwards SIGTERM to Python
- Uvicorn catches SIGTERM, does 1-second graceful shutdown (
timeout_graceful_shutdown=1)
- ASGI lifespan shutdown hook runs
stream_manager.stop() (terminates mngr observe/mngr event subprocesses)
- Uvicorn re-raises SIGTERM, process exits with code 143
If this chain breaks (orphaned mngr observe/mngr event processes appear), something is wrong -- investigate, do not just kill the orphans.
Rsync exclusions
just minds-start, mngr imbue_cloud admin pool create --mngr-source ..., and propagate_changes all share one form when rsyncing into vendor/mngr/:
rsync -a --delete --filter=':- .gitignore' --exclude=.git --exclude=uv.lock ...
--filter=':- .gitignore' is rsync's dir-merge filter: it reads .gitignore at each directory level under the source and applies its - (exclude) rules. That covers __pycache__, .venv, node_modules, .test_output, .mypy_cache, .ruff_cache, .pytest_cache, .external_worktrees, and anything else listed in the source repo's gitignore.
The two manual excludes are for things gitignore deliberately doesn't list:
.git -- gitignore never lists it (git's internal dir).
uv.lock -- intentionally committed at the mngr root, but each install context should regenerate its own.
propagate_changes additionally protects runtime/, .mngr/, and .claude/settings.local.json from deletion when rsyncing into /code/.
Editable installs
The Dockerfile uses uv tool install -e for mngr (vendored under vendor/mngr/) and for the system_interface (at apps/system_interface/), so Python code changes in either location are picked up immediately after rsync. Frontend changes require the npm run build step (done automatically by propagate_changes).
Template settings
The template's .mngr/settings.toml controls agent types, create templates, env vars, and extra_window entries. Notable knobs:
disable_plugin = ["recursive", "ttyd"] -- disables plugins that conflict with template-managed services
extra_window entries for bootstrap, telegram, terminal, reviewer_settings
env entries for IS_SANDBOX, IS_AUTONOMOUS, and reviewer toggles
Logs
| Path | Contents |
|---|
/tmp/claude-*/.../tasks/<id>.output | Electron app stdout when launched via just minds-start (path printed at launch) |
~/.minds/logs/minds.log, ~/.minds/logs/minds-events.jsonl | Minds backend (production) |
~/.minds-<env-name>/logs/minds.log, ~/.minds-<env-name>/logs/minds-events.jsonl | Minds backend (per-env) |
Cleaning up the legacy ~/.devminds/
If you used the pre-refactor layout (~/.devminds/ for all dev iteration plus ~/.devminds/envs/<dev-name>.toml per-env overrides), that root is now obsolete. No migration script -- just rm -rf ~/.devminds/ when convenient. A stale MINDS_ROOT_NAME=devminds in a parent shell is silently treated as unset (with a warning); the in-shell minds env activate <name> always wins.
Manual setup (fallback)
If a recipe is broken or you want to run something the recipes don't cover, here are the underlying steps the recipes wrap.
Create the FCT worktree by hand
cd ~/project/forever-claude-template
git worktree add /path/to/mngr/worktree/.external_worktrees/forever-claude-template -b <branch-name> origin/main
Sync mngr code into the FCT worktree's vendor/mngr/ by hand
rsync -a --delete \
--filter=':- .gitignore' \
--exclude=.git --exclude=uv.lock \
./ .external_worktrees/forever-claude-template/vendor/mngr/
This is what just minds-start does internally, what mngr imbue_cloud admin pool create --mngr-source ... does for the duration of the bake, and what propagate_changes does as step 1 on each iteration.
Start electron by hand without the just recipe
eval "$(uv run minds env activate <your-user>-dev)"
TEMPLATE_BRANCH=$(cd .external_worktrees/forever-claude-template && git branch --show-current)
(
set -a
source .env
set +a
export MINDS_WORKSPACE_GIT_URL="$(pwd)/.external_worktrees/forever-claude-template"
export MINDS_WORKSPACE_NAME="mindtest"
export MINDS_WORKSPACE_BRANCH="$TEMPLATE_BRANCH"
cd apps/minds && pnpm start
)