@lesliechan721/aw 0.1.0-beta.15 → 0.1.0-beta.17

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