A working Anna App scaffold: transport, storage, sampling, CI, and this publish runbook, extracted
from error-journal after a day spent discovering all of it the hard way. The goal is that the
next app starts from working code, not from zero.
scripts/rename.sh <your-slug> ["Your Title"]This copies the whole template to a sibling directory named <your-slug>, renames
executas/APP_SLUG and skills/APP_SLUG, and substitutes the APP_SLUG/APP_TITLE placeholders
everywhere (manifest.json, app.json, executa.json, pyproject.toml, bundle/, SKILL.md,
CI). It does not touch domain logic — that's executas/<slug>/domain.py, which starts as a
one-function placeholder.
Verify the scaffold actually runs before building anything on top of it:
cd <your-slug>/executas/<your-slug>
python3 test_domain.py
python3 test_harness.pyBoth should print ALL PASS / end with "degraded cleanly — no crash". If they don't, stop — a
template that doesn't run is worse than no template.
anna-appCLI installed and onPATH.anna-app login— device-flow login against your Nexus host; saves a PAT to~/.config/anna/credentials.json. Everything below needs this.anna-app doctorif anything below fails in a way that looks environmental (missinguv, matrix-nexus, or a dev key) rather than app-specific.
cd executas/<slug>
python3 test_domain.py # pure logic, no subprocess
python3 test_harness.py # real plugin subprocess + MockAgent
anna-app executa dev --dir . --describe --json # sanity-check the manifest
anna-app executa dev --dir . --invoke ping --args '{}' --json
cd ../..
anna-app validate # schema + ACL checks on manifest.json + bundle/
anna-app validate --strict
anna-app dev # full harness: UI + Executa, in-process. Storage is IN-MEMORY.
anna-app dev --storage aps # hits real production APS — use this before publishing anything
# that touches storage; the in-memory mode will hide bugs that
# only show up against real persistence (e.g. serialization of
# values that don't round-trip through JSON the way you assumed).anna-app dev hot-reloads only bundle/. If you edit plugin.py or domain.py and just
refresh the browser, you are looking at the old process. Restart the harness.
This is the sequence, in order, run from the repo root unless noted. Each step is a separate,
resumable action — if one fails, fix it and re-run that step; you don't restart from push.
anna-app apps push
# or, to bump the version in manifest.json as part of the push:
anna-app apps push --bump patchWhat it does: upserts the mutable working draft — manifest.json + the bundle/ UI — to the
server. This is not a frozen version; every push overwrites the same draft. By default it also
recursively registers/publishes any bundled_executas declared in app.json, minting or updating
their tool_ids so the app manifest's required_executas resolve correctly. Pass
--skip-executa-publish if you want to push the app shell without touching the bundled Executa
(e.g. you already published the Executa separately and just want cached tool_ids reused).
Failure modes:
- Schema/ACL errors — the same ones
anna-app validate --strictwould catch locally. Run that first; it's faster to iterate on than a round trip to the server. - If
executa.jsonis missingtool_idortype, the bundled-executa orchestration part of this step fails quietly — the warning is buried in output, and the symptom you actually notice is everytools.invokeagainst that Executa failing withnot_implementedlater, which points nowhere near the real cause. Checkexecuta.jsonfirst ifpush"succeeds" but the tool doesn't work afterward. - If
app.json'sbundled_executasis an array instead of an object keyed by handle, the handle resolves to"0"instead of your app's actual slug, andrequired_executasinmanifest.json(bundled:<handle>) won't match anything real.
anna-app apps cut 0.1.0What it does: snapshots whatever is currently in the mutable working draft (i.e., whatever the
last push uploaded) into an immutable version, freezing its dependencies. This is the
version identity reviewers and installs will actually reference — the mutable draft from step 1 is
a staging area, this is the artifact.
Failure modes:
<version>must be a version you haven't cut before, and should match what's actually inmanifest.json/executa.jsonat push time — cutting a version number that doesn't match what you intended to freeze is confusing later when you're trying to match a cut version back to a commit.- If it can't resolve your app's slug from
.anna/app.json(the local identity cache — see--cwd/--slug), you're probably running this from the wrong directory, or.anna/was deleted/never initialized. Re-runapps pushfirst; it writes that cache.
cd executas/<slug>
anna-app executa publish --bump patch
cd ../..What it does: mints or updates the Executa's own identity/version, separately from the Anna
App's own versioning in steps 1–2 — this is why executa.json has its own version field.
Run from inside executas/<slug>/ (it operates on executa.json in cwd). This does not flip
the Executa to publicly visible by default — pass --publish explicitly if you want that as part
of this step, otherwise it stays at whatever visibility it already had.
Failure modes:
- Forgetting to
cdinto the Executa's own directory first — it readsexecuta.jsonfromcwd, not from a path argument. - Editing an Executa in the Anna Hub UI resets its visibility to private. If you (or anyone
with access) opened this Executa in the Hub and changed anything after a previous publish, its
visibility silently reverted to private. The failure doesn't show up here — it shows up at step 5
(
apps submit-review), with an error message that names the wrong field, giving no hint that visibility is the actual problem. That's why step 4 exists: check visibility explicitly, right before you submit for review, so you're not debugging a misleading error message under time pressure.
anna-app apps status <slug> --json
anna-app executa status <slug-or-tool_id> --jsonWhat it does: shows server-known state — don't skip this. Given the Hub-edit trap in step 3, the only reliable way to know your Executa is actually visible/public before you submit the whole app for review is to check, not assume. Confirm the Executa's visibility matches what you expect (public, if that's what your app needs) before moving on.
Failure mode: if visibility is wrong here, go back to step 3 and re-run executa publish --publish rather than proceeding — submit-review failing on this is a worse debugging experience
than catching it now.
anna-app apps submit-review <slug>What it does: moves the app from DRAFT/REJECTED into PENDING_REVIEW. This is the actual
gate — a human or automated reviewer evaluates it from here before it's approved for end users.
Failure modes:
- Only works from
DRAFTorREJECTEDstate. If the app is alreadyPENDING_REVIEW,APPROVED, orPUBLISHED, this is a no-op-that-errors, not a re-submit — checkanna-app apps status <slug>first if you're unsure what state you're in. - If the underlying bundled Executa's visibility is wrong (see step 3's trap), this is where it actually surfaces — as an error that names an unrelated-looking field. If you hit that and you skipped step 4, go back and check visibility before doing anything else.
executa.json (version), pyproject.toml ([project].version), and MANIFEST["version"]
inside plugin.py must all change together. anna-app apps push --bump and
anna-app executa publish --bump will bump the JSON files for you (unless you pass
--no-write), but neither one touches plugin.py's MANIFEST["version"] — that one is on you,
every time. Getting this out of sync doesn't fail loudly; it just means describe reports a
different version than what's actually installed, which is a confusing thing to debug days later.
.github/workflows/build-executa.yml is workflow_dispatch-only — it never runs on push, because
binary builds are cut deliberately, not automatically on every commit. It builds
linux-x86_64 (the Cloud Agent target — its absence fails the release job outright),
darwin-arm64, and windows-x86_64, smoke-tests each binary by piping it a describe call and
grepping the response, then stages and releases all three together.
To trigger it: GitHub → Actions → "Build Executa Binaries" → Run workflow → enter the version
(must match what you put in executa.json).
Adding a platform (e.g. restoring darwin-x86_64 for Intel Macs) requires updating both
the workflow's matrix and executa.json's distribution.profiles.binary.binary_artifacts —
missing either one leaves you with a binary that built but that nothing knows how to install.
Once you've cut and released real binaries, switch executa.json's distribution.active from
"local" (this template's default, for a fresh unpublished app) to "binary".
See CLAUDE.md for the architecture (what's in executa_runtime.py vs. plugin.py vs.
domain.py and why), the full trap list, and local commands.