a-novel is the single command-line tool for the Agora storyverse: it builds,
tests, runs and releases every project across the
a-novel and
a-novel-kit organizations — one CLI in place
of the per-repo Makefiles and bash scripts.
This repository — the stack — hosts that CLI and anchors local checkouts of the projects it operates on:
a-novel/ this repo (github.com/a-novel-kit/stack)
├── cli/ the a-novel CLI (Go)
├── app/ a-novel org checkouts (services) — gitignored
├── kit/ a-novel-kit org checkouts (libraries) — gitignored
└── .secrets/ GitHub App private keys — gitignored
app/ and kit/ are not submodules — they are plain clones managed by
a-novel core sync, kept out of this repo's history.
Building the CLI needs Go; a-novel core setup then preflights your environment
and requires git and a running Podman. The rest of the toolchain (Node + pnpm,
gh) and the per-OS install commands live in the
developer onboarding guide.
go install github.com/a-novel-kit/stack/cli/cmd/a-novel@latest
a-novel core setup # one-time bootstrap: env checks, state dirs, shell rc, daemon
a-novel core sync # clone / fast-forward the workspace repos into app/ and kit/go install pulls and builds the binary straight from the module path — no
upfront clone. core setup then clones the stack repo itself into
~/git-projects/a-novel (the default stack) when it's missing, so you don't
bootstrap the workspace by hand. It is otherwise idempotent, and installs a block
in your shell rc (zsh, bash and fish are supported) that auto-starts the
background daemon and loads tab-completion in every new shell.
Verify:
a-novel --version # the binary resolves a build version
a-novel core status # the daemon is up
a-novel run ps # the daemon answers over its unix socketAfter a git pull that touches cli/, run a-novel install to rebuild and
reinstall — it hands the daemon off without dropping running services.
For day-to-day work, open the terminal UI:
a-novel run uiIt is a full-screen dashboard: a services list on the left, a tabbed detail and
live log viewer on the right, and a command palette (press Esc) covering the
entire CLI surface — :start, :kill, :infra-start, and so on. Press ?
for a searchable key and command reference; q quits.
The UI is a thin client over the same daemon as the command line, so anything you do in it is visible to the CLI and vice-versa. If you learn one command, learn this one.
Everything the UI does is also a discrete command — what you reach for in scripts, in CI, or when you already know exactly what you want.
Build and test discover their targets in the working tree and show an
interactive picker by default; -y skips it and runs everything sequentially
(also the default when there is no TTY):
a-novel test -y # run every Go + pnpm test in the tree
a-novel build -y --type=podman # build only the Podman imagesRun services. The daemon auto-starts a target's dependencies — Postgres, migrations — before the target itself:
a-novel run start service-json-keys/rest # <service>/<target>
a-novel run logs service-json-keys/rest --follow
a-novel run ps # what is running
a-novel run kill service-json-keys/restRelease. Cut a tag locally; CI's release workflow publishes from the pushed tag:
a-novel publish version 0.21.0 # or: patch / minor / majorRun a-novel <verb> --help for any command's full flags. The complete
reference — command tree, daemon architecture, state directories, the compose
contract — is in cli/README.md.
Lint, format and generate are deliberately not CLI verbs: each repo exposes them as uniform pnpm scripts (
pnpm lint:go,pnpm format:go,pnpm generate:go, …).
Two identities talk to GitHub from a dev machine: you (the operator, via
gh) and the org bot (a GitHub App, via a-novel core bot-gh). Anything
that authors or merges — PR creation, edits, ready/merge/close — runs as you.
The bot is strictly for comments, so automated notes attribute to <app>[bot]
rather than a human account.
gh auth login # operator: pick GitHub.com + SSH
a-novel core bot-gh a-novel -- pr comment 123 --body "automated review note"The CLI mints short-lived (1h) installation tokens from each org's App private
key — no long-lived bot token is ever stored. bot-gh hard-rejects every gh
subcommand that authors or state-changes a PR or issue, so those always run as
the operator and CI triggers and attribution stay correct.
One-time GitHub App setup (per org, then per machine)
Each org has a dedicated GitHub App (anovelbot-agent for a-novel,
anovelkitbot-agent for a-novel-kit). Both are already registered — these
steps are kept for new orgs or App re-creation.
Per org. Register a GitHub App on the org with:
- Repository permissions:
Issues: Read and write,Pull requests: Read and write,Metadata: Read-only— and nothing else, so the bot cannot push, merge or release. - Webhook: disabled. The App is outbound-only.
- Install the App on all repositories of the org.
Record the App ID and Installation ID (the trailing number in
https://github.com/organizations/<org>/settings/installations/<id>) in
botOrgs in cli/internal/cli/bot_cmd.go — both
IDs are public; only the private key is sensitive.
Per machine. Generate a private key from the App's settings page (downloads
a .pem), then move it into the gitignored .secrets/ directory under the
stack root:
mkdir -p ~/git-projects/a-novel/.secrets
mv ~/Downloads/<key>.pem ~/git-projects/a-novel/.secrets/anovelbot-agent.private-key.pem # a-novel
mv ~/Downloads/<key>.pem ~/git-projects/a-novel/.secrets/anovelkitbot-agent.private-key.pem # a-novel-kit
chmod 600 ~/git-projects/a-novel/.secrets/*.pemSet BOT_KEY_DIR to keep the keys elsewhere. Verify (mints a real token, prints
nothing on success):
a-novel core bot-token a-novel > /dev/null && echo "a-novel bot OK"
a-novel core bot-token a-novel-kit > /dev/null && echo "a-novel-kit bot OK"Releases, merges and pushes to master are human-only. a-novel publish version refuses to run without a TTY, and the real boundary is server-side:
branch protection on master, tag protection on v*, and a comments-only bot
App. The org-wide security policy lives in
a-novel/.github.