# Changelog (/docs/reference/changelog)

> Release notes for the better-compute packages and the documentation site, every notable change, newest first.



# Changelog [#changelog]

All notable changes to the better-compute packages are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every package in `packages/` shares one version.

## Unreleased [#unreleased]

## 0.1.1 (2026-10-07) [#011-2026-10-07]

Better IAM 0.3 support. Every package now accepts Better IAM `>=0.2.0 <0.4.0` (dependencies and peers), so
`npm install better-compute better-iam` resolves one Better IAM, the current 0.3.x, instead of a 0.3 umbrella beside
the 0.2 packages 0.1.0 asked for.

### Changed [#changed]

* **Better IAM range.** `^0.2.0` becomes `>=0.2.0 <0.4.0` on every `@better-iam/*` and `better-iam` dependency and
  peer. The workspace builds and tests against 0.3.0 (its full suite and the IAM contract tests pass); 0.2.x keeps
  working. Better IAM 0.3's breaking changes do not touch Better Compute: it declares none of the newly reserved
  principal or resource attribute names, and its resource resolver returns no `classification`.

## 0.1.0 (2026-10-07) [#010-2026-10-07]

The first release on npm: the umbrella package `better-compute` and 26 `@better-compute/*` packages, all at 0.1.0.
It needs Node.js 22.12 or later, PostgreSQL 16 or later (PGlite for development and tests) and Better IAM `^0.2.0`.

### Added [#added]

* **Control plane.** `betterCompute()` runs the API, gateway and worker in one process or separately. PostgreSQL is
  the source of truth: bookings are guarded by `EXCLUDE` constraints and all SQL reads one clock, `compute_now()`.
  Every mutation makes one Better IAM decision; fleet paths (heartbeats, inventory, commands) make none. Stores for
  PostgreSQL and PGlite ship in `@better-compute/postgres`.
* **Capacity and reservations** (`@better-compute/capacity`). Pools, selectors, NVLink- and NUMA-aware placement,
  quotas, previews and reservations for whole GPUs, MIG partitions and shared GPU slots. Reservations also book host
  resources (`vcpu`, `memoryGiB`, hugepages) and hand cgroup limits and a NUMA-local cpuset to the node. Usage is metered
  in hourly windows.
* **Bare metal** (`@better-compute/metal`, `@better-compute/provision`). Enroll, inspect, commission, deploy,
  release, retire and rescue workflows over BMC and PXE, driven by a workflow engine with profiles and a signed image
  catalog. A machine returns to `ready` only after an attested wipe. Also dedicated-root leases, two-phase BMC
  password rotation, and GPU health remediation within a pool's disruption budget.
* **VPS, sandboxes and volumes** (`@better-compute/vps`). Instances on your own hosts (IPAM, flavors with NUMA,
  pinning and hugepage placement) or on providers (`@better-compute/provider-hetzner`,
  `@better-compute/provider-proxmox`). Sandboxes add templates, TTLs, warm pools, snapshots and SSH exec with
  short-lived certificates. Sandbox volumes and persistent host volumes attach to instances.
* **Kubernetes workers** (`@better-compute/provider-kubernetes`). bcd runs as a DaemonSet and adopts a cluster's
  nodes and GPUs into a pool; reservations with `binding: 'kubernetes'` hand devices to the kubelet through device
  plugins or a DRA driver.
* **Gateway and protocol** (`@better-compute/gateway`, `@better-compute/protocol`). Daemon sessions over gRPC with
  mutual TLS, liveness, command dispatch, and join, renew and secret forwarding. The generated
  `bettercompute.agent.v1` wire contracts are frozen.
* **PKI and the destructive-batch quorum** (`@better-compute/pki`). Local CAs, SPIFFE node certificates, sealed join
  tokens, attestors (join token, TPM, cloud instance identity, x509 proof of possession), command signing, a sealed
  secret store and broker, and trust-root rotation. Destructive command batches can require a quorum of approvers
  (`commandBatches`, `pki.approvers`). `@better-compute/audit` adds signed Merkle checkpoints over the audit log.
* **Better IAM integration** (`@better-compute/iam`). The IAM plugin and catalog, the authorization projection, an
  embedded or remote `IamPort`, and the usage sink, on Better IAM `^0.2.0` from npm.
* **CLI** (`@better-compute/cli`, the `better-compute` command). PKI setup and certificate issuance, the bcd install
  script, capacity shortcuts, `iam setup` and release signing.
