Skip to content

plugin-clean

Repo box/github.com/opencharly/plugin-clean:v2026.287.2341
Plugin yes — see the plugin reference

COMPILED-IN charly COMMAND-class plugin that OWNS the externalized charly clean CLI — the build-artifact retention/prune surface. The plugin owns the command end to end: the flag grammar (–dry-run / –images / –check / –deep / –cache / –keep / –invalidate), the category orchestration, and the report output. No plugin-specific command LOGIC is left in core.

–deep is the store-wide untagged/dangling-image purge category — the CLI-only- interface gap-closing capability for issue #173: a multi-stage build’s INTERMEDIATE stage images are only ever labeled at the FINAL stage (WriteLabels emits at the end of the last stage), so they accumulate as unlabeled dangling images the default charly-labeled sweep (images/DanglingIDs, no flag needed) can never see. --deep removes EVERY untagged image in local storage, respecting the SAME InUse/live-build backstops as the default sweep (never a tagged image, never a container-referenced one, never mid-build — see pruneDanglingImages/selectDanglingImages in retention.go); removing a dangling image also frees any layer blobs it alone held (podman’s overlay storage GCs an unreferenced layer on last-reference removal), so this is EFFECTIVELY a dangling-image-plus-unused-layer prune. --deep NEVER fires implicitly on a plain charly clean (R5: zero default-behavior change) — it is strictly opt-in, mirroring --images/--check’s “runs ONLY this category” semantics. --deep --dry-run is the safe default probe: it reports the would-remove count + an UPPER-BOUND reclaimable-bytes figure (DeepBytes, summed from each candidate’s reported storage Size) and touches nothing. That figure is “up to”, never a firm prediction: RDD-verified live, a –deep purge removing 68 untagged images (3,552 → 3,484) reported ~92.6 GiB via the naive per-image Size sum but freed only ~4.6 GiB of real disk (132.6 GB → 128 GB), because most of those bytes were layers SHARED with the ~3,400 remaining (largely stale-tagged) images — removal only frees layers an image held UNIQUELY. Pair --deep with --invalidate (which removes stale TAGS, freeing their exclusively-held layers too) to get closer to the reported figure.

A live-build-guarded sweep DECLINES; it is not an error, and it no longer looks like an empty store. While any build is in flight (a held lock under ~/.cache/charly/ locks/builds) the dangling-image sweeps (dangling for the charly-labeled default, deep for the store-wide purge) and the buildah staging sweep remove NOTHING — and the CLI now prints <label>: SKIPPED — N build(s) in flight; <cause> (re-run when builds are idle) IN PLACE OF the removed count. Previously a declined sweep and a genuinely empty store printed the identical removed 0 untagged image(s) line, so a host with tens of GB of removable dangling images looked like a blind tool — which is how an operator ends up at raw podman rmi -f, deleting the build-layer cache that makes the next build ~8x faster. The staging line now also prints unconditionally, so its absence can no longer mean two different things.

