c8ctl-plugin-nano 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,493 @@
1
+ # c8ctl-plugin-nano
2
+
3
+ A [c8ctl](https://github.com/camunda/c8ctl) plugin that starts, inspects, and
4
+ stops a local Nano BPM (`nanobpmn`) cluster.
5
+
6
+ ## About Nano BPM
7
+
8
+ **A Rust research engine exploring high-performance BPMN execution and Camunda 8
9
+ compatibility.**
10
+
11
+ Nano BPM (`nanobpmn`) is a single self-contained binary that runs BPMN processes
12
+ behind a **Camunda 8-compatible v2 REST API**. It embeds a deterministic,
13
+ event-sourced BPMN engine (`engine-core`), an append-only journal for crash
14
+ durability, an SQLite-backed read model, optional multi-node Raft replication,
15
+ and a built-in web console — all in one executable with no runtime dependencies.
16
+
17
+ It is an advanced research prototype: a place to explore what a faster, smaller,
18
+ faster-to-iterate-on process engine can do, while staying API-compatible with
19
+ existing Camunda 8 clients and tooling. This plugin is the easiest way to run and
20
+ manage it — single node or a whole cluster — shipping a prebuilt binary for your
21
+ platform so there is nothing to compile.
22
+
23
+ It adds a single `nano` command:
24
+
25
+ ```bash
26
+ c8ctl nano start|status|stop|restart|logs|clean|set|config
27
+ ```
28
+
29
+ `nano start N` spawns **N** nanobpmn node processes wired to talk to each other
30
+ on `localhost` (round-robin partition ownership), tracks them in a state file,
31
+ and waits until every node is reachable.
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ # Start a single-node cluster (port 8080)
37
+ c8ctl nano start
38
+
39
+ # Start a 3-node cluster (ports 8080, 8081, 8082)
40
+ c8ctl nano start 3
41
+
42
+ # Start a 3-node Raft-replicated cluster (RF=3 enables Raft automatically)
43
+ c8ctl nano start 3 --rf 3
44
+
45
+ # Choose a different base port (nodes -> 9000, 9001, 9002)
46
+ c8ctl nano start 3 --port 9000
47
+
48
+ # Override the partition count (default = node count)
49
+ c8ctl nano start 3 --partitions 6
50
+
51
+ # Show cluster status and per-node health
52
+ c8ctl nano status
53
+
54
+ # Inspect a cluster c8ctl did NOT start (queries /v2/topology on the given port)
55
+ c8ctl nano status --port 8080
56
+
57
+ # Tail a node's log (-f / --follow to stream)
58
+ c8ctl nano logs 1 --follow
59
+
60
+ # Simulate a node failing (freeze it) and recovering (resume it)
61
+ c8ctl nano pause 1
62
+ c8ctl nano resume 1
63
+
64
+ # Stop the cluster (engine data is retained)
65
+ c8ctl nano stop
66
+
67
+ # Stop the cluster and delete per-node engine data
68
+ c8ctl nano stop --purge
69
+
70
+ # Stop then start fresh
71
+ c8ctl nano restart 3
72
+
73
+ # Wipe journal/data + logs from disk (keeps models & workers)
74
+ c8ctl nano clean
75
+
76
+ # Persist settings
77
+ c8ctl nano set bin ~/workspace/nanobpmn/server/target/release/nanobpm-gateway-rest-server
78
+ c8ctl nano set model-dir ~/bpmn-workspace
79
+
80
+ # Show current configuration and on-disk locations
81
+ c8ctl nano config
82
+ ```
83
+
84
+ ## Persistent assets: models & workers
85
+
86
+ Nano BPM separates **persistent authoring assets** (BPMN models and worker code)
87
+ from **ephemeral engine data** (journal, snapshots, variable spill):
88
+
89
+ - **Workspace** (`NANOBPMN_WORKSPACE_DIR`) — holds `models/` and `workers/`. It is
90
+ the authoring source of truth, **shared by every node**, and is **never** deleted
91
+ by `stop` or `clean`.
92
+ - **Engine data** (`NANOBPMN_DATA_DIR`) — per-node journal/snapshots/spill. Ephemeral;
93
+ removed by `stop --purge` and `clean`.
94
+
95
+ The plugin points every node at one shared workspace so a model deployed once is
96
+ visible cluster-wide and survives restarts. By default it lives at
97
+ `<state home>/workspace`; change it with:
98
+
99
+ ```bash
100
+ c8ctl nano set model-dir ~/bpmn-workspace
101
+ ```
102
+
103
+ This creates `~/bpmn-workspace/models/` and `~/bpmn-workspace/workers/`. Restart a
104
+ running cluster for a workspace change to take effect.
105
+
106
+ ## Cleaning up disk
107
+
108
+ ```bash
109
+ c8ctl nano clean # remove engine data + logs (cluster must be stopped)
110
+ c8ctl nano clean --workspace # ALSO delete models & workers (destructive!)
111
+ c8ctl nano stop --purge # stop and remove engine data in one step
112
+ ```
113
+
114
+ `clean` refuses to run while any node is alive.
115
+
116
+ ## Configuration (`set` / `config`)
117
+
118
+ Persistent settings are stored in `<state home>/config.json`:
119
+
120
+ | Setting | Env mapping | Set with |
121
+ |---------------------|--------------------------|-----------------------------------|
122
+ | Binary path | (used to launch nodes) | `c8ctl nano set bin <path>` |
123
+ | Workspace directory | `NANOBPMN_WORKSPACE_DIR` | `c8ctl nano set model-dir <path>` |
124
+
125
+ Show the effective configuration and all on-disk locations with `c8ctl nano config`.
126
+
127
+ ## Checking status
128
+
129
+ `c8ctl nano status` queries each node's always-on `GET /v2/topology`, which is the
130
+ authoritative cluster view. Because of this it works in three situations:
131
+
132
+ - **c8ctl-managed cluster** — shows per-node process liveness (PID), reachability,
133
+ and the live topology (partition leadership).
134
+ - **External cluster** — a Nano BPM cluster started outside c8ctl (e.g. by hand,
135
+ a script, or another tool). With no recorded state, status probes
136
+ `http://127.0.0.1:<port>/v2/topology` and reports what it finds, labelled
137
+ `(external — not started by c8ctl)`.
138
+ - **Nothing running** — reports `stopped`.
139
+
140
+ Point status at a specific endpoint with `--port`:
141
+
142
+ ```bash
143
+ c8ctl nano status # default: managed cluster, else probe port 8080
144
+ c8ctl nano status --port 9000
145
+ ```
146
+
147
+ ### Camunda vs Nano detection
148
+
149
+ Nano advertises itself in `GET /v2/topology` with a `nano` object
150
+ (`engine: "nanobpmn"`) — a superset of the Camunda Orchestration Cluster API.
151
+ A stock Camunda gateway answers the same endpoint without it, so `status` can
152
+ tell the two apart and prints a `product:` line (`Nano BPM` or `Camunda`) with
153
+ the version. If `status` finds a Camunda gateway on the probed port it says so
154
+ explicitly rather than pretending it is a Nano cluster.
155
+
156
+ For the same reason, `c8ctl nano start` refuses to launch on top of an existing
157
+ gateway. If any chosen port is already serving a Camunda (or Nano) endpoint it
158
+ reports exactly what is running and exits without starting:
159
+
160
+ ```
161
+ ✗ Port 8080 is already serving a Camunda gateway (v8.6.0).
162
+ ✗ Refusing to start Nano on top of a running Camunda instance.
163
+ Start on a free base port instead, e.g. "c8ctl nano start 1 --port 8180".
164
+ ```
165
+
166
+ To run Nano alongside a local Camunda, give it a different base port
167
+ (`--port`); the collision check only applies to the ports Nano would bind.
168
+
169
+ ## Fault injection: pause / resume a node
170
+
171
+ `c8ctl nano pause <nodeId>` and `c8ctl nano resume <nodeId>` let you simulate a
172
+ node failing and coming back online, to exercise Raft failover and recovery on a
173
+ local cluster:
174
+
175
+ ```bash
176
+ c8ctl nano start 3 --rf 3 # 3-node Raft-replicated cluster
177
+ c8ctl nano pause 1 # freeze node 1 (SIGSTOP) — like a hang or partition
178
+ c8ctl nano status # node 1 shows "paused"; the cluster is "degraded"
179
+ c8ctl nano resume 1 # unfreeze node 1 (SIGCONT) — it rejoins
180
+ ```
181
+
182
+ - **pause** sends `SIGSTOP`, which halts the process instantly and *cannot be
183
+ caught or ignored* — so the node stops responding without losing its PID or its
184
+ on-disk state, faithfully mimicking a hung/partitioned node.
185
+ - **resume** sends `SIGCONT`, and the process continues exactly where it left off.
186
+ - A paused node is reported as `paused` in `c8ctl nano status` and counts as
187
+ unhealthy, so the cluster shows `degraded`.
188
+ - `c8ctl nano stop` automatically resumes any paused node first, so it can shut
189
+ down gracefully rather than being force-killed.
190
+
191
+ ## Trace capture for historical replay (`--capture`)
192
+
193
+ Start a cluster with `--capture` to record every instance's inputs so runs can be
194
+ replayed and analysed later:
195
+
196
+ ```bash
197
+ c8ctl nano start 3 --capture
198
+ c8ctl nano status # shows "trace capture: on"
199
+ ```
200
+
201
+ `--capture` sets `NANOBPMN_TRACE_STIMULI=1` on **every** node. That single flag
202
+ enables the Tier 2 recorded-input (stimuli) log *and* auto-enables Tier 1 variable
203
+ capture. It must be set on all nodes because each node's `TraceStore` only sees
204
+ instances on its own partitions.
205
+
206
+ Read a trace back from any node:
207
+
208
+ ```
209
+ GET /console/api/traces/{instanceKey}
210
+ → { creationVariables, stimuli[], <per-incident variables> }
211
+ ```
212
+
213
+ Optional tuning is done with environment variables, which pass through from your
214
+ shell automatically (no dedicated flags):
215
+
216
+ | Env var | Default | Purpose |
217
+ |------------------------------------|---------|--------------------------------------|
218
+ | `NANOBPMN_TRACE_VARIABLES_MAX_BYTES` | 16384 | Max captured variable payload bytes |
219
+ | `NANOBPMN_TRACE_STIMULI_MAX` | 1024 | Max recorded stimuli per instance |
220
+ | `NANOBPMN_TRACE_CAPACITY` | 2000 | Max traced instances retained |
221
+
222
+ > Setting `NANOBPMN_TRACE_VARIABLES=1` alone enables only Tier 1 (variables); use
223
+ > `--capture` for full recorded-input replay.
224
+
225
+ ## How nodes are configured
226
+
227
+ Each node is the single `nanobpmn` server binary, configured entirely through
228
+ environment variables. For `nano start 3` the plugin spawns:
229
+
230
+ | Node | `PORT` | `NANOBPMN_NODE_ID` | `NANOBPMN_NODES` |
231
+ |------|--------|--------------------|--------------------------------------------------------------------|
232
+ | 0 | 8080 | 0 | `http://127.0.0.1:8080,http://127.0.0.1:8081,http://127.0.0.1:8082` |
233
+ | 1 | 8081 | 1 | (same) |
234
+ | 2 | 8082 | 2 | (same) |
235
+
236
+ Additionally every node gets:
237
+
238
+ - `NANOBPMN_PARTITIONS` — total partitions (default = node count)
239
+ - `NANOBPMN_RF` — replication factor (default `1`)
240
+ - `NANOBPMN_RAFT=1` — set automatically when `RF > 1` (or via `--raft`)
241
+ - `NANOBPMN_DATA_DIR` — a per-node engine data directory
242
+ - `NANOBPMN_DURABILITY=async` — set by default for throughput; override by
243
+ exporting `NANOBPMN_DURABILITY` (e.g. `sync`) before `nano start`
244
+ - `NANOBPMN_REPLICATE_ACTIVATION=digest` — set by default so activated-job
245
+ state is observable across the cluster; override by exporting
246
+ `NANOBPMN_REPLICATE_ACTIVATION` before `nano start`
247
+ - `NANOBPMN_REPLICATION=leader-durable` — set by default; override by exporting
248
+ `NANOBPMN_REPLICATION` before `nano start`
249
+ - `NANOBPMN_WORKSPACE_DIR` — the shared workspace (models & workers)
250
+ - `NANOBPMN_TRACE_STIMULI=1` — set on every node when `--capture` is passed
251
+
252
+ Partition ownership is deterministic (`partition_id % num_nodes`), so the nodes
253
+ agree on the cluster map with no coordinator. With `RF=1` each partition lives on
254
+ one node and the others forward to it; with `RF>1` partitions are Raft-replicated
255
+ across nodes.
256
+
257
+ ## Locating the binary
258
+
259
+ The plugin needs a built `nanobpmn` server binary. Resolution order:
260
+
261
+ 1. `--binary <path>`
262
+ 2. configured path (`c8ctl nano set bin <path>`)
263
+ 3. `NANOBPMN_BINARY=<path>`
264
+ 4. the matching **platform package** (`c8ctl-plugin-nano-<os>-<arch>`), installed
265
+ automatically as an `optionalDependency` when you install the plugin from npm
266
+ 5. `release` build under the nanobpmn repo
267
+ 6. `debug` build under the nanobpmn repo
268
+
269
+ Most users never need a local build: installing the plugin from npm pulls in the
270
+ prebuilt binary for their platform (step 4). Steps 5–6 are the local-dev path.
271
+
272
+ The repo root defaults to `~/workspace/nanobpmn` and can be overridden with
273
+ `NANOBPMN_REPO`. Build a binary with:
274
+
275
+ ```bash
276
+ cd ~/workspace/nanobpmn && make release-gateway # API-only gateway
277
+ # or
278
+ cd ~/workspace/nanobpmn && make release # includes the web console
279
+ ```
280
+
281
+ ## State & data locations
282
+
283
+ State, config, logs, per-node data, and the workspace live under a per-user
284
+ directory (override with `C8CTL_NANO_HOME`):
285
+
286
+ - **macOS**: `~/Library/Application Support/c8ctl-nano`
287
+ - **Linux**: `$XDG_DATA_HOME/c8ctl-nano` (or `~/.local/share/c8ctl-nano`)
288
+ - **Windows**: `%LOCALAPPDATA%\c8ctl-nano`
289
+
290
+ ```
291
+ <home>/config.json # persistent settings (binary path, workspace dir)
292
+ <home>/cluster.json # tracked cluster: nodes, pids, ports, config
293
+ <home>/data/node-<i>/ # per-node engine data (journal, spill, snapshots) — ephemeral
294
+ <home>/logs/node-<i>.log # per-node stdout/stderr
295
+ <home>/workspace/ # default shared workspace (models/, workers/) — persistent
296
+ ```
297
+
298
+ `nano stop` removes the state file but keeps `data/` by default so you can stop a
299
+ cluster and keep your journals; pass `--purge` to delete engine data too. The
300
+ workspace is never removed except by `nano clean --workspace`.
301
+
302
+ ## Flags
303
+
304
+ | Flag | Applies to | Description |
305
+ |----------------|------------|----------------------------------------------------------|
306
+ | `--port` | start | Base HTTP port; node *i* listens on `basePort+i` (8080) |
307
+ | `--partitions` | start | Total partitions across the cluster (default node count) |
308
+ | `--rf` | start | Replication factor; `>1` enables Raft (default `1`) |
309
+ | `--raft` | start | Force Raft on (default: on iff `rf>1`) |
310
+ | `--capture` | start | Enable trace capture (recorded-input replay) on every node |
311
+ | `--binary` | start | Path to the nanobpmn server binary (overrides `set bin`) |
312
+ | `--force` | start | Stop any existing cluster first |
313
+ | `--purge` | stop | Also delete per-node engine data |
314
+ | `--workspace` | clean | Also delete the workspace (models + workers) |
315
+ | `--follow`,`-f`| logs | Stream log output (`tail -F`) |
316
+
317
+ ProcessOS flags (`processos` command):
318
+
319
+ | Flag | Applies to | Description |
320
+ |----------------|------------|----------------------------------------------------------|
321
+ | `--port` | start | ProcessOS listen port (default 8090) |
322
+ | `--nano-url` | start | Target Nano BPM engine URL (default `http://localhost:8080`) |
323
+ | `--binary` | start | Path to the ProcessOS binary (overrides `set bin`) |
324
+ | `--spawn-nano` | start | Force spawning a pilot Nano engine (default on when a nano binary is available) |
325
+ | `--no-spawn-nano` | start | Don't spawn a pilot engine; reuse the `--nano-url` engine |
326
+ | `--force` | start | Stop any existing ProcessOS instance first |
327
+ | `--follow`,`-f`| logs | Stream log output (`tail -F`) |
328
+
329
+ ## Managing ProcessOS (`processos`)
330
+
331
+ ProcessOS is the optimization-plane server that analyses a running Nano BPM
332
+ engine. The plugin can manage a single local ProcessOS instance with the same
333
+ start/stop/status/logs lifecycle as `nano`.
334
+
335
+ Unlike the Nano BPM server binary (which is distributed via npm — see below),
336
+ **the ProcessOS binary is downloaded manually**: grab the build for your
337
+ platform, then point the plugin at it.
338
+
339
+ ```bash
340
+ # One-time: tell the plugin where the binary is
341
+ c8ctl processos set bin ~/Downloads/processos
342
+
343
+ # Start ProcessOS against the local Nano BPM engine (http://localhost:8080)
344
+ c8ctl processos start
345
+
346
+ # Or against a specific engine, on a specific port
347
+ c8ctl processos start --nano-url http://localhost:8080 --port 8090
348
+
349
+ # Inspect / stream logs / stop
350
+ c8ctl processos status
351
+ c8ctl processos logs --follow
352
+ c8ctl processos stop
353
+ ```
354
+
355
+ On a successful `start` the summary leads with the landing page:
356
+
357
+ ```
358
+ Start here http://127.0.0.1:8090/ (landing)
359
+ Cockpit http://127.0.0.1:8090/cockpit
360
+ Health http://127.0.0.1:8090/health
361
+ Target Nano http://localhost:8080
362
+ ```
363
+
364
+ ### Pilot engine (spawned by default)
365
+
366
+ ProcessOS uses a Nano engine in two roles: the **target** engine it analyses
367
+ (read-only, set with `--nano-url`), and its **own** internal "pilot" engine where
368
+ it runs experiments. **By default ProcessOS spawns its own pilot engine** as a
369
+ child process, so it never disturbs the target:
370
+
371
+ ```bash
372
+ c8ctl processos start # spawns a pilot engine automatically
373
+ c8ctl processos start --no-spawn-nano # reuse the --nano-url engine for the pilot too
374
+ ```
375
+
376
+ ProcessOS spawns its pilot engine from a Nano gateway binary given in
377
+ `PROCESSOS_NANO_BIN`. The plugin **auto-wires `PROCESSOS_NANO_BIN`** from the same
378
+ binary `c8ctl nano` uses (`--binary` / `nano set bin` / `$NANOBPMN_BINARY` / the
379
+ platform package / a repo build). A console-enabled nano build is required — the
380
+ npm-distributed binaries qualify. The spawned engine is torn down when ProcessOS
381
+ stops.
382
+
383
+ If no nano binary can be found, ProcessOS falls back to `--no-spawn-nano`
384
+ automatically (using the target engine as the pilot) and prints a warning. Force
385
+ the behaviour explicitly with `--spawn-nano` / `--no-spawn-nano`, override the
386
+ binary with `c8ctl processos set env PROCESSOS_NANO_BIN=<path>`, or set the mode
387
+ persistently with `c8ctl processos set env PROCESSOS_SPAWN_NANO=false`.
388
+
389
+ ### ProcessOS configuration
390
+
391
+ Settings persist under a `processos` key in the same `config.json` as `nano`.
392
+
393
+ ```bash
394
+ c8ctl processos set bin <path> # path to the downloaded ProcessOS binary
395
+ c8ctl processos set port <n> # listen port (default 8090)
396
+ c8ctl processos set nano-url <url> # target Nano BPM engine (default http://localhost:8080)
397
+ c8ctl processos set data-dir <path> # PROCESSOS_DATA_DIR (default <stateHome>/processos-data)
398
+ c8ctl processos set env KEY=VALUE # set any passthrough env var (e.g. PROCESSOS_LLM_MODEL)
399
+ c8ctl processos set env KEY= # unset a passthrough env var
400
+ c8ctl processos config # show current settings and on-disk paths
401
+ ```
402
+
403
+ The binary is resolved in this order: `--binary` flag → `set bin` →
404
+ `$PROCESSOS_BINARY` → a local `processos/target/{release,debug}/processos` build.
405
+ Typed settings (`port`, `nano-url`, `data-dir`) always win over generic `env`
406
+ passthrough values when launching.
407
+
408
+ ## Installing
409
+
410
+ ```bash
411
+ c8ctl load plugin --from file:///path/to/c8ctl-nano
412
+ ```
413
+
414
+ Then verify it shows up:
415
+
416
+ ```bash
417
+ c8ctl help | grep nano
418
+ ```
419
+
420
+ ## Distribution & releasing
421
+
422
+ Releases are automated with **semantic-release** (`.github/workflows/release.yml`,
423
+ `release.config.cjs`). Pushing conventional commits to `main` cuts a version,
424
+ publishes to npm, and creates a GitHub Release.
425
+
426
+ ### Platform packages
427
+
428
+ The server binary is shipped as a set of platform-specific npm packages, one per
429
+ target, gated by npm's `os`/`cpu` fields:
430
+
431
+ | package | os | cpu |
432
+ |----------------------------------|--------|-------|
433
+ | `c8ctl-plugin-nano-darwin-arm64` | darwin | arm64 |
434
+ | `c8ctl-plugin-nano-darwin-x64` | darwin | x64 |
435
+ | `c8ctl-plugin-nano-linux-x64` | linux | x64 |
436
+ | `c8ctl-plugin-nano-linux-arm64` | linux | arm64 |
437
+ | `c8ctl-plugin-nano-win32-x64` | win32 | x64 |
438
+
439
+ The root `c8ctl-plugin-nano` lists all five as `optionalDependencies` (pinned to
440
+ the exact release version, injected into the published tarball at release time).
441
+ npm installs only the one matching the host, so each user downloads a single
442
+ binary. The mapping lives in `platforms.mjs` — the single source of truth shared
443
+ by the build/publish scripts and the plugin's runtime resolution.
444
+
445
+ ### Binary delivery contract (upstream CI)
446
+
447
+ This repo never builds or references the private Nano BPM source. Instead, the
448
+ upstream cross-compile pipeline uploads prebuilt binaries as assets on a rolling
449
+ GitHub Release named **`binaries`** in this repo. The release workflow downloads
450
+ them (`gh release download binaries`) and packs them into the platform packages.
451
+
452
+ Each asset must be named exactly (see `PLATFORMS[].asset` in `platforms.mjs`):
453
+
454
+ ```
455
+ nanobpm-gateway-rest-server-darwin-arm64
456
+ nanobpm-gateway-rest-server-darwin-x64
457
+ nanobpm-gateway-rest-server-linux-x64
458
+ nanobpm-gateway-rest-server-linux-arm64
459
+ nanobpm-gateway-rest-server-win32-x64.exe
460
+ ```
461
+
462
+ The upstream job needs a token with `contents: write` on this repo and can upload
463
+ with e.g. `gh release upload binaries <files> --clobber`.
464
+
465
+ ### What triggers a release
466
+
467
+ The plugin's npm version is **decoupled** from the nanobpmn binary version, so
468
+ uploading new binaries does **not** by itself publish a new npm version —
469
+ `semantic-release` only releases on releasable commits to `main`.
470
+
471
+ To make a binary update ship, the upstream pipeline (after uploading the assets)
472
+ rewrites the tracked marker file **`nanobpmn-binary.json`** in this repo with the
473
+ new nanobpmn version/commit and pushes it as a `fix(binary): …` commit. That
474
+ commit triggers the release workflow, which downloads the just-uploaded binaries
475
+ and publishes a patch release. The marker is surfaced to users in `nano config`
476
+ (`bundled nano <version>`). The push is a no-op when the marker is unchanged.
477
+
478
+ ```json
479
+ // nanobpmn-binary.json — overwritten by upstream CI; "0.0.0-dev" = local checkout
480
+ { "version": "v1.4.2", "commit": "cdeb390", "updated": "2026-06-27T11:00:00Z" }
481
+ ```
482
+
483
+ ### OIDC / Trusted Publishing
484
+
485
+ The workflow is set up for npm **Trusted Publishing** (OIDC, `id-token: write`)
486
+ with provenance (`NPM_CONFIG_PROVENANCE: true`, requires this repo to be public).
487
+ Trusted Publishing is per-package and requires the package to already exist, so:
488
+
489
+ 1. **Bootstrap** the first release with a granular-automation `NPM_TOKEN` secret —
490
+ it is used automatically and creates all six packages.
491
+ 2. On npmjs.com, add a **Trusted Publisher** (this repo + `release.yml`) for the
492
+ root package and each of the five platform packages.
493
+ 3. Remove the `NPM_TOKEN` secret; subsequent releases authenticate via OIDC.