* **MCP** (`@better-compute/mcp`). Model Context Protocol tools for pools, capacity, GPU reservations, instances and
  sandboxes, gated by Better IAM with confirmations for risky actions.
* **Clients and consoles.** `@better-compute/client` is a typed, browser-safe API client with live `$watch`.
  `@better-compute/console-kit` provides framework-free view models for consoles.
* **Testing kit** (`@better-compute/testing`). Fixtures, the simulated `FakeDaemon`, a sim clock, the fleet
  simulation and conformance runners.
* **More packages.** `@better-compute/events` (signed webhooks and an SSE stream), `@better-compute/fleet` (fleet
  configuration as code), `@better-compute/observability` (Prometheus metrics), `@better-compute/rollout` (signed bcd
  releases and wave rollouts) and `@better-compute/driver-sdk` (out-of-process drivers).
* **The Go daemon bcd** (`daemon/`). The node agent is built and tested end to end against a real gateway on the
  fake-hardware build; the site and ramdisk modes are covered by Go unit tests. The release pipeline (goreleaser,
  SBOMs, cosign signatures, SLSA3 provenance) is in place, but this release publishes no bcd binaries.

### Known limitations [#known-limitations]

* **No real-hardware runs.** Everything is tested against fakes and simulations: no real server has run bcd, and the
  hardware gates (a GPU host, a KVM host, the reference benchmark machines) have not run. Local test runs and the
  benchmark numbers use PGlite rather than a PostgreSQL server.
* **bcd binaries are not released.** The install script and self-update need a published bcd, so until a release
  carries binaries, build bcd from `daemon/` with Go.
