planctl User Manual
planctl is the command-line toolchain for the Planck stack. A single binary takes care of host setup, project scaffolding, .zsx template compilation, deployments to a Workbench-supervised host, runtime lifecycle, and backup / restore.
This manual is the long form. planctl --help (or any invocation without a command) prints a short usage screen.
planctl <command> [args...]Table of contents
- Install and host setup
- Configure profiles
- Scaffold a project
- Add features and services
- ZSX template compiler
- Deploy
- Lifecycle: start, stop, restart, status
- Undeploy
- Backup and restore
- Schema: create and drop stores and indexes
- Export and import
- Service configuration reference
- Workflow recipes
- Troubleshooting
1. Install and host setup
Once the planctl binary is on your PATH, the system subcommands handle the rest of the host bootstrap. They lay out the directory tree, install the Workbench along with the system database, and register the same with the OS supervisor (launchd on macOS, systemd on Linux).
| Subcommand | Effect |
|---|---|
system init | Install Workbench + sysdb, register supervised services |
system start | Start every Planck-supervised process on this host |
system stop | Stop every Planck-supervised process |
system deinit | Unregister services. Binaries and data files stay on disk. |
planctl system init
planctl system startsystem init accepts --mode <dev|prod> to pick the install location:
devinstalls under$HOME/.planckand skips OS-level service registration.planctlspawns the processes directly. This is the macOS default.prodinstalls under/opt/planckand registers services with the platform supervisor. This is the Linux default.
About system start and system stop on macOS
On macOS these two are thin wrappers around launchctl. system stop walks /Library/LaunchDaemons for every plist owned by Planck and runs launchctl bootout system <plist> on each. system start does the same walk and runs launchctl bootstrap system <plist>. Touching /Library/LaunchDaemons usually needs root, so expect to prefix these with sudo.
bootout and bootstrap are no-ops when the service is already in the target state, so re-running either command is perfectly safe.
On Linux, system start and system stop are only placeholders today and print a notice. Use systemctl directly.
What system init lays down
| Path | Purpose |
|---|---|
~/.planck/bin | The planctl binary, the Workbench, the sysdb |
~/.planck/apps | Per-app deployment directories |
~/.planck/logs | Workbench and service logs |
~/.planck/system | System database files (planck.system.db) |
~/.planck/workbench | Workbench install and config |
In prod mode the same layout lives under /opt/planck.
2. Configure profiles
planctl reads its target host(s) from ~/.planctl/config.yaml. The config file declares one or more profiles, and each profile lists one or more workbench nodes.
profiles:
- name: dev
nodes:
- server: https://127.0.0.1:24011
uid: admin
key: <wb-admin-key>
- name: prod
nodes:
- server: https://prod-wb.example.com
uid: admin
key: <wb-admin-key>
- server: https://prod-wb-replica.example.com
uid: admin
key: <wb-admin-key>When a profile has multiple nodes, every command iterates them in order and runs against each one. That is how a prod profile can target both the command and the query workbench in a single deploy.
Every command that talks to a Workbench needs --profile <name>. There is no built-in default profile and no implicit fallback to localhost. If you forget --profile, the command exits with an error.
Per-invocation overrides
Three flags let you override individual fields from a profile for a single call:
| Flag | Purpose |
|---|---|
--server <url> | Replace the workbench URL for this call |
--uid <user> | Replace the admin uid |
--key <hex> | Replace the admin key |
--profile <n> | Pick which profile to start from |
--dry-run | Skip network calls. Print what would happen. |
Override precedence is decided field by field: a CLI flag wins, otherwise the profile's value is used. Mix and match freely.
# Use staging's URL and uid, but a one-off rotated key
planctl deploy --all --arch mono --profile staging --key "$ROTATED_KEY"We do not read environment variables for these values. Config lives in YAML, and that is the only source.
TLS in scaffolded mono apps
Mono templates default to TLS 1.3 with a self-signed certificate, generated on first boot. That covers local development, demos, and small single-node deploys without you having to think about cert plumbing at all.
When you want something else:
- Own cert. Drop your cert and key on disk and point
tls.cert_path/tls.key_pathindb.yamlat them. - Plain HTTP behind a proxy. Set
tls.enabled: false. The reverse proxy terminates TLS, and the app speaks HTTP on its bind.
3. Scaffold a project
planctl new <name> --type <hda|spa> --arch <mono|micro>Four templates ship embedded in the binary, so planctl new works offline:
| Type | Arch | Result |
|---|---|---|
hda | mono | Hypermedia-driven monolith. Single deployable, server-rendered HTML. |
hda | micro | Hypermedia shell + per-feature WASM services + an SSE service. |
spa | mono | SPA monolith. Vue bundle + a single API. |
spa | micro | SPA shell + per-feature WASM services + an SSE service. |
Flags:
| Flag | Meaning |
|---|---|
--type | hda (server-rendered HTML) or spa (Vue bundle) |
--arch | mono (one deployable) or micro (shell + services) |
--force, -f | Overwrite an existing <name>/ directory |
planctl new notes --type hda --arch mono
planctl new shop --type spa --arch microChoosing a reverse proxy
When you scaffold, you decide which reverse proxy fits your operations story: Caddy, nginx, or Traefik. planctl new does not pick one for you and does not hardcode supervision. Pick the proxy that matches your environment and drop its config into the project once you have one. The mono TLS default means you can run without a proxy during development. In production, a proxy in front lets you turn TLS off in the app and centralize cert handling.
The SSE subproject
All four templates ship with an sse/ subproject for live updates. It builds on ssehub, an in-process pub/sub library with topic-keyed fan-out. A scaffolded sse/ looks like this:
sse/
build.zig
build.zig.zon
sse.yaml
src/
main.zig
hub.zig
handlers/
example.zig
README.mdFor micro projects, the SSE service is deployed last so that the topic is live before browsers connect.
Project name rules
The name has to be a valid Zig package identifier: lowercase letters, digits, and underscores, starting with a letter. No hyphens. planctl new rejects bad names with a clear error.
Legacy planctl init
The old planctl init <name> [--type wasm|app] scaffolders still work and print a deprecation notice. Do use planctl new for any new project.
4. Add features and services
Inside an existing project root, planctl add materializes a new feature (for mono projects) or a new service (for micro projects).
planctl add <name> --type <feature|service> [--arch <hypermedia|rest>] [--port <n>] [--force]| Type | Where it works | What it adds |
|---|---|---|
feature | Mono projects (hda-mono, spa-mono) | src/features/<name>/ with routes + handlers |
service | Micro projects (hda-micro, spa-micro) | services/<name>/ with its own build |
The --arch flag is optional and accepts hypermedia (zsx-rendered routes) or rest (JSON-only). It is auto-detected from the project layout if you leave it off. --port lets you pin a service port, otherwise the next-available port is scanned.
# In a mono project root:
planctl add billing --type feature
# In a micro project root:
planctl add orders --type service --port 45015. ZSX template compiler
ZSX is a JSX-like template syntax that compiles to Zig (or Rust, or Go). Templates write directly into a byte buffer. There is no virtual DOM, no template runtime, and the generated code is inlinable plain code.
Compilation surface
# Single file to stdout (handy for inspection)
planctl src/features/tasks/zsx/task_list.zsx
# Batch transform a directory tree
planctl src/zsx/ src/fragments/
# Explicit codegen backend
planctl --target zig src/zsx/ src/fragments/
planctl --target rust src/zsx/ src/fragments/
planctl --target go src/zsx/ src/fragments/
# Watch mode (recompile on change)
planctl --watch src/zsx/ src/fragments/
# Remove generated files (only touches files with the AUTO-GENERATED header)
planctl clean src/fragments/You will rarely run the batch transform by hand. Each project's build.zig invokes it before compiling, so editing a .zsx file is enough. The fragment regenerates itself on the next zig build.
Template syntax
HTML elements look like HTML:
<div class="container">
<h1>Hello World</h1>
<br />
</div>Expressions live in { ... }. Strings are HTML-escaped automatically.
<span>{user.name}</span>
<p>Total: {count + 1}</p>For-loops and conditionals use opening / closing tags:
{for item in self.items}
<tr>
<td>{item.id}</td>
<td>{item.name}</td>
</tr>
{/for}
{if self.items.len == 0}
<p>No items found.</p>
{else}
<p>{self.items.len} items</p>
{/if}Attributes can be dynamic:
<div class={my_var}>
<button data-id="{item.id}">Click</button>
</div>PascalCase tags are component calls:
<Card title="Title">Content</Card>A worked example
Source (src/features/items/zsx/item_list.zsx):
const std = @import("std");
const Item = @import("../domain/item.zig").Item;
pub const ItemList = struct {
items: []const Item,
pub fn render(self: ItemList, out: *std.ArrayList(u8), allocator: std.mem.Allocator) !void {
return (
<div class="page">
<h1>Items</h1>
{if self.items.len == 0}
<p>No items found.</p>
{else}
<table>
{for item in self.items}
<tr>
<td>{item.id}</td>
<td>{item.name}</td>
</tr>
{/for}
</table>
{/if}
</div>
);
}
};Output (src/features/items/fragments/item_list.zig) is plain Zig with appendSlice for the static HTML chunks and appendValue (with escaping) for the dynamic expressions. The generated file starts with // AUTO-GENERATED by planctl, which is the marker planctl clean looks for.
6. Deploy
Builds artifacts and uploads them to the Workbench for the chosen profile.
planctl deploy --app --arch <mono|micro> --profile <name>
planctl deploy --service <svc> --profile <name>
planctl deploy --sse --profile <name>
planctl deploy --all --arch <mono|micro> --profile <name>| Target | Action |
|---|---|
--app | Build + upload the shell. Mono ships a WASM hosted by planck/db; micro a native bin. |
--service <name> | Build + upload one WASM service (micro projects). |
--sse | Build + upload only the sse/ subproject. Same builder used by --all. |
--all | Mono: --app + --sse. Micro: --app + every services/* + --sse. |
A pre-deploy port validator runs before any upload. It rejects port: 0, duplicate ports inside the project, and cross-app port collisions across the host.
Why --sse exists
When you are iterating on SSE templates or topic logic, you do not want to rebuild and re-upload the WASM app and the static assets every cycle. --sse runs only the sse/ build and uploads only that artifact.
What --all does, in order
Mono:
- Build
app/withzig build -Doptimize=ReleaseFast(WASM). - Upload the WASM,
db.yaml,app.yaml, andpublic/under the app name. - If
./sse/build.zigexists, build and upload the SSE service as<app>_sse. - Workbench restarts the supervised processes.
Micro:
- Build and upload the native shell binary +
app.yaml+public/. - Iterate
app/services/*and run the WASM service deploy on each. A single service failure is logged but does not abort the rest. - Build and upload
sse/last so the topic is alive before browsers connect. - Workbench restarts each updated process.
Flags
| Flag | Meaning |
|---|---|
--arch <m> | mono or micro. Defaults to micro for backward compatibility. Set this explicitly. |
--profile <n> | Required. Picks a profile from ~/.planctl/config.yaml. |
--dry-run | Skip network calls. Print what would happen. |
7. Lifecycle: start, stop, restart, status
Lifecycle commands act on deployed artifacts. Use them after a deploy to bring something back up, or just to peek at health.
planctl start --app | --service <name> | --sse <app> | --all --profile <p>
planctl stop --app | --service <name> | --sse <app> | --all --profile <p>
planctl restart --app | --service <name> | --sse <app> | --all --profile <p>
planctl status [--app | --service <name> | --sse <app> | --all] --profile <p>--sse <app> is just sugar for --service <app>_sse. It matches the naming convention that the deploy step uses for the SSE artifact.
planctl status defaults to --all when no target is given.
Example status output
SERVICE APP STATE PORT PID CPU% RSS(MB)
-------------------- ---------- ---------- -------- -------- ------- --------
product eshop running 24006 12345 2.3 45.6
orders eshop running 24016 12346 1.1 32.1
eshop_sse eshop running 24020 12347 0.5 28.48. Undeploy
Removes previously deployed artifacts. The target flags mirror those of deploy.
planctl undeploy --app --profile <p> [--force]
planctl undeploy --service <name> --profile <p> [--force]
planctl undeploy --all --profile <p> [--force]| Target | Action |
|---|---|
--app | Delete the shell app. Services must be removed first. |
--service <name> | Undeploy one WASM service. |
--all | Undeploy every service, then delete the app. |
--force (or -f) skips the confirmation prompts, which is useful in CI.
9. Backup and restore
planctl backup and planctl restore give you CLI parity with the Workbench UI's create-backup / restore actions. Handy when ops wants to drive things from outside the browser.
Backup
planctl backup --app <name> --profile <p> [--output <dir>]| Flag | Meaning |
|---|---|
--app <name> | App to back up |
--output <dir> | Local directory to download the archive into. Default: cwd. |
--profile <n> | Required |
The Workbench produces the archive, and planctl downloads the same. Archives are self-contained: data files, WASM, config, and a manifest with integrity hashes.
Restore
There are three modes, picked by the flags you pass.
# App restore
planctl restore --app <name> --backup <path> --profile <p>
# App + single service restore (micro projects)
planctl restore --app <name> --service <svc> --backup <path> --profile <p>
# Whole-system restore (Workbench + sysdb)
planctl restore --system --backup <path>| Flag | Meaning |
|---|---|
--app <name> | Target app to restore |
--service <svc> | Narrow the restore to a single service inside the app |
--system | Restore the Workbench + system DB instead of an app |
--backup <path> | Path to the archive (required) |
--profile <n> | Required for app / app-service modes |
The Workbench drives the restore. It verifies the manifest hashes, stops the running service, writes new files, then asks the supervisor to start it back up. If any step after stop fails, the service is left stopped so that an operator can inspect, which is by design. There is no auto-rollback.
10. Schema: create and drop stores and indexes
planctl create and planctl drop do data-definition (DDL) against a deployed service's database. They drive the Workbench's /api/schema endpoint, so they run wherever the service is deployed. Writes go to the primary node in the profile, and async replication propagates them to any replicas. You never point these at a replica directly.
planctl create store <store> [--description <d>] [--app <a>] [--service <s>] --profile <p>
planctl drop store <store> [--app <a>] [--service <s>] [--force] --profile <p>
planctl create index <store>.<index> [--type <t>] [--unique] [--field <f>] [--app <a>] [--service <s>] --profile <p>
planctl drop index <store>.<index> [--app <a>] [--service <s>] [--force] --profile <p>Targeting a service
A store or index lives in exactly one service, addressed by the slug <app>_<service_name>. For a mono app shop whose db.yaml service is db, that slug is shop_db.
- Inside the project tree, you can omit
--appand--service; they resolve fromapp.yaml/db.yamlthe same waydeploydoes. For a mono app this resolves to<app>_db. - From anywhere else, pass
--app <name>.--servicestill defaults todb, which is correct for mono. - Micro apps have multiple named services, so always pass
--service <name>.
Stores
# Create a store (optionally with a description)
planctl create store orders --app shop --service orders --description "customer orders" --profile dev
# Drop a store (prompts unless --force)
planctl drop store orders --app shop --service orders --force --profile dev
# Mono, from inside the project tree (resolves to shop_db):
planctl create store orders --profile devIndexes
The index name is <store>.<index>, and the indexed field defaults to the last segment of that name. Use --field when the field differs from the index name (for example a nested path, or a custom index name).
# Index name and field are both "status"
planctl create index orders.status --app shop --service orders --profile dev
# A unique float index on "total"
planctl create index orders.total --app shop --service orders --type f64 --unique --profile dev
# Custom index name + nested field path
planctl create index customers.address_city --field Address.City --type string --profile dev
# Drop an index
planctl drop index orders.status --app shop --service orders --force --profile devFlags
| Flag | Meaning |
|---|---|
--type <t> | Index field type: string (default), i32 / i64 / u32 / u64 / f32 / f64 / bool, or int (alias for i64) |
--unique | Make the index unique (default: non-unique) |
--field <name> | Indexed field, when it differs from the index name's last segment |
--description <d> | Optional store description |
--app <name> | Target app (else resolved from the project tree) |
--service <name> | Target service name (else resolved from db.yaml, default db) |
--force, -f | Skip the confirmation prompt on drop |
--profile <name> | Required |
Schema operations on the system database are rejected.
The engine treats a re-created store or index as idempotent, but the Workbench currently surfaces that "already exists" result as a generic error. So re-running
create store/create indexreports a failure rather than a clean no-op. The store or index is still present.
11. Export and import
planctl export and planctl import move data out of and into a store via a YAML manifest. planctl forwards the manifest to the Workbench's /api/export / /api/import endpoints, and the engine reads and writes the data files itself, on the planck host. The manifest is the only input planctl sends.
planctl export --manifest orders-export.yaml --app shop --service orders --profile dev
planctl import --manifest orders-import.yaml --app shop --service orders --force --profile devTargeting works exactly like the schema commands above: inside the project tree --app / --service are optional and resolve to <app>_db for mono; from elsewhere pass --app; micro apps always pass --service <name>.
The manifest
A manifest names one store, a format, and the entity layout. Only the non-string fields need a type; strings pass through as-is.
store: orders
format: json # bson | json | csv
# output_dir: /data/exim # optional; defaults to <base_dir>/exim on the host
# query: orders.filter(status = "shipped") # optional export filter
entities:
- name: orders
role: parent
file: orders.json
fields:
- name: OrderID
type: int
- name: Total
type: doubleFiles live on the planck host
This is the part that surprises people: exports are written to, and imports read from, a folder on the planck host, and not on the machine running planctl. When the manifest omits output_dir, the engine defaults it to <base_dir>/exim (or the exim_dir set in db.yaml) and creates the folder, the same way backup_dir works. For a large import source, copy the data files into that folder out of band (scp / rsync); planctl only ever transfers the manifest.
Flags
| Flag | Meaning |
|---|---|
--manifest <file> | Required. Path to the YAML manifest. |
--app <name> | Target app (else resolved from the project tree) |
--service <name> | Target service name (else db) |
--force, -f | Skip the confirmation prompt on import |
--profile <name> | Required |
12. Service configuration reference
Every Planck service has two YAML files that live next to its build:
db.yaml: storage and engine tuning. The planck/db process reads it.service.yaml: identity, the WASM runtime, and the outbound upstream allowlist. The Planck runtime reads it.
planctl new and planctl add generate both with sensible defaults, and you edit them later. The two are independent: changing one does not require you to touch the other.
db.yaml
Storage engine settings. Address, port, TLS, buffers, durability, GC, replication, and change streams.
address: "0.0.0.0"
port: 24010
primary: true # true for a primary; false for a replica
base_dir: "/var/lib/planck/product"
backup_dir: "/mnt/backups/product"
max_sessions: 128
tls:
enabled: true # Mono apps default to TLS 1.3 self-signed
cert_file: "" # Leave empty for auto self-signed
key_file: ""
session:
idle_timeout_ms: 604800000 # 7 days
buffers:
memtable: 16777216 # 16 MB
vlog: 4194304 # 4 MB
wal: 262144 # 256 KB
durability:
enabled: true
flush_interval_in_ms: 1000
log_archive:
enabled: false
dest_path: ""
retain_logs_days: 7
file_sizes:
vlog: 1073741824 # 1 GB segment
wal: 16777216 # 16 MB segment
gc:
dead_ratio: 30 # %, when to compact a vlog segment
replica:
enabled: true # Set on the primary; points at the replica below
sync_interval_ms: 5000
address: "10.0.0.42" # Replica host
port: 24011 # Replica's planck/db port
change_streams:
ring_capacity: 16384
stores:
- ns: "orders"
operations: [insert, update, delete]Notes on the key fields:
primary: truemarks this instance as the primary. To ship WAL to a replica, fill in thereplicablock below with the replica's address and port; nothing is auto-created. On the replica's owndb.yaml, setprimary: falseand leave thereplicablock disabled.backup_diris the destination for snapshots. Pick a path on a disk other thanbase_dir. The Workbench refuses to defaultbackup_dirto anything underbase_dir, since that defeats the very point of a backup.gc.dead_ratiocontrols when a value-log segment gets compacted. The workbench-drivengctask respects this threshold and skips the tail segment so that foreground writes are never contended.durability.log_archive.retain_logs_dayscontrols how long rotated WAL segments are kept around. The workbench-drivenwal_truncatetask respects this.
service.yaml
Identity, the WASM runtime, and the upstream allowlist for outbound calls from inside WASM.
name: "product"
description: "Product catalog service"
route: "/product"
wasm:
enabled: true
min_instances: 2
max_instances: 8
autoscale: true
http:
host: "0.0.0.0"
port: 3010 # You set this; pick a free port for the service
max_connections: 10000
max_body_size: 1048576 # 1 MB
idle_timeout_ms: 30000
upstreams:
- name: "payments"
url: "https://payments.example.com"
timeout_ms: 5000
max_in_flight: 16
breaker:
failure_threshold: 5
success_threshold: 2
open_duration_ms: 30000Notes on the key fields:
nameis the service identity. It has to match the directory name that the deploy CLI uploads under.wasm.http.portis the port the service listens on. You set it explicitly, since nothing auto-assigns. The pre-deploy validator rejectsport: 0, refuses duplicates inside the same project, and refuses ports already taken by another deployed app on the same workbench.wasm.min_instances/wasm.max_instancessize the WASM instance pool for concurrent request handling.autoscale: truelets the runtime grow up tomax_instancesunder load.upstreamsis an explicit allowlist. WASM cannot reach a URL that is not listed here. Each entry has its own timeout, circuit breaker, and in-flight cap.
Workbench, the control plane
The Workbench (wb) is the control plane for everything planctl does. It owns identity, orchestration, the identity provider, JWT signing keys, and the supervised process tree. When you run planctl deploy, you are asking the control plane to accept a new artifact and reconcile the running fleet against it. planctl is only the client; wb does the real work.
Operational tasks (backup, gc, WAL truncate, stats, export, import, restore) are scheduled and driven from workbench against each planck it supervises. They are not scheduled inside the service itself.
13. Workflow recipes
From zero to deployed in five steps
# 1. One-time host setup (on macOS, prefix system start with sudo)
planctl system init
planctl system start
# 2. New project
planctl new mystore --type hda --arch mono
cd mystore
# 3. Configure a profile in ~/.planctl/config.yaml.
# See the Configure profiles section.
# 4. Deploy (builds, uploads, restarts)
planctl deploy --all --arch mono --profile devAdd a feature to a mono project
cd myapp
planctl add billing --type feature
planctl deploy --app --arch mono --profile devAdd a service to a micro project
cd myapp
planctl add orders --type service --port 4501
planctl deploy --service orders --arch micro --profile devProvision and seed a store
After a deploy, create the store and its indexes, then load seed data from a manifest. For a mono app run from the project root, the slug resolves automatically.
cd myapp
planctl create store orders --description "customer orders" --profile dev
planctl create index orders.CustomerID --type i64 --profile dev
planctl create index orders.status --profile dev
planctl import --manifest app/seed/import.orders.yaml --profile devFor a micro app, pass --service <name> on each command. Later, dump the store back out:
planctl export --manifest app/seed/export.orders.json.yaml --profile devIterate on the SSE service only
planctl deploy --sse --profile dev
planctl restart --sse myapp --profile devTear down and rebuild
planctl undeploy --all --profile dev --force
planctl deploy --all --arch mono --profile devBackup before a risky migration
planctl backup --app product --profile prod --output ./backups
# Run the change.
planctl deploy --service product --arch micro --profile prod
# If it went sideways:
planctl restore --app product --backup ./backups/product-2026-06-13.tar --profile prodFresh clone or CI build
git clone git@github.com:yourorg/yourapp.git
cd yourapp
zig build test # fetches deps and runs tests
planctl deploy --all --arch mono --profile stagingLive-edit ZSX templates
# Terminal 1: watch templates
planctl --watch src/features/tasks/zsx src/features/tasks/fragments
# Terminal 2: rebuild + redeploy on demand
planctl deploy --app --arch mono --profile dev14. Troubleshooting
planctl: command not found
Add ~/.planck/bin (dev install) or /opt/planck/bin (prod install) to your PATH. In your shell profile:
export PATH="$HOME/.planck/bin:$PATH"planctl deploy: --profile <name> is required
There is no default profile. Pass --profile <n>, or list one in ~/.planctl/config.yaml and reference it explicitly.
profile '<name>' not found
The CLI prints the available profile list when this happens. Check the name in ~/.planctl/config.yaml; do note that profile names are case-sensitive.
Deploy aborted: PortInUse (or DuplicatePort or PortZero)
The pre-deploy port validator has caught a problem. PortZero means a db.yaml still has port: 0 after resolution. DuplicatePort means two services in the same project want the same port. PortInUse means a different app on the same host is already using that port. Edit the offending db.yaml and re-run.
app.yaml not found
Run planctl from the project root, where app.yaml lives. For micro projects, you can also run from app/services/<name>/; the deploy code knows to look up the tree for the parent app.
zig build failed
Check the Zig compile output. The most common cause on a fresh clone is missing dependencies. Re-run from the project root so that the build.zig can fetch them.
Modules disambiguated to bson0, utils0, etc.
A dep is being constructed twice in your build graph. This is almost always caused by calling b.dependency("foo", .{}).module("foo") somewhere, which asks the dep's own build.zig for a fresh module instance. In deep transitive graphs you end up with multiple instances of the same logical package. The fix is to switch to b.dependency("foo", .{}).path("src/root.zig") + b.createModule(...) + explicit addImport calls.
planctl clean did not remove a file
clean only removes files starting with // AUTO-GENERATED by planctl. Hand-written .zig files in the fragments directory stay put.
Workbench connection refused
Verify that the Workbench is running: curl https://127.0.0.1:24011/api/apps (or whatever your profile's server is). Check the profile's server field and any --server override.
planctl system stop did nothing on macOS
launchctl bootout needs root to touch /Library/LaunchDaemons. Re-run with sudo.
Service did not come back up after restore
This is by design. Restore stops the service, writes new files, then asks the Workbench to start it. If any step after stop fails, the service stays stopped for operator inspection. Check ~/.planck/logs/workbench-*.out.log for the failure, fix the cause, then run planctl start --service <name> --profile <p>.
planctl restore: integrity check failed
The archive's SHA-256 does not match the data / WASM bytes. The backup is corrupt. Do not edit the manifest; pick a different backup, or re-run the original planctl backup.
Backup archive missing after planctl backup
The Workbench writes the archive into its own backup directory and planctl downloads a copy into --output (default: cwd). In case the download landed but you cannot find it, check the working directory you ran from and the printed output path.