ov-dev/skills/install-plan/SKILL.md
The InstallPlan IR — the shared intermediate representation consumed by build-mode Containerfile emission (OCITarget), container deploys (ContainerDeployTarget), host deploys (HostDeployTarget), VM deploys (VmDeployTarget over SSH), and Kubernetes deploys (KubernetesDeployTarget). MUST be invoked before reading or modifying any of: ov/install_plan.go, ov/install_build.go, ov/build_target_oci.go, ov/deploy_target_host.go, ov/deploy_target_container.go, ov/deploy_target_vm.go, ov/deploy_target_k8s.go, or when adding a new step kind / deploy target / reverse-op kind.
npx skillsauth add overthinkos/overthink-plugins install-planInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
ov has five code paths that all need to know "what does applying this layer mean?":
ov image build / ov image generate emit Containerfiles.ov deploy add <name> <ref> runs the image via quadlet, optionally building an overlay image when add_layers: is set.ov deploy add host <ref> applies the recipe to the invoking user's filesystem.ov deploy add vm:<name> <ref> applies the recipe inside a running VM over SSH.ov deploy add <name> --target kubernetes emits a Kustomize base/overlays tree.Before the 2026-04 refactor these were separate walks over Layer / ResolvedImage. The refactor unifies them behind one IR. A pure compiler (BuildDeployPlan) turns Layer + ResolvedImage + HostContext into an InstallPlan; each code path becomes a DeployTarget consuming the plan.
This skill is the single source of truth for the IR shape. Add a new step kind by editing install_plan.go, the compiler in install_build.go, and each target's emit* method — this skill lists every place that needs to stay in sync.
| File | Role |
|---|---|
| ov/install_plan.go | IR types, enums, InstallStep interface, ReverseOp, DeployTarget interface, EmitOpts, GateEnabled |
| ov/install_build.go | BuildDeployPlan pure compiler: Layer → InstallPlan |
| ov/build_target_oci.go | OCITarget — emits Containerfile text |
| ov/deploy_target_container.go | ContainerDeployTarget — synthesizes overlay Containerfile when add_layers: present, delegates to quadlet/start |
| ov/deploy_target_host.go | HostDeployTarget — executes plan on host via shell + podman run <builder> |
| ov/deploy_target_vm.go | VmDeployTarget — executes plan inside a running VM via SSH + scp |
| ov/deploy_target_k8s.go | KubernetesDeployTarget — emits Kustomize base/overlays tree |
| ov/deploy_executor*.go | DeployExecutor interface + LocalExecutor + SSHExecutor — shell + file-copy abstraction shared by Host/VM targets |
| ov/install_plan_test.go | IR unit tests (scope/venue/gate/reverse derivations) |
| ov/install_build_test.go | Compiler integration tests (ripgrep, dev-tools, pixi layer) |
InstallPlantype InstallPlan struct {
DeployID string // per-deploy hash of image + add_layers
Image string
Version string // layer/image CalVer
Distro string // "fedora:43"
Layer string // set for per-layer plans; "" for merged whole-image plans
Steps []InstallStep
LayersIncluded []string // ordered topo-sorted layer names (for merged plans)
AddLayers []string // refs added via deploy.yml add_layers: (for provenance)
BuilderImage string // selected builder for VenueContainerBuilder steps
Meta map[string]string
}
One plan per layer when the compiler runs on a single Layer. For whole-image deploys, MergePlans(plans, image, addLayers) merges per-layer plans while preserving layer boundaries for refcount bookkeeping. DeployID is a deterministic 16-hex-char sha256 prefix over (image, layer_order, add_layers) — same inputs → same ID, so re-deploys are stable.
InstallStep interfacetype InstallStep interface {
Kind() StepKind
Scope() Scope
Venue() Venue
RequiresGate() Gate
Reverse() []ReverseOp
}
All eight concrete step kinds implement this interface. Reverse() is called at install time (not teardown) so the ledger records the exact reversal ops tied to the specific artifacts created.
Scope — where the effect lands:
ScopeSystem — /etc, /usr, /var; requires sudo on host; USER root in Containerfile.ScopeUser — $HOME/.pixi, $HOME/.cargo, $HOME/.local, user-scope systemd units.ScopeUserProfile — shell init surface (~/.bashrc / ~/.zshenv / fish conf.d + ~/.config/overthink/env.d/).Venue — where commands physically execute:
VenueHostNative — host shell (plain or sudo-wrapped) or a plain RUN in Containerfile.VenueContainerBuilder — inside a builder container: FROM <builder> AS stage in Containerfile; podman run <builder> on host target.VenueSkip — recorded with reason but not executed (container-runtime-only fields on host target; aur: on non-Arch hosts).Phase — three-phase template execution:
PhasePrepare — repo config, key import, copr enable.PhaseInstall — the actual package-manager or builder invocation.PhaseCleanup — teardown (copr disable, cache wipe).Each SystemPackagesStep carries one phase; --allow-repo-changes gating is a simple lookup on step.Phase == PhasePrepare.
Gate — opt-in flag name:
GateNone (default, always enabled)GateAllowRepoChangesGateAllowRootTasksGateWithServicesGates apply only to the host target. EmitOpts.AssumeYes enables all three. See GateEnabled(gate, opts) in install_plan.go.
StepKind — discriminator for concrete types:
SystemPackages, Builder, Task, File, ServicePackaged, ServiceCustom, ShellHook, ShellSnippet, RepoChange.The IR carries no image-fetch step kind. Deploys (any target) emit
zero image-pull / image-build steps; test-bed image preflight is a
separate, eval-time concern handled by ov/eval_image_preflight.go.
The retired EnsureImageStep / compileImagesSteps /
runImagesPrePass surface and its ReverseOpRemoveImage /
--reclaim-images flag were deleted in the 2026-05
deploy-fetch-narrowing cutover (CLAUDE.md "Deploy fetches NOTHING
speculative").
| Kind | What it carries | Venue default | Scope derivation |
|---|---|---|---|
| SystemPackagesStep | Format (rpm/deb/pac), Phase, Packages, Repos, Options, Copr, Modules, Exclude, Keys, CacheMounts | HostNative | Always system |
| BuilderStep | Builder (pixi/npm/cargo/aur), BuilderImage, LayerDir, Phase, Artifacts, RawStageContext | ContainerBuilder | aur→system, others→user |
| TaskStep | Task (raw), LayerName, LayerDir, CtxPath, ResolvedUser | HostNative | From ResolvedUser (root or 0 → system; else user) |
| FileStep | Source, Dest, Mode, Owner, LayerName | HostNative | pathIsSystemScoped(Dest) |
| ServicePackagedStep | Unit, TargetScope, Enable, OverridesText, OverridesPath, LayerName, PriorEnabled | HostNative | TargetScope field |
| ServiceCustomStep | Name, UnitText, UnitPath, TargetScope, Enable, LayerName | HostNative | TargetScope field |
| ShellHookStep | LayerName, EnvVars, PathAdd, EnvFile | HostNative | Always user-profile |
| ShellSnippetStep | LayerName, Origin, Shell (bash/zsh/fish/sh), Snippet, PathAppend, Destination, Marker, UseDropin, Priority | HostNative | pathIsSystemScoped(Destination) (system for container drop-ins, user-profile for ~/.bashrc etc.) |
| RepoChangeStep | Format, File, Content, Checksum, LayerName | HostNative | Always system |
ShellSnippetStep notes (2026-05 cutover):
compileShellSnippetSteps in install_build.go — applies the per-shell-wins-over-generic selection rule from layer.Shell().RUN mkdir -p ... && cat > <dest> <<EOF heredoc with a sha256-derived end-marker (anti-collision).command -v <shell> once at the top of Emit(); absent shells become VenueSkip-style no-ops with a logged reason. UseDropin=true → whole-file write; UseDropin=false → replaceOrAppendManagedBlock against the existing rc file with a per-layer marker.ReverseOpRmFileSystem / ReverseOpRmFileUser for drop-ins; ReverseOpRemoveManaged (with Extra["marker"]=LayerName) for managed-block append.LabelShell (org.overthinkos.shell) carries the merged set; CollectShell builds it at ov image build time, ExtractMetadata parses it at deploy time, MergeDeployShell overlays deploy.yml entries by id.Each step's Reverse() emits typed ReverseOp values. Adding a step kind means: (a) define the struct in install_plan.go, (b) decide its Scope/Venue/Gate/Reverse, (c) add a case to each target's step dispatch (emit* in OCITarget; exec* in HostDeployTarget), (d) ensure the compiler in install_build.go emits it.
BuildDeployPlanfunc BuildDeployPlan(layer *Layer, img *ResolvedImage, hostCtx HostContext) (*InstallPlan, error)
Pure — no I/O, no side effects. Given the same inputs, produces the same plan. Called:
ov image build (OCITarget walks the combined output).ov deploy add <container> (ContainerDeployTarget filters to add_layers: for overlay synthesis).ov deploy add host (HostDeployTarget walks the combined output).Pass HostContext{Target: "host", Distro: ..., GlibcVersion: ...} for host compilation; zero-value for build/container compilation.
Step emission order (mirrors today's writeLayerSteps):
ShellHookStep for env: + path_append: (deterministic map ordering).SystemPackagesStep(s) — distro-tag section wins over build-format section (first-match precedence from ResolvedImage.Distro order).TaskStep(s) in YAML order.BuilderStep(s) for each matching multi-stage or inline builder.ServicePackagedStep / ServiceCustomStep from the service: list — per-entry routing via IsPackaged() + ServiceSchema.SupportsPackaged.MergePlans([]*InstallPlan, image, addLayers) composes per-layer plans into a single whole-image plan for target-level walking (sudo batching, single dry-run output).
DeployTarget interfacetype DeployTarget interface {
Name() string // "oci" | "container" | "host" | "vm:<name>" | "kubernetes"
Emit(plans []*InstallPlan, opts EmitOpts) error
}
Five implementations:
OCITarget (ov/build_target_oci.go)Emits Containerfile text. Consumes phases.install.container from build.yml (falls back to install_template:). For multi-stage builders, delegates to Generator.buildStageContext for the existing BuildStageContext template rendering. For tasks, delegates to Generator.emitTasks with a temporary layer-tasks swap so the existing per-verb emitters (emitCopy, emitWrite, emitCmd, emitMkdirBatch, ...) run unchanged.
Used by: ov image build, ov image generate, ov deploy add <container> (overlay Containerfile synthesis).
ContainerDeployTarget (ov/deploy_target_container.go)Wraps the existing quadlet/podman pipeline with overlay-Containerfile synthesis for add_layers:. Picks an overlay tag deterministically from (base-image, sorted-layer-set). Removed on ov deploy del unless --keep-image.
Used by: ov deploy add <container> with add_layers: present.
HostDeployTarget (ov/deploy_target_host.go)Walks the IR; groups contiguous same-(Scope, Venue) steps via plan.StepsByVenue(); emits one heredoc per batch. Full executor: writes service units (packaged + custom), env.d files, managed blocks, ledger entries. Invokes builder containers via builder_run.go for VenueContainerBuilder steps. Gates (GateEnabled) applied per step; skipped steps logged.
See /ov-dev:local-infra for supporting files (hostdistro, ledger, reverse_ops, shell_profile, builder_run, service_render, deploy_ref).
VmDeployTarget (ov/deploy_target_vm.go)Same IR walking as HostDeployTarget, but shell bodies run via ssh guest 'sudo bash -s' through an SSHExecutor instead of local sudo bash. Ledger writes land on the guest filesystem; teardown runs in the guest via SSH. Preflight: WaitForSSH (120s) → WaitForCloudInit (cloud_image sources only) → EnsureOvInGuest (scp the ov binary per VmOvInstall.Strategy) → ensure guest ledger dir exists.
DeployExecutor is the abstraction that lets the same Emit logic retarget from local → SSH. LocalExecutor wraps local bash -c + file copy; SSHExecutor wraps ssh/scp via golang.org/x/crypto/ssh with persistent connection. Builder-container invocations (VenueContainerBuilder steps) run on the host, then artifacts scp into the guest — guests don't need podman installed.
Used by: ov deploy add vm:<name> <ref> / ov deploy del vm:<name>. See /ov-dev:vm-deploy-target for the full flow, VmDeployState persistence, and SSH-key idempotency.
KubernetesDeployTarget (ov/deploy_target_k8s.go)Emits a Kustomize base/ + overlays/ tree under .overthink/k8s/<name>/. Does NOT execute anything; the generated manifests are applied via kubectl apply -k out-of-band. Cluster-specific choices (storage class, ingress class, cert issuer, secret backend) come from a cluster profile (~/.config/ov/clusters/<name>.yaml), NOT the InstallPlan — the plan describes what the workload needs; the profile describes how K8s provides it.
See /ov-advanced:kubernetes for the user-facing surface and profile layout.
StepBatch batchingInstallPlan.StepsByVenue() partitions Steps into contiguous same-(Scope, Venue) runs. HostDeployTarget uses this to emit one shell heredoc per batch:
| Batch | Emission form |
|---|---|
| {ScopeSystem, VenueHostNative} | sudo bash <<'OV_ROOT' … OV_ROOT |
| {ScopeUser, VenueHostNative} | bash <<'OV_USER' … OV_USER |
| {_, VenueContainerBuilder} | podman run <builder> bash -s < script |
| {_, VenueSkip} | Logged, no exec |
OCITarget doesn't batch — it emits each step in order as Containerfile directives; adjacent same-USER steps collapse naturally via the existing USER switching logic in emitTasks.
ReverseOp catalogueSee /ov-advanced:local-deploy for the user-facing reverse-op table. The Go-level source of truth is ReverseOpKind in install_plan.go; each step's Reverse() method emits ops tagged with kind + targets + scope. Execution lives in ov/reverse_ops.go — one handler per kind, all routed through runReverseOps(ops, executor) in LIFO order.
Adding a new reverse kind requires:
ReverseOpKind constant in install_plan.go.Reverse() method.reverse_ops.go and register it in runReverseOp's dispatch switch.EmitOpts cross-cutting flagstype EmitOpts struct {
DryRun bool
FormatJSON bool
AllowRepoChanges bool
AllowRootTasks bool
WithServices bool
SkipIncompatible bool
AssumeYes bool
Verify bool
Pull bool
BuilderImageOverride string
}
CLI flags on DeployAddCmd / DeployDelCmd populate this struct; each target reads what it needs. AssumeYes enables all three opt-in gates (via GateEnabled).
install_plan_test.go — 13 unit tests over step-kind derivations (scope/venue/gate/reverse).install_build_test.go — 8 integration tests that load real layers/ via ScanAllLayersWithConfig; testHostContextWithDistro helper.install_build.go:200 comment — canonical fixture docs.When you add a step kind, add:
install_plan_test.go.install_build_test.go exercising the compiler path.build_target_oci_test.go and deploy_target_host_test.go./ov-dev:local-infra — supporting files (hostdistro, ledger, builder_run, shell_profile, reverse_ops, service_render, deploy_ref)/ov-dev:vm-deploy-target — VmDeployTarget + DeployExecutor + SSHExecutor + VmDeployState/ov-dev:vm-spec — VmSpec shape that VmDeployTarget reads/ov-dev:go — overall Go code map; Kong CLI framework; mode-purity invariant/ov-dev:generate — Containerfile generation call graph; how OCITarget plugs into Generator/ov-core:deploy — user-facing ov deploy add/del surface (host / container / vm: / kubernetes)/ov-advanced:local-deploy — host-target user-facing behavior (ledger, gates, ReverseOps)/ov-advanced:kubernetes — K8s-target user-facing behavior (cluster profiles, Kustomize layout)/ov-advanced:vm — VM command family; VmDeployTarget prerequisite (ov vm create before ov deploy add vm:...)/ov-build:build — build-mode user-facing surface; three-phase template story/ov-build:layer — layer.yml schema including unified service: that map to ServicePackagedStep / ServiceCustomStepMUST be invoked when reading or modifying any of ov/install_plan.go, ov/install_build.go, ov/build_target_*.go, ov/deploy_target_*.go; when adding a new step kind, target, or reverse-op kind; or when debugging why a particular layer produces a plan that doesn't match expectations. Invoke BEFORE reading the source files or running Explore agents against this subsystem.
development
Claude Code multi-agent support in Overthink — sub-agents, dynamic workflows, and agent teams, and how each drives the existing `ov eval` disposable beds to test and verify. MUST be invoked before authoring or invoking an ov sub-agent / dynamic workflow / agent team, wiring agent-lifecycle hooks, or asking "which primitive should drive the R10 beds?".
tools
Mounts a virtiofs share tagged `workspace` at /workspace inside a VM guest via a systemd .mount unit. Use when a kind:vm entity shares a host directory into the guest and you need it auto-mounted (and re-mounted at every boot).
development
MUST be invoked before any work involving: the `kind: android` schema kind, a `target: android` deploy, the `apk:` layer package format (installing Android apps declaratively), AndroidDeployTarget, an in-pod emulator OR a remote/physical adb-endpoint device, or nested `pod → android` deployment. The first-class Android device + app surface that sits above `ov eval adb`/`appium`.
tools
Use when committing, branching, pushing, merging, tagging, creating PRs, or approving/merging PRs with gh — the feat/-branch, R10-gated, never-force-push landing workflow across the main repo + the plugins submodule + image/<distro> submodules. Covers sync-to-upstream, branch/worktree pruning, the fork+PR path for contributors without write access, and cross-repo @github landing order.