* See the [roadmap](https://better-compute.com/docs/reference/roadmap) for the state of every phase.

## Development history (pre-0.1.0) [#development-history-pre-010]

The internal notes kept while the packages were built, before the first release. They cite sections of `PLAN.md`
and commit hashes, and are kept for reference; the 0.1.0 entry above summarizes them.

* Round 5, the complete node daemon (PLAN §3.8, v1.1):
  * Host resources on every host: `vcpu`, `memoryGiB`, `hugepages2Mi`, `hugepages1Gi` rows from inventory (`capacity.hosts`, `policy.hostReserve`). VPS hosts' memory now excludes hugepage pools.
  * Reservations take `resources` (`vcpu`, `memoryGiB`, `hugepages1Gi`) and default to the pool's per-GPU share (`policy.perGpu`, else the smallest host share). Host resources are not charged to quota. `capacity.preview` books the same.
  * `DeviceAssignment.limits` carry cgroup limits and a NUMA-local cpuset (without the host's reserved CPUs) to bcd.
  * `binding: 'kubernetes'` works on pools with `policy.kubernetes`. bcd hands those devices to the kubelet: device plugins or the DRA driver `gpu.bcd.dev` (`gpuApi`). Releasing a bound one keeps its GPUs and host share booked, and billed, until the node reports `AllocationReleased` (pods let go), a lost node, or the booking's end. The placer used to re-book them under running pods: 55/240 failed bindings in the 12-node sim. Meanwhile the reservation carries the `DevicesHeld` condition. A booking that pods still hold at its end is extended in 5-minute steps while the devices are free.
  * bcd advertises the Hello capabilities `devices.limits` and `binding.kubernetes`. Reservations that book host resources, or bind to Kubernetes, place only on nodes that advertise them. An older bcd would drop the limits or the binding.
  * Flavors take `placement` (`numaAffine`, `pinned`, `hugepages`). Hugepage guests book hugepages and are charged as `memoryGiB`. Pinned guests take isolated CPUs first. Guests that don't get their placement carry `PlacementDegraded`. Guests of pools that overcommit memory get a balloon.
  * Guests get their IPAM gateway and DNS (`setIpPool({ dns })`, `InstanceNic.gateway/nameservers`).
  * Agent memory: `sandboxVolumes`, persistent host volumes (`InstancesSection.volumes`, `InstanceSpec.volumes`). Placement takes `onlyNodes`.
  * Extension kind `pool-policy`: provision refuses host policies nodes can't take, at `pools.create`/`update`.
  * Telemetry specs carry `headersRef` and `resourceAttributes` (`nodeTelemetryModule`, `PoolPolicy.telemetry`).
  * Security:
    * `trustedProxies`: behind a load balancer, IAM sees the caller, and fails closed.
    * Hello capabilities `instance.ssh-host-only`, `instance.placement` and `instance.volumes` gate where sandboxes, placed flavors and volumes land.
    * Hosted-Kubernetes join tokens land nodes `tenant-controlled`.
    * Slots per shared GPU are capped at 48.
  * MCP `reserve_gpus` takes `vcpu`, `memoryGiB` and `kubernetes`.
  * **Breaking (IAM):** `sandboxVolumes.create` is decided on `compute-sandbox-volume/<draft id>`, whose projection carries the requested `sizeGiB`, instead of the tenant (6cb30e9). Policies that granted it on `compute-tenant` still reach it through the pool parent; policies that bound volume size now see it. `sandboxVolumes.resize` stages the new size the same way.
  * **Breaking (API):** `sandboxVolumes.list` defaults to the caller's own volumes; pass `scope: 'all'` for everyone's (77bf9f5).
  * **Breaking (metrics):** bcd's `bcd.instance.memory.balloon` is now `bcd.instance.memory.balloon.reclaimed` (9bdd43a). It reports flavor memory minus current guest memory, only for guests with a balloon. Dashboards and alerts on the old name need updating.
  * Hosted-Kubernetes bcd uses the kubelet's credentials (e7d0765; NodeRestriction confines it to its own Node), never a service account.
  * CI builds the DaemonSet image (daemon/Containerfile) and server-side dry-runs the rendered manifests on a kind cluster.

* Round 4, server/core/mcp/client:
  * Security: admissions decide on the committed row (`IamPort.refreshResource`; §12.4 "draft in memory before require" was unimplemented, so `extend` decided on stale projection rows: BoundedExtend allowed 72 h + 1 h). A node reports only its own allocations. With `pki.commandSigning`, a reservation goes active only on the node's own signed `AllocationBound` (`pkiAllocationBindingCheck`). The secret broker releases an instance's user data only when it names a secret of the instance's own tenant. Certificate roles live in the Subject OU `bc-role:<role>` (no private OIDs; Go cannot parse the 2.25 arc).
  * Envelope: async-with-wait (`MethodDefinition.poll`, `wait: { timeoutMs }`), `MethodAdmission.check` (budget checks outside transactions, no `denied`), W3C `traceparent` onto enqueued commands.
  * Server: audit sinks (`sinks.audit`, module extensions), module-listed event sinks and scheduler plugins; `sinks.usage` refused. `GET {basePath}/downloads/bcd/{version}/linux-{arch}`; `supportBundles` presigned `logs.collect` uploads; `projects.archive`, `commands.cancel`, `usage.export`; selfCheck §14 operations findings; fewer statements per join; the node pass wakes started reservations on connectivity/schedulability changes; `site` NodeSpec sections merge per field (`NodeSpecPart`); `vps-host` nodes may be declared lost.
  * Core/proto (additive): PoolPolicy runtime/singleTenant/workloadIdentity, InstanceSpec allocationId/nics, CapacityPort.reserve admission, ProviderImpl.identify, Reconciler.role, PlacementRequest.nodeLabels, SiteSpec.cacheImages; `driver.invoke` payloads, WorkloadToken frames (sealed JWE, purpose `workload-token`), StatusReport.pressure; catalog rows providerAccounts.bootstrap, workflows.pause/resume, metal.unrescue.
  * MCP: `iamConfirmations` (per-request polling), `remoteMcpIam` (the gate over HTTP IAM), reservation and instance tools. Client: `$watch`.
  * Later:
    * Sandboxes are a product feature (D24, user decision); the design is PLAN §3.7.
    * vps `instances.create` waits (`wait: { timeoutMs }`) and applies the pool's budget policy (`SPEND_LIMIT_REACHED`).
    * A node dedicated to one tenant wakes pending work (`mayBeSchedulableSql`).
    * Ramdisk certificates allow the commissioning kinds under their real names (`inventory.refresh`, `disk.wipe`, `logs.collect`); before this, commissioning could not inspect or wipe.
    * Proto `Hello.trust_version` / `StatusReport.trust_version` for the command-root switch gate.
    * `iamGuard` / `BETTER_COMPUTE_IAM_GUARD=warn|throw` reports or refuses an IamPort call made inside a compute transaction (§8.1); `computeFixture` defaults to `throw`.
    * Sandboxes seams: paused instances, `restore_from`, `instance.snapshot` messages, runtime `pause`/`resume`/`snapshot`.
    * Workload tokens are refused for suspended or deleted tenants.
    * MCP `create_instance` waits for the instance to start; MCP sandbox tools; FleetConfig `sandboxTemplates`.
    * Sandbox seams: `InstanceSpec.network_policy` (egress/private/none).
    * CI:
      * nightly workflow: simulation plus 10k cold start, the IAM-guarded suite, the whole suite on Postgres,
        and a green-night streak;
      * the bcd release pipeline: SBOMs, keyless cosign, SLSA3 provenance;
      * the API surface freeze check in `lint:repo`.
    * The §16 gateway gate is met on a workstation: 5k mTLS sessions, 0 drops, dispatch p99 26 ms.
    * v1.0 destructive-batch quorum, server side: the `quorum` module (`commandBatches` get/list/scope/approve/cancel,
      batch expiry) and `pki.approvers` in the trust bundle.
    * Quorum security review #6:
      * Leaves bind a payload digest, and BMC subjects are site-qualified.
      * Approvals sign the trust domain.
      * Tiers must be ordered.
      * The trust bundle version follows its content; `pki.trustVersion` is now a floor.
      * `pki.approvers.setSignatures` are published for the version they sign, and an unsigned quorum change is
        logged.
    * Envelope: `MethodAdmission.abandoned` cleans up after any failure following transaction 1 (not the IAM
      refusal, which runs `denied`).
    * Quorum API:
      * `commandBatches.create` (`batches:create`, idempotent), and `commands.run({ batchId })`; CommandInfo gains
        `batchId` and `awaitingApproval`.
      * Leaves may carry their payload, which approvers see in `scope`; its type must be the kind's, at most
        8 MiB per batch.
      * Batch subjects must be the tenant's own nodes.
      * MCP `propose_command_batch` (there is no approve tool).
    * The approver set has its own epoch (`pki.approvers.epoch`, TrustBundle field 8). A stale replica refuses to
      start rather than republish an old trust bundle.
    * `SiteSpec.proxy_targets` bounds `site.httpProxy` to the listed machines.
    * Rollout sends the signed release manifest with `NodeSpec.daemon` and `daemon.upgrade`, for bcd's self-update.
    * `lint:repo` checks the generated docs reference before the API freeze.
  * Tests: the §16 v0.3 MCP confirmation acceptance and §12.6 example policies end to end, §5.6 walkthrough 2 (acme-cloud), the authz matrix known-key replay step; `pnpm check` runs the remote-IAM suites, `proto-gen --check` and `buf breaking`. Tools: `scripts/private-commit.mjs` (compare-and-swap commits), `scripts/head-check.mjs` (tests the committed tree in a worktree).

* Round 4, capacity and sandboxes:
  * Capacity:
    * The pool-lease placer reads a lazy `PoolIndex`: per-class counts from `capacity_device_counts` (trigger-kept), booked devices only, and full devices and links per node on first view. It loads the index before taking the pool lock. At 10k nodes on PGlite, `placeQueued` goes from \~500 to \~1,000 placements/s and lock hold p90 from 875 to 60 ms.
    * Also: single-tenant offers, `dedicated=<tenant>` taints, node label constraints (`nodeLabels`, `'*'` any value), the MIG layout gate, attested `AllocationBound`, admit hooks for node leases, the budget admission check, reservation `operation`/`poll`, active conditions, `onPlacement` metrics, the I7 solver, fast-check planning properties, `capacityTenantDeleted`, and JSON Schemas for every method.
  * Sandboxes (§3.7, D24), a `sandboxes` + `sandboxTemplates` group on `vps()`:
    * Each sandbox drives one hosted instance: admission, placement, quota, IPAM, sealed user data, metering and deletion are the instance's. Instance item methods refuse a sandbox's instance.
    * Templates are checked against a hosted pool once. They carry TTL bounds, `warm: { min, max }`, `env` (tenant `secret://` refs written to `/etc/sandbox/env` inside broker-sealed user data) and `network` (`egress`/`private`/`none` → `InstanceSpec.networkPolicy`).
    * TTL expiry is an ordinary instance delete (`sandbox.expired`, then `deleted`). `extend` is decided with the new lifetime.
    * Warm pools: `paused` when the runtime can pause, otherwise `stopped`. A warm instance is claimed with `FOR UPDATE SKIP LOCKED` only after the create's own allow, and never from an older template shape. Warm capacity is metered as `vps.sandbox.warm` until claimed.
    * Snapshots: `sandboxes.snapshot` sends `instance.snapshot` through `vps({ snapshots: SnapshotStore })` into a tenant-private `sandbox_snapshots` table. `create({ fromSnapshot })` restores on the same runtime and `cpu.model`, and FakeHypervisor carries guest memory across.
    * Exec without an in-guest agent: tunnels' `sandboxes.exec` returns an `ssh` ticket (`act`, `inst`, `sbx`) plus a ≤15 min certificate from the `sandboxSsh` CA for `<account>@<instanceId>`, which vps renders into the guest's trust.
    * Also: `readyAt` stamped at first run, selfCheck findings, and `vps().tenantDeleted`, which deletes every instance of a deleted tenant (hosted instances' inline allocations were never released before).
    * Later the same day:
      * Guest host keys certified by the sandbox CA (exec pins guests). A sandbox never outlives its certificate: claims take only covering warm instances, warm pools recycle short ones, a fresh sandbox's expiry is capped, and restores or extends past it are refused. `sandboxes.exec` answers `hostCertificate: valid|expired|none`.
      * Secrets: templates use only secrets the tenant put, never fleet-scoped ones, and writing one decides `compute:secrets:manage`.
      * `private`/`none` networks place only on hosts whose bcd enforces them. Warm instances on a host that stops doing so are replaced, and running ones are a doctor error (`vps.sandboxes.network`).
      * §12.8 IAM reactions: `withdrawAgentSandboxes`, `reassignSandboxes` (`sandboxIamReactions` for the host) and `sandboxHolders` for the IamReconcile sweep.
      * Budgets: `extend` runs the budget check. Warm pools stop refilling on a spent budget, and a `drain` offer empties them.
      * `sandbox.expiring` (vps/0013), and `sandboxes.snapshot` is `UNSUPPORTED` up front without `RuntimeTraits.snapshot`.
  * `daemon/README.md` rewritten for the v0.1 daemon.

* Metal attestation and dedicated-root leases:
  * Security fix: a wipe or diagnostics result counts as attested only when the node signed it. The gateway stores `CommandUpdate.node_sig` (`commands.result_sig*`, `core/0012_v02_evidence`), and metal's default `nodeSignedResults()` verifies it with pki against `node_certs`. Before this, the kind's `attested` flag was trusted, so a gateway could forge a wipe. A lessee's `node-tenant` certificate never attests.
  * v0.5 `binding: 'node'` (metal side): a dedicated-root lease deploys the lessee's profile under `tenant-controlled` trust with a revoked platform identity, and marks the allocation bound. Its release runs the §13 teardown (fence, attested secure erase, DCGM, firmware baseline check with quarantine on drift, new identity generation).
  * `core/0012` also adds `node_bindings`, which `nodes.delete` and retirement clear. Provision exports `provisionedTargetResolver()` and `discoveryWorkflowNodeResolver()` for pki's attestors.
  * Two-phase BMC password rotation (`metal.rotateBmcCredential`, `bmc_credential_versions`): the password is generated and sealed, sent as refs only, and becomes current only on a site-signed login check. The old password stays current on failure. The tenant-root teardown rotates, and a failed rotation quarantines the machine.
  * The ramdisk's discovery token is single-use and MAC-bound. It reaches the site agent only as `ProxyDhcpEntry.discovery_token_ref`, and `boot.cleanUp` deletes it.
  * Firmware drift can be reflashed from the pool's baseline images (`policy.firmwareRemediation: 'reflash'`) before quarantine.
  * `tests/metal-capacity-lease.test.ts` runs capacity and metal together for `binding: 'node'`. The dev fleet gains `joinSite()` for site agents, and FakeDaemon signs attested results.

* v0.2 server groups and schema:
  * `joinRequests` list/approve/reject: the approval queue behind deferred joins (`discovery`, placeholder DMI). Decisions are idempotent, and an approval admits one join (the retry with the recorded CSR consumes it in the join transaction).
  * `siteTokens` create/revoke (role `site`), and the SiteBoot lease `site:<id>:boot`: one connected site agent holds it, and the standby is promoted only after it expires, under a new epoch.
  * `secrets` put/delete: write-only and sealed in the SecretStore, with a version per put. Events and audit rows carry only ref, name and version.
  * Also: `drivers.list`; `sites.update`/`sites.delete` (tombstoned once referenced); `nodes.delete` (retired or never-adopted nodes only; capacity rows released in lock order, certs revoked, hardware key freed, tombstone).
  * `core/0010_v02_server`: `join_requests`, `node_certs.allowed_kinds`, `nodes.deleted_at`, `secret_refs`.
  * `core/0011_v02_security`: `sites.deleted_at`, `attestation_uses` + `pruneAttestationUses`.
  * PGlite now runs `ANALYZE` after migrations and periodically, so dev and sim plans match Postgres.

* v0.2/v0.3 metal walkthroughs, chaos and runtime schema:
  * Talos (§5.6 #3): agentless `agent: 'none'` pools run out-of-band profiles. `httpViaSite` reaches a machine's API through its site agent (`site.httpProxy`), and it refuses before sending any command: other hosts, names, plain http off the provisioning network, redirects, oversize responses, and secret headers passed as values.
  * Ubuntu autoinstall (§5.6 #1): a third-party plugin renders a per-node ISO with a single-use `provisioned` token, and `media.insert` takes `fromStep`. The §16 v0.2 acceptance runs in sim form: 20 unattended enroll → ready → deployed → released → ready loops, and every `ready` follows a verified wipe.
  * `tests/metal-chaos.test.ts`: a worker or site agent dies mid-deploy, and no destructive command is sent twice.
  * `core/0009_runtime`: `outbox_cursors`, `work_queue.attempts`, `commands.owner`, tenant indexes, and retention via `pruneBehindCursors`, which deletes only rows every cursor has passed.

* v0.1 W5a server runtime: `betterCompute()` now runs the control plane. Table leases with epoch fencing (§8.5), the coalescing work queue, 256 sharded reconcilers with rendezvous assignment plus `Reconciler.periodic` keys and deterministic `compute.jobs.*.tick()`, module `start`/`stop`, the command queue (protobuf `CommandBody`, per-node `seq`, idempotency keys, cancel), `CommandSweeper`, the xmin-ordered outbox with sinks and `compute.events.subscribe`, the authz projection writer, and a DB-anchored clock with `ClockSkew`. The envelope runs request-as-resource admission (drafts, idempotent replay, delegated `CONFIRMATION_REQUIRED`), 30 s authentication and 10 s read-decision caches (`compute.caches`), and the list recipe. v0.1 groups: projects, sites, joinTokens, nodes, commands, usage, events. JoinService admits nodes through `@better-compute/pki` (sealed tokens, `hardware_key` from inventory, signed `JoinResponse`, Renew with proof of possession) behind `gatewayServices`; the NodeLifecycle, NodeSpec, Liveness and drain controllers; `capacityModule` is built in and backs `ctx.capacity`; `gatewayReportHandlers` for `createGateway({ reports })`. `tests/server-spine-v01.test.ts` runs join → adopt → reserve → bind → liveness end to end with zero IAM calls on fleet paths; the generated authz matrix covers all 42 v0.1 rows.

* v0.1 W3a capacity: `@better-compute/capacity`. Selectors (`matchExpressions` + CLI shorthand), schedulability with its SQL twin, the placement pipeline (filters, NVLink/NUMA-aware atom pick, `binpack`/`topology`/`fragmentation`/`spread` scorers, top-N pick) and the savepoint-guarded `inlinePlacer` (≤3 replans, then `pending`); all-or-nothing quotas; release/extend decided by the `EXCLUDE` constraint; rollups (deltas + rebuild); `capacity/0001_device_links` + `syncDeviceLinks`. `capacityModule` serves `pools`, `capacity` (summary, byPool, preview, timeline), `reservations` (admission drafts, placement in tx 2, lifecycle reconciler with bind-timeout retries, leases, `maxPending`, delegated confirmation) and `quotas`; `markAllocationBound/Failed/Released` for the gateway; `capacity.usage` writes hourly `usage_windows` (`gpu.<model>.hours`, reserved time from active, D14). The torture exit test (1,000 ops, every §10.4 invariant) fixed a quota/rollup lock-order deadlock.

* v0.3 VPS: `vps()` serves `instances` (admission drafts; hosted pools placed through capacity with IPAM leases and converged from `reportInstances`; provider pools driven through `ProviderImpl` outside transactions; lost-host recreate), `flavors` and `providerAccounts` (manifest-validated, SecretRef-only credentials); `NodeSpec.instances`; `vps.usage` meters vCPU/memory/GPU hours; `providerConformanceCases()`; a 200-instance churn test proves no leaks.

* v0.3 `@better-compute/provider-hetzner` (outside the umbrella): `hetznerCloud` (label-idempotent create, paginated offers with prices and availability, power/rebuild/list, rate-limit mapping) and `hetznerRobotProvider` (claims unnamed servers instead of ordering, resets, rescue, installimage bootstrap delivering the join token over SSH stdin only); both pass provider conformance against API emulators.

* v0.2 Health and remediation: `device_health` quarantines (`core/0007_device_health`) take one GPU out of placement without blocking its node. XID policies (48 quarantine, 63 drain+reset, 64 RMA, 79/95 drain+reboot) and inventory health findings (row remap, uncorrectable ECC, degraded NVLink) feed `recordXid`/`applyInventory`. The provision `health` reconciler runs `remediate-reboot`/`remediate-gpu-reset` after the node drains, within the pool's disruption budget (`policy.disruptionBudget`, default 5%), and stops for a pool with >10% unhealthy nodes in 10 min (circuit breaker); a failed, cancelled or exhausted remediation escalates to maintenance. v0.5 fractional GPUs (inventory side): `gpu.slot` atoms `<uuid>/s<k>` for timeslice/MPS pools, `partitionPending` freezes a GPU during a MIG mode change, and `gpuAtomFamily` feeds the atom rule. `userdata.render` renders cloud-init with a single-use `provisioned` token into a secret, which metal's deploy sends as `userDataRef`. Metal command payloads are now the proto3 JSON of each kind's message.

* v0.2 metal: `metal()` serves the `metal` group. `enroll` keeps inline BMC credentials only as a secret reference, and driver bindings are inherited from the pool's hardware profile. The `metal` reconciler runs each provision state's workflow (`metal-enroll`, `metal-inspect`, `metal-commission`, the tenant's deploy profile, `metal-release`, `metal-retire`, `metal-rescue`) and applies `complete`/`fail`. A release returns to `ready` only after an attested wipe. Control-plane steps relay BMC and boot work to the site agent and disk, diagnostics and image work to the ramdisk; `boot.kexec` completes on rejoin. There are site and ramdisk command kinds, and `metal/0001_nodes`.

* v0.2 provision additions: the image catalog and `images` group (`provision/0002_images`: digest plus Ed25519/P-256 signatures against signer allowlists, unsigned images private, platform-signed ramdisks, promotion with a wildcard authz row, site caches with LRU eviction). Also site-step routing to the boot-lease holder, workflow `owner` wake-ups and profile snapshots (`provision/0003_workflow_owner`), `awaitCommand` for relaying steps, `defineStep({ relays })` and `provisionModule({ modules })`. Migrations `core/0005_audit_commit_order` and `core/0006_pool_archive`.

* v0.2 provision: the workflow engine. `provisionModule({ steps, profiles })` owns `provision/0001_workflows` (`profiles`, `workflows`, `workflow_steps`, with one active workflow and one in-flight step per node), the `workflow` reconciler (sharded by node) and `workflowEngine(ctx)` for `ctx.workflows`. Profiles expand into persisted steps: registered steps, or driver operations sent as one `driver.invoke` command, plus `fallback:`, acceptance, on-failure and cleanup steps. Control-plane steps run between transactions (`stepNotReady` polls), agent steps are exactly one command (`wf:<id>:<step>`, `#<attempt>` on retry), and `await: 'rejoin'` completes on a new boot or role and fails only on timeout. Also `timeout`, `retries`, `onFailure`, `when:`, abort (cleanup still runs), pause/resume and retry-from-step, the built-in `wait.agentJoined` / `wait.generationObserved`, and the `profiles` and `workflows` API groups.

* v0.1 W3b inventory and store: section verification over received bytes, snapshot digests, diffs, devices and MIG atoms, `gpuLinks`, GFD labels, `GpuCountMismatch` / `GpuModelUnknown` / `DuplicateIdentity`, `hardware_key` (with `hardwareKeyFromClaims` for join), `recordInventoryHashes` / `ingestInventorySection` for the gateway, `applyInventory` + `inventoryReconciler`, and `inventory.get`. Migrations `core/0003_v01` (v0.1 columns, `node_inventory`, `node_inventory_sections`) and `core/0004_command_cancel`. The outbox conformance reader orders by `outbox.txid`, not its text alias.

* v0.2 PKI (ready for the server to wire): `localSealedSecretStore` over `sealed_secrets` (`pki/0001_sealed_secrets`, AES-256-GCM with ref+tenant AAD, key rotation via `reseal`), the `SecretRequest`/`SecretResponse` broker crypto with node signatures checked against `node_certs`, a command-signing root with 24 h worker keys and `signCommand`/`verifyCommand`/`signDesiredState`/`verifyDesiredState`, trust-root rotation, and `verifyAttestedUpdate`/`verifyAttestedReport`. Also: PKI bundles sealed under the deployment secret, `agentIdentityFromChain`, the agent-side join/renew helpers, `doctor` PKI findings, the capacity CLI shortcuts and `iam setup`.

* v0.1 W5c CLI: `pki init` (root + node/serving/internal CAs + JoinService keys, sealed under `BETTER_COMPUTE_SECRET`, optional `--offline-root`), `pki show` (bcd pins from the public half), `pki issue-gateway` (30-day serving certs, atomic replace), and `daemon install-script` (POSIX sh: token from `BCD_JOIN_TOKEN`/stdin only, pinned sha256 download, 0600 `config.toml` in bcd's TOML subset, hardened `bcd.service`, in-place upgrade without a token). The CLI framework routes two-word commands and `help <group>`.

* v0.1 W2 PKI: `@better-compute/pki`. Local root, node, serving and internal CAs with per-kind issuance policy (SPIFFE URI SAN + role extension, ≤ 24 h node certs, ≤ 2 h ramdisk), a strict DER/X.509/PKCS#10 layer on `node:crypto`, chain verification, join tokens (`issueJoinToken`, `verifyJoinToken`, `joinTokenAttestor` on `join_tokens` at `compute_now()`), sealed tokens bound to the join CSR (JWE `ECDH-ES+A256KW`/`A256GCM`), signed `JoinResponse` and `SignedTrustBundle`, `verifyRenewPop` with a malleability-proof replay cache, and a `RevocationSet`. `hardware_key` stays in `inventory`, the single source. Tests include a mutual-TLS handshake and the RFC 7518/3394 vectors.

* P0 scaffold: a pnpm workspace mirroring better-iam's, with the packages, contracts, repository rules and CI that v0.1 builds on. Plan `PLAN.md`; guide `docs/development.md`.
  * Packages `@better-compute/core`, `protocol`, `inventory`, `provision`, `metal`, `vps`, `postgres`, `server`, `client`, `cli` and `testing`, and the umbrella `better-compute` with subpaths `./core`, `./protocol`, `./inventory`, `./provision`, `./metal`, `./vps`, `./server`, `./postgres`, `./pglite`, `./client`, `./cli`, `./testing` and bin `better-compute`. `inventory`, `provision`, `metal` and `vps` are stubs: they export the frozen contract types, and their factories throw `NOT_IMPLEMENTED` naming the phase that implements them.
  * better-iam 0.1.0 is vendored as 23 tarballs in `vendor/better-iam` and pinned through `pnpm-workspace.yaml` `overrides` (`scripts/sync-better-iam.mjs`, `pnpm sync:iam`).
  * Repository rules under `pnpm lint:repo` (part of `pnpm check`): `scripts/check-deps.mjs` (server never imports injected modules, `gateway`, `iam`, `postgres`, providers or drivers; core's root entry imports no Node.js built-in; workspace dependencies match tsconfig references; nothing imports a `provider-*`), `scripts/check-sql-now.mjs` (SQL reads time only through `compute_now()`) and `scripts/umbrella.mjs --check` (the umbrella is generated from `packages/better-compute/umbrella.json`; `pnpm umbrella` rewrites it).
  * Packaging, ported from better-iam: `validate:packages` (Apache-2.0, `publishConfig.access`, `repository.directory`, `{ types, import }` exports whose files exist, publint, one version), `pack:all`, `check:packed-types` (`attw --profile esm-only`), `test:packed` (a fresh consumer installs every tarball with the vendored better-iam, imports `better-compute`, `/inventory`, `/provision`, `/metal`, `/vps`, `/pglite` and `/client`, and runs `better-compute --help`) and `scripts/release-version.mjs`.
  * CI: `.github/workflows/ci.yml` (Ubuntu and Windows × Node 22 and 24: `pnpm check`, `pnpm proto:check`, `buf breaking`, pack, attw, packed smoke; a `postgres:17` job for `pnpm test:postgres`; npm publishing that waits until better-iam is on npm) and `.github/workflows/daemon.yml` (`go vet`, `go test -tags nonvml,fakehw ./...`, `goreleaser check`).
  * `bench/REFERENCE.md` defines the reference hardware for the v0.1 gateway, IAM-lane and placement benchmarks.
