# Update App

> 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.

- Skill: `imbue-ai/update-app` (Agent Skill)
- Install (CLI): `npx skillmds@latest add imbue-ai/update-app`
- Raw SKILL.md: https://api.skillmd.com/api/skills/imbue-ai/update-app/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: imbue-ai (https://skillmd.com/u/imbue-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/imbue-ai/update-app

---


# 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:**

1. Check whether another agent is mid-edit:

   ```bash
   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.

2. Take your own lease:

   ```bash
   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`](../../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.

  ```bash
  supervisorctl restart <name>
  supervisorctl status <name>   # confirm it came back RUNNING
  ```

- **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):

  ```bash
  uv tool install -e system/apps/<package> --reinstall
  uv sync --all-packages            # the app is also a workspace member; keep the lockfile current
  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`](../../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:

```bash
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:

```bash
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](../build-app/references/verify.md);
  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`](../../shared/scripts/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:

  ```bash
  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>)
  # ...exercise the change at "$URL" (curl / Playwright); it can write freely...
  python3 .agents/shared/scripts/serve_isolated_instance.py down --name <name>-test
  rm -rf /tmp/<name>-scratch      # deleting a copy can't harm real data
  ```

  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.

- **Never "clean up" test data by deleting from the live store.** If you
  did leave a stray test record in it, leave it -- an additive junk record
  is a far cheaper mistake than a broad delete. Better: don't write to the
  live store in the first place (use the copy above).

- **Snapshot before any genuinely in-place change to the real store.** If a
  change truly must rewrite the live store (a data migration you can't run
  on a copy), `cp -r data/.apps/<name> /tmp/<name>-pre-<change>` first, run the
  change, confirm the real data survived, and only then remove the snapshot.
  The snapshot is a *recovery net* -- do **not** turn it into a routine
  "wipe live and restore backup" step: overwriting a running service's store
  tears its state, and any real writes that landed during your test window
  are silently lost on restore.

- **Retrofit older services when you touch them.** A service that predates
  this convention hardcodes `data/.apps/<name>/` and its listen port at its call
  sites. Add both overrides the scaffold now emits, as part of your change, so
  the throwaway instance above works: the data-dir override
  `DATA_DIR = Path(os.environ.get("<PACKAGE_UPPER>_DATA_DIR", "data/.apps/<name>"))`
  (route reads/writes through it), and the port override
  `PORT = int(os.environ.get("<PACKAGE_UPPER>_PORT", "<assigned-port>"))`
  (bind `PORT` in `run_simple`, never a hardcoded literal). If you genuinely
  can't, fall back to read-only verification plus the snapshot net.

- **The copy isolates local state, not external effects.** Pointing at a
  data copy does not stop a test run from really posting to Slack, calling a
  remote API, or sending a message. Guard those separately (a dry-run flag,
  test credentials) -- the data copy only protects the local store.

## 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`](../../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`](../../shared/references/harden-contention.md).

