# 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:

- From a source checkout with uv, Node.js and npm installed, `make ui` builds the production frontend and opens disposable sample state, normally at `http://127.0.0.1:4178`; no cluster or credentials are needed. The fixture accepts plain bookmarks in fresh browsers. Real-target services retain launch-token authentication. This is a long-running local preview; stop it with Ctrl+C. See docs/ui.md.
- `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). Composition sessions open infrastructure topology with component inspection, version comparison and reported attention; compact Sources and Environments link to workloads, and Cluster shows nodes and services. A Sync button writes the `piceli gitops sync` request. Operations → Logs (`/logs`) merges redacted, bounded logs of every readable scope with URL filters; Operations → Port forwards (`/forwards`) lists and starts forwards; in this UI a forward is a ticket for `piceli ui connect --server http://127.0.0.1:8790 …` on your machine. `piceli access ENV --cluster infra.py:CLUSTER [--ui]` forwards a composition environment's declared forwards and other Service ports (and with `--ui` the UI too); every kubectl stops with it, also when piceli is killed. Named environments have Stop/Start (as `piceli env stop|start ENV --cluster infra.py:CLUSTER`) and show their last checks. Refusals `access-ui-*`, `ui-sync-*`, `ui-controller-absent`. See docs/ui.md.
- The web workspace's Deployment history (`/delivery`) has Plans, Runs & logs and Compare revisions. The application's Plans tab also lists saved plans, including expired reviews and plans with no recorded run. Opening history never evaluates source or renews approval. Exact app/plan/digest identities connect recorded runs; captured logs require a separate logs grant. Revision comparison reads stored desired configurations, not proof of successful deployment; missing/redacted history remains explicit. Arbitrary CLI plan files and uncaptured terminal output are not imported.
- `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

- [Machines with OpenTofu](https://docs.pynenc.org/projects/piceli/en/latest/infrastructure.html): `Server`, `PrimaryIp`, `Firewall`/`Rule`, `DnsRecord`, `Hook`, `Infrastructure` in `piceli.infra` (Hetzner first, pinned `hcloud` provider); `piceli infra plan MODULE:ATTR` (read-only for resources; exit 3 with changes, estimate and plan hash), `apply|destroy --approve <plan_hash>` (owner only; never touches what Piceli did not create, `infra-foreign-resource`), `install SERVER` and `register SERVER` (`--approve <digest>`, owner only), `status --json` (read-only, offline); state encrypted by OpenTofu, locked, never in Git; tokens only through `piceli secrets provider NAME --prompt` (owner types them), never arguments or `HCLOUD_TOKEN`
- [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
- [Deliver to a cluster you never access](https://docs.pynenc.org/projects/piceli/en/stable/client_delivery.html): `piceli bundle MODULE:ATTR --env ENV --out DIR [--receipt R] [--platform P|--all-platforms]` writes a kustomize base and `overlays/dev` (namespace, registry and digests, resources, storage), `prepare.sh NAMESPACE` (generated Secrets made in the client's cluster with kubectl and openssl; no Secret shipped), OCI image archives with SBOM and provenance, `INSTALL.md`/`UNINSTALL.md`, `bundle.json`, `SHA256SUMS`; every object labelled part-of; a default-deny NetworkPolicy; a safety gate (`bundle-unsafe-<rule>`: non-root, read-only-root, no-privilege, resources, network, rbac, node-port) waived per object by `app.safety_exception(item, rule, reason=...)`; `app.cluster_identity(image=...)` reads the kube-system UID at runtime; `piceli support-bundle --kubeconfig F --context C --namespace NS --out FILE` is read-only (GET) and redacted
- [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]` (or, for `Registry.in_cluster`, `piceli artifacts retention --cluster infra.py:CLUSTER [--delete [--approve HASH]]`, which derives the registry, port-forward and cluster read from the Cluster, keeps what runs, every Deployment/StatefulSet's rollback target and Piceli's controller, builder and UI images, and lists every stored manifest, digest-only ones too, to collect the rest) 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 [MODULE:ATTR|infra.py:CLUSTER|infra.py] [--json]` (read-only: pod, mirror per node, the claim's storage use with `used_source`), `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)
- [Several clusters](https://docs.pynenc.org/projects/piceli/en/stable/multicluster.html): (0.15) one controller deploys to several declared `Cluster`s: `Environment(..., clusters=[Placement(edge, namespace=, on_nodes=, values=, pipeline=, replicas=), other_cluster], rollout=Rollout(order=["edge-canary", "edge-*"]))` (`from piceli.envs import Placement, Rollout`); the cluster with `Controller(...)` is home; each other cluster's credentials are a Secret written by `piceli secrets cluster --cluster MODULE:ATTR --kubeconfig FILE --context NAME [--server URL]` or `--prompt` (owner only, never printed); images are copied by digest into each cluster's own `Registry.in_cluster` through its API server; `gitops status --json` adds `envs.<env>.clusters.<cluster>{state, health, checks, revision, reason, last_contact}` (`state: unreachable`, `reason: cluster-unreachable`) and `clusters.<name>`; a later rollout wave is `held` (`rollout-stopped`) when an earlier one fails; `Environment(replicas={"workload": n})` and `Environment.per_branch(..., replicas=...)` set replica counts per environment
- [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); `piceli env stop|start ENV --cluster infra.py:CLUSTER` or `Environment(..., stopped=True)` (wins) stops a named environment (no replicas, volumes kept, not planned; owner's go-ahead; `gitops-env-stop-declared`); the status adds `sources`, `envs.<env>.revision`, `envs.<env>.components.<name>` (source, commit of the triggering source, `sources`, digest, state, health), `envs.<env>.checks` (last checks: passed N/N, names, when, trigger) and `failed_attempts`
- [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
- [Web workspace](https://docs.pynenc.org/projects/piceli/en/stable/ui.html): one-command checkout preview; source/component/environment topology and exact version comparison; compact Sources; resource ownership/configuration; Cluster status; saved plans, ordered action phases and resource diffs; recorded runs, captured logs and raw journals; read-only revision comparisons; capability-scoped navigation and exact approvals
- [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
