@lesliechan721/aw 0.1.0-beta.24 → 0.1.0-beta.26

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 (64) hide show
  1. package/dist/scripts/aw-skill-eval-materializer.d.ts +3 -1
  2. package/dist/scripts/aw-skill-eval-materializer.js +66 -18
  3. package/dist/scripts/aw-skill-eval-materializer.js.map +1 -1
  4. package/dist/src/commands/rebuild-integration.d.ts +1 -0
  5. package/dist/src/commands/rebuild-integration.js +10 -1
  6. package/dist/src/commands/rebuild-integration.js.map +1 -1
  7. package/dist/src/commands/skill-eval.js +2 -0
  8. package/dist/src/commands/skill-eval.js.map +1 -1
  9. package/dist/src/eval/manifest-v1.d.ts +4 -0
  10. package/dist/src/eval/manifest-v1.js +34 -6
  11. package/dist/src/eval/manifest-v1.js.map +1 -1
  12. package/dist/src/portable/commands.d.ts +39 -5
  13. package/dist/src/portable/commands.js +249 -30
  14. package/dist/src/portable/commands.js.map +1 -1
  15. package/dist/src/portable/skills-cli.d.ts +15 -7
  16. package/dist/src/portable/skills-cli.js +35 -19
  17. package/dist/src/portable/skills-cli.js.map +1 -1
  18. package/dist/src/review/skill-materialization.js +4 -1
  19. package/dist/src/review/skill-materialization.js.map +1 -1
  20. package/dist/src/review/source-binding.js +26 -6
  21. package/dist/src/review/source-binding.js.map +1 -1
  22. package/dist/src/tui/catalog-sources/session.js +9 -0
  23. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  24. package/dist/src/tui/catalog-store-task.d.ts +16 -0
  25. package/dist/src/tui/catalog-store-task.js +19 -0
  26. package/dist/src/tui/catalog-store-task.js.map +1 -0
  27. package/dist/src/tui/components.d.ts +15 -0
  28. package/dist/src/tui/components.js +24 -3
  29. package/dist/src/tui/components.js.map +1 -1
  30. package/dist/src/tui/console-app-v4.d.ts +2 -1
  31. package/dist/src/tui/console-app-v4.js +2150 -1474
  32. package/dist/src/tui/console-app-v4.js.map +1 -1
  33. package/dist/src/tui/controller-runtime.js +396 -16
  34. package/dist/src/tui/controller-runtime.js.map +1 -1
  35. package/dist/src/tui/controller.d.ts +131 -41
  36. package/dist/src/tui/controller.js +493 -33
  37. package/dist/src/tui/controller.js.map +1 -1
  38. package/dist/src/tui/dependency-graph.d.ts +11 -0
  39. package/dist/src/tui/dependency-graph.js +19 -0
  40. package/dist/src/tui/dependency-graph.js.map +1 -1
  41. package/dist/src/tui/diagnostic-projection.js +83 -0
  42. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  43. package/dist/src/tui/model.d.ts +4 -1
  44. package/dist/src/tui/model.js +18 -13
  45. package/dist/src/tui/model.js.map +1 -1
  46. package/dist/src/tui/operation-diagnostic-screens.js +3 -2
  47. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  48. package/dist/src/tui/operation-review-planner.js +95 -8
  49. package/dist/src/tui/operation-review-planner.js.map +1 -1
  50. package/dist/src/tui/plugin-content.d.ts +23 -0
  51. package/dist/src/tui/plugin-content.js +336 -0
  52. package/dist/src/tui/plugin-content.js.map +1 -0
  53. package/dist/src/tui/router.d.ts +27 -9
  54. package/dist/src/tui/router.js +13 -9
  55. package/dist/src/tui/router.js.map +1 -1
  56. package/dist/src/tui/screen-registry.d.ts +81 -14
  57. package/dist/src/tui/screen-registry.js +46 -17
  58. package/dist/src/tui/screen-registry.js.map +1 -1
  59. package/dist/src/tui/session-console-app.js +1 -1
  60. package/dist/src/tui/session-console-app.js.map +1 -1
  61. package/dist/src/tui/session-host.d.ts +1 -0
  62. package/dist/src/tui/session-host.js +13 -1
  63. package/dist/src/tui/session-host.js.map +1 -1
  64. package/package.json +3 -3
