Problem
Component exclusions chosen via mcs sync --customize are silently discarded — and erased from state — by the next plain interactive mcs sync.
Configurator.interactiveConfigure (Sources/mcs/Sync/Configurator.swift:128-135) hardcodes an empty exclusion set and only populates it when --customize is passed:
var excludedComponents: [String: Set<String>] = [:]
if customize, !selectedPacks.isEmpty {
excludedComponents = ConfiguratorSupport.selectComponentExclusions(...)
}
previousState is read only to seed the picker's checkboxes inside selectComponentExclusions — never to carry exclusions forward when the picker doesn't run.
Downstream, installAndReconcileArtifacts writes that empty set straight back to state:
// Configurator.swift:832
state.setExcludedComponents(excluded, for: pack.identifier)
So a plain mcs sync reinstalls every previously-excluded component and wipes the record that they were ever excluded.
Inconsistent with the non-interactive paths
--pack / --all get this right — SyncCommand loads persisted exclusions before delegating:
// SyncCommand.swift:87-89 (global), :145-147 (project)
persistedExclusions = try ProjectState(...).allExcludedComponents
and passes them at SyncCommand.swift:104 / :162. Only the interactive non-customize path drops them. Both scopes (project and --global) are affected, since interactiveConfigure is shared.
Reproduction
mcs sync --customize, deselect a component of some pack
mcs sync (plain, interactive, same pack selection)
- The deselected component is reinstalled, and
.claude/.mcs-project no longer lists it under excludedComponents
Fix sketch
Seed from persisted state when the picker doesn't run:
var excludedComponents = previousState.allExcludedComponents
if customize, !selectedPacks.isEmpty {
excludedComponents = ConfiguratorSupport.selectComponentExclusions(...)
}
Newly-selected packs contribute no entry, so they install fully — correct. Deselected packs are removed by unconfigurePack, which already clears their state entry (ProjectState.swift:162).
Worth confirming while fixing: removeNewlyExcludedComponentArtifacts (Configurator.swift:538-545) diffs previousExcluded vs currentExcluded. With the fix those are equal on a plain sync, so it correctly becomes a no-op.
Tests
Tests/MCSTests/LifecycleIntegrationTests.swift:
- customize-exclude a component → plain re-sync → component still absent, still recorded in
excludedComponents
- customize-exclude → plain re-sync →
--customize again shows the component unchecked
- newly added pack on a plain sync installs all its components (regression guard: persisted exclusions from other packs must not leak)
Relation to #356
#356 (requiredBy on prompts) depends on the selected-component set being stable across syncs — a prompt skipped under --customize would otherwise be re-asked on the next plain sync. This should land first.
Problem
Component exclusions chosen via
mcs sync --customizeare silently discarded — and erased from state — by the next plain interactivemcs sync.Configurator.interactiveConfigure(Sources/mcs/Sync/Configurator.swift:128-135) hardcodes an empty exclusion set and only populates it when--customizeis passed:previousStateis read only to seed the picker's checkboxes insideselectComponentExclusions— never to carry exclusions forward when the picker doesn't run.Downstream,
installAndReconcileArtifactswrites that empty set straight back to state:So a plain
mcs syncreinstalls every previously-excluded component and wipes the record that they were ever excluded.Inconsistent with the non-interactive paths
--pack/--allget this right —SyncCommandloads persisted exclusions before delegating:and passes them at
SyncCommand.swift:104/:162. Only the interactive non-customize path drops them. Both scopes (project and--global) are affected, sinceinteractiveConfigureis shared.Reproduction
mcs sync --customize, deselect a component of some packmcs sync(plain, interactive, same pack selection).claude/.mcs-projectno longer lists it underexcludedComponentsFix sketch
Seed from persisted state when the picker doesn't run:
Newly-selected packs contribute no entry, so they install fully — correct. Deselected packs are removed by
unconfigurePack, which already clears their state entry (ProjectState.swift:162).Worth confirming while fixing:
removeNewlyExcludedComponentArtifacts(Configurator.swift:538-545) diffspreviousExcludedvscurrentExcluded. With the fix those are equal on a plain sync, so it correctly becomes a no-op.Tests
Tests/MCSTests/LifecycleIntegrationTests.swift:excludedComponents--customizeagain shows the component uncheckedRelation to #356
#356 (
requiredByon prompts) depends on the selected-component set being stable across syncs — a prompt skipped under--customizewould otherwise be re-asked on the next plain sync. This should land first.