🤖 Generated by the Agentic Engineer
Evidence
Reproduced end to end with a binary built from main, in an empty directory. project init --multi-cluster prod scaffolds an environment named prod, and the env verbs then disagree about whether it exists:
$ ksail project init --multi-cluster prod
✚ created 'k8s/clusters/base/kustomization.yaml'
✚ created 'k8s/clusters/prod/kustomization.yaml'
✔ initialized project
$ ksail project env reconcile --experimental
ENVIRONMENT OVERLAY STATE
prod clusters/prod Present
$ ksail project env list
ℹ no environments declared; scaffold one with `ksail project env add <name> --from <env>`
$ ksail project env add staging --from prod
✗ failed to load source environment config (ksail.prod.yaml): ... no such file or directory
reconcile reports prod as a first-class declared environment; list reports none; add --from prod cannot find it. Same workspace, same moment.
The plain ksail project init path lands in the same place: it writes only ksail.yaml, so env list prints the identical hint, and there is no environment to pass to --from.
Root cause
The two verbs resolve "what is an environment" through different functions, and only one of them knows about the multi-cluster scaffold's initial environment:
environment.DerivePlan (used by env reconcile) calls appendBaseSyncedEntry, which reads the base ksail.yaml's kustomizationFile: clusters/prod and adds prod as a first-class entry. Its own comment names this case exactly: "the initial environment project init --multi-cluster scaffolds without a ksail..yaml".
environment.DeriveEnvironments (used by env list, by env add's --from load, by its available-environments hint, and by env rm) enumerates ksail.<name>.yaml files only, so the base-synced environment is invisible to all of them.
So the codebase already recognises this shape in one verb and not its siblings.
Affected audience and impact
Anyone following the multi-cluster path — the flow epic #5441 exists to support. Concretely:
- The advertised next step cannot be taken.
env add --from is the only way to create a second environment, and the initial environment cannot be named as its source. The multi-cluster scaffold therefore stops at one environment.
- The CLI's own remediation hint is unfollowable in exactly the state where it is printed: there is no
<env> to pass to --from, in either init mode. A guardrail that names a fix that cannot be applied trains people to distrust the message.
env list contradicts env reconcile on the same workspace, so neither can be trusted as the answer to "what environments do I have?".
There is a workaround, but it is undocumented and requires knowing the file convention: hand-write a ksail.prod.yaml next to ksail.yaml, after which env list and env add --from prod both work.
Expected behaviour
The env verbs agree on what an environment is. The environment that project init --multi-cluster <name> creates is visible to env list, usable as env add --from <name>, and removable by env rm, without hand-writing a config file.
Proposed direction
Give DeriveEnvironments the same base-synced awareness DerivePlan already has — most cheaply by extracting the appendBaseSyncedEntry resolution so both call sites share it, which also guarantees the two verbs cannot drift apart again. Two details worth settling while implementing:
- What
env list should show for DISTRIBUTION/PROVIDER of a base-synced environment (they come from the base ksail.yaml, so they are known).
- Whether
env add --from <base-synced> should also write the missing ksail.<name>.yaml for the source as it clones, so the workspace converges on one representation.
An alternative worth considering instead: have project init --multi-cluster <name> write ksail.<name>.yaml up front, so every environment has the same shape from the start. That is a smaller change but alters the scaffold's output, so it deserves an explicit decision rather than being assumed.
Acceptance criteria
Rough size
S–M. The resolver change is small and the seam already exists; most of the work is deciding the two questions above and covering both init modes.
Evidence
Reproduced end to end with a binary built from
main, in an empty directory.project init --multi-cluster prodscaffolds an environment namedprod, and the env verbs then disagree about whether it exists:reconcilereportsprodas a first-class declared environment;listreports none;add --from prodcannot find it. Same workspace, same moment.The plain
ksail project initpath lands in the same place: it writes onlyksail.yaml, soenv listprints the identical hint, and there is no environment to pass to--from.Root cause
The two verbs resolve "what is an environment" through different functions, and only one of them knows about the multi-cluster scaffold's initial environment:
environment.DerivePlan(used byenv reconcile) callsappendBaseSyncedEntry, which reads the baseksail.yaml'skustomizationFile: clusters/prodand addsprodas a first-class entry. Its own comment names this case exactly: "the initial environmentproject init --multi-clusterscaffolds without a ksail..yaml".environment.DeriveEnvironments(used byenv list, byenv add's--fromload, by its available-environments hint, and byenv rm) enumeratesksail.<name>.yamlfiles only, so the base-synced environment is invisible to all of them.So the codebase already recognises this shape in one verb and not its siblings.
Affected audience and impact
Anyone following the multi-cluster path — the flow epic #5441 exists to support. Concretely:
env add --fromis the only way to create a second environment, and the initial environment cannot be named as its source. The multi-cluster scaffold therefore stops at one environment.<env>to pass to--from, in either init mode. A guardrail that names a fix that cannot be applied trains people to distrust the message.env listcontradictsenv reconcileon the same workspace, so neither can be trusted as the answer to "what environments do I have?".There is a workaround, but it is undocumented and requires knowing the file convention: hand-write a
ksail.prod.yamlnext toksail.yaml, after whichenv listandenv add --from prodboth work.Expected behaviour
The env verbs agree on what an environment is. The environment that
project init --multi-cluster <name>creates is visible toenv list, usable asenv add --from <name>, and removable byenv rm, without hand-writing a config file.Proposed direction
Give
DeriveEnvironmentsthe same base-synced awarenessDerivePlanalready has — most cheaply by extracting theappendBaseSyncedEntryresolution so both call sites share it, which also guarantees the two verbs cannot drift apart again. Two details worth settling while implementing:env listshould show forDISTRIBUTION/PROVIDERof a base-synced environment (they come from the baseksail.yaml, so they are known).env add --from <base-synced>should also write the missingksail.<name>.yamlfor the source as it clones, so the workspace converges on one representation.An alternative worth considering instead: have
project init --multi-cluster <name>writeksail.<name>.yamlup front, so every environment has the same shape from the start. That is a smaller change but alters the scaffold's output, so it deserves an explicit decision rather than being assumed.Acceptance criteria
project init --multi-cluster prod,env listreportsprod.project init --multi-cluster prod,env add staging --from prodsucceeds and produces a workingstagingenvironment.env listandenv reconcilereport the same environment set for that workspace, pinned by a test that would fail if either resolver changes alone.--multi-cluster).Rough size
S–M. The resolver change is small and the seam already exists; most of the work is deciding the two questions above and covering both init modes.