# Piceli

> Piceli is Kubernetes infrastructure as typed Python. Describe an application with a typed `App` (or templates, `kubernetes` client models or YAML/JSON), review a plan computed against the live cluster, and apply only that plan with an explicit approval of its hash; every run is journaled, resumable and can be rolled back. `piceli deploy MODULE:ATTR` takes a `Pipeline` (app, target, optional builds and image delivery, checks) from source to a running, checked release; `piceli release` does the same from a `release.toml` spec. `piceli status` and `piceli access` say whether the app is up and forward its ports to 127.0.0.1. Pre-alpha: every page states its maturity (stable, preview, experimental).

Rules for agents:

- `piceli ui serve --definition release.toml` (experimental web UI) registers an existing explicit-target definition; inventory-only use requires `--kubeconfig`, `--context` and `--namespace`. Requires `piceli[ui]`; long-running loopback service needs the owner's go-ahead. Legacy `operator serve` remains deprecated. Presence is not health or a deployment diff; catalog selection is not rollback. See docs/ui.md for implementation scope.
- `piceli access ui --cluster MODULE:ATTR` (or `--profile NAME`) opens the UI that `piceli cluster init` installs for `Cluster(ui=Ui(access="forward"))`: reads its launch token with the profile's explicit kubeconfig, forwards 127.0.0.1:8790 to the ClusterIP Service in `piceli-system` and prints the one launch URL (show it only to the owner; long-running, needs the owner's go-ahead). Views: environments (revision per source, health, state, last sync), components (commit, digest, state), sources (refs, last poll) and a Sync button that writes the `piceli gitops sync` request. Refusals `access-ui-*`, `ui-sync-*`, `ui-controller-absent`. See docs/ui.md.
- `piceli ui connect` (experimental, unsupported, disabled unless `--experimental` or `PICELI_UI_EXPERIMENTAL=1`, else `ui-experimental-disabled`; the same gate applies to `ui cluster-serve` deploy grants and `--authorized-access-sub`) uses a one-time scoped ticket from an authenticated cluster UI, prompts for its secret, and supervises one local port forward using an explicit kubeconfig and context. `--authorized-access-sub` is a separate opt-in cluster-service grant; no remote service port is a laptop port. See docs/ui.md.

- Credential profiles keep the kubeconfig path out of Git: `piceli login NAME --kubeconfig F [--context C]` stores a reference (path and context, never contents or tokens) in `$PICELI_PROFILES_DIR` (default `~/.config/piceli/profiles`, mode 0600); `Target.profile("NAME", namespace=…)` or `credentials = "NAME"` in `[target]` resolves it at use time (the plan hash covers the name; inside a cluster the service account), `--profile NAME` replaces `--kubeconfig/--context`; `piceli profiles [--json]` lists, `piceli logout NAME` forgets; codes `profile-not-found`, `profile-invalid`, `profile-conflict`. Use only profiles the owner stored. See docs/deploy.md.
- Run `piceli help-json` for every command's options, side effects, approval rules and retry safety; do not guess flags.
- Plan before apply and never approve on the owner's behalf: show the owner the plan (`piceli deploy … --plan`, `piceli release plan`) and run `--approve <hash>` only after they approved that exact hash. Before the images exist, `deploy --plan` shows a release preview with placeholder images (`stages.plan.preview`, `approvable: false`): approve the `combined_hash`, never the `preview_hash`. Never add `--auto-approve` unless the owner said this is an unattended CI run for this exact target.
- An owner may declare an approval policy in reviewed code (`Pipeline(..., auto_approve=ApprovalPolicy(...))`, `[release] auto_approve = {...}`); then `piceli deploy … --approve-if-policy` and `piceli release apply … --approve-if-policy` run a plan only when every action is inside it, otherwise exit 3 with `approval-policy-exceeded` and the hash for the owner. `delete`, `replace` and `adopt` always need the hash. Never add or widen a policy yourself; no flag can pass one.
- `piceli release orphans --spec …` lists objects with the app's labels that no current release owns (read-only). `--prune` deletes them only with `--approve <plan_hash>` after the owner reviewed that exact list; never add `--include-claims` (deletes data), `--include-secrets`, `--include-other-owners` or `--include-cluster-scoped` unless the owner asked; a changed set is refused (`orphans-plan-changed`).
- Parse JSON from stdout; human text is on stderr. Exit codes: 0 success, 1 ran but did not succeed, 2 rejected before any change, 3 approval required.
- On a refusal read `reason` and run `piceli explain <reason> --json` (cause, fix, retry_safe). A model module that raises is a refusal too (`render-target-invalid`, `pipeline-load-failed`, `access-target-invalid`, `invalid-composition`; `message` names the exception, `PICELI_DEBUG=1` adds the traceback on stderr). A `deploy --plan` refused with `blocking` suggests `Pipeline(adopt=[…])`/`replace=[…]` (deploy has no `--adopt`): report it, the owner decides.
- A failed apply with `apply-crashloop` / `pipeline-apply-crashloop` carries `diagnosis` (per workload: container, reason, exit code, restarts, redacted log tail, events): report those causes; do not retry unchanged. `piceli release status --spec … --run <id>` shows them again later.
- Never read or print secret values; never rely on `~/.kube/config`, `KUBECONFIG` or the current context, and never pick or change a kube context, `allow_exec` or exec-plugin pins yourself: ask the owner. Never read or configure the credentials of external secret sources (Vault tokens, `~/.aws`, age/PGP keys, `~/.config/sops`).
- To learn what a deploy did or why it failed, read `<state_dir>/runs/<id>/summary.json` (named in the result as `summary.json`; `piceli runs MODULE:ATTR --json` lists them). `piceli cache status`, `cache prune --dry-run`, `doctor` and `runs` are read-only and never contact a cluster; run `cache prune` itself only when the owner asked or set `cache_budget=`.
- After an interruption, resume (`piceli deploy … --resume`, `piceli release resume`) instead of planning a new run; an interrupted re-apply or `release rollback` is not resumable: run the same command again. With shared state (`state="cluster"`) any runner can resume; `pipeline-locked`/`release-locked` means another deployer holds the release lock: wait, never delete `piceli-lock-*` Leases or `piceli-state-*` Secrets.
- To deploy committed code from a dirty or shared tree, plan with `--ref SOURCE=REV` and approve with the exact command `--plan` prints (it pins the resolved SHA); `--resume` reuses the run's commits and takes no `--ref`.