This plugin OWNS the SHARED retention ENGINE too (retention.go: pruneImagesByRetention / pruneCheckRuns / pruneBuildCandyDirs / invalidateImageTags / pruneDeepDanglingImages + the charly-labeled image-tag CalVer/label inventory) — K1-alpha core-minimization relocated it here from charly/retention.go, since it has ZERO core-only dependencies (kit.CalVer/kit.ParseCalVer/kit.ListLocalImages/kit.BuildActivityDir are all sdk-portable). Image-tag retention orders by CREATION TIME — the only recency key total over the tags charly mints — with the build-tag CalVer as the tiebreak, and budgets two ordinals per box (the newest N distinct IMAGES, a distinct image being a distinct content identity = the image-config digest the engine reports as the image ID, and at most N tags of each). The ai.opencharly.version label is DELIBERATELY NOT an ordering key: the schema-versioning-removal cutover stopped emitting it, so ordering by it ranked a pre-cutover labelled image ABOVE a newer unlabelled one — an inversion that selected the newest image for removal (opencharly/plugin-clean#10). It survives only as the charly box list tags version column, whose wire field the spec repo still defines; the retention exemption (a tag whose :YYYY.DDD.HHMM cannot be read is never removed) therefore guards on the build tag ALONE. charly clean’s own CLI calls the engine LOCALLY (no wire hop, same package). The other three callers — charly box build’s post-build prune, charly box list tags, and candy/plugin-check’s post-run prune — reach it via verb:retention (a spec.RetentionRequest → spec.RetentionReply Invoke), the SAME peer/core-adapter pattern verb:credential/verb:gpu/verb:tunnel already use: core’s two callers (already running LoadConfig in-process) resolve defaults.keep_images/ keep_check_runs themselves and pass the resolved ints in the request; plugin-check (a peer plugin, not core) reaches verb:retention via InvokeProvider.

The project’s defaults.keep_images/keep_check_runs, for ITS OWN CLI (charly clean, no –keep flag), resolve PLUGIN-SIDE via the shared sdk/loaderkit.ResolveRetentionDefaultsViaExecutor (K-wave 2 cone R6 — the former “retention-defaults” HostBuild seam charly/host_build_retention_defaults.go is DELETED: the loader is plugin-reachable over the reverse channel, so every verb:retention caller resolves the tunables itself; “retention” is a class-generic action noun, not a provider word (F11).

clean is COMPILED-IN (listed in charly/charly.yml compiled_plugins) because command:clean’s Invoke(OpRun) needs the in-proc reverse channel — threaded by dispatchInProcCommand (“Seam A”) — to reach the host loader legs for the retention-defaults resolve. The out-of-process CliMain path has no reverse channel, so the categories needing a resolved keep-default (images/check/deep) error there; list/invalidate need no default and run standalone even out-of-process. There is NO hidden core-command forward — the plugin does the work directly, resolving the project config default itself; no core symbol crosses the boundary, no ad-hoc podman.

The scopes category reaps HUNG transient build scopes — the systemd USER scopes buildah/podman put a build container into (<runtime>-buildah-<id>.scope; measured live on a real build: runc-buildah-buildah2754860408.scope). Image builds are the only charly operation that starts a transient scope, and nothing else reaps a hung one: the builder’s own teardown dies with the builder, and the run metadata charly reap-orphans scans is gone with the worktree (opencharly/plugin-clean#11 measured one ACTIVE for seven days at 309% CPU and 12.27 load on 16 cores). A scope is stopped (systemctl --user stop, whose default KillMode=control-group takes its whole tree down) only when it has been ACTIVE for at least 1h AND no buildah/podman builder process is alive anywhere on the host; the sweep also declines wholesale while charly’s own build-activity lock is held. It runs by default and with –deep, and any other explicit category suppresses it. It needs neither the engine binary nor the resolved keep-defaults, so it is a CLI-only host-hygiene surface: no peer reaches it over verb:retention, and no new spec wire field carries it.

Two capabilities: command:clean dispatches through the COMPILED-IN registry path (registerCompiledPlugin → resolve(ClassCommand,“clean”) → dispatchInProcCommand → Invoke(OpRun) with the threaded in-proc reverse channel); verb:retention is invoked directly by core adapters / peer plugins with no authored plugin_input, mirroring verb:credential. NewMeta advertises both while the served CUE schema carries no plugin_input (verb:retention’s params are the internal spec.RetentionRequest RPC, never an authored plan step; command:clean’s args are plain CLI tokens). The full charly clean end-to-end — including the --cache category’s CAS ArtifactStore GC — is exercised by this candy’s own Go tests (TestGCCacheStores / TestPrintRetentionResult_CacheCategory) and by a live charly clean --cache run against real stores; the --dry-run / --deep sweeps are additionally witnessed by the charly superproject’s check-commands-local bed.

This candy’s plan: — the runnable spec charly check executes against a live deployment. check: steps are idempotent probes; run: steps change state.

Intent Step
check the clean command plugin ships a buildable Go module (go.mod + the provider package) compiled into charly; the full charly clean end-to-end, INCLUDING –deep (the local retention engine + the plugin-side loader-resolved retention-defaults), is exercised by the live R10 (check-commands-local); the –cache category’s CAS ArtifactStore GC is covered by this candy’s own tests (TestGCCacheStores / TestPrintRetentionResult_CacheCategory)
check the retention engine ships the cache category (the CAS ArtifactStore GC)
check the retention engine ships the scopes category — the build-scope family matcher (buildScopeNameRe), the pure reaping decision (selectReapableBuildScopes) and the sweep (reapBuildScopes)