Operations

How to run it, how to prove it, how to land a change, how to ship the phone build, and how this documentation is published.


1. Run the daemon

STAAGE_TOKEN=<token> ./script/run-daemon.sh      # binds 127.0.0.1:4097
adb reverse tcp:4097 tcp:4097                    # once, with the phone on USB
STAAGE_TOKEN=<token> staage --url http://127.0.0.1:4097

The bearer comes from STAAGE_TOKEN or from a file whose first non-blank line is the token (--token-file <path>). There is deliberately no --token: a command line is printed by ps, pgrep -f and any listing over a pipe, so argv is the one place a secret cannot be kept. A named --token-file wins over the environment, and the daemon refuses rather than silently reading a different bearer. Neither program ever prints the value.

The model credential must exist in the environment — ANTHROPIC_API_KEY, DEEPSEEK_API_KEY or OPENAI_API_KEY. The daemon prints which provider and model it took, and never the key. Boot also prints which rails are offered and, for each that is not, the variable that would enable it; --require-rails turns that report into a refusal.

FlagMeaning
--port, --db, --scratch, --workspace, --skillsthe local surfaces
--max-threadsthe ceiling on concurrent lanes
--systemextra system text, appended after the product's own sections
--tool-choicethe provider's tool-choice reading
--delegateoffer task — delegation
--worktrees <dir>also offer apply_change, with a disposable checkout root
--require-railsrefuse a boot without the rails

bash's containment is one of three sandbox modes, stated at boot: product (network on, environment inherited whole — a credential exported into the daemon is still readable by name), bench (no network, credential-shaped variables scrubbed), off (unconfined).

2. Prove it

./script/check-all.sh                       # the suite; receipts under proof/transcripts/runs/
./script/check-daemon-v3.sh                 # one gate
./script/check-stability.sh check-daemon-loop 50 --load 6   # is it a flake?

A green run is one sample. Read Gates and proof for what each one covers and why a gate with no negative fixture is not a gate.

3. Land a change (the one writer)

Never edit the live tree and never push main.

cd dev/staage
git fetch origin main
git worktree add -b agent/<you>/<task> ../wt/<task> origin/main
# … work in the worktree, stage by name, commit …
git push -u origin agent/<you>/<task>
~/.staage/bin/staage-land.sh --repo staage --branch agent/<you>/<task>

staage-land.sh rebases on current main, runs the gates on the merge candidate, and fast-forwards main only when they are green. A conflict or a red gate is a reading, not an accident: main is untouched, so fix in the worktree, push, and land again.

The client has one extra step before landing: bump.

cd dev/staage-android
./bump.sh <descriptor>          # version.properties is the one source of truth, bump.sh its only writer

bump.sh checks every ref first — the counter is shared, and a step another lane already holds is not free.

4. Ship the phone build

From a clean main on the Mac:

~/opg/tools/infra-scripts/staage-release.sh

It reads the step the branch bumped and pushes to the release shelf, which is append-only (script/shelf-guard.sh). The phone's self-update fetches GET /v1/release and GET /v1/release/apk from that shelf, verifies the file by size and sha256, and installs it through PackageInstaller.

Report shape after a ship: installed <version> on <device> first, then what changed in one or two lines; reds and caveats last.

5. Publish this documentation

The Markdown in docs/ is the source; the site is a rendering of it. Two programs:

./script/docs-site.sh                 # render docs/*.md -> dist/docs/ (site + raw/*.md)
./script/docs-deploy.sh               # render, then publish to stage.tnvs.dev

How the site is served

PieceWhere
sourcedocs/*.md in this repository — the same bytes the site renders, published again under /raw/
rendererscript/docs-site.mjs — Node, no dependencies, no network, no clock: the same tree renders byte-identically, so the published site is reviewable as a diff
hosttnvsdock (AlphaVPS, public 45.59.103.127) — the same host as the other tnvs.dev vhosts
content/var/www/stage-docs — replaced wholesale by rsync --delete
vhost/etc/nginx/conf.d/stage.conf, installed from deploy/stage.conf in this repository
TLSLet's Encrypt, HTTP-01 through the nginx plugin; the name has a public A record, so it renews by the ordinary path
DNSstage.tnvs.dev → A 45.59.103.127 in the tnvs.dev zone at DNSimple

docs-deploy.sh is idempotent: it renders, syncs, installs the vhost only if the live file is missing, asks certbot for a certificate only if none exists, reloads nginx, and then reads the result — it prints the HTTP status of the published index rather than claiming success.

The site is a static directory. nginx adds the same security headers as the other tnvs.dev vhosts (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, a referrer policy, and a permissions policy that closes geolocation, microphone and camera). HTML revalidates on every load (max-age=0, must-revalidate), so a publish is visible immediately and a stale page is never served silently.

6. The operational rules that were learned the hard way

RuleWhy
the bearer is never in argvps prints it to anyone who can list processes
one writer per store, one lander per branchtwo writers do not corrupt SQLite, they corrupt the story
the shelf is append-onlya publish that replaces a build destroys the only source of that build
bump checks every ref, not the working copytwo lanes bumped to 80 within the hour on 2026-10-19 and one build was lost
a red gate is a readingre-running until green is how a real defect gets shipped
never main from a worktreethe one writer is the only thing that fast-forwards it
a published directory needs its SELinux labelon an enforcing host, a directory written over ssh inherits var_t and nginx answers 403 to every file in it while curl on the box itself looks fine. docs-deploy.sh runs restorecon on every deploy for this reason