@@ -11,7 +11,7 @@ import { readCatalogSourcesConfigSnapshot, recoverInvalidCatalogSourcesConfig, }
11
11
  import { withResolvedCatalogPlugin, withResolvedMultiSourceCatalog, formatPluginId, parsePluginId, } from '../utils/multi-source-catalog.js';
12
12
  import { coercePluginConfigUpdates, inspectPluginConfigResetTarget, listNamespacedPluginConfigStoreIdentities, readPluginConfigStore, resolveConfigRead, resolveEffectivePluginConfig, resolvePluginConfigStorePath, } from '../utils/plugin-config.js';
13
13
  import { assertProjectSharedConfigPathVisible } from '../utils/project-boundary.js';
14
- import { readPluginIntentState } from '../utils/plugin-intent-state.js';
14
+ import { calculatePluginIntentDigest, 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';
@@ -24,6 +24,7 @@ import { projectCatalogSourceLocation } from '../utils/catalog-source-presentati
24
24
  import { calculateCatalogSourceConfigFingerprint, calculateCatalogSourceIdentity, } from '../utils/catalog-source-identity.js';
25
25
  import { checkAwCapabilitySupport, readPluginManifest } from '../utils/manifest.js';
26
26
  import { withAwHomeMutationLock } from '../utils/aw-home-mutation-lock.js';
27
+ import { resolveCanonicalRepoProjectKey } from '../utils/state-paths.js';
27
28
  import { IMPLEMENTED_AW_CAPABILITIES } from '../commands/aw-capabilities.js';
28
29
  import { calculateCatalogGenerationSetDigest, resolveActiveCatalogGeneration, } from '../utils/catalog-generation-resolver.js';
29
30
  import { resolveCatalogOperationContext, resolvePartialCatalogOperationContext, selectCatalogOperationContext, } from '../utils/catalog-operation-context.js';
@@ -34,8 +35,10 @@ import { readCatalogGenerationState } from '../utils/catalog-generation-state.js
34
35
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
35
36
  import { CatalogProviderCompatibilityError } from '../utils/catalog-public-error.js';
36
37
  import { loadCatalogGenerationViewModels } from './catalog-generation.js';
37
- import { installPortableSkills, preparePortableInstall, PortableCommandError, } from '../portable/commands.js';
38
+ import { executePortableRetry, installPortableSkills, preparePortableInstall, preparePortableRetry, PortableCommandError, recheckPortableInstall, } from '../portable/commands.js';
38
39
  import { MANAGED_CODEX_PACKAGE_NAME, MANAGED_CODEX_VERSION, } from '../utils/codex-manager.js';
40
+ import { projectPluginContentOverview, } from './plugin-content.js';
41
+ import { executeCatalogStoreTask, inspectCatalogStoreTask, prepareCatalogStoreTask, } from './catalog-store-task.js';
39
42
  function reusableCandidateEvidenceIsComplete(candidate) {
40
43
  const evidence = candidate.probe.candidate;
41
44
  return [
@@ -151,6 +154,31 @@ function configIntentRequestsApply(intent) {
151
154
  return Object.keys(intent.updates ?? {}).length > 0
152
155
  || (intent.removeKeys?.length ?? 0) > 0;
153
156
  }
157
+ function configSaveBatchGroupIntent(intent, group) {
158
+ return {
159
+ kind: 'configSave',
160
+ pluginId: intent.pluginId,
161
+ scope: group.scope,
162
+ apply: intent.apply,
163
+ ...(intent.scriptTimeoutMs === undefined ? {} : { scriptTimeoutMs: intent.scriptTimeoutMs }),
164
+ ...(group.updates ? { updates: group.updates } : {}),
165
+ ...(group.removeKeys ? { removeKeys: group.removeKeys } : {}),
166
+ ...(group.sensitiveKeys ? { sensitiveKeys: group.sensitiveKeys } : {}),
167
+ ...(group.privateKeys ? { privateKeys: group.privateKeys } : {}),
168
+ ...(group.changes ? { changes: group.changes } : {}),
169
+ };
170
+ }
171
+ function assertConfigSaveBatchIntent(intent) {
172
+ const order = { project: 0, user: 1 };
173
+ if (intent.groups.length === 0 || intent.groups.length > 2
174
+ || new Set(intent.groups.map((group) => group.scope)).size !== intent.groups.length
175
+ || intent.groups.some((group, index) => index > 0
176
+ && order[intent.groups[index - 1].scope] >= order[group.scope])
177
+ || intent.groups.some((group) => Object.keys(group.updates ?? {}).length === 0
178
+ && (group.removeKeys?.length ?? 0) === 0)) {
179
+ throw new TuiOperationBlockedError('configBatchInvalid', '设置批次必须包含按当前项目、个人默认排序的唯一非空目标。');
180
+ }
181
+ }
154
182
  function operationReviewStale(message, diagnostics) {
155
183
  return new TuiOperationBlockedError('operationReviewStale', `operationReviewStale: ${message}`, diagnostics);
156
184
  }
@@ -170,6 +198,20 @@ function sourceEvidenceForPlugin(source, pluginName) {
170
198
  channel: source.manifest.channel,
171
199
  };
172
200
  }
201
+ async function configLayerStoreDigest(options) {
202
+ const store = await readPluginConfigStore({
203
+ sourceId: options.source.sourceId,
204
+ sourceIdentity: options.source.sourceIdentity,
205
+ ...('logicalIdentity' in options.source
206
+ ? { logicalIdentity: options.source.logicalIdentity }
207
+ : {}),
208
+ pluginName: options.pluginName,
209
+ scope: options.scope,
210
+ cwd: options.cwd,
211
+ env: options.env,
212
+ });
213
+ return digest({ exists: store.exists, rawDigest: store.rawDigest });
214
+ }
173
215
  async function inspectConfigShareability(options) {
174
216
  if (options.scope !== 'project')
175
217
  return undefined;
@@ -222,7 +264,7 @@ function reviewedScriptTimeoutMs(intent) {
222
264
  || intent.action === 'reinstall' || intent.action === 'sync')) {
223
265
  return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
224
266
  }
225
- if (intent.kind === 'configSave' && intent.apply) {
267
+ if ((intent.kind === 'configSave' || intent.kind === 'configSaveBatch') && intent.apply) {
226
268
  return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
227
269
  }
228
270
  return undefined;
@@ -292,6 +334,25 @@ async function resolveBatchGroups(intent, env, cwd, frozenRequestedPluginIds) {
292
334
  throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目全部操作只支持 project scope sync。');
293
335
  }
294
336
  const state = await readPluginIntentState({ scope: 'project', cwd, env });
337
+ const projectIntentDigest = calculatePluginIntentDigest(state);
338
+ if (intent.selection.kind === 'currentProjectIntentTargets') {
339
+ if (intent.selection.targets.length === 0
340
+ || intent.selection.targets.some((target) => target.scope !== 'project')) {
341
+ throw new TuiOperationBlockedError('batchTargetInvalid', '当前项目精确同步目标必须是非空 project targets。');
342
+ }
343
+ const exact = selectedBatchGroups({
344
+ ...intent,
345
+ selection: { kind: 'targets', targets: intent.selection.targets },
346
+ });
347
+ const declaredPlatforms = new Map(state.roots.map((root) => [
348
+ root.pluginId,
349
+ new Set(root.platforms),
350
+ ]));
351
+ if (intent.selection.targets.some((target) => !declaredPlatforms.get(target.pluginId)?.has(target.platform))) {
352
+ throw new TuiOperationBlockedError('batchTargetInvalid', '当前项目精确同步目标必须属于完整 project intent roots 声明。');
353
+ }
354
+ return { ...exact, projectIntentDigest };
355
+ }
295
356
  const selection = resolveSyncIntentSelection({
296
357
  roots: state.roots,
297
358
  selectors: frozenRequestedPluginIds ?? [],
@@ -303,6 +364,7 @@ async function resolveBatchGroups(intent, env, cwd, frozenRequestedPluginIds) {
303
364
  }
304
365
  return {
305
366
  requestedPluginIds: selection.requestedPluginIds,
367
+ projectIntentDigest,
306
368
  groups: selection.groups.map((group) => ({
307
369
  groupId: `${group.sourceId}\0${group.platform}\0${intent.scope}`,
308
370
  sourceId: group.sourceId,
@@ -757,8 +819,15 @@ export function createTuiControllerDependencies(context, progressFeed, options =
757
819
  }))).flat();
758
820
  signal.throwIfAborted();
759
821
  const statusById = new Map(result.payload.catalog.sources.map((source) => [source.id, source]));
822
+ const [projectRoot, projectIntentState] = await Promise.all([
823
+ resolveCanonicalRepoProjectKey(context.cwd),
824
+ readPluginIntentState({ scope: 'project', cwd: context.cwd, env: context.env }),
825
+ ]);
826
+ signal.throwIfAborted();
760
827
  return {
761
828
  cliChannel: packageInfo.version.includes('-') ? 'beta' : 'latest',
829
+ projectRoot,
830
+ projectIntentRoots: structuredClone(projectIntentState.roots),
762
831
  configuredSources: structuredClone(configured.sources),
763
832
  payload: result.payload,
764
833
  pluginSummaries: result.pluginSummaries,
@@ -797,8 +866,98 @@ export function createTuiControllerDependencies(context, progressFeed, options =
797
866
  }
798
867
  throw new TuiOperationBlockedError('operationReviewStale', '状态读取期间来源配置持续变化。请重试。');
799
868
  };
869
+ const loadPluginContent = async (pluginId, signal) => {
870
+ signal.throwIfAborted();
871
+ try {
872
+ const packageInfo = await readCliPackageInfo();
873
+ const catalogOperation = sessionContextForPlugin(pluginId);
874
+ return await withResolvedCatalogPlugin({
875
+ selector: pluginId,
876
+ cliVersion: packageInfo.version,
877
+ env: context.env,
878
+ signal,
879
+ ...(catalogOperation ? { operationContext: catalogOperation } : {}),
880
+ consume: ({ plugin, source }) => projectPluginContentOverview({
881
+ pluginId,
882
+ catalogVersion: source.manifest.catalogVersion,
883
+ plugin,
884
+ }),
885
+ });
886
+ }
887
+ catch {
888
+ signal.throwIfAborted();
889
+ const summary = sessionStatusResult?.pluginSummaries.find((entry) => entry.pluginId === pluginId);
890
+ let sourceId;
891
+ try {
892
+ sourceId = parsePluginId(pluginId).sourceId;
893
+ }
894
+ catch {
895
+ // The fixed unavailable result below does not repeat an unsafe selector.
896
+ }
897
+ const catalogVersion = sessionStatusResult?.payload.catalog.sources
898
+ .find((source) => source.id === sourceId)?.catalogVersion;
899
+ return {
900
+ pluginId,
901
+ pluginVersion: summary?.version ?? '待确认',
902
+ catalogVersion: catalogVersion ?? '待确认',
903
+ evidence: 'unavailable',
904
+ items: [],
905
+ notices: ['当前无法安全读取这个插件的目录内容。请检查来源状态后重试。'],
906
+ };
907
+ }
908
+ };
800
909
  const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
801
910
  reviewedScriptTimeoutMs(intent);
911
+ if (intent.kind === 'catalogStorePrune') {
912
+ signal?.throwIfAborted();
913
+ const config = await readCatalogSourcesConfigSnapshot(context.env);
914
+ const catalogStore = await prepareCatalogStoreTask({ env: context.env });
915
+ signal?.throwIfAborted();
916
+ return {
917
+ catalog: { configDigest: config.digest, compositeDigest: config.digest },
918
+ catalogStore,
919
+ };
920
+ }
921
+ if (intent.kind === 'configSaveBatch') {
922
+ assertConfigSaveBatchIntent(intent);
923
+ const groupEvidence = [];
924
+ let batchBase;
925
+ let batchCatalogOperation;
926
+ for (const group of intent.groups) {
927
+ const current = await captureEvidence(configSaveBatchGroupIntent(intent, group), signal, preparation, operationInventory, freshCatalog, reusableCandidate, codexObservationSession);
928
+ if (!current.config || !current.source || !current.targetFingerprint) {
929
+ throw operationReviewStale('设置批次缺少单层证据。');
930
+ }
931
+ if (batchBase && (JSON.stringify(batchBase.catalog) !== JSON.stringify(current.catalog)
932
+ || JSON.stringify(batchBase.source) !== JSON.stringify(current.source))) {
933
+ throw operationReviewStale('准备设置批次期间 Catalog 或来源发生变化。');
934
+ }
935
+ batchBase ??= current;
936
+ batchCatalogOperation ??= catalogOperationByEvidence.get(current);
937
+ groupEvidence.push({
938
+ scope: group.scope,
939
+ config: {
940
+ ...current.config,
941
+ storeDigest: await configLayerStoreDigest({
942
+ source: current.source,
943
+ pluginName: parsePluginId(intent.pluginId).pluginName,
944
+ scope: group.scope,
945
+ cwd: context.cwd,
946
+ env: context.env,
947
+ }),
948
+ },
949
+ targetFingerprint: current.targetFingerprint,
950
+ });
951
+ }
952
+ const evidence = {
953
+ catalog: batchBase.catalog,
954
+ source: batchBase.source,
955
+ configBatch: { groups: groupEvidence },
956
+ };
957
+ if (batchCatalogOperation)
958
+ catalogOperationByEvidence.set(evidence, batchCatalogOperation);
959
+ return evidence;
960
+ }
802
961
  if ((intent.kind === 'pluginWrite'
803
962
  && (intent.platform === 'codex' || intent.platform === 'both')
804
963
  || intent.kind === 'pluginWriteTask'
@@ -1808,6 +1967,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1808
1967
  ?? digest(groups.map((group) => [group.groupId, group.preview.operationPlan?.planFingerprint])),
1809
1968
  },
1810
1969
  requestedPluginIds: selection.requestedPluginIds,
1970
+ ...(selection.projectIntentDigest
1971
+ ? { projectIntentDigest: selection.projectIntentDigest }
1972
+ : {}),
1811
1973
  groups,
1812
1974
  targetVersions: [...new Map(targetVersions.map((target) => [[
1813
1975
  target.pluginId, target.platform, target.scope,
@@ -1877,23 +2039,39 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1877
2039
  }
1878
2040
  if (intent.kind === 'portableInstall') {
1879
2041
  await preparation?.reporter.emit('portable');
2042
+ if (intent.retry
2043
+ && (intent.retry.receipt.plugin.id !== intent.pluginId
2044
+ || intent.retry.receipt.scope !== intent.scope)) {
2045
+ throw operationReviewStale('Portable 重试结果与当前插件或范围不匹配。');
2046
+ }
1880
2047
  const portableSourceId = parsePluginId(intent.pluginId).sourceId;
1881
2048
  const portableCatalogOperation = sessionCatalogOperationContext
1882
2049
  ? selectCatalogOperationContext(sessionCatalogOperationContext, [portableSourceId])
1883
2050
  : undefined;
1884
- const portable = await preparePortableInstall({
1885
- selector: intent.pluginId,
1886
- scope: intent.scope,
1887
- ...(intent.selectedSkills.length > 0 ? { requestedSkills: intent.selectedSkills } : {}),
1888
- ...(intent.requestedAgents ? { requestedAgents: intent.requestedAgents } : {}),
1889
- cwd: context.cwd,
1890
- env: context.env,
1891
- cliVersion: packageInfo.version,
1892
- signal,
1893
- ...(portableCatalogOperation
1894
- ? { catalogOperationContext: portableCatalogOperation }
1895
- : {}),
1896
- });
2051
+ const portable = intent.retry
2052
+ ? await preparePortableRetry({
2053
+ receipt: intent.retry.receipt,
2054
+ cwd: context.cwd,
2055
+ env: context.env,
2056
+ cliVersion: packageInfo.version,
2057
+ signal,
2058
+ ...(portableCatalogOperation
2059
+ ? { catalogOperationContext: portableCatalogOperation }
2060
+ : {}),
2061
+ })
2062
+ : await preparePortableInstall({
2063
+ selector: intent.pluginId,
2064
+ scope: intent.scope,
2065
+ ...(intent.selectedSkills.length > 0 ? { requestedSkills: intent.selectedSkills } : {}),
2066
+ ...(intent.requestedAgents ? { requestedAgents: intent.requestedAgents } : {}),
2067
+ cwd: context.cwd,
2068
+ env: context.env,
2069
+ cliVersion: packageInfo.version,
2070
+ signal,
2071
+ ...(portableCatalogOperation
2072
+ ? { catalogOperationContext: portableCatalogOperation }
2073
+ : {}),
2074
+ });
1897
2075
  signal?.throwIfAborted();
1898
2076
  await preparation?.reporter.emit('evidence');
1899
2077
  signal?.throwIfAborted();
@@ -2326,6 +2504,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2326
2504
  }
2327
2505
  },
2328
2506
  loadStatus,
2507
+ loadPluginContent,
2329
2508
  async probeCatalogFreshness(sourceId, freshnessOptions, signal) {
2330
2509
  signal.throwIfAborted();
2331
2510
  const packageInfo = await readCliPackageInfo();
@@ -2439,6 +2618,13 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2439
2618
  }
2440
2619
  return;
2441
2620
  }
2621
+ if (intent.kind === 'configSaveBatch') {
2622
+ assertConfigSaveBatchIntent(intent);
2623
+ for (const group of intent.groups) {
2624
+ await dependencies.validateIntent(configSaveBatchGroupIntent(intent, group), signal);
2625
+ }
2626
+ return;
2627
+ }
2442
2628
  if (intent.kind === 'configSave') {
2443
2629
  const packageInfo = await readCliPackageInfo();
2444
2630
  await withResolvedCatalogPlugin({
@@ -2476,6 +2662,18 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2476
2662
  }
2477
2663
  },
2478
2664
  captureEvidence,
2665
+ inspectCatalogStore(signal) {
2666
+ signal.throwIfAborted();
2667
+ return inspectCatalogStoreTask({ env: context.env });
2668
+ },
2669
+ recheckPortableInstall(receipt, signal) {
2670
+ return recheckPortableInstall({
2671
+ receipt,
2672
+ cwd: context.cwd,
2673
+ env: context.env,
2674
+ signal,
2675
+ });
2676
+ },
2479
2677
  logError(_error, code, logContext) {
2480
2678
  return context.debugLogger.log({
2481
2679
  type: 'tui:controller-error',
@@ -2671,6 +2869,16 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2671
2869
  };
2672
2870
  },
2673
2871
  async executeIntent(intent, expectedEvidence, signal) {
2872
+ if (intent.kind === 'catalogStorePrune') {
2873
+ signal?.throwIfAborted();
2874
+ if (!expectedEvidence.catalogStore) {
2875
+ throw new TuiOperationBlockedError('catalogStoreReviewStale', '确认信息缺少精确清理目标。');
2876
+ }
2877
+ return executeCatalogStoreTask({
2878
+ env: context.env,
2879
+ authorizedItems: expectedEvidence.catalogStore.authorizedItems,
2880
+ });
2881
+ }
2674
2882
  if (intent.kind === 'catalogSourcesRecovery') {
2675
2883
  signal?.throwIfAborted();
2676
2884
  return recoverInvalidCatalogSourcesConfig({ env: context.env });
@@ -2844,6 +3052,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2844
3052
  }
2845
3053
  const packageInfo = await readCliPackageInfo();
2846
3054
  try {
3055
+ if (intent.retry) {
3056
+ if (!('retryPairs' in expectedEvidence.portable)) {
3057
+ throw operationReviewStale('确认信息缺少 Portable 重试目标。');
3058
+ }
3059
+ return await executePortableRetry({
3060
+ receipt: intent.retry.receipt,
3061
+ review: expectedEvidence.portable,
3062
+ expectedEvidenceDigest: expectedEvidence.portable.evidenceDigest,
3063
+ cwd: context.cwd,
3064
+ env: context.env,
3065
+ cliVersion: packageInfo.version,
3066
+ signal,
3067
+ onProgress(stage) {
3068
+ const phase = stage === 'prepare' ? '重新检查失败目标'
3069
+ : stage === 'add' ? '重试缺失的 Skills' : '验证重试结果';
3070
+ progressFeed?.setCurrent({ target: 'Skills CLI', phase }, `Portable Skills:${phase}`);
3071
+ },
3072
+ });
3073
+ }
2847
3074
  return await installPortableSkills({
2848
3075
  selector: intent.pluginId,
2849
3076
  scope: intent.scope,
@@ -2951,6 +3178,12 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2951
3178
  }
2952
3179
  catch (error) {
2953
3180
  const code = error instanceof TuiOperationBlockedError ? error.code : undefined;
3181
+ if (intent.kind === 'pluginWriteBatch'
3182
+ && (intent.selection.kind === 'currentProjectIntentRoots'
3183
+ || intent.selection.kind === 'currentProjectIntentTargets')
3184
+ && error instanceof TuiOperationBlockedError) {
3185
+ throw operationReviewStale('确认后完整项目 Intent 声明发生变化。');
3186
+ }
2954
3187
  if (intent.kind === 'pluginWriteTask'
2955
3188
  && (code === 'taskHasBlockedTargets' || code === 'taskNoExecutableTargets')) {
2956
3189
  throw operationReviewStale('确认后任务可执行目标发生变化。', error instanceof TuiOperationBlockedError ? error.diagnostics : undefined);
@@ -2960,6 +3193,12 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2960
3193
  if (intent.kind === 'pluginWriteBatch') {
2961
3194
  const expectedBatch = expectedEvidence;
2962
3195
  const currentBatch = currentEvidence;
3196
+ if ((intent.selection.kind === 'currentProjectIntentRoots'
3197
+ || intent.selection.kind === 'currentProjectIntentTargets')
3198
+ && (!expectedBatch.projectIntentDigest
3199
+ || expectedBatch.projectIntentDigest !== currentBatch.projectIntentDigest)) {
3200
+ throw operationReviewStale('确认后完整项目 Intent 声明发生变化。');
3201
+ }
2963
3202
  const reviewedGroups = expectedBatch.groups.map((group) => ({
2964
3203
  groupId: group.groupId,
2965
3204
  sourceId: group.sourceId,
@@ -3330,6 +3569,147 @@ export function createTuiControllerDependencies(context, progressFeed, options =
3330
3569
  throwIfWriteReviewStale(result);
3331
3570
  return result;
3332
3571
  }
3572
+ if (intent.kind === 'configSaveBatch') {
3573
+ assertConfigSaveBatchIntent(intent);
3574
+ const reviewedBatch = expectedEvidence.configBatch;
3575
+ if (!reviewedBatch || !currentEvidence.configBatch || !currentEvidence.source) {
3576
+ throw operationReviewStale('确认信息缺少设置批次证据。');
3577
+ }
3578
+ const approvedApplyTargets = new Set(reviewedBatch.groups.flatMap((group) => group.config.affectedTargets.map((target) => `${target.platform}\0${target.scope}`)));
3579
+ const approvedPreApplyTargets = reviewedBatch.groups.flatMap((group) => group.config.preApplyAuthorization ?? []);
3580
+ const results = [];
3581
+ const packageInfo = await readCliPackageInfo();
3582
+ for (const group of intent.groups) {
3583
+ const groupIntent = configSaveBatchGroupIntent(intent, group);
3584
+ const reviewedGroup = reviewedBatch.groups.find((item) => item.scope === group.scope);
3585
+ if (!reviewedGroup) {
3586
+ throw operationReviewStale('确认信息缺少设置层基线。');
3587
+ }
3588
+ let configSaved = false;
3589
+ const operationRebuildResults = [];
3590
+ try {
3591
+ const freshEvidence = await captureEvidence(groupIntent, undefined, undefined, operationInventory, true);
3592
+ const freshSource = freshEvidence.source;
3593
+ const freshConfig = freshEvidence.config;
3594
+ if (!freshSource || !freshConfig) {
3595
+ throw operationReviewStale('设置目标缺少最新证据。');
3596
+ }
3597
+ assertSourceEvidenceMatches(currentEvidence.source, freshSource);
3598
+ const freshLayerStoreDigest = await configLayerStoreDigest({
3599
+ source: freshSource,
3600
+ pluginName: parsePluginId(intent.pluginId).pluginName,
3601
+ scope: group.scope,
3602
+ cwd: context.cwd,
3603
+ env: context.env,
3604
+ });
3605
+ if (freshLayerStoreDigest !== reviewedGroup.config.storeDigest) {
3606
+ throw operationReviewStale('保存设置期间此设置层的原始内容发生变化。');
3607
+ }
3608
+ if (freshConfig.affectedTargets.some((target) => !approvedApplyTargets.has(`${target.platform}\0${target.scope}`))) {
3609
+ throw operationReviewStale('同批前一层变更扩大了未经确认的应用目标。');
3610
+ }
3611
+ const groupCatalogOperation = catalogOperationByEvidence.get(freshEvidence);
3612
+ if (expectedEvidence.catalog.generationSetDigest
3613
+ && groupCatalogOperation?.generationSetDigest
3614
+ !== expectedEvidence.catalog.generationSetDigest) {
3615
+ throw operationReviewStale('保存设置期间 Active Catalog generation 发生变化。');
3616
+ }
3617
+ const result = await withResolvedCatalogPlugin({
3618
+ selector: intent.pluginId,
3619
+ cliVersion: packageInfo.version,
3620
+ env: context.env,
3621
+ ...(groupCatalogOperation ? { operationContext: groupCatalogOperation } : {}),
3622
+ consume: async ({ plugin, source }) => {
3623
+ assertSourceEvidenceMatches(freshSource, sourceEvidenceForPlugin(source, plugin.manifest.name));
3624
+ const applyTargets = intent.apply
3625
+ ? freshConfig.affectedTargets.map((target) => ({
3626
+ pluginName: plugin.manifest.name,
3627
+ platform: target.platform,
3628
+ scope: target.scope,
3629
+ }))
3630
+ : [];
3631
+ return applyPluginConfigWrite({
3632
+ plugin,
3633
+ pluginName: plugin.manifest.name,
3634
+ scope: group.scope,
3635
+ cwd: context.cwd,
3636
+ env: context.env,
3637
+ nativeCliRunner: context.nativeCliRunner,
3638
+ rebuildRuntime: {
3639
+ ...context,
3640
+ awHomeMutationToken,
3641
+ operationDiagnosticSink: (entries) => {
3642
+ operationRebuildResults.push(...structuredClone(entries));
3643
+ },
3644
+ ...(reviewedScriptTimeoutMs(intent) === undefined ? {} : {
3645
+ scriptTimeoutMs: reviewedScriptTimeoutMs(intent),
3646
+ }),
3647
+ ...(groupCatalogOperation ? {
3648
+ catalogOperationContext: groupCatalogOperation,
3649
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
3650
+ } : {}),
3651
+ },
3652
+ awHomeMutationToken,
3653
+ applyTargets,
3654
+ request: { updates: group.updates, removeKeys: group.removeKeys },
3655
+ sharedNativeApplyConfirmed: true,
3656
+ preApplyConfirmed: true,
3657
+ preApplyAuthorizedTargets: approvedPreApplyTargets,
3658
+ });
3659
+ },
3660
+ });
3661
+ configSaved = result.configWrite === 'succeeded';
3662
+ const applyStatus = !intent.apply
3663
+ ? 'notRequested'
3664
+ : result.apply.state === 'succeeded'
3665
+ ? 'succeeded'
3666
+ : result.apply.state === 'skipped'
3667
+ ? 'notNeeded'
3668
+ : 'failed';
3669
+ results.push({
3670
+ scope: group.scope,
3671
+ status: applyStatus === 'failed' ? 'failed' : 'succeeded',
3672
+ configSaved,
3673
+ applyRequested: intent.apply,
3674
+ applyStatus,
3675
+ execution: { ...result, operationRebuildResults },
3676
+ ...(applyStatus === 'failed' ? {
3677
+ errorCode: result.apply.state === 'unavailable'
3678
+ ? result.apply.error?.code ?? result.apply.reason
3679
+ : result.apply.targetResults.find((target) => target.errorCode)?.errorCode
3680
+ ?? 'runtimeApplyFailed',
3681
+ } : {}),
3682
+ });
3683
+ }
3684
+ catch (error) {
3685
+ configSaved = error?.configWrite === 'succeeded';
3686
+ const errorCode = typeof error === 'object' && error !== null
3687
+ && 'code' in error && typeof error.code === 'string'
3688
+ ? error.code
3689
+ : configSaved ? 'runtimeApplyFailed' : 'configWriteFailed';
3690
+ results.push({
3691
+ scope: group.scope,
3692
+ status: 'failed',
3693
+ configSaved,
3694
+ applyRequested: intent.apply,
3695
+ applyStatus: intent.apply ? 'failed' : 'notRequested',
3696
+ errorCode,
3697
+ ...(operationRebuildResults.length > 0
3698
+ ? { execution: { operationRebuildResults } }
3699
+ : {}),
3700
+ });
3701
+ }
3702
+ }
3703
+ const succeeded = results.filter((result) => result.status === 'succeeded').length;
3704
+ return {
3705
+ status: succeeded === results.length
3706
+ ? 'succeeded'
3707
+ : succeeded > 0 || results.some((result) => result.configSaved)
3708
+ ? 'partial'
3709
+ : 'failed',
3710
+ groups: results,
3711
+ };
3712
+ }
3333
3713
  if (intent.kind === 'configSave' || intent.kind === 'configReset') {
3334
3714
  const operationRebuildResults = [];
3335
3715
  const operationDiagnosticSink = (results) => {