@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
@@ -15,7 +15,6 @@ import { readPluginIntentState } from '../utils/plugin-intent-state.js';
15
15
  import { applyPluginConfigWrite, replayConfigRuntimeWrites, resolveConfigPreApplyAuthorization, resetPluginConfigWithHelper, } from '../utils/plugin-config-write.js';
16
16
  import { analyzePluginConfigMutationImpact } from '../utils/plugin-config-impact.js';
17
17
  import { createBatchIntentCompatiblePlanFingerprint } from '../commands/operation-plan.js';
18
- import { offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from '../commands/offline-cleanup-evidence.js';
19
18
  import { inventoryCatalogSourceReferences } from '../utils/catalog-source-references.js';
20
19
  import { operationEvidenceMatches, operationEvidenceFingerprint, TuiConfigInvalidStoreError, TuiController, TuiOperationBlockedError, } from './controller.js';
21
20
  import { buildConfigGroups, buildConfigPage, buildTuiViewModel, projectPlatformPlacement, } from './model.js';
@@ -265,10 +264,12 @@ function selectedBatchGroups(intent) {
265
264
  for (const target of uniqueTargets) {
266
265
  const pluginId = target.pluginId;
267
266
  const sourceId = parsePluginId(pluginId).sourceId;
268
- const groupKey = [sourceId, target.platform, target.scope].join('\0');
267
+ const groupKey = intent.action === 'cleanup'
268
+ ? ['cleanup', target.platform, target.scope].join('\0')
269
+ : [sourceId, target.platform, target.scope].join('\0');
269
270
  const group = groups.get(groupKey) ?? {
270
271
  groupId: groupKey,
271
- sourceId,
272
+ sourceId: intent.action === 'cleanup' ? 'offline' : sourceId,
272
273
  platform: target.platform,
273
274
  scope: target.scope,
274
275
  requestedPluginIds: [],
@@ -1554,6 +1555,41 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1554
1555
  await preparation?.reporter.emit('status');
1555
1556
  const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
1556
1557
  signal?.throwIfAborted();
1558
+ if (intent.action === 'cleanup') {
1559
+ const groups = [];
1560
+ for (const group of selection.groups) {
1561
+ signal?.throwIfAborted();
1562
+ const preview = await runBatchWriteGroup({
1563
+ intent,
1564
+ group,
1565
+ cwd: context.cwd,
1566
+ runtime: {
1567
+ ...context,
1568
+ inventoryCatalogSourceReferences: operationInventory,
1569
+ previewOperationPlan: true,
1570
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1571
+ ...(preparation ? {
1572
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1573
+ } : {}),
1574
+ },
1575
+ });
1576
+ groups.push({ ...group, preview, executable: preview.operationPlan !== undefined });
1577
+ }
1578
+ await preparation?.reporter.emit('evidence');
1579
+ signal?.throwIfAborted();
1580
+ const evidence = {
1581
+ catalog: {
1582
+ configDigest: 'cleanup-offline',
1583
+ compositeDigest: digest(groups.map((group) => [
1584
+ group.groupId,
1585
+ group.preview.operationPlan?.planFingerprint,
1586
+ ])),
1587
+ },
1588
+ requestedPluginIds: selection.requestedPluginIds,
1589
+ groups,
1590
+ };
1591
+ return evidence;
1592
+ }
1557
1593
  const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1558
1594
  const groups = [];
1559
1595
  const ownsObservationSession = !codexObservationSession
@@ -1669,17 +1705,44 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1669
1705
  };
1670
1706
  return evidence;
1671
1707
  }
1708
+ if (intent.kind === 'pluginWrite' && intent.action === 'cleanup') {
1709
+ const preview = await cleanupWriteCommand([intent.pluginId], {
1710
+ cwd: context.cwd,
1711
+ platform: intent.platform,
1712
+ scope: intent.scope,
1713
+ force: intent.force === true,
1714
+ }, {
1715
+ ...context,
1716
+ inventoryCatalogSourceReferences: operationInventory,
1717
+ previewOperationPlan: true,
1718
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1719
+ ...(preparation ? {
1720
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1721
+ } : {}),
1722
+ });
1723
+ signal?.throwIfAborted();
1724
+ if (!preview.operationPlan) {
1725
+ throw new TuiOperationBlockedError('cleanupPlanUnavailable', '无法建立可信的清理计划。');
1726
+ }
1727
+ const offlineCleanupPlans = preview.offlineCleanupPlans ?? [];
1728
+ await preparation?.reporter.emit('evidence');
1729
+ return {
1730
+ catalog: {
1731
+ configDigest: 'cleanup-offline',
1732
+ compositeDigest: preview.operationPlan.planFingerprint,
1733
+ },
1734
+ operationPlan: preview.operationPlan,
1735
+ offlineCleanupPlans,
1736
+ ...(offlineCleanupPlans.length === 1
1737
+ ? { offlineCleanup: offlineCleanupPlans[0] }
1738
+ : {}),
1739
+ targetFingerprint: preview.operationPlan.planFingerprint,
1740
+ };
1741
+ }
1672
1742
  const packageInfo = await readCliPackageInfo();
1673
- const offlineCleanupReason = intent.kind === 'pluginWrite'
1674
- && intent.action === 'cleanup'
1675
- && intent.originIssue?.actionId === 'cleanup'
1676
- && (intent.originIssue?.category === 'sourceMissing'
1677
- || intent.originIssue?.category === 'sourceUnavailable')
1678
- ? intent.originIssue.category === 'sourceMissing' ? 'sourceMissing' : 'bundleUnavailable'
1679
- : undefined;
1680
1743
  let catalogOperation;
1681
1744
  let catalogOperationError;
1682
- if (!offlineCleanupReason && intent.kind !== 'portableInstall') {
1745
+ if (intent.kind !== 'portableInstall') {
1683
1746
  try {
1684
1747
  const sourceIds = [parsePluginId(intent.pluginId).sourceId];
1685
1748
  catalogOperation = !freshCatalog && sessionCatalogOperationContext
@@ -1850,9 +1913,8 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1850
1913
  return evidence;
1851
1914
  }
1852
1915
  await preparation?.reporter.emit('status');
1853
- const statusCatalogOperation = offlineCleanupReason
1854
- ? undefined
1855
- : catalogOperation ?? (!freshCatalog ? sessionCatalogOperationContext : undefined);
1916
+ const statusCatalogOperation = catalogOperation
1917
+ ?? (!freshCatalog ? sessionCatalogOperationContext : undefined);
1856
1918
  const status = await statusCommandV7({
1857
1919
  plugins: [...new Set([intent.pluginId,
1858
1920
  ...(intent.kind === 'pluginWrite' && intent.originIssue?.consumerPluginId
@@ -1877,7 +1939,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1877
1939
  }
1878
1940
  if (catalogOperationError)
1879
1941
  throw catalogOperationError;
1880
- if (!offlineCleanupReason && !catalogOperation) {
1942
+ if (!catalogOperation) {
1881
1943
  catalogOperation = await resolveCatalogOperationContext({
1882
1944
  sourceIds: [parsePluginId(intent.pluginId).sourceId],
1883
1945
  configSnapshot,
@@ -1889,122 +1951,80 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1889
1951
  let dependencyPlan;
1890
1952
  let codexReconcilePlans;
1891
1953
  let operationPlan;
1892
- let forceCleanupEvidence;
1893
1954
  await preparation?.reporter.emit('catalog');
1894
- const offlineCleanupTarget = offlineCleanupReason && intent.kind === 'pluginWrite'
1895
- ? status.payload.targets.find((target) => target.pluginId === intent.pluginId
1896
- && target.platform === intent.platform
1897
- && target.scope === intent.scope
1898
- && target.reasons.some((error) => error.code === offlineCleanupReason))
1899
- : undefined;
1900
- const catalogSource = offlineCleanupTarget
1901
- ? status.payload.catalog.sources.find((source) => source.id === offlineCleanupTarget.sourceId)
1902
- : undefined;
1903
- let offlineCleanup;
1904
- if (offlineCleanupReason && offlineCleanupTarget) {
1905
- await preparation?.reporter.emit('inventory');
1906
- const inventory = await operationInventory({
1907
- cwd: context.cwd,
1908
- env: context.env,
1909
- });
1910
- signal?.throwIfAborted();
1911
- offlineCleanup = offlineCleanupEvidenceFromInventory({
1912
- reason: offlineCleanupReason,
1913
- pluginId: offlineCleanupTarget.pluginId,
1914
- pluginName: offlineCleanupTarget.pluginName,
1915
- sourceId: offlineCleanupTarget.sourceId,
1916
- sourceIdentity: offlineCleanupTarget.sourceIdentity,
1917
- platform: offlineCleanupTarget.platform,
1918
- scope: offlineCleanupTarget.scope,
1919
- inventory,
1920
- }) ?? undefined;
1921
- if (!offlineCleanup) {
1922
- throw operationReviewStale('离线清理现场证据已消失或无法验证。');
1923
- }
1924
- }
1925
1955
  const configVisibleTargets = intent.kind === 'configSave'
1926
1956
  ? configAffectedStatusTargets(status.payload.targets, intent.pluginId, intent.scope)
1927
1957
  : [];
1928
- const selected = offlineCleanupTarget
1929
- ? {
1930
- pluginName: offlineCleanupTarget.pluginName,
1931
- source: {
1932
- sourceId: offlineCleanupTarget.sourceId,
1933
- sourceIdentity: offlineCleanupTarget.sourceIdentity,
1934
- ...(catalogSource?.requestedRef ? { requestedRef: catalogSource.requestedRef } : {}),
1935
- ...(catalogSource?.resolvedCommit ? { resolvedCommit: catalogSource.resolvedCommit } : {}),
1936
- },
1937
- }
1938
- : await withResolvedCatalogPlugin({
1939
- selector: intent.pluginId,
1940
- cliVersion: packageInfo.version,
1941
- configSnapshot,
1942
- env: context.env,
1943
- ...(catalogOperation ? { operationContext: catalogOperation } : {}),
1944
- ...(signal ? { signal } : {}),
1945
- consume: async ({ plugin, source }) => {
1946
- if (preparation?.validateIntent && intent.kind === 'configSave') {
1947
- await preparation.reporter.emit('config');
1948
- if (!plugin.manifest.config) {
1949
- throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
1950
- }
1951
- await coercePluginConfigUpdates({
1952
- schema: plugin.manifest.config.fields,
1953
- updates: intent.updates ?? {},
1954
- cwd: context.cwd,
1955
- });
1956
- for (const key of intent.removeKeys ?? []) {
1957
- if (!plugin.manifest.config.fields[key]) {
1958
- throw new TuiOperationBlockedError('unknownConfigKey', '设置字段已变化,请重新检查。');
1959
- }
1958
+ const selected = await withResolvedCatalogPlugin({
1959
+ selector: intent.pluginId,
1960
+ cliVersion: packageInfo.version,
1961
+ configSnapshot,
1962
+ env: context.env,
1963
+ ...(catalogOperation ? { operationContext: catalogOperation } : {}),
1964
+ ...(signal ? { signal } : {}),
1965
+ consume: async ({ plugin, source }) => {
1966
+ if (preparation?.validateIntent && intent.kind === 'configSave') {
1967
+ await preparation.reporter.emit('config');
1968
+ if (!plugin.manifest.config) {
1969
+ throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
1970
+ }
1971
+ await coercePluginConfigUpdates({
1972
+ schema: plugin.manifest.config.fields,
1973
+ updates: intent.updates ?? {},
1974
+ cwd: context.cwd,
1975
+ });
1976
+ for (const key of intent.removeKeys ?? []) {
1977
+ if (!plugin.manifest.config.fields[key]) {
1978
+ throw new TuiOperationBlockedError('unknownConfigKey', '设置字段已变化,请重新检查。');
1960
1979
  }
1961
1980
  }
1962
- const impact = intent.kind === 'configSave' && !intent.retryApply
1963
- ? await analyzePluginConfigMutationImpact({
1981
+ }
1982
+ const impact = intent.kind === 'configSave' && !intent.retryApply
1983
+ ? await analyzePluginConfigMutationImpact({
1984
+ plugin,
1985
+ scope: intent.scope,
1986
+ cwd: context.cwd,
1987
+ env: context.env,
1988
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets),
1989
+ mutation: {
1990
+ kind: 'patch',
1991
+ updates: intent.updates,
1992
+ removeKeys: intent.removeKeys,
1993
+ },
1994
+ })
1995
+ : undefined;
1996
+ return {
1997
+ pluginName: plugin.manifest.name,
1998
+ source: sourceEvidenceForPlugin(source, plugin.manifest.name),
1999
+ impact,
2000
+ preApplyAuthorization: intent.kind === 'configSave'
2001
+ && configIntentRequestsApply(intent)
2002
+ ? (await resolveConfigPreApplyAuthorization({
1964
2003
  plugin,
1965
2004
  scope: intent.scope,
1966
2005
  cwd: context.cwd,
1967
2006
  env: context.env,
1968
- visibleTargets: configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets),
1969
- mutation: {
1970
- kind: 'patch',
1971
- updates: intent.updates,
1972
- removeKeys: intent.removeKeys,
2007
+ applyTargets: intent.retryApply
2008
+ ? configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets)
2009
+ : impact.affectedTargets,
2010
+ nativeCliRunner: context.nativeCliRunner,
2011
+ rebuildRuntime: {
2012
+ ...(catalogOperation ? { catalogOperationContext: catalogOperation } : {}),
2013
+ ...(catalogOperation?.generationSetDigest
2014
+ ? { expectedGenerationSetDigest: catalogOperation.generationSetDigest }
2015
+ : {}),
2016
+ inventoryCatalogSourceReferences: operationInventory,
1973
2017
  },
1974
- })
1975
- : undefined;
1976
- return {
1977
- pluginName: plugin.manifest.name,
1978
- source: sourceEvidenceForPlugin(source, plugin.manifest.name),
1979
- impact,
1980
- preApplyAuthorization: intent.kind === 'configSave'
1981
- && configIntentRequestsApply(intent)
1982
- ? (await resolveConfigPreApplyAuthorization({
1983
- plugin,
1984
- scope: intent.scope,
1985
- cwd: context.cwd,
1986
- env: context.env,
1987
- applyTargets: intent.retryApply
1988
- ? configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets)
1989
- : impact.affectedTargets,
1990
- nativeCliRunner: context.nativeCliRunner,
1991
- rebuildRuntime: {
1992
- ...(catalogOperation ? { catalogOperationContext: catalogOperation } : {}),
1993
- ...(catalogOperation?.generationSetDigest
1994
- ? { expectedGenerationSetDigest: catalogOperation.generationSetDigest }
1995
- : {}),
1996
- inventoryCatalogSourceReferences: operationInventory,
1997
- },
1998
- })).targets
1999
- : [],
2000
- sharedNativeRenderTargets: intent.kind === 'configSave'
2001
- ? intent.retryApply
2002
- ? configRetrySharedNativeRenderTargets(plugin)
2003
- : impact.sharedNativeRenderTargets
2004
- : [],
2005
- };
2006
- },
2007
- });
2018
+ })).targets
2019
+ : [],
2020
+ sharedNativeRenderTargets: intent.kind === 'configSave'
2021
+ ? intent.retryApply
2022
+ ? configRetrySharedNativeRenderTargets(plugin)
2023
+ : impact.sharedNativeRenderTargets
2024
+ : [],
2025
+ };
2026
+ },
2027
+ });
2008
2028
  if (intent.kind === 'pluginWrite' && intent.expectedUpdate) {
2009
2029
  const revision = status.targetRevisions.find((entry) => entry.pluginId === intent.pluginId && entry.platform === intent.platform
2010
2030
  && entry.scope === intent.scope);
@@ -2060,17 +2080,6 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2060
2080
  signal?.throwIfAborted();
2061
2081
  codexReconcilePlans = preview?.codexReconcilePlans;
2062
2082
  operationPlan = preview?.operationPlan;
2063
- if (intent.action === 'cleanup' && intent.force === true) {
2064
- const candidates = preview?.offlineCleanupPlans?.filter((candidate) => candidate.forceExclusive === true
2065
- && candidate.pluginId === intent.pluginId
2066
- && candidate.platform === intent.platform
2067
- && candidate.scope === intent.scope
2068
- && candidate.plan.actions.length > 0) ?? [];
2069
- if (candidates.length !== 1) {
2070
- throw new TuiOperationBlockedError('forceCleanupPathUnavailable', '没有唯一且可验证的精确清理目标。');
2071
- }
2072
- [forceCleanupEvidence] = candidates;
2073
- }
2074
2083
  if (hasDeclaredDependencies
2075
2084
  && (intent.action === 'install'
2076
2085
  || intent.action === 'refreshAndSync'
@@ -2124,8 +2133,6 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2124
2133
  : {}),
2125
2134
  },
2126
2135
  source: selected.source,
2127
- ...(offlineCleanup ? { offlineCleanup } : {}),
2128
- ...(forceCleanupEvidence ? { offlineCleanup: forceCleanupEvidence } : {}),
2129
2136
  ...(dependencyPlan ? { dependencyPlan } : {}),
2130
2137
  ...(codexReconcilePlans ? { codexReconcilePlans } : {}),
2131
2138
  ...(operationPlan ? { operationPlan } : {}),
@@ -2857,7 +2864,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2857
2864
  requestedPluginIds: group.requestedPluginIds,
2858
2865
  executable: group.executable,
2859
2866
  planFingerprint: group.preview.operationPlan
2860
- ? createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2867
+ ? intent.action === 'cleanup'
2868
+ ? group.preview.operationPlan.planFingerprint
2869
+ : createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2861
2870
  : undefined,
2862
2871
  }));
2863
2872
  const currentGroups = currentBatch.groups.map((group) => ({
@@ -2872,7 +2881,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2872
2881
  ?.executable === false
2873
2882
  ? undefined
2874
2883
  : group.preview.operationPlan
2875
- ? createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2884
+ ? intent.action === 'cleanup'
2885
+ ? group.preview.operationPlan.planFingerprint
2886
+ : createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2876
2887
  : undefined,
2877
2888
  }));
2878
2889
  if (expectedBatch.catalog.configDigest !== currentBatch.catalog.configDigest
@@ -2884,6 +2895,12 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2884
2895
  await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
2885
2896
  const groupResults = [];
2886
2897
  const completedTargetKeys = new Set();
2898
+ const expectedIntentDigests = new Map();
2899
+ for (const group of expectedBatch.groups) {
2900
+ for (const intentDigest of group.preview.operationPlan?.evidence.intentDigests ?? []) {
2901
+ expectedIntentDigests.set(intentDigest.scope, intentDigest.digest);
2902
+ }
2903
+ }
2887
2904
  let executedGroupCount = 0;
2888
2905
  const appendStoppedGroups = (startIndex, staleMessage) => {
2889
2906
  for (const [offset, stopped] of expectedBatch.groups.slice(startIndex).entries()) {
@@ -2916,7 +2933,8 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2916
2933
  ? operationInventory
2917
2934
  : createOperationInventoryReader();
2918
2935
  let executionPreview = current.preview;
2919
- if (completedTargetKeys.size > 0) {
2936
+ if (completedTargetKeys.size > 0
2937
+ || (intent.action === 'cleanup' && executedGroupCount > 0)) {
2920
2938
  executionPreview = await runBatchWriteGroup({
2921
2939
  intent,
2922
2940
  group: current,
@@ -2925,12 +2943,17 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2925
2943
  ...context,
2926
2944
  awHomeMutationToken,
2927
2945
  inventoryCatalogSourceReferences: groupInventory,
2928
- expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2946
+ ...(intent.action === 'cleanup' ? {} : {
2947
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2948
+ }),
2929
2949
  previewOperationPlan: true,
2930
2950
  ...(codexObservationSession ? { codexObservationSession } : {}),
2931
2951
  },
2932
2952
  });
2933
- if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
2953
+ const cleanupIntentDigestChanged = intent.action === 'cleanup'
2954
+ && executionPreview.operationPlan?.evidence.intentDigests.find((entry) => entry.scope === current.scope)?.digest !== expectedIntentDigests.get(current.scope);
2955
+ if (cleanupIntentDigestChanged
2956
+ || !reviewed.preview.operationPlan || !executionPreview.operationPlan
2934
2957
  || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
2935
2958
  appendStoppedGroups(groupIndex, '前序批量结果改变了这个组的实际写入计划。');
2936
2959
  break;
@@ -2946,7 +2969,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2946
2969
  awHomeMutationToken,
2947
2970
  preApplyConfirmed: true,
2948
2971
  inventoryCatalogSourceReferences: groupInventory,
2949
- expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2972
+ ...(intent.action === 'cleanup' ? {} : {
2973
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2974
+ }),
2950
2975
  ...(executionPreview.operationPlan ? {
2951
2976
  expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint,
2952
2977
  } : {}),
@@ -2962,6 +2987,11 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2962
2987
  appendStoppedGroups(groupIndex, '这个组的实际写入计划发生变化。');
2963
2988
  break;
2964
2989
  }
2990
+ if (intent.action === 'cleanup') {
2991
+ for (const change of result.operationPlan?.intentChanges ?? []) {
2992
+ expectedIntentDigests.set(change.scope, change.afterDigest);
2993
+ }
2994
+ }
2965
2995
  executedGroupCount += 1;
2966
2996
  for (const target of result.targetResults) {
2967
2997
  if (target.state === 'completed') {
@@ -3150,16 +3180,28 @@ export function createTuiControllerDependencies(context, progressFeed, options =
3150
3180
  cleanup: cleanupWriteCommand,
3151
3181
  sync: syncWriteCommand,
3152
3182
  }[intent.action];
3153
- const currentSource = currentEvidence.source;
3154
- const offlineCleanup = currentEvidence.offlineCleanup;
3155
- const reviewedOfflineCleanup = expectedEvidence.offlineCleanup;
3156
- if ((offlineCleanup || reviewedOfflineCleanup)
3157
- && (!offlineCleanup
3158
- || !reviewedOfflineCleanup
3159
- || !offlineCleanupEvidenceMatches(reviewedOfflineCleanup, offlineCleanup))) {
3160
- throw new TuiOperationBlockedError('operationReviewStale', '确认后离线清理证据发生变化。');
3183
+ if (intent.action === 'cleanup') {
3184
+ if (!expectedEvidence.operationPlan || !currentEvidence.operationPlan
3185
+ || expectedEvidence.operationPlan.planFingerprint
3186
+ !== currentEvidence.operationPlan.planFingerprint) {
3187
+ throw operationReviewStale('确认后清理计划发生变化。');
3188
+ }
3189
+ const result = await command([intent.pluginId], options, {
3190
+ ...context,
3191
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3192
+ awHomeMutationToken,
3193
+ preApplyConfirmed: true,
3194
+ inventoryCatalogSourceReferences: operationInventory,
3195
+ expectedPlanFingerprint: expectedEvidence.operationPlan.planFingerprint,
3196
+ ...(expectedEvidence.codexManager?.state === 'requires-install'
3197
+ ? { managedCodexAuthorizer: async () => true }
3198
+ : {}),
3199
+ });
3200
+ throwIfWriteReviewStale(result);
3201
+ return result;
3161
3202
  }
3162
- if (!offlineCleanup && (!expectedEvidence.source || !currentSource)) {
3203
+ const currentSource = currentEvidence.source;
3204
+ if (!expectedEvidence.source || !currentSource) {
3163
3205
  throw new TuiOperationBlockedError('operationReviewStale', '确认信息缺少来源身份。');
3164
3206
  }
3165
3207
  const result = await command([intent.pluginId], options, {
@@ -3179,9 +3221,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
3179
3221
  ...(expectedEvidence.codexManager?.state === 'requires-install'
3180
3222
  ? { managedCodexAuthorizer: async () => true }
3181
3223
  : {}),
3182
- ...(reviewedOfflineCleanup
3183
- ? { expectedOfflineCleanupEvidence: reviewedOfflineCleanup }
3184
- : { reviewPreflight: (actual) => assertSourceEvidenceMatches(currentSource, actual) }),
3224
+ reviewPreflight: (actual) => assertSourceEvidenceMatches(currentSource, actual),
3185
3225
  });
3186
3226
  throwIfWriteReviewStale(result);
3187
3227
  return result;