| name | update-system-interface |
| description | Canonical flow for changing the system interface (the web workspace UI at apps/system_interface) -- its frontend (dockview shell, chat rendering, progress view) or backend (Flask server, agent discovery, layout ops). Use whenever the user wants to edit, fix, restyle, or add to the workspace UI / chat interface / dockview. |
Updating the system interface
apps/system_interface is the live web UI the user is looking at right now
(the dockview shell, the chat panels, the progress view). A broken build here is
served straight to the user, so you never edit the served copy directly: you
make every change in an isolated worktree clone, verify it builds and passes
there, and only merge it back into the served tree once it's known-good. This
skill is the single canonical path for that.
This is the system-interface specialization of the generic artifact
lifecycle. It reuses the generic update orchestration -- the task file, the
generic harden-worker, and the report poll -- from update-artifact with
artifact=system-interface, and adds the one thing the system interface needs
that no other artifact does: a safe-reveal go-live (pre-merge preview, then
a reveal-or-roll-back script). The worker/orchestration core is shared; the
preview and reveal are owned here.
The hard rule
Never edit the system-interface tree that is being served to the user. Do
not run Edit/Write on files under apps/system_interface/ in this (the
served) checkout, and do not rebuild or restart the live UI from uncommitted
edits here. Every change is made in a separate, isolated clone of the source,
built and tested there, and merged back only after it passes. The only things
you do to the served tree are committing the merge and running this skill's
preview / reveal / unpreview commands -- and preview/unpreview never
modify the served tree at all (they only boot throwaway servers against the
worker's separate, already-built work_dir, so even the pre-merge preview can't
reach what the user is looking at).
That isolated clone is the generic harden worker -- its own git worktree and
copy of the source, so a half-broken build can never reach the user. The worker
is just the mechanism for that safe, separate place to work.
Flow overview
- Delegate the change to the generic worker via the
update-artifact
orchestration core (artifact=system-interface). The worker follows
harden-artifact.md + op-update.md + artifact-system-interface.md, which
own how to build, test, and verify the change in isolation.
- The worker implements + builds + tests it on its own branch, then reports
done (the system interface emits no gate -- approval is your preview).
- You preview the change before merging: the worker is a local
worktree-agent in this container that already built its own work_dir, so one
command boots that folder and serves it -- wrapped in a labeled "preview"
frame -- for the user to click around. The user approves or rejects.
- On approval, you record the known-good revision, then merge the
worker's branch.
- You reveal the merged change with one command (refresh dependencies,
rebuild/restart as needed, verify the live UI is healthy, auto-rollback on
failure), then tear down the preview. On rejection, you tear down the
preview and hand back -- nothing is merged.
1-2. Delegate via the generic update orchestration
Check for a concurrent system-interface pass first. Passes here are named
per-slug, so two can coexist without colliding on names -- but both edit
apps/system_interface, so whichever merges second is guaranteed stale (see
the freshness check in Step 4). Look for another pass in flight:
grep -l 'artifact: system-interface' runtime/harden/update-*/task.md
For any hit, check whether its update <slug> tracking ticket is still
open/in-progress in tk ready (and its worker alive, per lead-proxy.md's
liveness probe). If a live one exists, tell the user and let them decide --
dispatching in parallel is allowed but means the second merge will have to
rebase and re-verify. This is a warning, not a hard block.
Then follow update-artifact Steps 1-3 (open a ticket, write the task file,
launch the worker, background-poll the report) with these system-interface
specifics:
- Pick a slug
$SLUG for the change. The worker agent / branch is
update-$SLUG / mngr/update-$SLUG; the runtime dir is
runtime/harden/update-$SLUG/.
- Task-file frontmatter:
operation: update, artifact: system-interface,
plus the standard lead_agent / finish_report_path
(runtime/harden/update-$SLUG/reports/report.md). Per the system-interface
exception in op-update.md, there is no ## Change origin marker -- the
body is a plain change brief, not an absorb/verify incident.
- Task-file body:
## What to do (the actual UI change the user asked for),
## Context (which panel, desired behavior, constraints), ## Real scenario
when a real conversation motivates the change (the motivating agent id + what
looks wrong in plain words -- see the next bullet; omit it, or write "no real
scenario", for net-new work), and ## Success criteria (what "done" looks
like, plus the standing line: follow the installed harden-worker sub-skill;
it composes harden-artifact.md, op-update.md, and
artifact-system-interface.md for how to run, test, verify, and what not to
touch; report done only when its testing contract and the review gates all
pass).
- Judge whether a real scenario motivates the change, and if so point the
worker straight at it -- don't transcribe it. Most UI fixes come from how
something actually renders in a specific real conversation -- "this gap looks
wrong here", or "I don't like how permission requests render" said in a
conversation where a permission request happened (the discontent is with that
rendering, in that conversation). The worker is not cut off from that
conversation: the system interface discovers agents from the shared
MNGR_HOST_DIR, so an instance the worker boots in its own worktree renders
the very same real conversations the user is looking at. So do not measure
the DOM and write it into the brief -- that is a lossy game of telephone (you
look at the real thing, transcribe it, and the worker reconstructs it from your
prose, losing fidelity at each hop). Instead, just decide which case you are in
and say so:
- A real conversation motivated it (the usual case for a bug report): name
the motivating agent -- usually your own
$MNGR_AGENT_ID, or whichever agent
the user was looking at when they complained -- in the ## Real scenario
section, and describe in plain words what looks wrong about it. Tell the
worker to open that conversation in its own instance and look at it before
changing anything. The worker sees the real thing firsthand; it does not
reconstruct it from your description.
- No real scenario (e.g. a net-new control or layout with no precedent in
any existing conversation): say so in the brief and let the worker build a
representative fixture as usual. Don't manufacture a fake "real scenario."
A change can be partly both -- anchored in a real conversation but adding
something new -- in which case name the real anchor and call out the new part.
Use your judgment.
- Launch with
--template subskill-worker (installs the generic
harden-worker) per update-artifact Step 3, then background-poll per
.agents/shared/references/lead-proxy.md.
- Terminal handling differs: the system interface emits no gate, and on
done you do not merge here (that is update-artifact Step 4's behavior
for other artifacts). Instead, go to the preview below. On stuck or a
dead-worker timeout, surface to the user per
.agents/skills/launch-task/references/worker-failure.md -- do not
preview, merge, or reveal, and do not retry silently.
3. Preview the change before merging
On a terminal done, show the user the change before merging. The worker's
work_dir is a folder it has already built (its done contract runs
uv sync + npm run build). The preview just boots that folder -- no fetch, no
re-checkout, no rebuild. Do not destroy the worker yet -- the preview serves
its work_dir in place, so the worker must stay alive until the user gives a
verdict.
First resolve the worker's work_dir, then boot it:
WORK_DIR=$(mngr ls --include 'name == "update-<slug>"' --format json \
| python3 -c 'import sys, json; print(json.load(sys.stdin)["agents"][0]["work_dir"])')
python3 .agents/skills/update-system-interface/scripts/reveal_system_interface.py preview \
--slug update-<slug> --work-dir "$WORK_DIR"
This boots the worker's already-built instance on a free port with layout
persistence neutered (it reads the same agents, so the user's real conversations
render, but it cannot clobber the live layout.json), then boots a small wrapper
page that embeds it in a labeled "preview" frame. The user-facing si-preview
service points at that wrapper (the inner instance is registered separately as
si-preview-app), so the tab reads as a clearly-marked proposed change. It does
not merge, touch the served tree, or modify the worker's folder. Exit 0
means the preview is up; a non-zero exit means it failed to boot (or the
work_dir was wrong / the worker was already destroyed) and tore itself down --
diagnose before retrying. One boot-refusal case is deliberate: the si-preview
tab can only show one pass at a time, so if another pass's preview is already
up, preview refuses rather than hijacking it -- surface that to the user and
coordinate with the other pass (its stderr says how to tear down an abandoned
one).
Open it as a tab and ask the user to explore:
python3 scripts/layout.py open si-preview
Self-verify against the real scenario before you ask the user. The preview's
inner instance opens on its server's primary agent -- the worker's own (empty)
agent, not the motivating conversation -- so the real case isn't on screen by
default. If a real conversation motivated the change:
- Open the motivating conversation in the preview's inner app with Playwright
(
--no-sandbox): use the tab bar's add-tab (+) dropdown and pick the real
agent (its .dockview-add-tab-dropdown-item), or otherwise navigate to it.
- Look at it and confirm the change actually fixed the real case, comparing
it against what looked wrong in the original complaint. A worker reporting
done with passing tests is not proof the real case is fixed -- you have the
real conversation right in front of you, so confirm it with your own eyes. If
it still looks wrong, the fix missed the real DOM: re-brief the worker rather
than merging.
- Tell the user how to see the real case themselves (the preview opens on the
worker's empty agent; they switch via the
+ dropdown to the motivating
agent).
Then confirm with the user: a binary keep/discard and
room for free-form notes (what looks off, what they'd change). Wait for their
answer before doing anything else.
4. On approval: record known-good, then merge
If the user approves the preview:
-
Take the editing lease so no other chat's merge or reveal interleaves
with yours -- the reveal's auto-rollback restores a captured revision, so a
foreign merge landing mid-motion could be swept away by it. Same advisory
mechanics as update-service's "One editor at a time": first check
tk ready for another agent's editing service system_interface lease and
surface it to the user instead of proceeding if one is held; then
LEASE_ID=$(tk create "editing service system_interface" -t chore \
-d "Held by $MNGR_AGENT_NAME across merge + reveal; released after teardown.")
and tk start "$LEASE_ID" (as its own command). Unlike a live service
edit, this lease deliberately spans the whole merge + reveal motion; it is
released at the end of Step 5, never held across the wait for the user's
preview verdict (that wait happens before this step).
-
Freshness check -- the branch is only mergeable if
apps/system_interface/ has not changed since the worker branched (for
example, another pass merged in the meantime):
BASE=$(git merge-base HEAD "mngr/update-$SLUG")
git diff --name-only "$BASE" HEAD -- apps/system_interface/
Empty output means fresh: continue. Any output means the pass is stale --
do not merge, and never hand-resolve a conflicted merge (the
principle in .agents/shared/references/harden-contention.md). Release
the lease, re-brief the worker to rebase onto the current tree and
re-verify, and come back through preview once it reports done again.
-
Capture the known-good revision -- the served branch's current
HEAD, before you merge. This is what the reveal rolls back to if the
change breaks:
ROLLBACK_TO=$(git rev-parse HEAD)
-
Merge the worker's branch (mngr/update-$SLUG) into the working branch
the live UI is served from. Commit the merge so the tree is clean (the reveal
refuses to run on a dirty tree, so a rollback can never clobber unrelated
work).
If the user rejects, do not merge. Tear down the preview (see the end of the
next section) and hand back with their feedback -- decide with them whether to
re-brief the worker for another pass. Re-briefing is your judgment, not an
automatic loop.
Note: the built static/ bundle is gitignored, so the merge brings only source
and dependency-manifest (pyproject.toml / package.json / lockfile) changes,
not the worker's build output. The reveal step rebuilds it.
5. Reveal the change (after merge), then tear down the preview
Run the reveal sub-command with the known-good revision you captured:
python3 .agents/skills/update-system-interface/scripts/reveal_system_interface.py reveal \
--rollback-to "$ROLLBACK_TO"
That single command owns the whole reveal as one deterministic, self-healing
motion -- you do not run npm/uv/mngr by hand. It:
- Classifies what the merge changed (frontend source, frontend manifest,
backend source, backend manifest).
- Refreshes dependencies only if a manifest changed --
npm ci for the
frontend, uv tool install -e apps/system_interface --reinstall for the
backend. This is essential: a plain restart does not re-resolve the
editable-installed tool's dependencies, so a backend dependency addition would
otherwise crash the service on restart.
- Pre-flights a backend change by booting the merged code on a throwaway port
before touching the live service. If it can't boot, the live service is never
restarted -- the UI never goes down.
- Reveals: rebuilds the gitignored
static/ bundle and broadcasts a
reload_system_interface op so open browsers reload into the new assets
(frontend); restarts the services agent so the editable backend re-imports the
merged .py (backend).
- Verifies the live service is healthy by polling its loopback endpoint.
- Auto-rolls-back on any failure: restores the tree to
--rollback-to as a
forward revert commit, rebuilds/restarts from it, and re-confirms the UI is
healthy.
Interpret the exit code and report it to the user:
0 -- revealed; the live UI is updated and healthy.
2 -- the change was bad and was automatically rolled back; the live UI is
healthy on the previous revision, but the requested change did not land.
Report this and diagnose before retrying.
3 -- emergency: even rollback could not restore a healthy UI. The
interface may be down; escalate immediately.
1 -- precondition error (e.g. a dirty tree); nothing was changed.
Once you no longer need the preview (after a successful reveal, or after a
rejection where nothing was merged), tear it down:
python3 .agents/skills/update-system-interface/scripts/reveal_system_interface.py unpreview --slug update-<slug>
way to clean up after a preview that failed partway.
unpreview only handles the service side; it does not touch the workspace
layout. The si-preview tab you opened earlier with layout.py open is a
separate concern (a layout panel, not a service), so you must close it yourself
-- otherwise the user is left with a stale tab pointing at a now-deregistered
service:
python3 scripts/layout.py close si-preview
Do this whenever you tear the preview down -- after a successful reveal or
after a rejection where nothing was merged. Once the preview is down and its tab
is closed, the worker can be destroyed per launch-task. Close the
update-$SLUG ticket the orchestration opened, and release the editing lease
taken in Step 4 with tk close "$LEASE_ID" "Merge and reveal finished." (on a
rejection no lease was taken -- Step 4 never ran).
Why this exists as a script and not a checklist: if the backend fails to start,
the user loses their entire chat UI -- there is nowhere left to surface an error
message. The recover-or-revert logic must therefore run identically every time
and can never be skipped, which is exactly what belongs in a deterministic script
rather than agent prose.
scripts/layout.py refresh (the manage-layout skill) is unrelated -- it only
reloads a single inner iframe/panel for arranging the workspace, not the
top-level page, so it does not reveal a system-interface code change.
Why this shape
The UI is what the user is actively looking at, so the design goal is "never
serve a half-broken UI," not "iterate in place fast." The worker's isolated clone
(in-process testing, Playwright verification, review gates) makes it safe to
merge; the pre-merge preview lets the user actually click around the change
before anything lands -- and since the worker already built its own work_dir, the
preview just boots that folder in place rather than re-cloning or rebuilding; and
the reveal script's pre-flight, health probe, and autonomous rollback make it
safe to reveal in one motion. Preview setup and teardown are deterministic, so
they live as preview/unpreview sub-commands of the same script rather than as
agent prose -- the only non-deterministic part, gating on the user's judgment,
stays with you.