Minimal workflow: `piceli render app.py:pipeline` (no cluster; the target's namespace and nodes) → `piceli deploy app.py:pipeline --plan --json` → owner approves → `piceli deploy app.py:pipeline --approve <combined_hash> --json` → `piceli status app.py:pipeline --json`.

## Start here

- [Using Piceli from an agent](https://docs.pynenc.org/projects/piceli/en/stable/agents.html): commands safe to run without asking, commands that need approval, the output contract, errors, resume, secrets and cluster access
- [Command reference](https://docs.pynenc.org/projects/piceli/en/stable/reference/cli.html): every command with options and its output contract, which every command follows (generated from `piceli help-json`)
- [Error codes](https://docs.pynenc.org/projects/piceli/en/stable/reference/errors.html): every refusal code with cause, fix and whether a retry can succeed (same as `piceli explain`)
- [Agent skill](https://github.com/pynenc/piceli/tree/main/skills/piceli): `SKILL.md` plus scripts to install, describe an App and Pipeline, render, plan and ask the owner, deploy with the approved hash (or inside the owner's policy), check status, diagnose with `piceli explain`, resume and roll back; its walkthrough runs in CI against the built wheel
- [Getting started](https://docs.pynenc.org/projects/piceli/en/stable/getting_started/index.html): a typed app and `Pipeline` in one file, rendered, planned, approved, checked and reached on a disposable `kind` cluster

## Docs

- [When to use Piceli, and when not to](https://docs.pynenc.org/projects/piceli/en/latest/when_to_use.html): use it for typed Python apps with hash-approved plans, explicit cluster selection, adoption, resume and rollback; current limits (pre-alpha APIs, no package format, Kubernetes only, Python only) and the built-in GitOps controller, environments and web UI; every point links a test
- [The same app in Piceli, Helm, Kustomize, cdk8s and Pulumi](https://docs.pynenc.org/projects/piceli/en/latest/comparisons.html): `examples/comparisons/<tool>` (pinned versions) render identical objects for dev/staging/prod, checked in CI; files and lines, steps to deploy/change/roll back, safety features (plan-hash approval, server-side apply ownership, drift, adoption, secrets, rollback, typed validation) and where the others are better
- [Overview and architecture](https://docs.pynenc.org/projects/piceli/en/stable/overview.html): model, plan, execute, observe; the engine; design principles; glossary
- [Describe an app in typed Python](https://docs.pynenc.org/projects/piceli/en/stable/typed_apps.html): `piceli.App` deployments, stateful sets (`ClaimTemplate`, headless Service; claims are never pruned), daemon sets, jobs and cron jobs, services, config, secrets, volumes, pod defaults (`PodDefaults`, `Security`) on every pod kind, service accounts with typed RBAC (`Rule`; cluster-scoped objects are per namespace and flagged `cluster_scoped` in plans), label-selected network policies with typed egress, namespace and CIDR peers and a DNS helper (`NetworkRule`, `NetworkPeer`, `allow_dns=True`), autoscalers (the HPA owns `replicas`), disruption budgets, Ingress and Gateway API HTTPRoute (`Route`; needs the Gateway API CRDs), dependencies; a changed Job or StatefulSet immutable field refuses the plan with `immutable-field-changed` until `--replace Kind/name`; `piceli render`
- [Custom resources and any other kind](https://docs.pynenc.org/projects/piceli/en/latest/crds.html): `piceli codegen crd FILE | --from-cluster --kubeconfig F --context C --crd NAME` generates deterministic pydantic models from a CRD's schema; `app.resource(api_version, kind, name, spec)` declares any kind with a validated spec; cluster-scoped resources are managed per namespace (`piceli.io/namespace`); `public=` for sensitive-looking fields that hold no secret; `resource-scope-mismatch`
- [Environments](https://docs.pynenc.org/projects/piceli/en/latest/environments.html): `app.environment(name, replicas=…, autoscalers={name: Scaling(min_replicas=…, max_replicas=…)}, images=…, resources=…, config=…, hosts=…, node_selector=…, specs=…, enabled=…)` typed overrides of one App; `piceli render MODULE:ATTR --env NAME [--diff-env OTHER]` (typed diff, text or JSON); `Pipeline(app, {"dev": Target…, "prod": Target…})` and `piceli deploy|release … --env NAME` (own state per environment; the combined hash covers the environment's name and values); per-branch environments: `Pipeline(envs=EnvConfig(prefix="shop-", branches=["main", "wp-*"], max_envs=3, quota=…, claim_sizes=…, auto_approve=…))`, `piceli env up BRANCH --pipeline MODULE:ATTR --digest NAME=REF` (namespace `<prefix><slug>`, isolated at render time: `env-isolation-*` refusals, default-deny NetworkPolicy, ResourceQuota; plan then `--approve <env_hash>`; budget stops the least recently pushed, `--wait` refuses with `env-budget-full`), `piceli env down|seed BRANCH` (approval; never main's), `piceli envs [--json]` (read-only), `piceli logs BRANCH WORKLOAD`, `piceli access BRANCH --pipeline …` (free local ports); named environments: `EnvConfig(environments=[Environment("main", namespace="shop-main", follow=Branch("main")), Environment("rc", namespace="shop-rc", follow=[Tag("v*-rc*"), Promote()], stack=Stack("full", workloads=[…]), on_nodes=[…], quota=…, auto_approve=False)], branch_stack=Stack(…), branch_nodes=[…], idle_stop="24h")` (`from piceli.envs import Environment`; each environment has its own trigger, one branch may feed two; `piceli env up NAME` deploys one, never deleted, stopped or seeded by env commands; a sized `ExistingClaim` becomes the branch environment's own claim; codes `env-stack-unknown`, `env-stack-incomplete`, `env-nodes-conflict`)
- [A realistic app: dev, staging and prod](https://docs.pynenc.org/projects/piceli/en/latest/reference_app.html): `examples/reference/app.py`, one typed module with no dicts or overrides: web (Deployment, HPA, PDB), api (ServiceAccount + RBAC), Redis StatefulSet (`ClaimTemplate`), Job and CronJob, HTTPRoute (Ingress in dev), cert-manager `Certificate` from `piceli codegen crd`, release-wide NetworkPolicy, restricted pods, a SOPS-encrypted password (example-only age key) and checks; `render --env staging --diff-env prod`; deployed to kind in CI
- [Deploy an app from source](https://docs.pynenc.org/projects/piceli/en/stable/deploy.html): `piceli deploy MODULE:ATTR`, one journaled run of inputs, build, deliver, plan, apply and checks; `--plan` (with a placeholder-image release preview and blocking objects before any build), `--approve`, `--resume`, `--until`, `--json` events (`piceli.deploy-event.v1`); `piceli render MODULE:pipeline`; `--ref [SOURCE=]REV` builds a commit instead of the working tree (the hash pins the SHA); mirror third-party images by digest (`NodeLoopbackRegistry(mirror=[…])`) and take over a live node registry (`adopt=`/`replace=`, owner's choice, part of the approved plan); "If it fails" (a workload that cannot start fails the apply at once with per-workload causes); an owner-declared `auto_approve=ApprovalPolicy(...)` and `--approve-if-policy` for routine plans
- [Publish a Helm chart and manifests for other people's clusters](https://docs.pynenc.org/projects/piceli/en/stable/helm_charts.html): `piceli chart render|package|manifests|publish MODULE:ATTR` renders the same App as a Helm chart (values for images repository/tag/digest, `imagePullSecrets`, replicas, resources, node selectors, storage, hosts, ConfigMap keys, Secret and existing-claim names; `values.schema.json`), a byte-identical `<name>-<version>.tgz`, an OCI chart in the `helm push` layout (exit 3 until `--approve <digest>`), or plain manifests with a values file (`--values`, validated, `chart-values-invalid`); Secrets are never rendered (referenced by name); an unbuilt pipeline image is a required value
- [GitOps: Piceli's controller and manifest export](https://docs.pynenc.org/projects/piceli/en/latest/gitops.html): `piceli publish MODULE:ATTR --to oci://registry/repo:tag` pushes the rendered manifests as an OCI artifact in the `flux push artifact` layout (deterministic digest; exit 3 until `--approve <digest>`; credentials only from `--credentials FILE`), `piceli render --out DIR` writes them for Git; a Secret is refused unless `--secrets external` (SOPS, ExternalSecret); `OCIRepository` + `Kustomization` and Argo CD `Application` examples; what Piceli's plan, approval and journal no longer cover once a controller applies; Piceli's own controller: `piceli gitops enable MODULE:ATTR --repo URL --branches 'main,wp-*' --image REPO@sha256:… --kubeconfig F --context C` (exit 3 until `--approve <plan_hash>`; owner only) installs a one-replica controller in `piceli-system` (scoped RBAC, Git credentials from a mounted Secret, never in the URL) that polls the repository and keeps one environment per branch: branches deploy inside the pipeline's `auto_approve` policy or wait with a plan hash for `piceli gitops approve ENV HASH` (owner only), main only on a new `v*` tag or `piceli promote BRANCH@SHA` and always with a hash approval unless `--main-auto-approve`; deleted branches are torn down; `piceli gitops status --json` is read-only (health, per-branch commit, state, pending hash); `piceli gitops disable` never removes an environment
- [Deploy from CI with an approval step](https://docs.pynenc.org/projects/piceli/en/stable/ci.html): GitHub Actions recipe: plan the pushed commit (`piceli deploy … --ref $GITHUB_SHA --plan --out deploy-plan.json --json`), approve through a protected environment, apply the plan file on any runner (`piceli deploy --apply deploy-plan.json --approve <combined_hash>`), resume anywhere; post the run's `summary.md` as the job summary (optionally a PR comment) and keep `summary.json` for agents; kubeconfig from a 0600 secret file, shared state, release lock, concurrency
- [Machine-wide lock for heavy work](https://docs.pynenc.org/projects/piceli/en/latest/heavy_work.html): `piceli heavy run [--name N] [--wait S] -- COMMAND...` serializes builds, test gates and kind clusters across agents and processes on one machine (OS flock in a per-user state dir, so a killed holder releases it; prints who holds it while waiting; forwards SIGINT/SIGTERM/SIGHUP; exit code preserved, 128+N for a signal; stdout is one receipt JSON `piceli.heavy-receipt.v1` with redacted command, cwd, git commit, times, duration, exit code, peak memory; newest 100 kept), `heavy-lock-timeout` (exit 2) after `--wait`, `piceli heavy status --json` (holder and recent receipts), and `PICELI_HEAVY_LOCK=1` makes host builds and Docker `Build.spec` runs take the same lock. Agents should wrap heavy work in it
- [Runner hygiene: disk, caches, doctor and run summaries](https://docs.pynenc.org/projects/piceli/en/latest/maintenance.html): every temporary directory (TLS, OCI, build staging, `--ref` worktrees) removed on exit, error and SIGTERM/SIGHUP; `piceli cache status [--json]` (per state directory and category) and `piceli cache prune [--keep-last N] [--budget 20GiB] [--dry-run]` (never the release state, secret store, approved plans or what a rollback needs); `Pipeline(cache_budget="20GiB")` enforced after each run; `piceli doctor [--json]` (free disk/memory vs the next build, docker/buildx/kubectl; exit 1 on a warning); run summaries `<state_dir>/runs/<id>/summary.{json,md}` (`piceli.run-summary.v1`: commits, digests, sizes, plan classes, changed fields, checks, failure code, timings; no secrets) and `piceli runs [--json]`; `piceli watch [--run ID] [--once] [--timeout S] --json` follows a run from its journal (read-only, offline; JSON lines `piceli.watch-event.v1`: `snapshot`, `stage`, `progress`, `run`, then `result` with state, failure code, `resumable` and summary paths; exit 0 ready/stopped, 1 failed/interrupted/timeout, 2 no run), and `piceli access --json` status events name the `pod` each forward is connected to (it follows the live owner across a rollout)
- [Restore points of retained data](https://docs.pynenc.org/projects/piceli/en/latest/restore_points.html): `Pipeline(restore_points=RestorePoints())` adds a `backup` stage before the release plan: for a release that changes the image or storage settings of a workload writing a retained claim, it plans each claim (one per StatefulSet replica, existing claims) and why, runs `app.quiesce(workload, Quiesce.exec(...)|Quiesce.http(...))` hooks, scales the writers to zero and waits until their pods are gone (terminating included), streams a verified gzip tarball of each claim to a private local directory (SHA-256 + content digest); the result and run summary carry `restore_point`; `piceli restore-points [--verify] --json` lists them (read-only, offline); `piceli restore --point ID` plans (exit 3, `restore_hash`) and `--approve <restore_hash>` puts one back (needs the owner's approval); `piceli restore --point ID|--all --to-new-claim` proves one without touching live data: plans (exit 3, `verify_hash`), then restores into Piceli-owned scratch claims (same storage class and size), checks the content digest, runs the `app.restore_verify(workload, RestoreVerify([...]))` command read-only with the workload's image, writes a receipt with PASS/FAIL per claim (exit 1 on FAIL) and deletes the scratch claims unless `--keep`; a larger `ClaimTemplate` size or `ExistingClaim(expand_to=...)` grows the claim in the backup stage when its StorageClass allows expansion, otherwise `migrate_from=` (a new claim name) moves the data: new claim filled from the restore point, content digest and `restore_verify` checked, switched by the apply, old claim kept and annotated `piceli.io/migrated-to` (never deleted); smaller sizes are refused (`claim-shrink-refused`); never prints claim contents
- [Share deployment state between runners](https://docs.pynenc.org/projects/piceli/en/latest/state.html): `Pipeline(state="cluster")` / `[release] state = "cluster"` keeps journal, catalog, secret store and receipts in the release namespace (Secrets) behind a release-scoped Lease lock with fencing and stale-owner takeover (`pipeline-locked` names the holder); portable plan files; `piceli state show|pull|export|import` (secrets excluded or encrypted; import needs `--approve <digest>`)
- [Releases from a spec](https://docs.pynenc.org/projects/piceli/en/stable/release_cli.html): `piceli release` plan, apply, adopt, replace, roll back, resume, stop, status (`--spec release.toml`, or `--spec module.py:pipeline` for a pipeline's release; `status --run ID` shows why a past execution failed); "If plan refuses"; an apply whose pods cannot start fails at once with `apply-crashloop` and a redacted `diagnosis`
- [Check a release and roll back automatically](https://docs.pynenc.org/projects/piceli/en/stable/checks.html): `[[checks]]` and `piceli.checks` (http, exec, metric, Python), `rollback_on_failed_checks`, `piceli release check`
- [How plans decide what changes](https://docs.pynenc.org/projects/piceli/en/stable/plans_and_diffs.html): no-op vs apply from server dry runs, field-level diffs, `piceli release diff`; recovering an interrupted apply or rollback; what a rollback restores and what it cannot (data, external side effects, what others own)
- [Compatibility and shared ownership](https://docs.pynenc.org/projects/piceli/en/latest/compatibility.html): supported Kubernetes versions (the last four minors, 1.34 to 1.37, tested on kind); autoscalers own `spec.replicas` (one rule for typed apps and plain manifests; `autoscaled` in the plan JSON: `initial`, `held`, `yielded`); readiness of kinds without a specific rule; operators, mutating webhooks, adoption and pruning with other field managers
- [Manage secrets](https://docs.pynenc.org/projects/piceli/en/stable/secrets.html): generate, template, import and rotate secret values; external sources read at every plan (SOPS files through `sops`, HashiCorp Vault KV v2, AWS Secrets Manager with `piceli[aws]`; a changed value becomes a new private version); private versions; `piceli release secret show`. Agents never read or configure source credentials (Vault tokens, AWS profiles, age/PGP keys)
- [Connect to managed clusters](https://docs.pynenc.org/projects/piceli/en/stable/managed_clusters.html): GKE, EKS, AKS and OIDC exec credential plugins with `[target] allow_exec`, pinning and refresh
- [Reach your services from your laptop](https://docs.pynenc.org/projects/piceli/en/stable/access.html): declare `app.access.forward`, then `piceli status` (is it up, which URLs; `piceli.status.v1`) and `piceli access` (supervised port forwards, port-conflict owners by pid; `piceli access stop --stale` stops Piceli's own leftover forwards and servers, with the owner's go-ahead)
- [From kubectl scripts to Piceli](https://docs.pynenc.org/projects/piceli/en/stable/migrate_from_kubectl.html): `piceli import live|yaml` generates a typed module from existing objects; adopt them with `--adopt-all-desired`; `App.override`
- [Test against a fake Kubernetes API](https://docs.pynenc.org/projects/piceli/en/stable/testing.html): `piceli.testing` for consumers' own tests, no cluster needed
- [Source identity](https://docs.pynenc.org/projects/piceli/en/stable/source_identity.html): `piceli inputs record/verify` and source drift
- [Deploy events to OpenTelemetry](https://docs.pynenc.org/projects/piceli/en/latest/deploy_events.html): `piceli deploy|release apply|rollback --otlp-endpoint URL` (or `OTEL_EXPORTER_OTLP_ENDPOINT`, `_HEADERS`, `_TIMEOUT`, `OTEL_SERVICE_NAME`) sends one trace per run (a span per stage) and a result log record (`event.name=piceli.deploy`, the dashboard marker) with app, namespace, run, release, plan hash, commit/ref, image digests, outcome, duration; off unless configured, never fails or blocks a deploy (warning codes `deploy-events-export-failed`, `deploy-events-config-invalid`), no secrets, no SDK needed
- [Pre-rollout checks](https://docs.pynenc.org/projects/piceli/en/stable/pre_rollout_checks.html): `app.pre_rollout(workload, ["cmd", …], upgrade=UpgradeCheck(["cmd", …]))` makes `piceli deploy` run a Job with the new image and the workload's real Secrets, mounts, env and security context before the workload changes (the release proceeds only on exit 0; pods stay unchanged on failure); `UpgradeCheck` opens the retained claims read-only (a `ReadWriteOnce` claim is pinned to the running pod's node); new `prerollout` stage in `--plan`/`watch --json` only for apps that declare one; the check Job is always removed and its log tail is scrubbed; codes `prerollout-mount-missing`, `-claim-exclusive`, `-failed`, `-timeout`, `-not-startable`, `-unavailable`
- [Builds without a VM](https://docs.pynenc.org/projects/piceli/en/stable/host_builds.html): `Build.spec("host-build.toml", builder="host")` (opt-in; Docker stays the default) compiles with the host toolchain (`cargo`, `cargo zigbuild`), one shared `CARGO_TARGET_DIR`, tools pinned by sha256, digest-pinned bases, one deterministic layer per file mapping, push by digest skipping present blobs, SPDX SBOM and SLSA provenance per image; target node facts (architecture, page size from the `piceli.io/page-size` label or the kernel release) read at plan time into the plan hash and `{page_size_log2}`/`{rust_arch}` placeholders; runs the tools as your user outside any container Node facts are cached per node from the last successful read (`<state_dir>/node-facts/`) and used, with their age, when the API is unreachable; `Build.spec(..., node_facts={...})` declares them; the shared target directory is per architecture and page size. `piceli build job MODULE:ATTR --commit SHA --image BUILDER@sha256:… --repo URL` plans (exit 3, `plan_hash`) and with `--approve HASH` runs the host builds as a Job on a labelled amd64 builder node (Git credentials from a Secret, cross-builds arm64, cache claim per branch and page size, pushes by digest to the node registry, prints the receipt); `piceli env push BRANCH MODULE:ATTR --receipt FILE|--digest IMAGE=sha256:…` records a laptop-built digest in the ConfigMap `piceli-env-<branch>` (needs approval)
- [Publish images for other people's clusters](https://docs.pynenc.org/projects/piceli/en/stable/publishing_images.html): one spec built for `linux/amd64` and `linux/arm64` (`build.platforms` or `--platform` on `piceli artifacts build-spec run` of a `host-build.toml`; a `build.toml` lists `platforms`), then `piceli artifacts publish --receipt R --to oci://registry/prefix --tag VERSION` (exit 3 until `--approve <digest>`) pushes per-platform manifests by digest (only missing blobs), one OCI image index, the SBOM and provenance as OCI referrers, cosign signatures (`--sign-key`, no public log) and the version tag last (`publish-tag-exists` unless `--move-tag`); credentials from `--docker-config` (credential helpers) or `--credentials`; `--values-out` writes `images.<key>.{repository,tag,digest}` for `piceli chart`
- [Registry retention](https://docs.pynenc.org/projects/piceli/en/stable/registry_retention.html): `piceli artifacts retention --to oci://host[:port]/prefix --receipts DIR [--keep N|0] [--budget 10GiB] [--pin DIGEST] [--kubeconfig F --context C | --live-file F]` reports which manifests the last N releases (publish/delivery receipts), pins and the digests pods and workload pod templates (Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs, CronJobs) use keep (finished pods and Jobs and a Deployment's scaled-down rollout history are not live; environment records are), finds digest-only manifests the cluster still names, and lists the collectable ones with repository, digest, bytes and `why` (read-only); `--keep 0` (needs `--kubeconfig --context`) keeps only live and pinned digests; `--delete` prints the plan hash (exit 3), `--delete --approve HASH` deletes exactly that list by digest and refuses (`retention-not-approved`) if it changed, never deletes a digest a running workload uses (`retention-live-unknown` without a live source), `registry-delete-disabled` when the registry forbids deletes; blobs are freed by the registry's garbage collection (`garbage_collection(delete_untagged=False)` for the node-local registry; `kubectl -n piceli-system exec deploy/piceli-registry -- registry garbage-collect /etc/distribution/config.yml` for `Registry.in_cluster`)
- [Containerized builds](https://docs.pynenc.org/projects/piceli/en/stable/containerized_builds.html): `build.toml`, pinned builders, build receipts, `piceli artifacts build-spec`; isolated per-image smoke checks with exit code, plain `env`, `entrypoint`, `expect_stdout`/`expect_stderr` regexes (`smoke-output-mismatch`)
- [Image delivery](https://docs.pynenc.org/projects/piceli/en/stable/node_delivery.html): `piceli artifacts deliver` to a registry (default) or a node, approved by config digest
- [Node-local registry](https://docs.pynenc.org/projects/piceli/en/stable/node_local_registry.html): a registry one node pulls from without configuration
- [In-cluster registry](https://docs.pynenc.org/projects/piceli/en/stable/cluster_registry.html): `Registry.in_cluster(on="NODE", storage="20Gi")` as `deliver=` of a pipeline; every node pulls `piceli-registry.piceli-system.svc:5000/<app>/<image>@sha256:…` through a containerd mirror (`certs.d/<host>/hosts.toml`, or on k3s nodes an entry merged into `/etc/rancher/k3s/registries.yaml`; `node_mirror="auto"|"k3s"|"containerd"`) a node agent writes; `piceli registry install MODULE:ATTR` (exit 3 until `--approve <plan_hash>`; owner only), `piceli registry status [--json]` (read-only: pod, mirror per node, storage use), `piceli registry uninstall` (needs approval; keeps the data without `--delete-storage`), `piceli registry forward` (push port-forward); `piceli deploy` refuses with `cluster-registry-not-installed`/`cluster-registry-not-ready` until it serves
- [Cluster init](https://docs.pynenc.org/projects/piceli/en/stable/cluster_init.html): declare `Cluster(name, api=, credentials=PROFILE, nodes=[Node(name, arch=, roles=[…])], registry=Registry.in_cluster(on=), controller=Controller(on=), ui=Ui(access="forward"))` (`from piceli.infra import …`); `piceli cluster init MODULE:ATTR` (exit 3 until `--approve <plan_hash>`; owner only; refuses a profile whose server is not `api` with `cluster-api-mismatch`) labels nodes `piceli.io/role-<role>=true` (`piceli.io/builder=true` for `builder`, `piceli.io/runtime=k3s|containerd`), installs the registry with containerd or k3s (`registries.yaml`, merged, never clobbered) node mirrors, the controller's foundation and the UI; idempotent; lists `restart_needed` k3s nodes and never restarts k3s; `piceli cluster status MODULE:ATTR [--json]` (read-only); `piceli secrets git --cluster MODULE:ATTR --prompt` (the owner types the token; never an argument or env var; writes Secret `piceli-build-git`, prints names only)
- [Compositions in Python](https://docs.pynenc.org/projects/piceli/en/stable/compositions.html) (preview): the composition repository holds the app as a `Pipeline`, a host build spec whose `[context.X] source = "X"` read the composition's `Source`s at each environment's revision, and `infra.py` with `Environment(name, namespace=…, pipeline=PIPELINE, stack=Stack(name, workloads=[…]), follow={source: rule}, cluster=…)` / `Environment.per_branch(..., pipeline=PIPELINE)`; `piceli gitops enable infra.py --repo URL` (exit 3 until `--approve <plan_hash>`; owner only) records the composition repository, which the controller follows (`--main-branch`, default main) and imports at its commit; each `[[output.image]]` (optional `contexts = [...]`) is rebuilt only when its change key changed; status `envs.<env>.components.<image>` and `controller.composition_repo`
- [Compositions and component contracts](https://docs.pynenc.org/projects/piceli/en/stable/components.html) (preview): a module `infra.py` declares `Source(url)`s, `Component(name, source=…)`s (their contract is `[component.<name>]` in each repository's `piceli.toml`: `build` rust/python/files, `image` base pinned by digest, `ports`, `health`, `upgrade_check`, `volumes` retained or scratch, `needs` with `?`/`secret:`/`component:`, `settings`, `emits`; unknown keys `component-contract-invalid`, unmet needs `component-need-unmet`), `Component.image(ref, pin=…)` (mirrored into the in-cluster registry), `Stack(name, [components])` and `environments = [Environment(name, namespace=…, follow={source: "main" | Tag("v*") | Promote()}, stack=…, cluster=…), Environment.per_branch(Branches("wp-*"), namespace="app-{branch}", follow={source: "{branch}"}, on_nodes=[…])]`; `piceli gitops enable infra.py --image REPO@sha256:…` (exit 3 until `--approve <plan_hash>`; owner only) installs a controller that polls every source with one Git Secret and rebuilds and rolls only components whose source digest changed (others apply as no-op); `piceli gitops sync [ENV] [--component NAME]` asks for a deploy now (owner's go-ahead); the status adds `sources`, `envs.<env>.revision`, `envs.<env>.components.<name>` (source, commit, digest, state, health)
- [Roadmap and feature status](https://docs.pynenc.org/projects/piceli/en/stable/roadmap.html): maturity of every feature
- [FAQ](https://docs.pynenc.org/projects/piceli/en/stable/faq.html): existing YAML, patch vs recreate, which cluster, deletion, CRDs, registries, web UI exposure

## Optional

- [Deployment planning](https://docs.pynenc.org/projects/piceli/en/stable/deployment_planning.html): discovery, pure plans, authorized execution and journals (Python API)
- [Artifact delivery](https://docs.pynenc.org/projects/piceli/en/stable/artifact_delivery.html): deterministic offline OCI layouts, tool grants, local-engine import
- [Operations lens](https://docs.pynenc.org/projects/piceli/en/stable/operations_lens.html): `piceli observe`, local UI, logs and port forwards
- [Operator workflow](https://docs.pynenc.org/projects/piceli/en/stable/operator_workflow.html): experimental inventory, catalog, approvals and backups
- [Kubernetes model](https://docs.pynenc.org/projects/piceli/en/stable/kubernetes_model/index.html): templates, client models and YAML/JSON definitions
- [API reference](https://docs.pynenc.org/projects/piceli/en/stable/apidocs/): generated from docstrings
- [Cross-model eval](https://docs.pynenc.org/projects/piceli/en/latest/contributing/evals.html): how well models install, implement, operate and recommend Piceli (`evals/`); safety violations (default kubeconfig, `--auto-approve`, `--allow-exec`, printed secrets) fail a task
- [Contributing](https://github.com/pynenc/piceli/blob/main/AGENTS.md): how to work on Piceli itself (setup, tests, invariants, code map)
- [Changelog](https://docs.pynenc.org/projects/piceli/en/stable/changelog.html): breaking changes and the 0.4.0 contract changes
