Use a novel cli
Development tools backing a-novel and a-novel-kit. Home of a-novel CLI and AI skills.
npx -y skills add a-novel-kit/stack --skill use-a-novel-cliAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Canonical reference for the `a-novel` CLI. ALWAYS load alongside any skill that runs tests, builds artifacts, releases, or starts/stops local services. Covers the groups `test`, `build`, `publish`, `repo` (repository config, rulesets, required checks), `run` (daemon-backed `start`/`kill`/`logs`/`env`/`volume`/`ui`) and `core` (daemon lifecycle). Prefer `a-novel <verb>` over raw commands; lint/format/generate live in pnpm scripts (`pnpm lint:go`, `pnpm format:go`), never in Makefiles — deleted from every repo.
SKILL.md
27.3 KB, as published. Nobody here has run it
Use the a-novel CLI
The a-novel CLI is the single user-facing entrypoint for local-dev workflows. It replaces
the deleted Makefiles and per-repo bash scripts (go test wrappers, podman build, podman compose, publish.sh) with one coherent command surface:
a-novel
├── test standalone — runs Go + pnpm tests in the working tree
├── build standalone — builds Go binaries, pnpm bundles, Podman images
├── publish standalone — release doc helpers (releases themselves run in CI)
├── repo standalone — GitHub repo config, rulesets, required checks
├── core daemon control (start, setup, kill, status, prepare-reinstall)
├── run daemon-backed verbs (services + targets)
├── secrets standalone — local encrypted secrets, injected into child envs
├── install standalone — rebuild + reinstall the CLI, state-preserving
├── claude standalone — launch Claude Code rooted at the stack
└── version standalone — print the CLI version
secrets, install, claude and version complete the surface and have their own sections
below; cli/README.md in the stack repo remains the exhaustive reference.
Always prefer a-novel <verb> over the equivalent raw command when one exists.
Makefiles are gone from every repo — make is never the answer. What the CLI doesn't
cover lives in pnpm scripts (lint/format/generate, see "When NOT to use the CLI") so
each repo's surface is exactly: a-novel <verb> + pnpm <script> + raw go/git.
Load this skill alongside any skill that runs tests, builds artifacts, releases, or starts local services.
Quick mapping: raw / legacy → a-novel
| Raw / legacy (deleted) | a-novel equivalent |
|---|---|
make test-unit (gone) | a-novel test --type=go -y |
make test-pkg (gone) | a-novel test --type=go -y (CLI auto-discovers pkg/go targets) |
make test-pkg-js (gone) | a-novel test --type=pnpm -y |
make test (gone) | a-novel test -y |
go test ./... | a-novel test --type=go --dir=. |
make build (gone) | a-novel build -y |
podman build -f Dockerfile -t name:local . | a-novel build --type=podman |
pnpm build | a-novel build --type=pnpm |
go run ./cmd/<target> (service local-dev) | a-novel run start <service>/<target> |
podman compose --profile X up -d | a-novel run start <service>/<target> --mode=container |
podman compose up <infra> | a-novel run service infra start <service> |
podman compose down | a-novel run service infra kill <service> |
podman logs -f <container> | a-novel run logs <service>/<target> --follow |
podman volume export + manual tar | a-novel run volume backup <service> |
scripts/publish.sh patch / pnpm publish:* | release workflow in CI (release-core action) — no local verb |
scripts/prepublish-version.sh <prefix> <file> | a-novel publish stamp <prefix> <file> |
make lint-go (gone) | pnpm lint:go (not a CLI verb — see "When NOT to use") |
make format (gone) | pnpm format:go / pnpm format / pnpm format:proto |
make generate (gone) | pnpm generate:go (plus pnpm generate:mjml where present) |
a-novel <verb> --help (or a-novel help <verb>) prints the full flag list of any
subcommand. Every subcommand carries exhaustive Short/Long/Example help text.
Driving the CLI non-interactively (agents, CI, scripts)
The CLI is interactive only where a human benefits — the test / build pickers
and the run ui TUI. Everything else runs to completion and returns. Agents, CI
jobs and scripts drive it like this:
-
test/build: always pass-y. It skips the picker and runs every discovered target sequentially (CI-safe). Both fall back to that path with no TTY, but pass-yexplicitly — it states intent and survives a stray PTY. Pair with--dry-runto inspect the target list first.a-novel test -y --type=go # all Go tests, no prompt a-novel build -y --type=podman # all Podman images, no prompt -
runverbs are already non-interactive —start,kill,restart,logs,ps,service,volume,topology,env,watch,execanddebugall complete and return an exit code. Onlyrun ui(the TUI) is interactive: never launch it from an agent or CI — use the discrete verbs. -
Observe state with
run watch, do not pollps. It subscribes to the daemon's event stream and emits one newline-delimited JSON object per state change (phase transition, exit, health flip), so you react the moment a target turns healthy. Narrow it with--service/--target.a-novel run watch --service=service-json-keys # NDJSON, one event per line -
Ask for machine-readable output where it exists.
run ps --jsonemits one JSON object per service (with canonical fully-qualified target IDs).run env --format=json(ordotenv) replaces the default eval-ableshellform.a-novel run ps --json a-novel run env <service> --format=json
a-novel test — running tests
Discovers every Go test target (go test ./... per module, scoped by
builds/podman-compose.go[.<path>].test.yaml when present) and every pnpm
test/test:* script in the working tree, lets you pick which to run via a TUI
picker, runs the selection, and prints a pass/fail report. Test envs come up and down
per-target, so independent envs run in parallel safely.
Common patterns:
a-novel test # interactive picker (everything selected by default)
a-novel test -y # run everything non-interactively (CI-safe)
a-novel test --type=go # only Go tests
a-novel test --type=pnpm # only pnpm tests
a-novel test --type=go -y # all Go tests, no prompt
a-novel test --dry-run # show what would run; exit without running
a-novel test --no-cover # skip coverage (on by default)
a-novel test -j 4 # cap parallelism at 4 (interactive only)
When to use: ALWAYS for local-dev test runs — there is no make fallback
(Makefiles and the scripts/test*.sh family are deleted). Raw go test ./<path>/...
remains for a single package/test while iterating. CI runs gotestsum directly
through the kit/workflows composite actions, not through the CLI.
Test plan checkboxes in PR bodies:
- [ ] `a-novel test --type=go -y` passes
- [ ] `a-novel test --type=pnpm -y` passes (if JS changed)
a-novel build — building artifacts
Discovers Go modules, pnpm build scripts, and builds/*.Dockerfile targets under
the working directory. Same interactive-picker / -y-non-interactive shape as
a-novel test.
a-novel build # interactive picker
a-novel build -y # build everything non-interactively
a-novel build --type=go # only Go binaries
a-novel build --type=podman # only Podman images
a-novel build --type=go,pnpm # union filter
a-novel build --dry-run # list targets without building
When to use: ALWAYS for local-dev builds, especially to validate a Dockerfile
change. Avoid raw podman build -f ...: a-novel build --type=podman discovers all
Dockerfiles, builds them with the same convention CI uses, and prints a pass/fail report.
a-novel publish — release doc helpers
Releases are cut in CI: trigger the repo's release workflow and pick a release
type (patch / minor / major), and the release-core action (in a-novel-kit/workflows)
bumps the version, refreshes doc refs, commits, tags vX.Y.Z, pushes, and creates the
GitHub Release. The [Agent] bot performs the push. manage-versions covers it in depth.
There is no local release command — stamp is the only verb under a-novel publish.
a-novel publish stamp <prefix> <file> is the doc-stamping helper the prepublish:doc
pnpm scripts call: it rewrites <prefix>vX.Y.Z references (prefix is a regex) to the
current package.json version.
a-novel repo — repository config and governance
create scaffolds a repository from its class template; update reconciles an existing one. This is
how the governance workflows, the branch rulesets, and the required-check list reach every repo — so
after adding or renaming a job in a repo's .github/workflows/main.yaml, its ruleset stays stale
until update runs.
The class is inferred from the repo name: service-* → a Go backend service, platform-* → a
SvelteKit frontend platform (a terminal app — it ships a container image and a healthcheck route but
exports no package), workflows / .github → the shared-CI and meta repos, everything else → a shared
library (golib, nodelib, jwt, stack). A repo needing a different class carries a
repos/<org>_<repo>.yaml override, which wins over the name-based guess.
a-novel repo update --dry-run # print the API operations, no writes — the agent-safe form
a-novel repo update # interactive, human-only: a human must run this
a-novel repo update --all # every whitelisted checkout present under app/ or kit/
Four behaviours to know before running it:
- Required checks are derived, not configured. They are the jobs in the repo's
main.yaml(minusreport-*and master-only jobs) plus the always-required set. A new job becomes a required check on the nextupdate, and not before. - Config comes from the working tree, not from GitHub — and only
--allguards that. The batch sweep skips a checkout carrying ongoing work (off its default branch, or a dirty tree) and reports each one as⏸ <org>/<repo> — on <branch>, skipped, so a partial run is visible in the output rather than silent. The single-repo form has no such guard: run from a feature branch, it reconciles from that branch'smain.yaml. Be on an up-to-date default branch before running it. --allsharescore sync's whitelist. Both readworkspace-repos.yamlat the workspace root through the same loader, so the batch covers every whitelisted repo actually cloned underapp/orkit/, plus the stack repo itself. A whitelisted repo not yet cloned is simply absent. (repo createtakes its<org> <name>explicitly — the repo does not exist yet, so no whitelist applies.)- A newer deployed pin survives. For files pinning
a-novel-kit/workflowsactions, a version already ahead of the template's is kept, soupdatenever rolls back a bump Renovate landed.
Agents stop at --dry-run: the write path refuses a non-TTY.
a-novel run — daemon-backed service operations
The entire surface for starting, stopping, observing, and inspecting locally-running
services. Requires the a-novel daemon (a-novel core start; lives in ~/.zshrc
after a-novel core setup).
Run it from a single repo, or from the stack root — where it fans out across every app/service-*
and app/platform-* checkout, so a platform's dev-server run/run:* script shows up in the
picker beside the services' targets.
Lifecycle
a-novel run start <service>/<target> # go-exec mode (default)
a-novel run start <service>/<target> --mode=container
a-novel run kill <service>/<target>
a-novel run restart <service>/<target>
a-novel run service infra start <service> # bring up infra + auto-run one-shots
a-novel run service infra kill <service> # refuses if any target running
a-novel run service infra kill <service> --force # cascade-kill
The supervisor auto-walks dependencies: a-novel run start service-X/rest
brings up postgres, runs migrations + rotate-keys (one-shots), then starts rest.
Mutual exclusion is enforced (refuses with hint if the target is already running
in the other mode). One-shots are tracked per infra-up session and re-run on every
infra start; they are idempotent by contract, so re-applying migrations locally
is by design.
Observability
a-novel run ps # list services + target states
a-novel run topology --service=<svc> # ASCII dep tree
a-novel run logs <service>/<target> # snapshot
a-novel run logs <service>/<target> --follow # stream live
a-novel run logs <service>/<target> --previous # most recent archived run
a-novel run env <service> # shell-evalable env block
eval "$(a-novel run env <service>)" # inject env into your shell
The daemon writes JSON-line logs to ~/.local/state/a-novel/logs/... (current +
5 archived runs per target). run logs reads from there; --follow subscribes
through the daemon so multiple followers see the same stream.
Volumes (service-scoped)
a-novel run volume list <service>
a-novel run volume backup <service> --tag=<label>
a-novel run volume restore <service> [--from=<timestamp>]
a-novel run volume clear <service> [--no-backup]
All destructive ops (backup/restore/clear) refuse while the service is up. Pass
--force to cascade-stop first. Backups land in ~/.local/share/a-novel/backups/
as tar.zst archives (max 5 per volume, oldest pruned).
TUI
a-novel run ui # full-screen TUI
# Inside: ? for help, Esc for command palette, q to quit
The TUI is a thin client over the same RPCs as the CLI — actions taken in the UI
are observable from a-novel run watch and vice-versa. Agents and CI use the discrete
verbs instead, see Driving the CLI non-interactively.
a-novel core — daemon lifecycle + workspace tooling
a-novel core setup # one-time interactive bootstrap (run once after install)
a-novel core start # idempotent + silent if already running (lives in .zshrc)
a-novel core restart # stop then start (use --preserve-targets for checkpoint replay)
a-novel core status # is it running? what stacks? checkpoint pending?
a-novel core kill [--force] # graceful shutdown (--force also tears down infra)
a-novel core prepare-reinstall # used by `a-novel install` — checkpoints + exits
# Workspace tooling (ported from the old sync / bot-token bash scripts, now deleted).
a-novel core sync # clone/ff-pull the curated workspace whitelist
a-novel core sync --allow=a-novel-kit/golib # subset to specific repos
a-novel core sync --ignore=<org>/<repo> # skip specific repos
a-novel core bot-comment <org> <repo> <number> --body <text> [--reply-to <id>]
# comment as the org App bot (see below)
# Stack lifecycle — allocate, audit, give back.
a-novel core stacks new <name> # clone a fresh stack under the OS temp dir
a-novel core stacks new <name> --root=<path> # ...or somewhere durable
a-novel core stacks list # every stack: path, targets up, infra up, volumes
a-novel core stacks prune <name> # kill its targets + infra, clear its volumes, remove its files
a-novel core stacks prune <name> --dry-run # report what would be reclaimed
a-novel core stacks prune <name> --purge-backups # also delete its volume backups
a-novel core stacks prune --all -y # sweep every stack but the default
Pruning a scratch stack. A stack allocates three things and only one is a
file, so deleting the root reclaims the checkout but leaves containers holding
host ports and volumes in the container store. stacks prune releases all three,
in that order.
It refuses the default stack — that is the workspace, not scratch space — and
--all sweeps every other registered stack, the pass to run after a batch of
agent sessions. It also refuses a stack whose checkouts hold work that exists
nowhere else (dirty tree, a non-default branch, unpushed commits) unless --force.
$A_NOVEL_STACKS lives in your shell config, so prune prints the entry to drop
instead of editing the file under you.
Volume backups survive: ClearVolume takes one on the way past, so the artefact
that undoes a prune outlives it. --purge-backups deletes them too.
Where a new stack lives. stacks new defaults to <os temp dir>/a-novel-stacks/<name>
via Go's os.TempDir(), which honours $TMPDIR — a per-user /var/folders/…/T
on macOS, /tmp on Linux. The OS reclaims both, so a stack nobody prunes expires
instead of accumulating. Pass --root for somewhere durable.
Because that home is swept, a registration can outlive its files. The daemon
skips such a stack rather than refusing to start over it, and stacks list
flags it (files are gone — drop it from A_NOVEL_STACKS) so the stale entry
stays visible.
bot-comment is the only way to post a PR/issue/review comment as
<app-slug>[bot]. It mints no local token: it triggers the centralized
bot-comment workflow in a-novel-kit/stack with your own gh token, and
that workflow (which alone holds the App keys) posts the comment and is watched
to completion. No .pem ever lives on a dev machine; you need only gh +
actions:write on the dispatcher repo. The bot can only comment — PR
authoring/merge/close are impossible through it.
core setup is interactive; everything else is non-interactive and .zshrc-safe.
Sub-agents spawning fresh stacks: run a-novel core sync --root=<new-stack-root>
as the first action in the new workspace, so later test/build/run commands have
something to operate on.
workspace-repos.yaml at the workspace root is the whitelist — the single
source of truth for which repos exist locally, read at runtime by both
core sync and repo update --all. Add a repo by editing that file; no rebuild,
no code change. Do not restate its contents anywhere (this doc used to name six
repos and went stale as the list grew); read the file.
a-novel secrets — local encrypted secrets
A local encrypted store for values a repo needs but must never commit — injected into a child
process's environment only, never printed, logged, or placed on a command line by any command.
Secrets are encrypted at rest with AES-256-GCM under a 0600 local key; set reads the value with
no echo.
a-novel secrets init # create the local key + store dir (idempotent)
a-novel secrets set <id> # read a value with no echo, store it encrypted
a-novel secrets ls # list secret ids (never values)
a-novel secrets rm <id> # delete a secret
a-novel secrets exec --env NAME=<id> -- <cmd> # run <cmd> with the secret in its env only
Auto-injection. A service repo can commit a value-free manifest at .a-novel/secrets.yaml — a
secrets: list of {env, id, optional description} — and the declared secrets are injected
automatically into the child env of a-novel test, a-novel run and a-novel run ui. A
declared-but-unset secret is skipped with a descriptive warning, never failed silently. The
manifest carries no values, so it is safe to commit.
a-novel install — rebuild and reinstall the CLI
The dev-loop reinstall cycle in one command: checkpoint daemon state, rebuild and install the binary
from source, then restart the daemon and replay the checkpoint — so the running containers and
go-exec targets survive the swap. Equivalent to a-novel core prepare-reinstall →
go install ./cmd/a-novel → a-novel core start, in order.
a-novel install # rebuild from <default-stack>/cli, state-preserving
a-novel install --source ~/forks/stack/cli # build from a different checkout
Source defaults to <default-stack>/cli (typically ~/git-projects/a-novel/cli). After it exits,
a-novel core status reports the freshly-built binary's version and the same targets that were
running before. Run it after editing the CLI itself.
a-novel claude — launch Claude Code from the stack root
Launches Claude Code with the stack root as its working directory, so the domain skills in
.agents/skills and the whole app/ + kit/ workspace are in scope no matter where you invoked it
from. Arguments pass straight through to the underlying claude CLI.
a-novel claude # interactive session, rooted at the stack
a-novel claude -p "<prompt>" # non-interactive: print and exit
a-novel version — print the CLI version
a-novel version # print the installed a-novel CLI version
The binary version the daemon reports (a-novel core status) can diverge from this after you
rebuild the source without reinstalling; a-novel install is what reconciles the two.
pnpm scripts vs. the CLI — the boundary
When you touch a repo's package.json scripts (or review a PR that does), apply one rule:
A pnpm script earns its place only when it carries something specific to the repo — a local package, a config file, a fixed argument set, or a hook the CLI itself invokes. A script that merely mirrors a CLI capability is indirection and must be deleted; run the CLI directly instead.
- Delete (pure mirrors):
publish:major|minor|patch— releases are cut in CI by the release workflow (therelease-coreaction), never a pnpm script or a local command. These wrappers added nothing and drifted; delete them. - Keep (repo-specific constructs the CLI discovers or invokes):
test(vitest run …),build:rest(vite build …) — the concrete invocationsa-novel test/a-novel builddiscover and run.lint:go/lint:proto/format:go/format:proto/generate:go— lint/format/generate have no CLI verb by design (see below); these are their canonical home.prepublish:docand itsprepublish:doc:readme/:openapichildren — the release flow (release-core) runsprepublish:docas a hook, and the children carry this repo's stamp prefix + file (a-novel publish stamp '<prefix>' <file>). Those repo-specific args justify the script.
The smell test for a new/edited script: strip the repo-specific part — if
what's left is just an a-novel <verb> call, the script shouldn't exist.
Naming: generic does everything, language lanes are suffixed
A second rule governs how the surviving scripts are named:
A generic verb (
format,lint,build,generate,test) must do everything that verb covers in the repo. A script scoped to one language/lane is suffixed (format:go,lint:proto,format:js). A bare verb that silently runs only one lane is the bug this rule forbids.
- Multi-lane verb → umbrella + suffixes. A service has Go, Protobuf and a
JS package, so
format=pnpm format:go && pnpm format:proto && pnpm format:js, andlintlikewise. Each lane is a:-suffixed script; the bare verb chains them. The classic violation:formataliased to Prettier only, sopnpm formatleaves Go unformatted and the contributor tripslint-goin CI. - Single-lane verb → stay generic, do NOT suffix. A pure-JS repo
(
nodelib), a Prettier-only repo (workflows), orbuild/testin a service (only a JS pnpm lane — Go is built/tested viaa-novel) already do everything under the bare verb. A redundant:js/:goalias there is overdoing it: the suffix disambiguates multiple lanes. - Name the lane by what it actually contains. The Node/Prettier lane is
:jswhen the repo ships a real JS/TS package (the lane runs eslint + tsc + prettier on actual JS). When the lane only runs Prettier over docs/config and there is no JS (golib), name it:prettier—format:jsin a Go-only repo is the confusion this rule exists to prevent. - CI calls the lane, not the umbrella. The
lint-nodecomposite action runs on a node-only runner with no Go/buf toolchain, so it must target the node lane (lint:ci→lint:js, orlint_action: "lint:prettier"), never the barelintumbrella. The per-language CI jobs (lint-go,lint-proto) invoke their tools directly, not through pnpm. When you turn a bare verb into a Go-inclusive umbrella, re-point that repo'slint-nodeat the node lane in the same change or you red-build CI.
When NOT to use the CLI
Some tasks fall outside the CLI and use pnpm scripts or raw commands:
- Lint / format / generate: pnpm scripts, uniform across repos —
pnpm lint:go/pnpm lint:proto/pnpm lint(node) andpnpm format:go/pnpm format:proto/pnpm format(prettier), pluspnpm generate:go(mocks/proto stubs). Each is a one-line wrapper over the raw form (go tool -modfile=golangci-lint.mod golangci-lint run ./...,go tool -modfile=buf.mod buf format -w,go generate ./...), so the raw forms stay valid too. Every Go tool is pinned in its own<tool>.mod, so each raw invocation names the modfile it comes from. - Direct database access:
a-novel run exec <service>/<target> -- psql .... For a running container-mode target, the command runs inside its container (podman exec); for a go-exec or stopped target it runs on the host with the target's resolved env (POSTGRES_DSN,*_PORT, …) — e.g.a-novel run exec <service>/migrations -- psqlto getpsqlwith the right DSN. Rawpodman exec <container-name> psql ...still works against a running container. - CI workflows: CI never shells into the CLI — the
kit/workflowscomposite actions invokegotestsum/golangci-lint/pnpm run <script>directly. Skills documenting CI behavior reference those actions, not local commands. - Git operations: standard
git/gh— the operator's user token for PR ops,a-novel core bot-commentfor comments.
Failure mode: daemon down
If a-novel run <verb> reports "daemon not reachable", run a-novel core status
to confirm. If down, a-novel core start brings it up (silent if already running).
First-time setup: a-novel core setup.
The daemon refuses to start if the default stack isn't set up — surface the error verbatim to the user.