@lesliechan721/aw 0.1.0-beta.16 → 0.1.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import path from 'node:path';
2
3
  import fs from 'fs-extra';
3
4
  import { getCodexOperationRequirement, probeAgentVersion } from '../utils/agent-version.js';
@@ -38,7 +39,7 @@ import { AwCapabilityUnsupportedError, IMPLEMENTED_AW_CAPABILITIES, } from './aw
38
39
  import { checkAwCapabilitySupport } from '../utils/manifest.js';
39
40
  import { createDependencyPlacementPlan, } from './dependency-operation-plan.js';
40
41
  import { createReconcileOperationPlan, } from './reconcile-operation-plan.js';
41
- import { forceOfflineCleanupEvidence, offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from './offline-cleanup-evidence.js';
42
+ import { forceOfflineCleanupEvidence, intentOnlyOfflineCleanupEvidence, offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from './offline-cleanup-evidence.js';
42
43
  async function measureCommandPerformanceStage(runtime, name, operation) {
43
44
  const startedAt = Date.now();
44
45
  let phase = 'success';
@@ -108,6 +109,37 @@ function omitSelectedPluginConfigErrors(inventory, targets) {
108
109
  function selectedPlatforms(selection) {
109
110
  return selection === 'both' ? ['codex', 'claude'] : [selection];
110
111
  }
112
+ async function selectedProjectCodexSkillReferences(options) {
113
+ const skillsRoot = path.join(resolveRepoRootSync(options.cwd), '.agents', 'skills');
114
+ const entries = await fs.readdir(skillsRoot, { withFileTypes: true }).catch((error) => {
115
+ const code = error.code;
116
+ if (code === 'ENOENT' || code === 'ENOTDIR')
117
+ return [];
118
+ throw error;
119
+ });
120
+ const references = [];
121
+ for (const entry of entries) {
122
+ if (!entry.isDirectory() || entry.isSymbolicLink())
123
+ continue;
124
+ const skillRoot = path.join(skillsRoot, entry.name);
125
+ const marker = await readSkillOwnerMarker({ skillRoot, allowedRoot: skillsRoot }).catch(() => null);
126
+ if (!marker?.sourceId
127
+ || marker.pluginId !== `${marker.sourceId}/${marker.plugin}`
128
+ || marker.platform !== 'codex'
129
+ || marker.scope !== 'project')
130
+ continue;
131
+ references.push({
132
+ kind: 'namespacedOwner',
133
+ location: path.join(skillRoot, SKILL_OWNER_MARKER),
134
+ sourceId: marker.sourceId,
135
+ pluginName: marker.plugin,
136
+ platform: marker.platform,
137
+ scope: marker.scope,
138
+ });
139
+ }
140
+ return references;
141
+ }
142
+ const cleanupProjectSkillsByInventory = new WeakMap();
111
143
  function selectCodexActivationPlans(plans, targetScopes, preservedActivations) {
112
144
  return plans.flatMap((plan) => {
113
145
  const includesScope = (pluginId, scope) => targetScopes.has(`${pluginId}\0${scope}`);
@@ -116,10 +148,7 @@ function selectCodexActivationPlans(plans, targetScopes, preservedActivations) {
116
148
  : []));
117
149
  if (pluginIds.size === 0)
118
150
  return [];
119
- const includesActivationAction = (action) => includesScope(action.pluginId, action.scope)
120
- || !(action.scope === plan.requirement.scope
121
- ? plan.managedInventory.desired
122
- : plan.managedInventory.related.desired).includes(action.codexPluginId);
151
+ const includesActivationAction = (action) => includesScope(action.pluginId, action.scope) || !action.desired;
123
152
  const sharedNativeActions = plan.sharedNativeActions.filter((action) => pluginIds.has(action.pluginId));
124
153
  const userActions = plan.activationActions.user.filter((action) => pluginIds.has(action.pluginId) && includesActivationAction(action));
125
154
  const projectActions = plan.activationActions.project.filter((action) => pluginIds.has(action.pluginId) && includesActivationAction(action));
@@ -251,12 +280,16 @@ function deriveDesiredIntentState(options) {
251
280
  if (options.command === 'install') {
252
281
  for (const root of options.selectedRoots)
253
282
  next = mergePluginIntentRootPlatforms(next, root);
254
- next = updateIntentOverrides({ state: next, plan: options.plan });
283
+ if (options.selectedRoots.length > 0) {
284
+ next = updateIntentOverrides({ state: next, plan: options.plan });
285
+ }
255
286
  }
256
- else if (options.command === 'uninstall') {
287
+ else if (options.command === 'uninstall' || options.command === 'cleanup') {
257
288
  for (const root of options.selectedRoots)
258
289
  next = removePluginIntentRootPlatforms(next, root);
259
- next = pruneUnreachableDependencyScopeOverrides(next, options.catalogEntries);
290
+ if (options.command === 'uninstall') {
291
+ next = pruneUnreachableDependencyScopeOverrides(next, options.catalogEntries);
292
+ }
260
293
  }
261
294
  return next;
262
295
  }
@@ -767,6 +800,199 @@ function projectLegacyTargets(references) {
767
800
  return [`${target.pluginName}\0${target.platform}\0${target.scope}`, target];
768
801
  })).values()];
769
802
  }
803
+ function cleanupIntentTargets(options) {
804
+ const selectedPlatforms = new Set(options.platforms);
805
+ const liveByKey = new Map(options.liveTargets.map((target) => [
806
+ `${target.pluginId}\0${target.platform}`,
807
+ target,
808
+ ]));
809
+ const selectedIds = options.all
810
+ ? new Set(options.state.roots.map((root) => root.pluginId))
811
+ : new Set(options.selectors.flatMap((selector) => {
812
+ const parsed = parsePluginId(selector);
813
+ if (parsed.sourceId)
814
+ return [formatPluginId(parsed)];
815
+ const matches = options.state.roots
816
+ .filter((root) => parsePluginId(root.pluginId).pluginName === parsed.pluginName)
817
+ .map((root) => root.pluginId);
818
+ const liveMatches = options.liveTargets
819
+ .filter((target) => target.pluginName === parsed.pluginName)
820
+ .map((target) => target.pluginId);
821
+ const identities = [...new Set([...matches, ...liveMatches])];
822
+ if (identities.length > 1) {
823
+ throw new Error(`pluginSelectorAmbiguous: Plugin "${parsed.pluginName}" has multiple Intent or live matches: ${identities.sort().join(', ')}.`);
824
+ }
825
+ return identities.length === 1
826
+ ? identities
827
+ : options.primarySourceId
828
+ ? [formatPluginId({ sourceId: options.primarySourceId, pluginName: parsed.pluginName })]
829
+ : [];
830
+ }));
831
+ return options.state.roots.flatMap((root) => root.platforms.flatMap((platform) => {
832
+ if (!selectedIds.has(root.pluginId) || !selectedPlatforms.has(platform))
833
+ return [];
834
+ const live = liveByKey.get(`${root.pluginId}\0${platform}`);
835
+ if (live)
836
+ return [live];
837
+ const parsed = parsePluginId(root.pluginId);
838
+ return [{
839
+ pluginId: root.pluginId,
840
+ pluginName: parsed.pluginName,
841
+ sourceId: parsed.sourceId,
842
+ sourceIdentity: 'unavailable',
843
+ platform,
844
+ scope: options.scope,
845
+ }];
846
+ }));
847
+ }
848
+ function cleanupSelectedRoots(targets) {
849
+ const platformsByPluginId = new Map();
850
+ for (const target of targets) {
851
+ const platforms = platformsByPluginId.get(target.pluginId) ?? new Set();
852
+ platforms.add(target.platform);
853
+ platformsByPluginId.set(target.pluginId, platforms);
854
+ }
855
+ return [...platformsByPluginId].map(([pluginId, platforms]) => ({
856
+ pluginId,
857
+ platforms: [...platforms],
858
+ }));
859
+ }
860
+ function cleanupIntentResult(target) {
861
+ return {
862
+ status: 'succeeded',
863
+ command: 'cleanup',
864
+ target: {
865
+ pluginId: target.pluginId,
866
+ pluginName: target.pluginName,
867
+ sourceId: target.sourceId,
868
+ sourceIdentity: target.sourceIdentity,
869
+ pluginVersion: '0.0.0',
870
+ platform: target.platform,
871
+ scope: target.scope,
872
+ cwd: '.',
873
+ },
874
+ actions: [],
875
+ cleanupAttempted: false,
876
+ removed: [],
877
+ absent: [],
878
+ residual: [],
879
+ quarantined: [],
880
+ failures: [],
881
+ };
882
+ }
883
+ function cleanupInventoryWarnings(inventory, targets) {
884
+ return inventory.errors.flatMap((error) => {
885
+ const relevance = error.relevance;
886
+ const relevant = relevance.global || targets.some((target) => (relevance.sourceId === undefined || relevance.sourceId === target.sourceId)
887
+ && (relevance.pluginName === undefined || relevance.pluginName === target.pluginName)
888
+ && (relevance.platform === undefined || relevance.platform === target.platform)
889
+ && (relevance.scope === undefined || relevance.scope === target.scope));
890
+ if (!relevant)
891
+ return [];
892
+ const digest = crypto.createHash('sha256').update(JSON.stringify(error)).digest('hex');
893
+ return [`stateInventoryResidual: ${error.kind} (sha256:${digest})`];
894
+ }).sort();
895
+ }
896
+ function cleanupInventoryErrorsForTarget(inventory, target) {
897
+ return inventory.errors.filter((error) => {
898
+ const relevance = error.relevance;
899
+ return relevance.global || ((relevance.sourceId === undefined || relevance.sourceId === target.sourceId)
900
+ && (relevance.pluginName === undefined || relevance.pluginName === target.pluginName)
901
+ && (relevance.platform === undefined || relevance.platform === target.platform)
902
+ && (relevance.scope === undefined || relevance.scope === target.scope));
903
+ });
904
+ }
905
+ function cleanupInventoryErrorsForSelection(options) {
906
+ const selectors = options.selectors.map(parsePluginId);
907
+ return options.inventory.errors.filter((error) => {
908
+ const relevance = error.relevance;
909
+ if (relevance.global)
910
+ return true;
911
+ if (relevance.scope !== undefined && relevance.scope !== options.scope)
912
+ return false;
913
+ if (relevance.platform !== undefined && !options.platforms.includes(relevance.platform))
914
+ return false;
915
+ if (options.all)
916
+ return true;
917
+ return selectors.some((selector) => (relevance.pluginName === undefined || relevance.pluginName === selector.pluginName)
918
+ && (selector.sourceId === undefined
919
+ || relevance.sourceId === undefined
920
+ || relevance.sourceId === selector.sourceId));
921
+ });
922
+ }
923
+ function cleanupActionCoversLocation(evidence, location, env, cwd) {
924
+ const rawRepoRoot = resolveRepoRootSync(cwd);
925
+ const sensitiveRoots = [
926
+ canonicalizePathThroughExistingAncestorSync(getCodexHome(env)),
927
+ canonicalizePathThroughExistingAncestorSync(getClaudeHome(env)),
928
+ canonicalizePathThroughExistingAncestorSync(rawRepoRoot),
929
+ getCodexHome(env),
930
+ getClaudeHome(env),
931
+ rawRepoRoot,
932
+ ];
933
+ const normalizedLocation = location.replaceAll('\\', '/');
934
+ return evidence?.plan.actions.some((action) => {
935
+ if (action.action !== 'removeOwnedOutput')
936
+ return false;
937
+ const normalizedTarget = sanitizeCatalogSourceErrorMessage(action.target, env, sensitiveRoots).replaceAll('\\', '/');
938
+ const relative = path.posix.relative(normalizedTarget, normalizedLocation);
939
+ return relative === ''
940
+ || (relative !== '..' && !relative.startsWith('../') && !path.posix.isAbsolute(relative));
941
+ }) ?? false;
942
+ }
943
+ function cleanupLocatorPaths(component) {
944
+ const locator = component.locator;
945
+ switch (locator.kind) {
946
+ case 'claude-permissions': return [locator.settingsPath];
947
+ case 'codex-instruction': return [locator.configPath];
948
+ case 'codex-activation': return [locator.configPath];
949
+ case 'legacy-codex-mcp': return [locator.configPath];
950
+ case 'legacy-codex-hook': return [locator.hooksPath];
951
+ case 'codex-role-registration': return [locator.configPath, locator.rolePath];
952
+ case 'codex-marketplace':
953
+ case 'claude-marketplace': return [locator.marketplaceRoot];
954
+ case 'corrupted': return [];
955
+ }
956
+ }
957
+ function cleanupPathAuthorized(pathValue, evidence) {
958
+ const normalizedPath = canonicalizePathThroughExistingAncestorSync(pathValue);
959
+ return evidence.plan.actions.some((action) => {
960
+ if (action.action !== 'removeOwnedOutput')
961
+ return false;
962
+ const actionPath = action.target.split('#', 1)[0];
963
+ const normalizedActionPath = canonicalizePathThroughExistingAncestorSync(actionPath);
964
+ const relative = path.relative(normalizedPath, normalizedActionPath);
965
+ return relative === ''
966
+ || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
967
+ });
968
+ }
969
+ function cleanupComponentAuthorized(component, evidence) {
970
+ if ('path' in component)
971
+ return cleanupPathAuthorized(component.path, evidence);
972
+ if (component.kind === 'native-plugin') {
973
+ return evidence.plan.actions.some((action) => action.action === 'removeNativeRegistration' && action.target === component.pluginId);
974
+ }
975
+ const locator = component.locator;
976
+ if (locator.kind === 'codex-activation') {
977
+ return evidence.plan.actions.some((action) => action.action === 'removeNativeRegistration'
978
+ && action.target === locator.codexPluginId);
979
+ }
980
+ if (locator.kind === 'codex-marketplace'
981
+ || locator.kind === 'claude-marketplace') {
982
+ return evidence.plan.actions.some((action) => action.action === 'removeNativeRegistration'
983
+ && action.marketplaceName === locator.marketplaceName);
984
+ }
985
+ if (evidence.plan.actions.some((action) => action.action === 'removeOwnedOutput' && action.target === component.label))
986
+ return true;
987
+ return cleanupLocatorPaths(component).some((candidate) => cleanupPathAuthorized(candidate, evidence));
988
+ }
989
+ function cleanupComponentResidual(component) {
990
+ if ('path' in component)
991
+ return component.path;
992
+ if (component.kind === 'native-plugin')
993
+ return component.pluginId;
994
+ return component.label;
995
+ }
770
996
  function resolveLiveCleanupTargets(options) {
771
997
  const visible = options.references.filter((reference) => (reference.kind === 'namespacedOwner'
772
998
  || reference.kind === 'namespacedMarketplace')
@@ -857,84 +1083,145 @@ async function cleanupOfflineOwnedComponents(options) {
857
1083
  };
858
1084
  const integration = createRebuildIntegration({ context });
859
1085
  for (const target of options.targets) {
860
- const inventoryErrors = (options.inventoryErrors ?? []).filter((error) => error.relevance.global || ((error.relevance.sourceId === undefined || error.relevance.sourceId === target.sourceId)
861
- && (error.relevance.pluginName === undefined || error.relevance.pluginName === target.pluginName)
862
- && (error.relevance.platform === undefined || error.relevance.platform === target.platform)
863
- && (error.relevance.scope === undefined || error.relevance.scope === target.scope)));
864
- const evidence = options.evidence?.find((candidate) => candidate.pluginId === target.pluginId
865
- && candidate.sourceIdentity === target.sourceIdentity
866
- && candidate.platform === target.platform
867
- && candidate.scope === target.scope);
868
- const activationAction = target.platform === 'codex'
869
- ? evidence?.plan.actions.find((action) => action.action === 'removeNativeRegistration'
870
- && action.activationScope === target.scope)
871
- : undefined;
872
- let activationDisposition = 'remove';
873
- if (activationAction && target.scope === 'user') {
874
- activationDisposition = 'disable';
875
- }
876
- const plugin = createSyntheticPlugin(target.pluginName, options.env);
877
- plugin.sourceId = target.sourceId;
878
- plugin.sourceIdentity = target.sourceIdentity;
879
- plugin.pluginId = target.pluginId;
880
- const marketplaceName = target.marketplaceName ?? 'aw';
881
- plugin.marketplaceName = marketplaceName;
882
- const desired = await integration.buildTarget({
883
- sourceId: target.sourceId,
884
- sourceIdentity: target.sourceIdentity,
885
- pluginId: target.pluginId,
886
- marketplaceName,
887
- marketplaceRoot: path.join(resolveAwHome(options.env), 'marketplaces', target.sourceId),
888
- plugin,
889
- platform: target.platform,
890
- scope: target.scope,
891
- cwd: options.cwd,
892
- command: 'cleanup',
893
- catalog: {
894
- rootDir: resolveAwHome(options.env),
895
- source: 'local',
896
- catalogVersion: '0.0.0',
897
- channel: 'beta',
898
- },
899
- includeNativeCleanupCandidate: target.nativeRegistration === true,
900
- preserveCodexSharedNativePayload: target.platform === 'codex',
901
- ...(activationAction
902
- ? {
903
- codexActivationCleanup: [{
904
- codexPluginId: activationAction.target,
905
- scope: activationAction.activationScope,
906
- disposition: activationDisposition,
907
- }],
1086
+ try {
1087
+ const inventoryErrors = (options.inventoryErrors ?? []).filter((error) => error.relevance.global || ((error.relevance.sourceId === undefined || error.relevance.sourceId === target.sourceId)
1088
+ && (error.relevance.pluginName === undefined || error.relevance.pluginName === target.pluginName)
1089
+ && (error.relevance.platform === undefined || error.relevance.platform === target.platform)
1090
+ && (error.relevance.scope === undefined || error.relevance.scope === target.scope)));
1091
+ const evidence = options.evidence?.find((candidate) => candidate.pluginId === target.pluginId
1092
+ && candidate.sourceIdentity === target.sourceIdentity
1093
+ && candidate.platform === target.platform
1094
+ && candidate.scope === target.scope);
1095
+ const activationAction = target.platform === 'codex'
1096
+ ? evidence?.plan.actions.find((action) => action.action === 'removeNativeRegistration'
1097
+ && action.activationScope === target.scope)
1098
+ : undefined;
1099
+ let activationDisposition = 'remove';
1100
+ if (activationAction && target.scope === 'user') {
1101
+ activationDisposition = 'disable';
1102
+ }
1103
+ const plugin = createSyntheticPlugin(target.pluginName, options.env);
1104
+ plugin.sourceId = target.sourceId;
1105
+ plugin.sourceIdentity = target.sourceIdentity;
1106
+ plugin.pluginId = target.pluginId;
1107
+ const marketplaceName = target.marketplaceName ?? 'aw';
1108
+ plugin.marketplaceName = marketplaceName;
1109
+ const desired = await integration.buildTarget({
1110
+ sourceId: target.sourceId,
1111
+ sourceIdentity: target.sourceIdentity,
1112
+ pluginId: target.pluginId,
1113
+ marketplaceName,
1114
+ marketplaceRoot: path.join(resolveAwHome(options.env), 'marketplaces', target.sourceId),
1115
+ plugin,
1116
+ platform: target.platform,
1117
+ scope: target.scope,
1118
+ cwd: options.cwd,
1119
+ command: 'cleanup',
1120
+ catalog: {
1121
+ rootDir: resolveAwHome(options.env),
1122
+ source: 'local',
1123
+ catalogVersion: '0.0.0',
1124
+ channel: 'beta',
1125
+ },
1126
+ includeNativeCleanupCandidate: target.nativeRegistration === true,
1127
+ preserveCodexSharedNativePayload: target.platform === 'codex',
1128
+ ...(activationAction
1129
+ ? {
1130
+ codexActivationCleanup: [{
1131
+ codexPluginId: activationAction.target,
1132
+ scope: activationAction.activationScope,
1133
+ disposition: activationDisposition,
1134
+ }],
1135
+ }
1136
+ : {}),
1137
+ });
1138
+ const unplannedCandidates = evidence
1139
+ ? desired.ownedComponents.filter((component) => !cleanupComponentAuthorized(component, evidence))
1140
+ : [];
1141
+ const unplannedComponents = unplannedCandidates.length > 0
1142
+ ? (await inspectOwnedComponents({
1143
+ components: unplannedCandidates,
1144
+ executor: integration.driver.ownership,
1145
+ })).components.flatMap((entry) => entry.state === 'present' ? [entry.component] : [])
1146
+ : [];
1147
+ if (evidence) {
1148
+ const authorized = (component) => cleanupComponentAuthorized(component, evidence);
1149
+ desired.ownedComponents = desired.ownedComponents.filter(authorized);
1150
+ desired.desiredOwnedComponents = desired.desiredOwnedComponents?.filter(authorized);
1151
+ desired.optionalOwnedComponents = desired.optionalOwnedComponents?.filter(authorized);
1152
+ desired.artifacts = desired.artifacts.filter((artifact) => cleanupPathAuthorized(artifact.path, evidence));
1153
+ const nativeAuthorized = evidence.plan.actions.some((action) => action.action === 'removeNativeRegistration');
1154
+ desired.requiresNative &&= nativeAuthorized;
1155
+ desired.writeNativeRequired &&= nativeAuthorized;
1156
+ desired.operationActions = desired.operationActions?.filter((action) => {
1157
+ if (action.kind === 'removeOwnedArtifact') {
1158
+ return cleanupPathAuthorized(action.path, evidence);
1159
+ }
1160
+ if (action.kind === 'removeNativeRegistration') {
1161
+ return evidence.plan.actions.some((planned) => planned.action === 'removeNativeRegistration'
1162
+ && planned.target === action.registration);
1163
+ }
1164
+ if (action.kind === 'setActivation')
1165
+ return nativeAuthorized;
1166
+ return false;
1167
+ });
1168
+ }
1169
+ const result = await (options.executeCleanupOperation ?? executeRebuildOperation)({
1170
+ command: 'cleanup',
1171
+ target: desired,
1172
+ driver: integration.driver,
1173
+ });
1174
+ if (target.marketplaceIdentityUnresolved) {
1175
+ result.residual.push('nativeMarketplaceIdentityUnresolved');
1176
+ warnings.push(`nativeMarketplaceIdentityUnresolved: Preserved ambiguous native registration for ${target.pluginId} on ${target.platform}/${target.scope}.`);
1177
+ }
1178
+ let finalResult = result;
1179
+ if (unplannedComponents.length > 0) {
1180
+ const message = 'operationPlanResidual: Newly discovered owned output was not included in the cleanup plan and was preserved.';
1181
+ result.residual = [...new Set([
1182
+ ...result.residual,
1183
+ ...unplannedComponents.map(cleanupComponentResidual),
1184
+ ])];
1185
+ warnings.push(message);
1186
+ if (result.status === 'succeeded') {
1187
+ finalResult = {
1188
+ ...result,
1189
+ status: 'failed',
1190
+ failures: [...result.failures, { phase: 'cleanup.owned', message }],
1191
+ error: { phase: 'cleanup.owned', message },
1192
+ };
908
1193
  }
909
- : {}),
910
- });
911
- const result = await executeRebuildOperation({
912
- command: 'cleanup',
913
- target: desired,
914
- driver: integration.driver,
915
- });
916
- if (target.marketplaceIdentityUnresolved) {
917
- result.residual.push('nativeMarketplaceIdentityUnresolved');
918
- warnings.push(`nativeMarketplaceIdentityUnresolved: Preserved ambiguous native registration for ${target.pluginId} on ${target.platform}/${target.scope}.`);
919
- }
920
- let finalResult = result;
921
- if (inventoryErrors.length > 0) {
922
- const message = 'stateInventoryResidual: Some weak state could not be verified. Retry cleanup, use cleanup --force only for exclusively owned damaged output, or run reinstall/sync to restore the target.';
923
- result.residual = [...new Set([
924
- ...result.residual,
925
- ...inventoryErrors.map((error) => error.location),
926
- ])];
927
- warnings.push(...inventoryErrors.map((error) => `stateInventoryResidual: ${error.kind}: ${error.message}`));
928
- if (result.status === 'succeeded') {
929
- finalResult = {
930
- ...result,
931
- status: 'failed',
932
- failures: [...result.failures, { phase: 'cleanup.owned', message }],
933
- error: { phase: 'cleanup.owned', message },
934
- };
935
1194
  }
1195
+ if (inventoryErrors.length > 0) {
1196
+ const message = 'stateInventoryResidual: Some weak state could not be verified. Retry cleanup, use cleanup --force only for exclusively owned damaged output, or run reinstall/sync to restore the target.';
1197
+ result.residual = [...new Set([
1198
+ ...result.residual,
1199
+ ...inventoryErrors.map((error) => error.location),
1200
+ ])];
1201
+ warnings.push(...inventoryErrors.map((error) => `stateInventoryResidual: ${error.kind}: ${error.message}`));
1202
+ if (finalResult.status === 'succeeded') {
1203
+ finalResult = {
1204
+ ...finalResult,
1205
+ status: 'failed',
1206
+ failures: [...finalResult.failures, { phase: 'cleanup.owned', message }],
1207
+ error: { phase: 'cleanup.owned', message },
1208
+ };
1209
+ }
1210
+ }
1211
+ results.push(finalResult);
1212
+ }
1213
+ catch (error) {
1214
+ results.push(failedResult('cleanup', {
1215
+ pluginId: target.pluginId,
1216
+ pluginName: target.pluginName,
1217
+ sourceId: target.sourceId,
1218
+ sourceIdentity: target.sourceIdentity,
1219
+ pluginVersion: '0.0.0',
1220
+ platform: target.platform,
1221
+ scope: target.scope,
1222
+ cwd: options.cwd,
1223
+ }, error instanceof Error ? error.message : String(error), 'cleanup.owned'));
936
1224
  }
937
- results.push(finalResult);
938
1225
  }
939
1226
  const obsoleteContracts = path.join(resolveAwHome(options.env), 'installation-contracts');
940
1227
  try {
@@ -973,9 +1260,16 @@ async function previewOfflineOwnedComponents(options) {
973
1260
  await options.onPreviewStage?.('dependencyPlan');
974
1261
  return preview;
975
1262
  }
976
- function createOfflineCleanupOperationPlan(evidence) {
1263
+ function createOfflineCleanupOperationPlan(evidence, intent, warnings = []) {
1264
+ const beforeDigest = intent ? calculatePluginIntentDigest(intent.before) : undefined;
1265
+ const afterDigest = intent ? calculatePluginIntentDigest(intent.after) : undefined;
1266
+ const removesIntent = (target) => intent !== undefined
1267
+ && intent.before.roots.some((root) => root.pluginId === target.pluginId && root.platforms.includes(target.platform))
1268
+ && !intent.after.roots.some((root) => root.pluginId === target.pluginId && root.platforms.includes(target.platform));
977
1269
  return createOperationPlan({
978
- intentChanges: [],
1270
+ intentChanges: intent && beforeDigest !== afterDigest
1271
+ ? [{ scope: intent.scope, beforeDigest: beforeDigest, afterDigest: afterDigest }]
1272
+ : [],
979
1273
  actions: evidence.flatMap((target) => target.plan.actions.map((action) => action.action === 'removeNativeRegistration'
980
1274
  ? {
981
1275
  kind: 'removeNativeRegistration',
@@ -992,14 +1286,19 @@ function createOfflineCleanupOperationPlan(evidence) {
992
1286
  path: action.target,
993
1287
  ownerFingerprint: target.targetFingerprint,
994
1288
  })),
995
- changes: evidence.flatMap((target) => target.plan.actions.map((action) => `${target.pluginId}:${action.action}:${action.target}`)),
996
- warnings: [],
1289
+ changes: [
1290
+ ...evidence.flatMap((target) => removesIntent(target)
1291
+ ? [`${target.pluginId}:${target.platform}:${target.scope}:removeIntent`]
1292
+ : []),
1293
+ ...evidence.flatMap((target) => target.plan.actions.map((action) => `${target.pluginId}:${target.platform}:${target.scope}:${action.action}:${action.target}`)),
1294
+ ],
1295
+ warnings: [...warnings],
997
1296
  evidence: {
998
1297
  catalogDigests: evidence.map((target) => ({
999
1298
  sourceId: target.sourceId,
1000
1299
  digest: target.sourceIdentity,
1001
1300
  })),
1002
- intentDigests: [],
1301
+ intentDigests: intent ? [{ scope: intent.scope, digest: beforeDigest }] : [],
1003
1302
  configDigests: [],
1004
1303
  targetOwnerFingerprints: evidence.map((target) => ({
1005
1304
  pluginId: target.pluginId,
@@ -1244,15 +1543,51 @@ async function runRebuildCommandInSession(options) {
1244
1543
  ?? inventoryCatalogSourceReferences;
1245
1544
  let ordinaryInventory;
1246
1545
  const readOrdinaryInventory = async () => {
1247
- ordinaryInventory ??= await measureCommandPerformanceStage(options.runtime, 'inventory', () => readInventory({
1248
- cwd: options.commandOptions.cwd,
1249
- env: runtimeEnv,
1250
- }));
1546
+ ordinaryInventory ??= await measureCommandPerformanceStage(options.runtime, 'inventory', async () => {
1547
+ const inventory = await readInventory({
1548
+ cwd: options.commandOptions.cwd,
1549
+ env: runtimeEnv,
1550
+ });
1551
+ if (options.command !== 'cleanup'
1552
+ || options.commandOptions.scope !== 'project'
1553
+ || !selectedPlatforms(options.commandOptions.platform).includes('codex'))
1554
+ return inventory;
1555
+ let projectSkills = cleanupProjectSkillsByInventory.get(inventory);
1556
+ if (!projectSkills) {
1557
+ projectSkills = await selectedProjectCodexSkillReferences({
1558
+ cwd: options.commandOptions.cwd,
1559
+ });
1560
+ cleanupProjectSkillsByInventory.set(inventory, projectSkills);
1561
+ }
1562
+ const selectors = options.pluginNames.map(parsePluginId);
1563
+ const selectedProjectSkills = options.all === true
1564
+ ? projectSkills
1565
+ : projectSkills.filter((reference) => selectors.some((selector) => selector.pluginName === reference.pluginName
1566
+ && (selector.sourceId === undefined || selector.sourceId === reference.sourceId)));
1567
+ return {
1568
+ ...inventory,
1569
+ references: [...new Map([...inventory.references, ...selectedProjectSkills].map((reference) => [[
1570
+ reference.kind,
1571
+ reference.location,
1572
+ reference.sourceId ?? '',
1573
+ reference.pluginName ?? '',
1574
+ reference.platform ?? '',
1575
+ reference.scope ?? '',
1576
+ ].join('\0'), reference])).values()],
1577
+ };
1578
+ });
1251
1579
  return ordinaryInventory;
1252
1580
  };
1253
1581
  let requestedPluginNames = options.pluginNames;
1254
1582
  const cleanupForce = options.command === 'cleanup'
1255
1583
  && options.commandOptions.force === true;
1584
+ const cleanupIntentBefore = options.command === 'cleanup'
1585
+ ? await readPluginIntentState({
1586
+ scope: options.commandOptions.scope,
1587
+ cwd: options.commandOptions.cwd,
1588
+ env: runtimeEnv,
1589
+ })
1590
+ : undefined;
1256
1591
  const expectedOfflineCleanup = options.runtime?.expectedOfflineCleanupEvidence;
1257
1592
  let cleanupInventory;
1258
1593
  let cleanupLiveTargets = [];
@@ -1269,7 +1604,9 @@ async function runRebuildCommandInSession(options) {
1269
1604
  });
1270
1605
  requestedPluginNames = live.selectors;
1271
1606
  cleanupLiveTargets = live.targets;
1272
- if (expectedOfflineCleanup && expectedOfflineCleanup.forceExclusive !== true) {
1607
+ if (expectedOfflineCleanup
1608
+ && expectedOfflineCleanup.forceExclusive !== true
1609
+ && expectedOfflineCleanup.plan.actions.length > 0) {
1273
1610
  const liveTarget = cleanupLiveTargets.find((target) => target.pluginId === expectedOfflineCleanup.pluginId
1274
1611
  && target.pluginName === expectedOfflineCleanup.pluginName
1275
1612
  && target.sourceId === expectedOfflineCleanup.sourceId
@@ -1338,17 +1675,96 @@ async function runRebuildCommandInSession(options) {
1338
1675
  const pluginId = formatPluginId({ sourceId: reference.sourceId, pluginName: reference.pluginName });
1339
1676
  identities.add(pluginId);
1340
1677
  }
1678
+ if (options.command === 'cleanup') {
1679
+ for (const root of cleanupIntentBefore.roots)
1680
+ identities.add(root.pluginId);
1681
+ }
1341
1682
  requestedPluginNames = [...identities];
1342
1683
  }
1684
+ let cleanupTargets = [];
1685
+ let cleanupIntentAfter;
1686
+ let cleanupSelectionErrors = [];
1687
+ if (options.command === 'cleanup') {
1688
+ const primarySourceId = cleanupForce
1689
+ ? await readCatalogSourcesConfigSnapshot(runtimeEnv)
1690
+ .then((snapshot) => snapshot.config.sources.find((source) => source.primary)?.id)
1691
+ .catch(() => undefined)
1692
+ : undefined;
1693
+ const intentTargets = cleanupIntentTargets({
1694
+ state: cleanupIntentBefore,
1695
+ selectors: options.pluginNames,
1696
+ platforms: selectedPlatforms(options.commandOptions.platform),
1697
+ scope: options.commandOptions.scope,
1698
+ all: options.all === true,
1699
+ liveTargets: cleanupLiveTargets,
1700
+ primarySourceId,
1701
+ });
1702
+ const damagedTargets = options.pluginNames.flatMap((selector) => {
1703
+ const parsed = parsePluginId(selector);
1704
+ if (!parsed.sourceId)
1705
+ return [];
1706
+ return selectedPlatforms(options.commandOptions.platform).flatMap((platform) => cleanupInventory.errors.some((error) => error.relevance.global || ((error.relevance.sourceId === undefined || error.relevance.sourceId === parsed.sourceId)
1707
+ && (error.relevance.pluginName === undefined || error.relevance.pluginName === parsed.pluginName)
1708
+ && (error.relevance.platform === undefined || error.relevance.platform === platform)
1709
+ && (error.relevance.scope === undefined || error.relevance.scope === options.commandOptions.scope))) ? [{
1710
+ pluginId: formatPluginId(parsed),
1711
+ pluginName: parsed.pluginName,
1712
+ sourceId: parsed.sourceId,
1713
+ sourceIdentity: 'unavailable',
1714
+ platform,
1715
+ scope: options.commandOptions.scope,
1716
+ }] : []);
1717
+ });
1718
+ cleanupTargets = [...new Map([...damagedTargets, ...intentTargets, ...cleanupLiveTargets].map((target) => [
1719
+ `${target.pluginId}\0${target.platform}\0${target.scope}`,
1720
+ target,
1721
+ ])).values()];
1722
+ requestedPluginNames = [...new Set([
1723
+ ...requestedPluginNames,
1724
+ ...cleanupTargets.map((target) => target.pluginId),
1725
+ ])];
1726
+ cleanupIntentAfter = deriveDesiredIntentState({
1727
+ command: 'cleanup',
1728
+ state: cleanupIntentBefore,
1729
+ selectedRoots: cleanupSelectedRoots(cleanupTargets),
1730
+ plan: { requestedPluginIds: [], resolvedPluginIds: [], dependencyEdges: [], targets: [], scopeOverrides: [] },
1731
+ });
1732
+ const filtered = omitSelectedPluginConfigErrors(cleanupInventory, cleanupTargets);
1733
+ cleanupInventory = filtered.inventory;
1734
+ malformedSelectedConfigPreserved ||= filtered.omitted;
1735
+ cleanupSelectionErrors = cleanupInventoryErrorsForSelection({
1736
+ inventory: cleanupInventory,
1737
+ selectors: options.pluginNames,
1738
+ platforms: selectedPlatforms(options.commandOptions.platform),
1739
+ scope: options.commandOptions.scope,
1740
+ all: options.all === true,
1741
+ });
1742
+ }
1343
1743
  const selectors = requestedPluginNames.map(parsePluginId);
1344
1744
  const sourceIds = planPluginSelectorSourceIds(selectors);
1345
- if (options.all && selectors.length === 0) {
1745
+ if (options.all && selectors.length === 0 && options.command !== 'cleanup') {
1346
1746
  if (legacyLiveTargets.length > 0) {
1347
1747
  throw namespacedPreflightError(`legacyStateUnsupported: legacy live runtime state exists for ${legacyLiveTargets.map((target) => `${target.pluginName}:${target.platform}/${target.scope}`).join(', ')}.`, [], legacyLiveTargets);
1348
1748
  }
1349
1749
  return mergeRebuildCommandResults([]);
1350
1750
  }
1351
- if (options.command === 'cleanup' && !options.all) {
1751
+ if (options.command === 'cleanup') {
1752
+ if (!cleanupForce
1753
+ && cleanupTargets.length === 0
1754
+ && cleanupSelectionErrors.length === 0
1755
+ && legacyLiveTargets.length === 0) {
1756
+ const result = mergeRebuildCommandResults([]);
1757
+ const operationPlan = createOfflineCleanupOperationPlan([], {
1758
+ scope: options.commandOptions.scope,
1759
+ before: cleanupIntentBefore,
1760
+ after: cleanupIntentAfter,
1761
+ }, cleanupInventoryWarnings(cleanupInventory, []));
1762
+ if (options.runtime?.expectedPlanFingerprint) {
1763
+ assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1764
+ }
1765
+ result.operationPlan = operationPlan;
1766
+ return result;
1767
+ }
1352
1768
  if (cleanupForce) {
1353
1769
  const requestedPlatforms = selectedPlatforms(options.commandOptions.platform);
1354
1770
  const cleanupSourceSnapshot = await readCatalogSourcesConfigSnapshot(runtimeEnv).catch(() => undefined);
@@ -1379,18 +1795,27 @@ async function runRebuildCommandInSession(options) {
1379
1795
  && (error.relevance.scope === undefined || error.relevance.scope === options.commandOptions.scope));
1380
1796
  return missingLiveTarget || damagedExclusiveEvidence;
1381
1797
  });
1382
- const supplement = selectorsNeedingSupplement.length > 0
1383
- ? await buildForceExclusiveCleanupPlan({
1384
- selectors: selectorsNeedingSupplement,
1385
- platforms: requestedPlatforms,
1386
- scope: options.commandOptions.scope,
1387
- cwd: options.commandOptions.cwd,
1388
- env: runtimeEnv,
1389
- primarySourceId: primarySource,
1390
- damagedOnly: cleanupLiveTargets.length > 0,
1391
- })
1392
- : { targets: [], evidence: [] };
1393
- const forcePlan = await mergeForceCleanupPlans(livePlan, supplement);
1798
+ const supplementAttempts = await Promise.allSettled(selectorsNeedingSupplement.map((selector) => buildForceExclusiveCleanupPlan({
1799
+ selectors: [selector],
1800
+ platforms: requestedPlatforms,
1801
+ scope: options.commandOptions.scope,
1802
+ cwd: options.commandOptions.cwd,
1803
+ env: runtimeEnv,
1804
+ primarySourceId: primarySource,
1805
+ damagedOnly: cleanupLiveTargets.length > 0,
1806
+ })));
1807
+ const forcePlan = await mergeForceCleanupPlans(livePlan, ...supplementAttempts.flatMap((attempt) => attempt.status === 'fulfilled' ? [attempt.value] : []));
1808
+ const forceCleanupTargets = [...new Map([
1809
+ ...cleanupTargets,
1810
+ ...forcePlan.targets.map((target) => ({
1811
+ pluginId: target.identity.pluginId,
1812
+ pluginName: target.identity.pluginName,
1813
+ sourceId: target.identity.sourceId,
1814
+ sourceIdentity: target.identity.sourceIdentity,
1815
+ platform: target.identity.platform,
1816
+ scope: target.identity.scope,
1817
+ })),
1818
+ ].map((target) => [`${target.pluginId}\0${target.platform}\0${target.scope}`, target])).values()];
1394
1819
  const requestedTargets = requestedPluginNames.flatMap((selector) => {
1395
1820
  const parsed = parsePluginId(selector);
1396
1821
  const sourceId = parsed.sourceId ?? primarySource;
@@ -1405,7 +1830,11 @@ async function runRebuildCommandInSession(options) {
1405
1830
  });
1406
1831
  const unresolvedTargets = requestedTargets.filter((requested) => !forcePlan.targets.some((target) => target.identity.pluginId === requested.pluginId
1407
1832
  && target.identity.platform === requested.platform
1408
- && target.identity.scope === requested.scope));
1833
+ && target.identity.scope === requested.scope)
1834
+ && cleanupInventory.errors.some((error) => error.relevance.global || ((error.relevance.sourceId === undefined || error.relevance.sourceId === requested.sourceId)
1835
+ && (error.relevance.pluginName === undefined || error.relevance.pluginName === requested.pluginName)
1836
+ && (error.relevance.platform === undefined || error.relevance.platform === requested.platform)
1837
+ && (error.relevance.scope === undefined || error.relevance.scope === requested.scope))));
1409
1838
  if (expectedOfflineCleanup?.forceExclusive === true) {
1410
1839
  const actual = forcePlan.evidence.find((candidate) => candidate.pluginId === expectedOfflineCleanup.pluginId
1411
1840
  && candidate.platform === expectedOfflineCleanup.platform
@@ -1414,21 +1843,23 @@ async function runRebuildCommandInSession(options) {
1414
1843
  throw namespacedPreflightError('operationReviewStale: force cleanup path or ownership evidence changed before mutation.', cleanupLiveTargets);
1415
1844
  }
1416
1845
  }
1417
- if (unresolvedTargets.length > 0) {
1418
- throw namespacedPreflightError('forceCleanupPathUnavailable: No exact exclusive output is available from live evidence or the local Active shallow plan. Restore the local Active bundle, then retry cleanup --force, or inspect and remove the exact output manually.', unresolvedTargets);
1419
- }
1420
- if (forcePlan.targets.length === 0) {
1421
- throw namespacedPreflightError('forceCleanupPathUnavailable: No exact exclusive output is available from live evidence or the local Active shallow plan. Restore the local Active bundle, then retry cleanup --force, or inspect and remove the exact output manually.', requestedTargets);
1422
- }
1423
- const operationPlan = createOfflineCleanupOperationPlan(forcePlan.evidence);
1846
+ const forceEvidence = forceCleanupTargets.map((target) => forcePlan.evidence.find((candidate) => candidate.pluginId === target.pluginId
1847
+ && candidate.platform === target.platform
1848
+ && candidate.scope === target.scope)
1849
+ ?? intentOnlyOfflineCleanupEvidence(target));
1850
+ const operationPlan = createOfflineCleanupOperationPlan(forceEvidence, {
1851
+ scope: options.commandOptions.scope,
1852
+ before: cleanupIntentBefore,
1853
+ after: cleanupIntentAfter,
1854
+ }, cleanupInventoryWarnings(cleanupInventory, forceCleanupTargets));
1424
1855
  if (options.runtime?.expectedPlanFingerprint) {
1425
1856
  assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1426
1857
  }
1427
1858
  if (options.runtime?.previewOperationPlan) {
1428
1859
  return {
1429
1860
  ...await previewOfflineOwnedComponents({
1430
- targets: cleanupLiveTargets,
1431
- evidence: forcePlan.evidence,
1861
+ targets: forceCleanupTargets,
1862
+ evidence: forceEvidence,
1432
1863
  cwd: options.commandOptions.cwd,
1433
1864
  env: runtimeEnv,
1434
1865
  onPreviewStage: options.runtime.onPreviewStage,
@@ -1436,6 +1867,14 @@ async function runRebuildCommandInSession(options) {
1436
1867
  operationPlan,
1437
1868
  };
1438
1869
  }
1870
+ if (calculatePluginIntentDigest(cleanupIntentBefore)
1871
+ !== calculatePluginIntentDigest(cleanupIntentAfter)) {
1872
+ await (options.runtime?.writePluginIntentState ?? writePluginIntentState)({
1873
+ scope: options.commandOptions.scope,
1874
+ cwd: options.commandOptions.cwd,
1875
+ env: runtimeEnv,
1876
+ }, cleanupIntentAfter);
1877
+ }
1439
1878
  const forceResults = await Promise.all(forcePlan.targets.map(async (target) => {
1440
1879
  const integration = createRebuildIntegration({
1441
1880
  context: {
@@ -1446,86 +1885,89 @@ async function runRebuildCommandInSession(options) {
1446
1885
  },
1447
1886
  });
1448
1887
  const { finalizeCleanup: _finalizeCleanup, ...pathOnlyDriver } = integration.driver;
1449
- const result = await executeRebuildOperation({ command: 'cleanup', target, driver: pathOnlyDriver });
1888
+ const result = await (options.runtime?.executeCleanupOperation ?? executeRebuildOperation)({
1889
+ command: 'cleanup', target, driver: pathOnlyDriver,
1890
+ });
1450
1891
  if (cleanupLiveTargets.some((liveTarget) => liveTarget.marketplaceIdentityUnresolved
1451
1892
  && liveTarget.pluginId === result.target.pluginId
1452
1893
  && liveTarget.platform === result.target.platform
1453
1894
  && liveTarget.scope === result.target.scope)) {
1454
1895
  result.residual.push('nativeMarketplaceIdentityUnresolved');
1455
1896
  }
1897
+ const targetEvidence = forceEvidence.find((candidate) => candidate.pluginId === target.identity.pluginId
1898
+ && candidate.platform === target.identity.platform
1899
+ && candidate.scope === target.identity.scope);
1900
+ const targetDescriptor = forceCleanupTargets.find((candidate) => candidate.pluginId === target.identity.pluginId
1901
+ && candidate.platform === target.identity.platform
1902
+ && candidate.scope === target.identity.scope);
1903
+ const unresolvedErrors = cleanupInventoryErrorsForTarget(cleanupInventory, targetDescriptor).filter((error) => !cleanupActionCoversLocation(targetEvidence, error.location, runtimeEnv, options.commandOptions.cwd));
1904
+ if (unresolvedErrors.length > 0) {
1905
+ const message = 'stateInventoryResidual: Some weak state could not be verified after force cleanup.';
1906
+ result.residual = [...new Set([
1907
+ ...result.residual,
1908
+ ...unresolvedErrors.map((error) => error.location),
1909
+ ])];
1910
+ if (result.status === 'succeeded') {
1911
+ return {
1912
+ ...result,
1913
+ status: 'failed',
1914
+ failures: [...result.failures, { phase: 'cleanup.owned', message }],
1915
+ error: { phase: 'cleanup.owned', message },
1916
+ };
1917
+ }
1918
+ }
1456
1919
  return result;
1457
1920
  }));
1921
+ const unresolvedResults = unresolvedTargets.map((target) => failedResult('cleanup', { ...target, pluginVersion: '0.0.0', cwd: options.commandOptions.cwd }, 'forceCleanupPathUnavailable: No exact exclusive output is available from live evidence or the local Active shallow plan. Restore the local Active bundle, then retry cleanup --force, or inspect and remove the exact output manually.'));
1922
+ const intentOnlyResults = forceCleanupTargets
1923
+ .filter((target) => !forcePlan.targets.some((forceTarget) => forceTarget.identity.pluginId === target.pluginId
1924
+ && forceTarget.identity.platform === target.platform
1925
+ && forceTarget.identity.scope === target.scope)
1926
+ && !unresolvedTargets.some((unresolved) => unresolved.pluginId === target.pluginId
1927
+ && unresolved.platform === target.platform
1928
+ && unresolved.scope === target.scope))
1929
+ .map(cleanupIntentResult);
1930
+ const allResults = [...forceResults, ...unresolvedResults, ...intentOnlyResults];
1458
1931
  const warnings = cleanupLiveTargets.flatMap((target) => target.marketplaceIdentityUnresolved
1459
1932
  ? [`nativeMarketplaceIdentityUnresolved: Preserved ambiguous native registration for ${target.pluginId} on ${target.platform}/${target.scope}.`]
1460
1933
  : []);
1461
1934
  return leaseSafeRebuildResult({
1462
- platforms: [...new Set(forceResults.map((entry) => entry.target.platform))],
1463
- plugins: forceResults.filter((entry) => entry.status === 'succeeded')
1935
+ platforms: [...new Set(allResults.map((entry) => entry.target.platform))],
1936
+ plugins: allResults.filter((entry) => entry.status === 'succeeded')
1464
1937
  .map((entry) => entry.target.pluginName),
1465
1938
  warnings,
1466
- rebuildResults: forceResults,
1467
- targetResults: forceResults.map(toTargetResult),
1468
- overallState: forceResults.every((entry) => entry.status === 'succeeded')
1939
+ rebuildResults: allResults,
1940
+ targetResults: allResults.map(toTargetResult),
1941
+ overallState: allResults.every((entry) => entry.status === 'succeeded')
1469
1942
  ? 'succeeded'
1470
- : forceResults.some((entry) => entry.status === 'succeeded') ? 'partial' : 'failed',
1471
- receiptTargets: forceResults.map((entry) => entry.target),
1472
- offlineCleanupPlans: forcePlan.evidence,
1943
+ : operationPlan.intentChanges.length > 0
1944
+ || allResults.some((entry) => entry.status === 'succeeded'
1945
+ || entry.actions.some((action) => action.status === 'succeeded'))
1946
+ ? 'partial' : 'failed',
1947
+ receiptTargets: allResults.map((entry) => entry.target),
1948
+ offlineCleanupPlans: forceEvidence,
1473
1949
  operationPlan,
1474
1950
  });
1475
1951
  }
1476
- if (cleanupLiveTargets.length === 0) {
1477
- const requestedPlatforms = selectedPlatforms(options.commandOptions.platform);
1478
- const primarySource = await readCatalogSourcesConfigSnapshot(runtimeEnv)
1479
- .then((snapshot) => snapshot.config.sources.find((source) => source.primary)?.id)
1480
- .catch(() => undefined);
1481
- const unresolvedTargets = requestedPluginNames.flatMap((selector) => {
1482
- const parsed = parsePluginId(selector);
1483
- const sourceId = parsed.sourceId ?? primarySource;
1484
- return sourceId ? requestedPlatforms.map((platform) => ({
1485
- pluginId: formatPluginId({ sourceId, pluginName: parsed.pluginName }),
1486
- pluginName: parsed.pluginName,
1487
- sourceId,
1488
- sourceIdentity: 'unavailable',
1489
- platform,
1490
- scope: options.commandOptions.scope,
1491
- })) : [];
1492
- });
1493
- const qualifiedTargets = requestedPluginNames.flatMap((selector) => {
1494
- const parsed = parsePluginId(selector);
1495
- return parsed.sourceId ? [{ sourceId: parsed.sourceId, pluginName: parsed.pluginName }] : [];
1496
- });
1497
- const filtered = cleanupInventory && qualifiedTargets.length > 0
1498
- ? omitSelectedPluginConfigErrors(cleanupInventory, qualifiedTargets)
1499
- : { inventory: cleanupInventory, omitted: false };
1500
- malformedSelectedConfigPreserved ||= filtered.omitted;
1501
- const inventoryError = filtered.inventory?.errors.find((error) => {
1502
- if (!cleanupForce
1503
- || (error.kind !== 'ownerMarkerRead' && error.kind !== 'ownerMarkerValidation')
1504
- || error.relevance.global)
1505
- return true;
1506
- const relevant = unresolvedTargets.some((target) => !error.relevance.global
1507
- && (error.relevance.sourceId === undefined || error.relevance.sourceId === target.sourceId)
1508
- && (error.relevance.pluginName === undefined || error.relevance.pluginName === target.pluginName)
1509
- && (error.relevance.platform === undefined || error.relevance.platform === target.platform)
1510
- && (error.relevance.scope === undefined || error.relevance.scope === target.scope));
1511
- return relevant
1512
- && !(error.relevance.platform === 'codex' && error.relevance.scope === 'project');
1513
- });
1514
- if (inventoryError) {
1515
- throw namespacedPreflightError(`stateInventoryFailed: ${inventoryError.message}`, unresolvedTargets);
1516
- }
1952
+ if (cleanupTargets.length === 0) {
1517
1953
  const result = mergeRebuildCommandResults([]);
1954
+ const operationPlan = createOfflineCleanupOperationPlan([], {
1955
+ scope: options.commandOptions.scope,
1956
+ before: cleanupIntentBefore,
1957
+ after: cleanupIntentAfter,
1958
+ }, cleanupInventoryWarnings(cleanupInventory, []));
1959
+ if (options.runtime?.expectedPlanFingerprint) {
1960
+ assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1961
+ }
1962
+ result.operationPlan = operationPlan;
1963
+ result.overallState = cleanupSelectionErrors.length > 0 || legacyLiveTargets.length > 0
1964
+ ? 'failed'
1965
+ : 'succeeded';
1966
+ result.warnings.push(...cleanupSelectionErrors.map((error) => `stateInventoryResidual: ${error.kind}: ${error.message}`));
1967
+ result.warnings.push(...legacyLiveTargets.map((target) => `legacyStateUnsupported: legacy live runtime state exists for ${target.pluginName}:${target.platform}/${target.scope}.`));
1518
1968
  if (malformedSelectedConfigPreserved) {
1519
1969
  result.warnings.push(MALFORMED_CONFIG_PRESERVED_WARNING);
1520
1970
  }
1521
- if (cleanupForce && !options.runtime?.previewOperationPlan) {
1522
- try {
1523
- await (options.runtime?.removeObsoleteInstallationContracts ?? fs.remove)(path.join(resolveAwHome(runtimeEnv), 'installation-contracts'));
1524
- }
1525
- catch {
1526
- result.warnings.push(OBSOLETE_CONTRACTS_CLEANUP_WARNING);
1527
- }
1528
- }
1529
1971
  return result;
1530
1972
  }
1531
1973
  const cleanupSourceSnapshot = await readCatalogSourcesConfigSnapshot(runtimeEnv).catch(() => undefined);
@@ -1545,19 +1987,37 @@ async function runRebuildCommandInSession(options) {
1545
1987
  });
1546
1988
  return value ? [value] : [];
1547
1989
  });
1990
+ evidence = cleanupTargets.map((target) => evidence.find((candidate) => candidate.pluginId === target.pluginId
1991
+ && candidate.platform === target.platform
1992
+ && candidate.scope === target.scope)
1993
+ ?? intentOnlyOfflineCleanupEvidence(target));
1994
+ const operationPlan = createOfflineCleanupOperationPlan(evidence, {
1995
+ scope: options.commandOptions.scope,
1996
+ before: cleanupIntentBefore,
1997
+ after: cleanupIntentAfter,
1998
+ }, cleanupInventoryWarnings(cleanupInventory, cleanupTargets));
1548
1999
  if (options.runtime?.expectedPlanFingerprint) {
1549
- assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, createOfflineCleanupOperationPlan(evidence));
2000
+ assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1550
2001
  }
1551
2002
  if (options.runtime?.previewOperationPlan) {
1552
2003
  const previewResult = await previewOfflineOwnedComponents({
1553
- targets: cleanupLiveTargets,
2004
+ targets: cleanupTargets,
1554
2005
  evidence,
1555
2006
  cwd: options.commandOptions.cwd,
1556
2007
  env: runtimeEnv,
1557
2008
  onPreviewStage: options.runtime.onPreviewStage,
1558
2009
  });
2010
+ previewResult.operationPlan = operationPlan;
1559
2011
  return previewResult;
1560
2012
  }
2013
+ if (calculatePluginIntentDigest(cleanupIntentBefore)
2014
+ !== calculatePluginIntentDigest(cleanupIntentAfter)) {
2015
+ await (options.runtime?.writePluginIntentState ?? writePluginIntentState)({
2016
+ scope: options.commandOptions.scope,
2017
+ cwd: options.commandOptions.cwd,
2018
+ env: runtimeEnv,
2019
+ }, cleanupIntentAfter);
2020
+ }
1561
2021
  const cleanupResult = await cleanupOfflineOwnedComponents({
1562
2022
  targets: cleanupLiveTargets,
1563
2023
  evidence,
@@ -1567,7 +2027,32 @@ async function runRebuildCommandInSession(options) {
1567
2027
  removeObsoleteInstallationContracts: options.runtime?.removeObsoleteInstallationContracts,
1568
2028
  nativeCliRunner: options.runtime?.nativeCliRunner,
1569
2029
  codexManager: options.runtime?.codexManager,
2030
+ executeCleanupOperation: options.runtime?.executeCleanupOperation,
2031
+ });
2032
+ const liveKeys = new Set(cleanupLiveTargets.map((target) => `${target.pluginId}\0${target.platform}\0${target.scope}`));
2033
+ const intentOnlyResults = cleanupTargets
2034
+ .filter((target) => !liveKeys.has(`${target.pluginId}\0${target.platform}\0${target.scope}`))
2035
+ .map((target) => {
2036
+ const inventoryError = cleanupInventory.errors.find((error) => error.relevance.global || ((error.relevance.sourceId === undefined || error.relevance.sourceId === target.sourceId)
2037
+ && (error.relevance.pluginName === undefined || error.relevance.pluginName === target.pluginName)
2038
+ && (error.relevance.platform === undefined || error.relevance.platform === target.platform)
2039
+ && (error.relevance.scope === undefined || error.relevance.scope === target.scope)));
2040
+ return inventoryError
2041
+ ? failedResult('cleanup', { ...cleanupIntentResult(target).target, cwd: options.commandOptions.cwd }, `stateInventoryResidual: ${inventoryError.message}`)
2042
+ : cleanupIntentResult(target);
1570
2043
  });
2044
+ cleanupResult.rebuildResults.push(...intentOnlyResults);
2045
+ cleanupResult.targetResults.push(...intentOnlyResults.map(toTargetResult));
2046
+ cleanupResult.receiptTargets.push(...intentOnlyResults.map((result) => result.target));
2047
+ cleanupResult.platforms = [...new Set([...cleanupResult.platforms, ...intentOnlyResults.map((result) => result.target.platform)])];
2048
+ cleanupResult.plugins = [...new Set([...cleanupResult.plugins, ...intentOnlyResults.map((result) => result.target.pluginName)])];
2049
+ cleanupResult.operationPlan = operationPlan;
2050
+ if (cleanupResult.overallState === 'failed'
2051
+ && operationPlan.intentChanges.length > 0)
2052
+ cleanupResult.overallState = 'partial';
2053
+ else if (intentOnlyResults.some((result) => result.status === 'failed')) {
2054
+ cleanupResult.overallState = operationPlan.intentChanges.length > 0 ? 'partial' : 'failed';
2055
+ }
1571
2056
  if (malformedSelectedConfigPreserved) {
1572
2057
  cleanupResult.warnings.push(MALFORMED_CONFIG_PRESERVED_WARNING);
1573
2058
  }