| name | update-app |
| description | Use immediately whenever the user asks you to update, change, fix, restyle, extend, restart, or otherwise modify an existing app or background service -- load this BEFORE touching its code. Applies to any change to an app's or service's backend or frontend logic, or how it runs. Covers both apps (a tab the user can open) and background services (host-backup, share-gateway, and other supervisord programs with no tab). This is the front door for app and service edits: it owns the live change loop (apply the change so it takes effect, refresh the user's view, verify) and hands the change to the turn-end hardening flow. For creating a brand-new app use build-app; for the workspace UI itself use update-system-interface. |
| metadata | {"author":"imbue"} |
Changing an existing app or service
Both apps and background services run as a [program:<name>] under
supervisord (see system/supervisord.conf). They differ only in whether
there's a tab to refresh:
- App -- the user opens it as a tab rendering at the service's own
origin,
http://<name>.<workspace-host>/ (scaffolded via
build-app). Lives under system/apps/<package>/.
- Background service -- a supervisord program with no tab (
host-backup,
share-gateway, forwarders), standalone under system/services/ or co-owned
by an app (named <app>-<role>, code in the app's folder).
Two things are easy to forget when editing either, and both leave the user
looking at stale state: a code change doesn't take effect until the process
is reloaded, and an open app tab keeps showing the old page until it's
refreshed. The live change loop below handles both.
If you're doing something other than editing an existing app or service:
- Creating a new app ->
build-app.
- Changing the workspace UI itself (
system/apps/system_interface -- the
dockview shell, the sidebar, the New Tab launcher) -> update-system-interface
(it never edits the served tree directly; it previews in isolation and
applies only when known-good).
- Rearranging tabs (split/move/focus/rename/close) ->
manage-layout.
Match the flow to the scope of the change
Not every change is a quick edit. Before you start, decide which of these
the request is -- it changes what you do before touching code:
-
Small / contained change -- a bug fix, a copy tweak, a new field, a
backend logic fix, a config or command change, a restart. Go straight
to the live change loop below: edit, apply, refresh, verify.
-
Larger-scope change -- a redesign, a new page or view, a meaningful
shift in look-and-feel, or a new user-facing capability. Run the same
mock-confirm flow build-app used to create the service: read
.agents/shared/references/interactive-delivery.md, put a cheap,
throwaway version of the proposed change in front of the user, loop until
they explicitly confirm the shape, and only then build the real thing
to a usable state. Never build heavy against an unconfirmed shape.
When a hand mock won't convince -- a redesign, or a data-touching change --
boot the actually changed service as a labeled preview tab beside the
live one via the shared serve_isolated_instance.py script (invocation
under "Protect the user's data while you verify"; it's the same preview
mechanism the system-interface flow uses). Keep the lighter hand mock for
quick look-and-feel loops. Either way, reading the live store to render a
preview is fine; never let a preview or verification write to it.
A new view or capability bolted onto an existing service is its own
delivery with its own feedback gate (interactive-delivery phase 8):
confirm and ship it on its own rather than bundling it with unrelated
changes.
When in doubt about which bucket you're in, treat a change that alters what
the user sees or perceives as larger-scope (confirm the shape first) and a
change that only alters behavior behind an unchanged surface as contained.
Either way, the mechanics of surfacing the change to the user are the same
live loop:
One editor at a time: the service lease
Every chat shares this one working tree and this one live process, so two
agents editing the same service at the same time interleave destructively --
there is no merge step where that could be reconciled. Concurrent edits must
be serialized, and the serialization token is an advisory lease held as a
regular tk ticket (regular tickets are visible across agents).
Pre-flight, before touching the service's code or config:
-
Check whether another agent is mid-edit:
tk ready > /tmp/service-leases.txt
grep "editing service <name>" /tmp/service-leases.txt
If a lease exists and tk show <id> says it is not yours, do not
silently proceed: tell the user another chat is currently modifying this
service and let them decide (wait, or explicitly override). If the holder
looks abandoned -- its agent is no longer running, or the lease is hours
old with no notes -- say that too and offer to break it. The lease is
advisory: it is broken deliberately by the user's call, never silently.
-
Take your own lease:
LEASE_ID=$(tk create "editing service <name>" -t chore \
-d "Held by $MNGR_AGENT_NAME while editing this service; released at turn end.")
then tk start "$LEASE_ID" (as its own command).
Release the lease at the end of every editing turn with
tk close "$LEASE_ID" "Done editing for this turn." -- never hold it across
an idle wait for user feedback. On the next feedback round, take a fresh
lease before editing again. Between turns the changes are committed, so
another chat editing sequentially on top is safe; only simultaneous editing
needs the lease.
An in-flight harden pass does not block you -- the foreground wins. If
tk ready also shows an in-progress update <name> or heal <name> ticket
(a background pass hardening an earlier change to this service), proceed with
your edit; your change simply makes that pass stale. Leave a note on that
ticket (tk add-note <id> "...") so its owner coalesces at merge time. The
full contention rules live in
.agents/shared/references/harden-contention.md.
The live change loop
Make the change interactive and keep the user's view in sync as you go.
1. Make the change
Edit the service's code under system/apps/<package>/ (or wherever the program's
command points). If the change renders HTML a person looks at, invoke the
frontend-design skill before writing markup, and if it calls Claude,
follow use-ai-integration -- the same rules as when the service was
built.
2. Apply it so it actually takes effect
The scaffolded web runner runs with use_reloader=False, and daemons
don't watch their own source, so a code change is not live until the
process restarts:
-
Backend change (Python / server logic, for an app or a
daemon): restart the program.
supervisorctl restart <name>
supervisorctl status <name>
-
Dependency or entry-point change to an app (its pyproject.toml):
an app with an app.toml manifest runs from its own uv tool environment,
an editable install of system/apps/<package>/ that picks up source
edits on its own but not a new dependency or console script. Reinstall
the tool, then restart (an app with no app.toml runs from the root
venv: for it, only the uv sync --all-packages below is needed):
uv tool install -e system/apps/<package> --reinstall
uv sync --all-packages
supervisorctl restart <name>
(Background services under system/services/ run from the root venv
instead: uv sync --all-packages, then restart.)
-
Frontend-only change (templates, static JS/CSS served fresh on each
request): no restart needed -- the next request already serves the new
markup. Skip straight to the refresh.
-
Change to the service definition (its port, its command, its log
config, or adding/removing a program): edit system/supervisord.conf, then
supervisorctl reread && supervisorctl update. The full program schema,
the add/remove/inspect mechanics, and the forward_port.py wiring live
in .agents/shared/references/service-processes.md.
This surgical reload is for iterating on a single service. It is not the
path for landing an update-self merge -- that restarts the whole services
agent (mngr start --restart system-services) so bootstrap re-runs too,
and must be followed by
python3 system/scripts/refresh_workspace_view.py (see step 3).
If it doesn't come back RUNNING, read
/var/log/supervisor/<name>-stderr.log or
supervisorctl tail <name> stderr.
3. Refresh the user's view
If the service has a user-facing tab, the open iframe is still showing the
pre-change page. Refresh it so the user sees the update without being told
to click Refresh:
python3 system/scripts/layout.py refresh <name>
refresh reloads every iframe for the service. If no tab is open yet and
the change is ready to show, surface it instead with
python3 system/scripts/layout.py open <name>: with no --view it lands in
the view the user is looking at, and --view <name> targets one view (it
fails fast and harmlessly when no client has that view active).
For any other tab manipulation, see manage-layout. Background daemons have
no tab -- skip the tab refresh, but not the rest of this step.
If you restarted the whole services agent rather than a single program, one
tab refresh is not enough -- the workspace shell itself was bounced. Rebuild
the user's whole view instead:
python3 system/scripts/refresh_workspace_view.py
Nothing else does this for you. The Minds app only intervenes when a workspace
looks unreachable for a sustained stretch, and a services restart that comes
back quickly never crosses that bar, so the user is left reading the page the
previous build rendered. The helper is fire-and-forget and always exits 0; it
names any channel that did not land on stderr and is never a reason to stop.
4. Verify
Confirm the change actually does the right thing, exercised as the user
would (not just "the process is up"):
- App:
curl against the registered backend URL
http://127.0.0.1:<port>/ then a Playwright assertion on a
marker unique to your change. The recipe is in
build-app's verify reference;
the symptom-indexed gotchas (connection refused, a tab stuck on the
loading page, broken WebSockets) are in that skill's
cross-flow-gotchas.md.
- Daemon: watch its log (
supervisorctl tail -f <name> stderr) and
confirm the new behavior actually fires.
Protect the user's data while you verify
The service's persistent store -- data/.apps/<name>/ (whatever DATA_DIR
resolves to) -- is the user's real data. The recurring, expensive
failure mode is not the code edit: it is verifying a change by writing
test data into the live store and then "cleaning up" with a delete/reset
whose predicate is too broad and takes real records with it. The delete is
where the data dies. Encode these, cheapest first:
-
Read-only verification needs no ceremony. Most changes (UI, copy, a
backend read path) can be exercised by curl/Playwright against the live
service without writing anything. Reading the live store -- including to
render a preview -- is fine; the danger is only writes.
-
If exercising the change must write, mutate, or delete data, never
point it at the live store. Copy the store to a scratch path outside
data/ (so it is neither served by the live service nor backed up), boot a
throwaway instance against the copy on a spare port, exercise it there,
then delete the copy. The shared
serve_isolated_instance.py
script owns the boot + teardown -- it picks a free port, injects it (via the
<PACKAGE_UPPER>_PORT override) plus your data-dir override, waits for the
instance to answer, and prints its URL:
cp -r data/.apps/<name> /tmp/<name>-scratch
URL=$(python3 .agents/shared/scripts/serve_isolated_instance.py up \
--name <name>-test --cwd . \
--port-env <PACKAGE_UPPER>_PORT \
--env <PACKAGE_UPPER>_DATA_DIR=/tmp/<name>-scratch \
--health-path /health \
-- uv run <name>)
python3 .agents/shared/scripts/serve_isolated_instance.py down --name <name>-test
rm -rf /tmp/<name>-scratch
This is the point of the DATA_DIR + <PACKAGE_UPPER>_PORT overrides: the
isolation you need is data isolation, not code isolation, and it's a
copy-plus-one-command setup, not a worktree. The live store is only ever
read (once, to make the copy); the only delete lands on a disposable path
where real data never lived. If you want the user to see the throwaway
instance -- a redesign, or a risky change where a hand mock won't convince --
add --service-name <name>-preview-app --preview-service-name <name>-preview --preview-title "<change>" to the up call to surface it as a labeled
"preview" tab (open it with
python3 system/scripts/layout.py open <name>-preview);
that is the same machinery the system-interface flow uses. Use judgment on
when that is worth it.
Removing a service
Dropping a service is the definition-level case of step 2: remove its
[program:<name>] block, supervisorctl reread && supervisorctl update,
and (for an app) python3 system/scripts/forward_port.py --name <name> --remove plus reverting the scaffolded lib. The mechanics are in
.agents/shared/references/service-processes.md; for a
scaffolded web lib, build-app's cleanup.md reference has the
full teardown.
Teardown stops at the code and the process. Leave the service's data
(data/.apps/<name>/) in place -- removing a service is not license to
delete the user's records. Delete the data dir only if the user explicitly
asks, and confirm before you do.
Turn-end: get feedback, then harden the change
The live loop above delivers the change to the user interactively. At
turn-end, formalize it through the background worker pipeline -- the main
agent never runs the thorough test passes or the review gates itself.
Get the user's feedback before you start any hardening pass. Delivering
the change live is not the same as the user wanting it, so never dispatch
the hardening worker in the same turn you make the change. This holds for
every change, contained or larger-scope -- even a one-line copy tweak gets
shown and confirmed first. It can take several rounds: treat each
response as another live iteration -- make the change, show it, ask again --
and hold the harden pass until you are sure the user is satisfied. A single
"looks fine" mid-thread while they're still tweaking isn't done. (For a
larger-scope change this gate is the working result, not just the mock,
exactly as build-app's Step 5 gates on the working site.)
- A change you and the user discussed and applied live, or repeatable
work you did by hand -> invoke
update-creation with
type=app. It opens a tracking ticket, dispatches the generic
harden worker to verify/test the change on its own branch, proxies the
gates, merges, and refreshes the tab on go-live.
- The service errored or produced a wrong result and you worked around
it -> invoke
heal-creation with type=app (or type=service for a
background service) at turn-end instead.
- The workspace UI (
system/apps/system_interface) -> update-system-interface
owns its own preview-before-merge and its go-live through the atomic update
apply; use it rather than this flow.
update-creation and heal-creation also stand on their own as turn-end
skills; this skill's turn-end step is just the service-shaped entry into
them.
Both flows enforce single-flight per creation: if another chat already has a
harden pass in flight for this service, they leave a note on its ticket
instead of dispatching a sibling, and the eventual superseding pass covers
both changes. See
.agents/shared/references/harden-contention.md.