@lesliechan721/aw 0.1.0-beta.10 → 0.1.0-beta.12

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 (227) hide show
  1. package/dist/authoring/assets/aw-config.d.mts +1 -50
  2. package/dist/authoring/assets/aw-config.mjs +58 -928
  3. package/dist/authoring/assets/aw-config.mjs.map +1 -1
  4. package/dist/authoring/scripts/init-plugin.mjs +28 -33
  5. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  6. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +1 -0
  7. package/dist/authoring/scripts/lib/plugin-version-check.mjs +229 -8
  8. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  9. package/dist/authoring/scripts/validate-plugin.mjs +159 -48
  10. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  11. package/dist/scripts/bundle-plugins.js +18 -4
  12. package/dist/scripts/bundle-plugins.js.map +1 -1
  13. package/dist/scripts/test-plugin-scripts.d.ts +2 -0
  14. package/dist/scripts/test-plugin-scripts.js +21 -5
  15. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  16. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  17. package/dist/src/commands/aw-capabilities.js +1 -1
  18. package/dist/src/commands/aw-capabilities.js.map +1 -1
  19. package/dist/src/commands/catalog-dev-source.d.ts +6 -3
  20. package/dist/src/commands/catalog-dev-source.js +96 -35
  21. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  22. package/dist/src/commands/catalog-publication-inspection.d.ts +1 -1
  23. package/dist/src/commands/catalog-publication-inspection.js +54 -8
  24. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  25. package/dist/src/commands/catalog-publication-journal.js +1 -1
  26. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  27. package/dist/src/commands/catalog-source.d.ts +19 -4
  28. package/dist/src/commands/catalog-source.js +121 -12
  29. package/dist/src/commands/catalog-source.js.map +1 -1
  30. package/dist/src/commands/catalog-store.d.ts +26 -7
  31. package/dist/src/commands/catalog-store.js +361 -42
  32. package/dist/src/commands/catalog-store.js.map +1 -1
  33. package/dist/src/commands/console.js +28 -17
  34. package/dist/src/commands/console.js.map +1 -1
  35. package/dist/src/commands/operation-plan.d.ts +2 -1
  36. package/dist/src/commands/operation-plan.js +24 -0
  37. package/dist/src/commands/operation-plan.js.map +1 -1
  38. package/dist/src/commands/operation-planner.d.ts +2 -2
  39. package/dist/src/commands/operation-planner.js +22 -20
  40. package/dist/src/commands/operation-planner.js.map +1 -1
  41. package/dist/src/commands/options.d.ts +2 -2
  42. package/dist/src/commands/options.js +18 -4
  43. package/dist/src/commands/options.js.map +1 -1
  44. package/dist/src/commands/plugin-capabilities.js +2 -2
  45. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  46. package/dist/src/commands/rebuild-command.d.ts +19 -0
  47. package/dist/src/commands/rebuild-command.js +828 -283
  48. package/dist/src/commands/rebuild-command.js.map +1 -1
  49. package/dist/src/commands/rebuild-integration.d.ts +25 -47
  50. package/dist/src/commands/rebuild-integration.js +1176 -1016
  51. package/dist/src/commands/rebuild-integration.js.map +1 -1
  52. package/dist/src/commands/rebuild-operations.d.ts +50 -12
  53. package/dist/src/commands/rebuild-operations.js +207 -101
  54. package/dist/src/commands/rebuild-operations.js.map +1 -1
  55. package/dist/src/commands/status-v6.d.ts +21 -2
  56. package/dist/src/commands/status-v6.js +163 -31
  57. package/dist/src/commands/status-v6.js.map +1 -1
  58. package/dist/src/commands/sync-command.d.ts +17 -0
  59. package/dist/src/commands/sync-command.js +45 -37
  60. package/dist/src/commands/sync-command.js.map +1 -1
  61. package/dist/src/commands/write-commands.d.ts +5 -0
  62. package/dist/src/commands/write-commands.js +50 -3
  63. package/dist/src/commands/write-commands.js.map +1 -1
  64. package/dist/src/generators/local-plugin.js +14 -4
  65. package/dist/src/generators/local-plugin.js.map +1 -1
  66. package/dist/src/index.js +260 -91
  67. package/dist/src/index.js.map +1 -1
  68. package/dist/src/output/command-receipt.js +1 -3
  69. package/dist/src/output/command-receipt.js.map +1 -1
  70. package/dist/src/output/rebuild-progress.js +1 -3
  71. package/dist/src/output/rebuild-progress.js.map +1 -1
  72. package/dist/src/portable/materializer.js +10 -8
  73. package/dist/src/portable/materializer.js.map +1 -1
  74. package/dist/src/tui/catalog-generation.js +41 -1
  75. package/dist/src/tui/catalog-generation.js.map +1 -1
  76. package/dist/src/tui/catalog-sources/session.d.ts +1 -0
  77. package/dist/src/tui/catalog-sources/session.js +16 -3
  78. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  79. package/dist/src/tui/catalog-sources/types.d.ts +3 -2
  80. package/dist/src/tui/components.d.ts +4 -0
  81. package/dist/src/tui/components.js +36 -24
  82. package/dist/src/tui/components.js.map +1 -1
  83. package/dist/src/tui/console-app-v4.d.ts +13 -4
  84. package/dist/src/tui/console-app-v4.js +2065 -857
  85. package/dist/src/tui/console-app-v4.js.map +1 -1
  86. package/dist/src/tui/controller-runtime.js +731 -62
  87. package/dist/src/tui/controller-runtime.js.map +1 -1
  88. package/dist/src/tui/controller.d.ts +156 -10
  89. package/dist/src/tui/controller.js +1187 -163
  90. package/dist/src/tui/controller.js.map +1 -1
  91. package/dist/src/tui/diagnostic-projection.d.ts +22 -0
  92. package/dist/src/tui/diagnostic-projection.js +437 -0
  93. package/dist/src/tui/diagnostic-projection.js.map +1 -0
  94. package/dist/src/tui/diagnostics.d.ts +1 -1
  95. package/dist/src/tui/diagnostics.js +7 -2
  96. package/dist/src/tui/diagnostics.js.map +1 -1
  97. package/dist/src/tui/glossary.d.ts +23 -0
  98. package/dist/src/tui/glossary.js +42 -0
  99. package/dist/src/tui/glossary.js.map +1 -0
  100. package/dist/src/tui/index.d.ts +4 -1
  101. package/dist/src/tui/index.js +4 -1
  102. package/dist/src/tui/index.js.map +1 -1
  103. package/dist/src/tui/keys.d.ts +44 -0
  104. package/dist/src/tui/keys.js +43 -15
  105. package/dist/src/tui/keys.js.map +1 -1
  106. package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
  107. package/dist/src/tui/lifecycle-action-view.js +34 -0
  108. package/dist/src/tui/lifecycle-action-view.js.map +1 -0
  109. package/dist/src/tui/model.d.ts +12 -2
  110. package/dist/src/tui/model.js +128 -52
  111. package/dist/src/tui/model.js.map +1 -1
  112. package/dist/src/tui/operation-diagnostic-screens.d.ts +21 -0
  113. package/dist/src/tui/operation-diagnostic-screens.js +101 -0
  114. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
  115. package/dist/src/tui/operation-history.d.ts +80 -0
  116. package/dist/src/tui/operation-history.js +130 -0
  117. package/dist/src/tui/operation-history.js.map +1 -0
  118. package/dist/src/tui/operation-review-planner.js +366 -99
  119. package/dist/src/tui/operation-review-planner.js.map +1 -1
  120. package/dist/src/tui/router.d.ts +82 -17
  121. package/dist/src/tui/router.js +186 -38
  122. package/dist/src/tui/router.js.map +1 -1
  123. package/dist/src/tui/screen-registry.d.ts +546 -0
  124. package/dist/src/tui/screen-registry.js +179 -0
  125. package/dist/src/tui/screen-registry.js.map +1 -0
  126. package/dist/src/tui/session-console-app.d.ts +6 -0
  127. package/dist/src/tui/session-console-app.js +13 -0
  128. package/dist/src/tui/session-console-app.js.map +1 -0
  129. package/dist/src/tui/session-host.d.ts +44 -0
  130. package/dist/src/tui/session-host.js +197 -0
  131. package/dist/src/tui/session-host.js.map +1 -0
  132. package/dist/src/types.d.ts +64 -3
  133. package/dist/src/types.js +1 -1
  134. package/dist/src/types.js.map +1 -1
  135. package/dist/src/utils/agent-version.js +1 -1
  136. package/dist/src/utils/agent-version.js.map +1 -1
  137. package/dist/src/utils/artifact-plan.d.ts +2 -2
  138. package/dist/src/utils/artifact-plan.js +8 -6
  139. package/dist/src/utils/artifact-plan.js.map +1 -1
  140. package/dist/src/utils/artifact-renderer.d.ts +2 -1
  141. package/dist/src/utils/artifact-renderer.js +139 -120
  142. package/dist/src/utils/artifact-renderer.js.map +1 -1
  143. package/dist/src/utils/artifact-request.d.ts +5 -0
  144. package/dist/src/utils/artifact-request.js +40 -0
  145. package/dist/src/utils/artifact-request.js.map +1 -1
  146. package/dist/src/utils/bundle-layout.js +4 -4
  147. package/dist/src/utils/bundle-layout.js.map +1 -1
  148. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  149. package/dist/src/utils/catalog-capability-projection.js +14 -0
  150. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  151. package/dist/src/utils/catalog-generation-resolver.d.ts +14 -2
  152. package/dist/src/utils/catalog-generation-resolver.js +49 -27
  153. package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
  154. package/dist/src/utils/catalog-generation-service.d.ts +8 -1
  155. package/dist/src/utils/catalog-generation-service.js +81 -47
  156. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  157. package/dist/src/utils/catalog-generation-store.d.ts +1 -0
  158. package/dist/src/utils/catalog-generation-store.js +60 -5
  159. package/dist/src/utils/catalog-generation-store.js.map +1 -1
  160. package/dist/src/utils/catalog-operation-context.js +1 -0
  161. package/dist/src/utils/catalog-operation-context.js.map +1 -1
  162. package/dist/src/utils/catalog-public-error.js +4 -0
  163. package/dist/src/utils/catalog-public-error.js.map +1 -1
  164. package/dist/src/utils/catalog-source-impact.d.ts +3 -2
  165. package/dist/src/utils/catalog-source-impact.js +142 -20
  166. package/dist/src/utils/catalog-source-impact.js.map +1 -1
  167. package/dist/src/utils/catalog-source-references.js +95 -9
  168. package/dist/src/utils/catalog-source-references.js.map +1 -1
  169. package/dist/src/utils/catalog-sources.d.ts +1 -1
  170. package/dist/src/utils/catalog-sources.js +19 -3
  171. package/dist/src/utils/catalog-sources.js.map +1 -1
  172. package/dist/src/utils/codex-manager.js +8 -0
  173. package/dist/src/utils/codex-manager.js.map +1 -1
  174. package/dist/src/utils/codex-native-observation.d.ts +34 -1
  175. package/dist/src/utils/codex-native-observation.js +132 -0
  176. package/dist/src/utils/codex-native-observation.js.map +1 -1
  177. package/dist/src/utils/codex-reconciler.d.ts +0 -2
  178. package/dist/src/utils/codex-reconciler.js.map +1 -1
  179. package/dist/src/utils/codex-shared-native.d.ts +2 -26
  180. package/dist/src/utils/codex-shared-native.js +8 -82
  181. package/dist/src/utils/codex-shared-native.js.map +1 -1
  182. package/dist/src/utils/git-catalog-provider.d.ts +3 -0
  183. package/dist/src/utils/git-catalog-provider.js +40 -1
  184. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  185. package/dist/src/utils/local-marketplace.d.ts +1 -0
  186. package/dist/src/utils/local-marketplace.js +69 -73
  187. package/dist/src/utils/local-marketplace.js.map +1 -1
  188. package/dist/src/utils/multi-source-catalog.js +1 -0
  189. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  190. package/dist/src/utils/native-cli.d.ts +9 -4
  191. package/dist/src/utils/native-cli.js +280 -34
  192. package/dist/src/utils/native-cli.js.map +1 -1
  193. package/dist/src/utils/path-catalog-provider.js +2 -0
  194. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  195. package/dist/src/utils/plugin-config-impact.d.ts +33 -0
  196. package/dist/src/utils/plugin-config-impact.js +298 -0
  197. package/dist/src/utils/plugin-config-impact.js.map +1 -0
  198. package/dist/src/utils/plugin-config-write.d.ts +36 -14
  199. package/dist/src/utils/plugin-config-write.js +709 -225
  200. package/dist/src/utils/plugin-config-write.js.map +1 -1
  201. package/dist/src/utils/plugin-config.d.ts +58 -4
  202. package/dist/src/utils/plugin-config.js +328 -60
  203. package/dist/src/utils/plugin-config.js.map +1 -1
  204. package/dist/src/utils/plugin-scripts.d.ts +20 -12
  205. package/dist/src/utils/plugin-scripts.js +389 -114
  206. package/dist/src/utils/plugin-scripts.js.map +1 -1
  207. package/dist/src/utils/process-tree.d.ts +2 -2
  208. package/dist/src/utils/process-tree.js +1 -1
  209. package/dist/src/utils/process-tree.js.map +1 -1
  210. package/dist/src/utils/project-boundary.d.ts +11 -1
  211. package/dist/src/utils/project-boundary.js +33 -11
  212. package/dist/src/utils/project-boundary.js.map +1 -1
  213. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  214. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  215. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  216. package/dist/src/utils/sensitive-content.d.ts +4 -0
  217. package/dist/src/utils/sensitive-content.js +36 -0
  218. package/dist/src/utils/sensitive-content.js.map +1 -0
  219. package/dist/src/utils/windows-claude-shim.d.ts +9 -0
  220. package/dist/src/utils/windows-claude-shim.js +141 -0
  221. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  222. package/dist/vitest.config.js +91 -10
  223. package/dist/vitest.config.js.map +1 -1
  224. package/package.json +9 -9
  225. package/dist/authoring/templates/aw-config-runtime.d.mts +0 -2
  226. package/dist/authoring/templates/aw-config-runtime.mjs +0 -407
  227. package/dist/authoring/templates/aw-config-runtime.mjs.map +0 -1
@@ -2,13 +2,20 @@ import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { statusCommandV7, statusTargetIntent, } from '../commands/status-v6.js';
4
4
  import { addCatalogSourceCommand, listCatalogSources, preflightCatalogSourceMutation, removeCatalogSourceCommand, setPrimaryCatalogSourceCommand, updateCatalogSourceCommand, } from '../commands/catalog-source.js';
5
- import { cleanupWriteCommand, installWriteCommand, reinstallWriteCommand, syncWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
5
+ import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
6
+ import { resolveSyncIntentSelection } from '../commands/sync-command.js';
7
+ import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, } from '../commands/options.js';
6
8
  import { readCliPackageInfo } from '../utils/local-marketplace.js';
9
+ import { createCodexNativeObservationSession, } from '../utils/codex-native-observation.js';
7
10
  import { readCatalogSourcesConfigSnapshot, recoverInvalidCatalogSourcesConfig, } from '../utils/catalog-sources.js';
8
11
  import { withResolvedCatalogPlugin, withResolvedMultiSourceCatalog, formatPluginId, parsePluginId, } from '../utils/multi-source-catalog.js';
9
- import { coercePluginConfigUpdates, inspectPluginConfigResetTarget, readPluginConfigStore, resolveConfigRead, resolveEffectivePluginConfig, } from '../utils/plugin-config.js';
10
- import { applyPluginConfigWrite, replayConfigRuntimeWrites, resetPluginConfigWithHelper, } from '../utils/plugin-config-write.js';
11
- import { offlineCleanupEvidenceFromInventory, } from '../commands/offline-cleanup-evidence.js';
12
+ import { coercePluginConfigUpdates, inspectPluginConfigResetTarget, listNamespacedPluginConfigStoreIdentities, readPluginConfigStore, resolveConfigRead, resolveEffectivePluginConfig, resolvePluginConfigStorePath, } from '../utils/plugin-config.js';
13
+ import { assertProjectSharedConfigPathVisible } from '../utils/project-boundary.js';
14
+ import { readPluginIntentState } from '../utils/plugin-intent-state.js';
15
+ import { applyPluginConfigWrite, replayConfigRuntimeWrites, resolveConfigPreApplyAuthorization, resetPluginConfigWithHelper, } from '../utils/plugin-config-write.js';
16
+ import { analyzePluginConfigMutationImpact } from '../utils/plugin-config-impact.js';
17
+ import { createBatchIntentCompatiblePlanFingerprint } from '../commands/operation-plan.js';
18
+ import { offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from '../commands/offline-cleanup-evidence.js';
12
19
  import { inventoryCatalogSourceReferences } from '../utils/catalog-source-references.js';
13
20
  import { operationEvidenceMatches, operationEvidenceFingerprint, TuiConfigInvalidStoreError, TuiController, TuiOperationBlockedError, } from './controller.js';
14
21
  import { buildConfigGroups, buildConfigPage, projectPlatformPlacement, } from './model.js';
@@ -112,16 +119,32 @@ export function configAffectedStatusTargets(targets, pluginId, configScope) {
112
119
  .sort((left, right) => `${left.platform}\0${left.scope}`
113
120
  .localeCompare(`${right.platform}\0${right.scope}`, 'en'));
114
121
  }
115
- function configSharedNativeRenderTargets(plugin, changedKeys) {
116
- const changed = new Set(changedKeys);
122
+ function configRetrySharedNativeRenderTargets(plugin) {
117
123
  return (plugin.manifest.artifacts?.targets ?? []).flatMap((target) => target.platform === 'codex'
118
124
  && target.target === 'codex.skill'
119
125
  && target.delivery.mode === 'codex-native'
120
126
  && target.skillKey
121
- && target.refreshKeys.some((key) => changed.has(key))
127
+ && target.refreshKeys.length > 0
122
128
  ? [{ id: target.id, skillKey: target.skillKey, refreshKeys: [...target.refreshKeys] }]
123
129
  : []);
124
130
  }
131
+ function configImpactVisibleTargets(pluginName, targets) {
132
+ return targets.map((target) => ({
133
+ pluginName,
134
+ platform: target.platform,
135
+ scope: target.scope,
136
+ }));
137
+ }
138
+ function configIntentRequestsApply(intent) {
139
+ if (!intent.apply)
140
+ return false;
141
+ if (intent.retryApply === true)
142
+ return true;
143
+ if (intent.changes)
144
+ return intent.changes.length > 0;
145
+ return Object.keys(intent.updates ?? {}).length > 0
146
+ || (intent.removeKeys?.length ?? 0) > 0;
147
+ }
125
148
  function operationReviewStale(message) {
126
149
  return new TuiOperationBlockedError('operationReviewStale', `operationReviewStale: ${message}`);
127
150
  }
@@ -130,6 +153,7 @@ function sourceEvidenceForPlugin(source, pluginName) {
130
153
  return {
131
154
  sourceId: source.sourceId,
132
155
  sourceIdentity: source.sourceIdentity,
156
+ logicalIdentity: source.logicalIdentity,
133
157
  requestedRef: source.requestedRef,
134
158
  resolvedCommit: source.resolvedCommit,
135
159
  manifestDigest: source.manifestDigest,
@@ -140,9 +164,27 @@ function sourceEvidenceForPlugin(source, pluginName) {
140
164
  channel: source.manifest.channel,
141
165
  };
142
166
  }
167
+ async function inspectConfigShareability(options) {
168
+ if (options.scope !== 'project')
169
+ return undefined;
170
+ const targetPath = resolvePluginConfigStorePath({
171
+ sourceId: options.sourceId,
172
+ pluginName: options.pluginName,
173
+ scope: 'project',
174
+ cwd: options.cwd,
175
+ env: options.env,
176
+ });
177
+ const visibility = await assertProjectSharedConfigPathVisible({
178
+ cwd: options.cwd,
179
+ targetPath,
180
+ env: options.env,
181
+ });
182
+ return visibility.state === 'unverified' ? 'unverified' : 'verified';
183
+ }
143
184
  function assertSourceEvidenceMatches(expected, actual) {
144
185
  if (actual.sourceId !== expected.sourceId
145
186
  || actual.sourceIdentity !== expected.sourceIdentity
187
+ || actual.logicalIdentity !== expected.logicalIdentity
146
188
  || actual.resolvedCommit !== expected.resolvedCommit
147
189
  || actual.manifestDigest !== expected.manifestDigest
148
190
  || actual.snapshotDigest !== expected.snapshotDigest
@@ -150,10 +192,12 @@ function assertSourceEvidenceMatches(expected, actual) {
150
192
  throw operationReviewStale('确认后来源内容发生变化。');
151
193
  }
152
194
  }
153
- function throwIfWriteReviewStale(result) {
154
- const stale = result.targetResults.find((target) => target.errorCode === 'operationReviewStale'
195
+ function writeReviewIsStale(result) {
196
+ return result.targetResults.some((target) => target.errorCode === 'operationReviewStale'
155
197
  || target.errorCode === 'operationPlanStale');
156
- if (stale) {
198
+ }
199
+ function throwIfWriteReviewStale(result) {
200
+ if (writeReviewIsStale(result)) {
157
201
  throw operationReviewStale('确认后实际写入计划发生变化。');
158
202
  }
159
203
  }
@@ -163,6 +207,122 @@ const SOURCE_PROGRESS_PHASES = {
163
207
  function pluginIntent(intent) {
164
208
  return 'pluginId' in intent;
165
209
  }
210
+ function reviewedScriptTimeoutMs(intent) {
211
+ if ((intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch')
212
+ && (intent.action === 'install' || intent.action === 'refreshAndSync'
213
+ || intent.action === 'reinstall' || intent.action === 'sync')) {
214
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
215
+ }
216
+ if (intent.kind === 'configSave' && intent.apply) {
217
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
218
+ }
219
+ return undefined;
220
+ }
221
+ function selectedBatchGroups(intent) {
222
+ if (intent.selection.kind !== 'selected' && intent.selection.kind !== 'targets') {
223
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目全部同步必须从项目 intent roots 解析。');
224
+ }
225
+ const rawTargets = intent.selection.kind === 'targets'
226
+ ? intent.selection.targets
227
+ : intent.selection.pluginIds.map((pluginId) => ({
228
+ pluginId, platform: intent.platform, scope: intent.scope,
229
+ }));
230
+ const normalizedTargets = rawTargets.map((target) => {
231
+ if ((target.platform !== 'codex' && target.platform !== 'claude' && target.platform !== 'both')
232
+ || target.scope !== intent.scope
233
+ || (intent.selection.kind === 'targets' && target.platform === 'both')) {
234
+ throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用共同 scope 和单个平台。');
235
+ }
236
+ const pluginId = target.pluginId;
237
+ const parsed = parsePluginId(pluginId);
238
+ if (!parsed.sourceId) {
239
+ throw new TuiOperationBlockedError('invalidPluginId', '批量操作必须使用 canonical pluginId。');
240
+ }
241
+ const canonicalPluginId = formatPluginId({ sourceId: parsed.sourceId, pluginName: parsed.pluginName });
242
+ if (intent.selection.kind === 'targets' && canonicalPluginId !== pluginId) {
243
+ throw new TuiOperationBlockedError('invalidPluginId', '批量精确目标必须使用 canonical pluginId。');
244
+ }
245
+ return {
246
+ pluginId: canonicalPluginId,
247
+ platform: target.platform,
248
+ scope: target.scope,
249
+ };
250
+ }).sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
251
+ .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
252
+ const uniqueTargets = [...new Map(normalizedTargets.map((target) => [
253
+ [target.pluginId, target.platform, target.scope].join('\0'), target,
254
+ ])).values()];
255
+ const requestedPluginIds = [...new Set(uniqueTargets.map((target) => target.pluginId))];
256
+ if (requestedPluginIds.length === 0) {
257
+ throw new TuiOperationBlockedError('syncSelectionRequired', '批量操作至少需要一个插件。');
258
+ }
259
+ const groups = new Map();
260
+ for (const target of uniqueTargets) {
261
+ const pluginId = target.pluginId;
262
+ const sourceId = parsePluginId(pluginId).sourceId;
263
+ const groupKey = [sourceId, target.platform, target.scope].join('\0');
264
+ const group = groups.get(groupKey) ?? {
265
+ groupId: groupKey,
266
+ sourceId,
267
+ platform: target.platform,
268
+ scope: target.scope,
269
+ requestedPluginIds: [],
270
+ };
271
+ group.requestedPluginIds.push(pluginId);
272
+ groups.set(groupKey, group);
273
+ }
274
+ return { requestedPluginIds, groups: [...groups.values()] };
275
+ }
276
+ async function resolveBatchGroups(intent, env, cwd, frozenRequestedPluginIds) {
277
+ if (intent.selection.kind === 'selected' || intent.selection.kind === 'targets') {
278
+ return selectedBatchGroups(intent);
279
+ }
280
+ if (intent.action !== 'sync' || intent.scope !== 'project') {
281
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目全部操作只支持 project scope sync。');
282
+ }
283
+ const state = await readPluginIntentState({ scope: 'project', cwd, env });
284
+ const selection = resolveSyncIntentSelection({
285
+ roots: state.roots,
286
+ selectors: frozenRequestedPluginIds ?? [],
287
+ all: frozenRequestedPluginIds === undefined,
288
+ platform: intent.platform,
289
+ });
290
+ if (selection.requestedPluginIds.length === 0) {
291
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目没有可同步的 intent roots。');
292
+ }
293
+ return {
294
+ requestedPluginIds: selection.requestedPluginIds,
295
+ groups: selection.groups.map((group) => ({
296
+ groupId: `${group.sourceId}\0${group.platform}\0${intent.scope}`,
297
+ sourceId: group.sourceId,
298
+ platform: group.platform,
299
+ scope: intent.scope,
300
+ requestedPluginIds: group.pluginIds,
301
+ })),
302
+ };
303
+ }
304
+ async function runBatchWriteGroup(options) {
305
+ const commandOptions = {
306
+ cwd: options.cwd,
307
+ platform: options.group.platform,
308
+ scope: options.group.scope,
309
+ ...(reviewedScriptTimeoutMs(options.intent) === undefined
310
+ ? {}
311
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(options.intent) }),
312
+ };
313
+ switch (options.intent.action) {
314
+ case 'install':
315
+ return installWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
316
+ case 'sync':
317
+ return syncWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
318
+ case 'reinstall':
319
+ return reinstallWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
320
+ case 'uninstall':
321
+ return uninstallWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
322
+ case 'cleanup':
323
+ return cleanupWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
324
+ }
325
+ }
166
326
  async function loadDependencyOverviews(options) {
167
327
  const graph = await withResolvedMultiSourceCatalog({
168
328
  cliVersion: options.cliVersion,
@@ -437,6 +597,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
437
597
  };
438
598
  };
439
599
  const catalogOperationByEvidence = new WeakMap();
600
+ const codexManagerByEvidence = new WeakMap();
440
601
  let sessionCatalogOperationContext;
441
602
  let sessionStatusResult;
442
603
  const sessionContextForPlugin = (pluginId) => {
@@ -450,6 +611,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
450
611
  const stores = await Promise.all(scopes.map((scope) => readPluginConfigStore({
451
612
  sourceId: config.sourceId,
452
613
  sourceIdentity: config.sourceIdentity,
614
+ logicalIdentity: config.logicalIdentity,
453
615
  pluginName: config.pluginName,
454
616
  scope,
455
617
  cwd: context.cwd,
@@ -541,14 +703,38 @@ export function createTuiControllerDependencies(context, progressFeed, options =
541
703
  signal.throwIfAborted();
542
704
  const catalogGenerations = await loadCatalogGenerations();
543
705
  signal.throwIfAborted();
706
+ const configStoreInventory = (await Promise.all(['user', 'project'].map(async (scope) => {
707
+ try {
708
+ return (await listNamespacedPluginConfigStoreIdentities({
709
+ cwd: context.cwd,
710
+ env: context.env,
711
+ scope,
712
+ })).map(({ pluginId, sourceId, pluginName }) => ({
713
+ pluginId, sourceId, pluginName, scope,
714
+ }));
715
+ }
716
+ catch {
717
+ await context.debugLogger.log({
718
+ type: 'tui:config-inventory-error',
719
+ level: 'debug',
720
+ target: 'debugLog',
721
+ message: 'TUI saved config inventory could not be read.',
722
+ data: { scope },
723
+ });
724
+ return [];
725
+ }
726
+ }))).flat();
727
+ signal.throwIfAborted();
544
728
  const statusById = new Map(result.payload.catalog.sources.map((source) => [source.id, source]));
545
729
  return {
546
730
  cliChannel: packageInfo.version.includes('-') ? 'beta' : 'latest',
731
+ configuredSources: structuredClone(configured.sources),
547
732
  payload: result.payload,
548
733
  pluginSummaries: result.pluginSummaries,
549
734
  targetRevisions: result.targetRevisions,
550
735
  dependencyOverviews,
551
736
  portablePluginIds,
737
+ configStoreInventory,
552
738
  catalogGenerations,
553
739
  sessionGenerationKeys: Object.fromEntries((catalogOperationContext?.generationSet.sources ?? [])
554
740
  .map((source) => [source.sourceId, source.storeKey])),
@@ -580,7 +766,8 @@ export function createTuiControllerDependencies(context, progressFeed, options =
580
766
  }
581
767
  throw new TuiOperationBlockedError('operationReviewStale', '状态读取期间来源配置持续变化。请重试。');
582
768
  };
583
- const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate) => {
769
+ const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
770
+ reviewedScriptTimeoutMs(intent);
584
771
  if (intent.kind === 'configReset') {
585
772
  const parsed = parsePluginId(intent.pluginId);
586
773
  if (!parsed.sourceId || !parsed.pluginName) {
@@ -598,10 +785,16 @@ export function createTuiControllerDependencies(context, progressFeed, options =
598
785
  if (target.kind === 'special') {
599
786
  throw new TuiOperationBlockedError('configResetTargetUnsupported', '设置目标是特殊文件系统节点,不能自动重置。请手动移动后重试。');
600
787
  }
601
- let source;
788
+ const relevantTargets = sessionStatusResult
789
+ ? configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope)
790
+ : [];
791
+ const relevantRevisions = (sessionStatusResult?.targetRevisions ?? []).filter((revision) => relevantTargets.some((statusTarget) => statusTarget.platform === revision.platform
792
+ && statusTarget.scope === revision.scope
793
+ && statusTarget.pluginId === revision.pluginId));
794
+ let selected;
602
795
  try {
603
796
  const packageInfo = await readCliPackageInfo();
604
- source = await withResolvedCatalogPlugin({
797
+ selected = await withResolvedCatalogPlugin({
605
798
  selector: intent.pluginId,
606
799
  cliVersion: packageInfo.version,
607
800
  env: context.env,
@@ -609,16 +802,23 @@ export function createTuiControllerDependencies(context, progressFeed, options =
609
802
  ? { operationContext: sessionContextForPlugin(intent.pluginId) }
610
803
  : {}),
611
804
  ...(signal ? { signal } : {}),
612
- consume: async ({ plugin, source: resolvedSource }) => sourceEvidenceForPlugin(resolvedSource, plugin.manifest.name),
805
+ consume: async ({ plugin, source: resolvedSource }) => ({
806
+ source: sourceEvidenceForPlugin(resolvedSource, plugin.manifest.name),
807
+ impact: await analyzePluginConfigMutationImpact({
808
+ plugin,
809
+ scope: intent.scope,
810
+ cwd: context.cwd,
811
+ env: context.env,
812
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, relevantTargets),
813
+ mutation: { kind: 'reset' },
814
+ }),
815
+ }),
613
816
  });
614
817
  }
615
818
  catch {
616
- source = undefined;
819
+ selected = undefined;
617
820
  }
618
821
  signal?.throwIfAborted();
619
- const relevantTargets = sessionStatusResult
620
- ? configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope)
621
- : [];
622
822
  await preparation?.reporter.emit('evidence');
623
823
  return {
624
824
  catalog: sessionStatusResult ? {
@@ -628,11 +828,20 @@ export function createTuiControllerDependencies(context, progressFeed, options =
628
828
  configDigest: 'config-reset-offline',
629
829
  compositeDigest: 'config-reset-target',
630
830
  },
631
- ...(source ? { source } : {}),
831
+ ...(selected ? { source: selected.source } : {}),
832
+ ...(sessionStatusResult ? {
833
+ targetFingerprint: statusTargetFingerprint(relevantTargets, relevantRevisions),
834
+ } : {}),
632
835
  config: {
633
836
  scope: intent.scope,
634
- storeDigest: target.fingerprint,
635
- affectedTargets: relevantTargets.map(({ platform, scope }) => ({ platform, scope })),
837
+ storeDigest: selected?.impact.storeDigest ?? target.fingerprint,
838
+ impactState: selected?.impact.impactState ?? 'unknown',
839
+ ...(selected?.impact.noEffectiveChange ? { noEffectiveChange: true } : {}),
840
+ affectedTargets: (selected?.impact.affectedTargets
841
+ ?? relevantTargets).map(({ platform, scope }) => ({ platform, scope })),
842
+ ...((selected?.impact.sharedNativeRenderTargets.length ?? 0) > 0
843
+ ? { sharedNativeRenderTargets: selected.impact.sharedNativeRenderTargets }
844
+ : {}),
636
845
  },
637
846
  resetTarget: {
638
847
  sourceId: parsed.sourceId,
@@ -684,12 +893,16 @@ export function createTuiControllerDependencies(context, progressFeed, options =
684
893
  && (preview.identityChanged || sourceFingerprintChanged);
685
894
  if (preview.identityChanged)
686
895
  sourceIdentityChangeImpact = {
687
- impactSchemaVersion: 1,
896
+ impactSchemaVersion: 3,
688
897
  sourceId: preview.sourceId,
898
+ primaryChange: preview.references.primaryChange,
689
899
  inventoryComplete: preview.references.inventoryComplete,
690
900
  references: preview.references.references,
691
901
  affectedReferences: preview.references.references,
692
- changes: { added: [], removed: [], changed: [], newlyIncompatible: [], dependencyChanged: [] },
902
+ changes: {
903
+ added: [], removed: [], changed: [], newlyIncompatible: [],
904
+ dependencyChanged: [], platformSupportChanged: [],
905
+ },
693
906
  errors: preview.references.errors,
694
907
  };
695
908
  if (!explicitSourceChange) {
@@ -828,7 +1041,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
828
1041
  if (intent.action !== 'replaceDamaged' && !explicitSourceChange && candidateRelation === 'rollbackDetected') {
829
1042
  throw new TuiOperationBlockedError('catalogRollbackDetected', '候选 Catalog 版本低于当前 Active。普通更新已阻止。');
830
1043
  }
831
- const impact = intent.action === 'repair' ? undefined
1044
+ const impact = exactRepair ? undefined
832
1045
  : sourceIdentityChangeImpact ?? await inspectCatalogSourceImpact({
833
1046
  sourceId: intent.sourceId,
834
1047
  cwd: context.cwd,
@@ -837,6 +1050,29 @@ export function createTuiControllerDependencies(context, progressFeed, options =
837
1050
  mode: 'generation',
838
1051
  candidateManifest: probe.manifest,
839
1052
  });
1053
+ const primaryPreview = intent.setPrimaryAfterActivation
1054
+ ? await preflightCatalogSourceMutation({
1055
+ request: { action: 'setPrimary', sourceId: intent.sourceId },
1056
+ cwd: context.cwd,
1057
+ env: context.env,
1058
+ })
1059
+ : undefined;
1060
+ if (primaryPreview && primaryPreview.configDigest !== config.digest) {
1061
+ throw operationReviewStale('准备主来源切换期间来源配置发生变化。');
1062
+ }
1063
+ const primaryImpact = primaryPreview ? {
1064
+ impactSchemaVersion: 3,
1065
+ sourceId: primaryPreview.sourceId,
1066
+ primaryChange: primaryPreview.references.primaryChange,
1067
+ inventoryComplete: primaryPreview.references.inventoryComplete,
1068
+ references: primaryPreview.references.references,
1069
+ affectedReferences: primaryPreview.references.references,
1070
+ changes: {
1071
+ added: [], removed: [], changed: [], newlyIncompatible: [],
1072
+ dependencyChanged: [], platformSupportChanged: [],
1073
+ },
1074
+ errors: primaryPreview.references.errors,
1075
+ } : undefined;
840
1076
  return {
841
1077
  catalog: {
842
1078
  configDigest: config.digest,
@@ -857,6 +1093,11 @@ export function createTuiControllerDependencies(context, progressFeed, options =
857
1093
  impactDigest: calculateCatalogSourceImpactDigest(impact),
858
1094
  impactMode: sourceIdentityChangeImpact ? 'identityChange' : 'generation',
859
1095
  } : {}),
1096
+ ...(primaryPreview && primaryImpact ? {
1097
+ primaryImpact,
1098
+ primaryImpactDigest: primaryPreview.impactDigest,
1099
+ primaryConfigDigest: primaryPreview.configDigest,
1100
+ } : {}),
860
1101
  configDigest: config.digest,
861
1102
  ...(proposedConfigDigest ? { proposedConfigDigest } : {}),
862
1103
  ...(proposedSource ? { proposedSource } : {}),
@@ -896,12 +1137,16 @@ export function createTuiControllerDependencies(context, progressFeed, options =
896
1137
  referenceCount: preview.references.references.length,
897
1138
  inventoryComplete: preview.references.inventoryComplete,
898
1139
  impact: {
899
- impactSchemaVersion: 1,
1140
+ impactSchemaVersion: 3,
900
1141
  sourceId: preview.sourceId,
1142
+ primaryChange: preview.references.primaryChange,
901
1143
  inventoryComplete: preview.references.inventoryComplete,
902
1144
  references: preview.references.references,
903
1145
  affectedReferences: preview.references.references,
904
- changes: { added: [], removed: [], changed: [], newlyIncompatible: [], dependencyChanged: [] },
1146
+ changes: {
1147
+ added: [], removed: [], changed: [], newlyIncompatible: [],
1148
+ dependencyChanged: [], platformSupportChanged: [],
1149
+ },
905
1150
  errors: preview.references.errors,
906
1151
  },
907
1152
  },
@@ -912,10 +1157,95 @@ export function createTuiControllerDependencies(context, progressFeed, options =
912
1157
  }
913
1158
  return evidence;
914
1159
  }
915
- if (!pluginIntent(intent)) {
1160
+ if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch') {
916
1161
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
917
1162
  }
918
1163
  signal?.throwIfAborted();
1164
+ if (intent.kind === 'pluginWriteBatch') {
1165
+ await preparation?.reporter.emit('status');
1166
+ const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
1167
+ signal?.throwIfAborted();
1168
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1169
+ const groups = [];
1170
+ const ownsObservationSession = !codexObservationSession
1171
+ && selection.groups.some((group) => {
1172
+ const reviewed = frozenBatchEvidence?.groups.find((candidate) => candidate.groupId === group.groupId);
1173
+ return reviewed?.executable !== false
1174
+ && (group.platform === 'codex' || group.platform === 'both');
1175
+ });
1176
+ const observationSession = codexObservationSession ?? (ownsObservationSession
1177
+ ? await createCodexNativeObservationSession({
1178
+ exactCwd: context.cwd,
1179
+ env: context.env,
1180
+ runner: context.nativeCliRunner,
1181
+ })
1182
+ : undefined);
1183
+ try {
1184
+ await preparation?.reporter.emit('catalog');
1185
+ for (const group of selection.groups) {
1186
+ signal?.throwIfAborted();
1187
+ const reviewed = frozenBatchEvidence?.groups.find((candidate) => candidate.groupId === group.groupId);
1188
+ if (reviewed?.executable === false) {
1189
+ groups.push({ ...group, preview: reviewed.preview, executable: false });
1190
+ continue;
1191
+ }
1192
+ let catalogOperation;
1193
+ try {
1194
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
1195
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [group.sourceId])
1196
+ : await resolveCatalogOperationContext({
1197
+ sourceIds: [group.sourceId],
1198
+ env: context.env,
1199
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
1200
+ ...(signal ? { signal } : {}),
1201
+ });
1202
+ }
1203
+ catch {
1204
+ catalogOperation = undefined;
1205
+ }
1206
+ const preview = await runBatchWriteGroup({
1207
+ intent,
1208
+ group,
1209
+ cwd: context.cwd,
1210
+ runtime: {
1211
+ ...context,
1212
+ inventoryCatalogSourceReferences: operationInventory,
1213
+ expectedCatalogConfigDigest: configSnapshot.digest,
1214
+ ...(catalogOperation ? {
1215
+ catalogOperationContext: catalogOperation,
1216
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1217
+ } : {}),
1218
+ previewOperationPlan: true,
1219
+ ...(observationSession ? { codexObservationSession: observationSession } : {}),
1220
+ ...(preparation ? {
1221
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1222
+ } : {}),
1223
+ },
1224
+ });
1225
+ groups.push({ ...group, preview, executable: preview.operationPlan !== undefined });
1226
+ }
1227
+ }
1228
+ finally {
1229
+ if (ownsObservationSession)
1230
+ await observationSession?.dispose();
1231
+ }
1232
+ await preparation?.reporter.emit('evidence');
1233
+ signal?.throwIfAborted();
1234
+ const merged = mergeWriteCommandResults(groups.map((group) => group.preview));
1235
+ const evidence = {
1236
+ catalog: {
1237
+ configDigest: configSnapshot.digest,
1238
+ compositeDigest: sessionStatusResult?.payload.catalog.compositeDigest
1239
+ ?? digest(groups.map((group) => [group.groupId, group.preview.operationPlan?.planFingerprint])),
1240
+ },
1241
+ requestedPluginIds: selection.requestedPluginIds,
1242
+ groups,
1243
+ ...(merged.dependencyPlan ? { dependencyPlan: merged.dependencyPlan } : {}),
1244
+ ...(merged.codexReconcilePlans ? { codexReconcilePlans: merged.codexReconcilePlans } : {}),
1245
+ ...(merged.operationPlan ? { operationPlan: merged.operationPlan } : {}),
1246
+ };
1247
+ return evidence;
1248
+ }
919
1249
  const packageInfo = await readCliPackageInfo();
920
1250
  const offlineCleanupReason = intent.kind === 'pluginWrite'
921
1251
  && intent.action === 'cleanup'
@@ -988,12 +1318,14 @@ export function createTuiControllerDependencies(context, progressFeed, options =
988
1318
  if (configSnapshot.digest !== sessionStatusResult.payload.catalog.configDigest) {
989
1319
  throw operationReviewStale('准备设置确认期间来源配置发生变化。');
990
1320
  }
1321
+ const relevantTargets = configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope);
1322
+ const preparedCatalogOperation = catalogOperation;
991
1323
  const selected = await withResolvedCatalogPlugin({
992
1324
  selector: intent.pluginId,
993
1325
  cliVersion: packageInfo.version,
994
1326
  configSnapshot,
995
1327
  env: context.env,
996
- ...(catalogOperation ? { operationContext: catalogOperation } : {}),
1328
+ operationContext: preparedCatalogOperation,
997
1329
  ...(signal ? { signal } : {}),
998
1330
  consume: async ({ plugin, source }) => {
999
1331
  if (!plugin.manifest.config) {
@@ -1011,30 +1343,56 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1011
1343
  }
1012
1344
  }
1013
1345
  }
1346
+ const impact = intent.retryApply ? undefined : await analyzePluginConfigMutationImpact({
1347
+ plugin,
1348
+ scope: intent.scope,
1349
+ cwd: context.cwd,
1350
+ env: context.env,
1351
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, relevantTargets),
1352
+ mutation: {
1353
+ kind: 'patch',
1354
+ updates: intent.updates,
1355
+ removeKeys: intent.removeKeys,
1356
+ },
1357
+ });
1358
+ const affectedTargets = intent.retryApply
1359
+ ? configImpactVisibleTargets(plugin.manifest.name, relevantTargets)
1360
+ : impact.affectedTargets;
1014
1361
  return {
1015
1362
  pluginName: plugin.manifest.name,
1016
1363
  source: sourceEvidenceForPlugin(source, plugin.manifest.name),
1017
- sharedNativeRenderTargets: intent.kind === 'configSave'
1018
- ? configSharedNativeRenderTargets(plugin, [
1019
- ...Object.keys(intent.updates ?? {}),
1020
- ...(intent.removeKeys ?? []),
1021
- ])
1364
+ impact,
1365
+ preApplyAuthorization: configIntentRequestsApply(intent)
1366
+ ? (await resolveConfigPreApplyAuthorization({
1367
+ plugin,
1368
+ scope: intent.scope,
1369
+ cwd: context.cwd,
1370
+ env: context.env,
1371
+ applyTargets: affectedTargets,
1372
+ nativeCliRunner: context.nativeCliRunner,
1373
+ rebuildRuntime: {
1374
+ catalogOperationContext: preparedCatalogOperation,
1375
+ expectedGenerationSetDigest: preparedCatalogOperation.generationSetDigest,
1376
+ inventoryCatalogSourceReferences: operationInventory,
1377
+ },
1378
+ })).targets
1022
1379
  : [],
1380
+ sharedNativeRenderTargets: intent.retryApply
1381
+ ? configRetrySharedNativeRenderTargets(plugin)
1382
+ : impact.sharedNativeRenderTargets,
1023
1383
  };
1024
1384
  },
1025
1385
  });
1026
- const store = await readPluginConfigStore({
1027
- sourceId: selected.source?.sourceId ?? parsePluginId(intent.pluginId).sourceId,
1028
- ...(selected.source?.sourceIdentity
1029
- ? { sourceIdentity: selected.source.sourceIdentity }
1030
- : {}),
1386
+ const retryStore = intent.retryApply ? await readPluginConfigStore({
1387
+ sourceId: selected.source.sourceId,
1388
+ sourceIdentity: selected.source.sourceIdentity,
1389
+ logicalIdentity: selected.source.logicalIdentity,
1031
1390
  pluginName: selected.pluginName,
1032
1391
  scope: intent.scope,
1033
1392
  cwd: context.cwd,
1034
1393
  env: context.env,
1035
- });
1394
+ }) : undefined;
1036
1395
  signal?.throwIfAborted();
1037
- const relevantTargets = configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope);
1038
1396
  const relevantRevisions = (sessionStatusResult.targetRevisions ?? []).filter((revision) => relevantTargets.some((target) => target.platform === revision.platform
1039
1397
  && target.scope === revision.scope
1040
1398
  && target.pluginId === revision.pluginId));
@@ -1050,8 +1408,15 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1050
1408
  config: {
1051
1409
  scope: intent.scope,
1052
1410
  ...(selected.source?.manifestDigest ? { schemaIdentity: selected.source.manifestDigest } : {}),
1053
- storeDigest: digest({ exists: store.exists, rawDigest: store.rawDigest }),
1054
- affectedTargets: relevantTargets.map(({ platform, scope }) => ({ platform, scope })),
1411
+ storeDigest: selected.impact?.storeDigest
1412
+ ?? digest({ exists: retryStore?.exists, rawDigest: retryStore?.rawDigest }),
1413
+ impactState: selected.impact?.impactState ?? 'known',
1414
+ ...(selected.impact?.noEffectiveChange ? { noEffectiveChange: true } : {}),
1415
+ affectedTargets: (selected.impact?.affectedTargets
1416
+ ?? relevantTargets).map(({ platform, scope }) => ({ platform, scope })),
1417
+ ...(selected.preApplyAuthorization.length > 0
1418
+ ? { preApplyAuthorization: selected.preApplyAuthorization }
1419
+ : {}),
1055
1420
  ...(selected.sharedNativeRenderTargets.length > 0
1056
1421
  ? { sharedNativeRenderTargets: selected.sharedNativeRenderTargets }
1057
1422
  : {}),
@@ -1099,6 +1464,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1099
1464
  let dependencyPlan;
1100
1465
  let codexReconcilePlans;
1101
1466
  let operationPlan;
1467
+ let forceCleanupEvidence;
1102
1468
  await preparation?.reporter.emit('catalog');
1103
1469
  const offlineCleanupTarget = offlineCleanupReason && intent.kind === 'pluginWrite'
1104
1470
  ? status.payload.targets.find((target) => target.pluginId === intent.pluginId
@@ -1131,6 +1497,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1131
1497
  throw operationReviewStale('离线清理现场证据已消失或无法验证。');
1132
1498
  }
1133
1499
  }
1500
+ const configVisibleTargets = intent.kind === 'configSave'
1501
+ ? configAffectedStatusTargets(status.payload.targets, intent.pluginId, intent.scope)
1502
+ : [];
1134
1503
  const selected = offlineCleanupTarget
1135
1504
  ? {
1136
1505
  pluginName: offlineCleanupTarget.pluginName,
@@ -1165,19 +1534,54 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1165
1534
  }
1166
1535
  }
1167
1536
  }
1537
+ const impact = intent.kind === 'configSave' && !intent.retryApply
1538
+ ? await analyzePluginConfigMutationImpact({
1539
+ plugin,
1540
+ scope: intent.scope,
1541
+ cwd: context.cwd,
1542
+ env: context.env,
1543
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets),
1544
+ mutation: {
1545
+ kind: 'patch',
1546
+ updates: intent.updates,
1547
+ removeKeys: intent.removeKeys,
1548
+ },
1549
+ })
1550
+ : undefined;
1168
1551
  return {
1169
1552
  pluginName: plugin.manifest.name,
1170
1553
  source: sourceEvidenceForPlugin(source, plugin.manifest.name),
1554
+ impact,
1555
+ preApplyAuthorization: intent.kind === 'configSave'
1556
+ && configIntentRequestsApply(intent)
1557
+ ? (await resolveConfigPreApplyAuthorization({
1558
+ plugin,
1559
+ scope: intent.scope,
1560
+ cwd: context.cwd,
1561
+ env: context.env,
1562
+ applyTargets: intent.retryApply
1563
+ ? configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets)
1564
+ : impact.affectedTargets,
1565
+ nativeCliRunner: context.nativeCliRunner,
1566
+ rebuildRuntime: {
1567
+ ...(catalogOperation ? { catalogOperationContext: catalogOperation } : {}),
1568
+ ...(catalogOperation?.generationSetDigest
1569
+ ? { expectedGenerationSetDigest: catalogOperation.generationSetDigest }
1570
+ : {}),
1571
+ inventoryCatalogSourceReferences: operationInventory,
1572
+ },
1573
+ })).targets
1574
+ : [],
1171
1575
  sharedNativeRenderTargets: intent.kind === 'configSave'
1172
- ? configSharedNativeRenderTargets(plugin, [
1173
- ...Object.keys(intent.updates ?? {}),
1174
- ...(intent.removeKeys ?? []),
1175
- ])
1576
+ ? intent.retryApply
1577
+ ? configRetrySharedNativeRenderTargets(plugin)
1578
+ : impact.sharedNativeRenderTargets
1176
1579
  : [],
1177
1580
  };
1178
1581
  },
1179
1582
  });
1180
1583
  let codexManager;
1584
+ let resolvedCodexManager;
1181
1585
  const hasDeclaredDependencies = (status.pluginSummaries.find((summary) => summary.pluginId === intent.pluginId)?.dependencyTotalCount ?? 0) > 0;
1182
1586
  if (intent.kind === 'pluginWrite') {
1183
1587
  const command = {
@@ -1192,6 +1596,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1192
1596
  cwd: context.cwd,
1193
1597
  platform: intent.platform,
1194
1598
  scope: intent.scope,
1599
+ ...(reviewedScriptTimeoutMs(intent) === undefined
1600
+ ? {}
1601
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1195
1602
  ...(intent.action === 'install' && intent.dependencyScopeOverrides
1196
1603
  ? { dependencyScopeOverrides: intent.dependencyScopeOverrides }
1197
1604
  : {}),
@@ -1205,6 +1612,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1205
1612
  expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1206
1613
  } : {}),
1207
1614
  previewOperationPlan: true,
1615
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1208
1616
  ...(preparation ? {
1209
1617
  onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1210
1618
  } : {}),
@@ -1212,6 +1620,17 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1212
1620
  signal?.throwIfAborted();
1213
1621
  codexReconcilePlans = preview?.codexReconcilePlans;
1214
1622
  operationPlan = preview?.operationPlan;
1623
+ if (intent.action === 'cleanup' && intent.force === true) {
1624
+ const candidates = preview?.offlineCleanupPlans?.filter((candidate) => candidate.forceExclusive === true
1625
+ && candidate.pluginId === intent.pluginId
1626
+ && candidate.platform === intent.platform
1627
+ && candidate.scope === intent.scope
1628
+ && candidate.plan.actions.length > 0) ?? [];
1629
+ if (candidates.length !== 1) {
1630
+ throw new TuiOperationBlockedError('forceCleanupPathUnavailable', '没有唯一且可验证的精确清理目标。');
1631
+ }
1632
+ [forceCleanupEvidence] = candidates;
1633
+ }
1215
1634
  if (hasDeclaredDependencies
1216
1635
  && (intent.action === 'install'
1217
1636
  || intent.action === 'refreshAndSync'
@@ -1234,6 +1653,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1234
1653
  throw new TuiOperationBlockedError(managerPreview.code, `${managerPreview.code}: ${message}`);
1235
1654
  }
1236
1655
  if (managerPreview) {
1656
+ if (managerPreview.state === 'system' || managerPreview.state === 'cached') {
1657
+ resolvedCodexManager = managerPreview.manager;
1658
+ }
1237
1659
  codexManager = {
1238
1660
  state: managerPreview.state,
1239
1661
  packageName: MANAGED_CODEX_PACKAGE_NAME,
@@ -1246,7 +1668,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1246
1668
  await preparation?.reporter.emit('evidence');
1247
1669
  signal?.throwIfAborted();
1248
1670
  const evidenceTargets = intent.kind === 'configSave'
1249
- ? configAffectedStatusTargets(status.payload.targets, intent.pluginId, intent.scope)
1671
+ ? configVisibleTargets
1250
1672
  : status.payload.targets;
1251
1673
  const evidenceRevisions = intent.kind === 'configSave'
1252
1674
  ? (status.targetRevisions ?? []).filter((revision) => evidenceTargets.some((target) => target.platform === revision.platform
@@ -1263,6 +1685,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1263
1685
  },
1264
1686
  source: selected.source,
1265
1687
  ...(offlineCleanup ? { offlineCleanup } : {}),
1688
+ ...(forceCleanupEvidence ? { offlineCleanup: forceCleanupEvidence } : {}),
1266
1689
  ...(dependencyPlan ? { dependencyPlan } : {}),
1267
1690
  ...(codexReconcilePlans ? { codexReconcilePlans } : {}),
1268
1691
  ...(operationPlan ? { operationPlan } : {}),
@@ -1270,30 +1693,53 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1270
1693
  targetFingerprint: statusTargetFingerprint(evidenceTargets, evidenceRevisions),
1271
1694
  };
1272
1695
  if (intent.kind === 'configSave') {
1273
- const store = await readPluginConfigStore({
1696
+ const impact = 'impact' in selected ? selected.impact : undefined;
1697
+ const retryStore = intent.retryApply ? await readPluginConfigStore({
1274
1698
  sourceId: selected.source.sourceId,
1275
1699
  sourceIdentity: selected.source.sourceIdentity,
1700
+ ...('logicalIdentity' in selected.source
1701
+ ? { logicalIdentity: selected.source.logicalIdentity }
1702
+ : {}),
1276
1703
  pluginName: selected.pluginName,
1277
1704
  scope: intent.scope,
1278
1705
  cwd: context.cwd,
1279
1706
  env: context.env,
1280
- });
1707
+ }) : undefined;
1281
1708
  signal?.throwIfAborted();
1282
1709
  evidence.config = {
1283
1710
  scope: intent.scope,
1284
1711
  ...('manifestDigest' in selected.source
1285
1712
  ? { schemaIdentity: selected.source.manifestDigest }
1286
1713
  : {}),
1287
- storeDigest: digest({ exists: store.exists, rawDigest: store.rawDigest }),
1288
- affectedTargets: evidenceTargets.map(({ platform, scope }) => ({ platform, scope })),
1714
+ storeDigest: impact?.storeDigest
1715
+ ?? digest({ exists: retryStore?.exists, rawDigest: retryStore?.rawDigest }),
1716
+ impactState: impact?.impactState ?? 'known',
1717
+ ...(impact?.noEffectiveChange ? { noEffectiveChange: true } : {}),
1718
+ affectedTargets: (impact?.affectedTargets
1719
+ ?? evidenceTargets).map(({ platform, scope }) => ({ platform, scope })),
1720
+ ...(intent.scope === 'project' ? {
1721
+ shareability: await inspectConfigShareability({
1722
+ sourceId: selected.source.sourceId,
1723
+ pluginName: selected.pluginName,
1724
+ scope: intent.scope,
1725
+ cwd: context.cwd,
1726
+ env: context.env,
1727
+ }),
1728
+ } : {}),
1289
1729
  ...('sharedNativeRenderTargets' in selected
1290
1730
  && selected.sharedNativeRenderTargets.length > 0
1291
1731
  ? { sharedNativeRenderTargets: selected.sharedNativeRenderTargets }
1292
1732
  : {}),
1733
+ ...('preApplyAuthorization' in selected
1734
+ && selected.preApplyAuthorization.length > 0
1735
+ ? { preApplyAuthorization: selected.preApplyAuthorization }
1736
+ : {}),
1293
1737
  };
1294
1738
  }
1295
1739
  if (catalogOperation)
1296
1740
  catalogOperationByEvidence.set(evidence, catalogOperation);
1741
+ if (resolvedCodexManager)
1742
+ codexManagerByEvidence.set(evidence, resolvedCodexManager);
1297
1743
  return evidence;
1298
1744
  };
1299
1745
  const dependencies = {
@@ -1501,6 +1947,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1501
1947
  const stores = await readConfigStoresForTui({
1502
1948
  sourceId: plugin.sourceId,
1503
1949
  sourceIdentity: plugin.sourceIdentity,
1950
+ logicalIdentity: plugin.catalogLogicalIdentity,
1504
1951
  pluginName: plugin.manifest.name,
1505
1952
  schema: plugin.manifest.config.fields,
1506
1953
  scope: layer.scope,
@@ -1535,6 +1982,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1535
1982
  const effectiveRaw = await resolveEffectivePluginConfig({
1536
1983
  sourceId: plugin.sourceId,
1537
1984
  sourceIdentity: plugin.sourceIdentity,
1985
+ logicalIdentity: plugin.catalogLogicalIdentity,
1538
1986
  pluginName: plugin.manifest.name,
1539
1987
  manifest: plugin.manifest,
1540
1988
  targetScope: layer.scope === 'user' ? 'user' : 'project',
@@ -1569,9 +2017,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1569
2017
  throw new TuiOperationBlockedError('invalidPluginId', '设置目标必须使用 canonical pluginId。');
1570
2018
  }
1571
2019
  let sourceIdentity;
2020
+ let logicalIdentity;
1572
2021
  try {
1573
2022
  const packageInfo = await readCliPackageInfo();
1574
- sourceIdentity = await withResolvedCatalogPlugin({
2023
+ const source = await withResolvedCatalogPlugin({
1575
2024
  selector: pluginId,
1576
2025
  cliVersion: packageInfo.version,
1577
2026
  env: context.env,
@@ -1579,11 +2028,17 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1579
2028
  ...(sessionContextForPlugin(pluginId)
1580
2029
  ? { operationContext: sessionContextForPlugin(pluginId) }
1581
2030
  : {}),
1582
- consume: async ({ source }) => source.sourceIdentity,
2031
+ consume: async ({ source }) => ({
2032
+ sourceIdentity: source.sourceIdentity,
2033
+ logicalIdentity: source.logicalIdentity,
2034
+ }),
1583
2035
  });
2036
+ sourceIdentity = source.sourceIdentity;
2037
+ logicalIdentity = source.logicalIdentity;
1584
2038
  }
1585
2039
  catch {
1586
2040
  sourceIdentity = undefined;
2041
+ logicalIdentity = undefined;
1587
2042
  }
1588
2043
  signal.throwIfAborted();
1589
2044
  const target = await inspectPluginConfigResetTarget({
@@ -1598,6 +2053,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1598
2053
  const store = await readPluginConfigStore({
1599
2054
  sourceId: parsed.sourceId,
1600
2055
  ...(sourceIdentity ? { sourceIdentity } : {}),
2056
+ ...(logicalIdentity ? { logicalIdentity } : {}),
1601
2057
  pluginName: parsed.pluginName,
1602
2058
  scope: layer.scope,
1603
2059
  cwd: context.cwd,
@@ -1645,13 +2101,21 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1645
2101
  expectedConfigDigest: expected.configDigest,
1646
2102
  };
1647
2103
  if (intent.sourceMutation.action === 'add') {
1648
- await addCatalogSourceCommand({ ...common, source: intent.sourceMutation.source });
2104
+ await addCatalogSourceCommand({
2105
+ ...common,
2106
+ source: intent.sourceMutation.source,
2107
+ expectedProposedConfigDigest: expected.proposedConfigDigest,
2108
+ expectedImpactDigest: expected.impactDigest,
2109
+ requireImpactConfirmation: true,
2110
+ confirmImpact: true,
2111
+ });
1649
2112
  }
1650
2113
  else {
1651
2114
  await updateCatalogSourceCommand({
1652
2115
  ...common,
1653
2116
  sourceId: intent.sourceId,
1654
2117
  update: intent.sourceMutation.update,
2118
+ expectedProposedConfigDigest: expected.proposedConfigDigest,
1655
2119
  expectedImpactDigest: expected.impactDigest,
1656
2120
  requireIdentityChangeConfirmation: true,
1657
2121
  confirmIdentityChange: true,
@@ -1751,6 +2215,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1751
2215
  sourceId: intent.sourceId,
1752
2216
  cwd: context.cwd,
1753
2217
  env: context.env,
2218
+ expectedConfigDigest: expected.primaryConfigDigest,
2219
+ expectedImpactDigest: expected.primaryImpactDigest,
2220
+ requireImpactConfirmation: true,
2221
+ confirmImpact: true,
1754
2222
  });
1755
2223
  primaryChanged = true;
1756
2224
  }
@@ -1761,7 +2229,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1761
2229
  configSaved,
1762
2230
  generation,
1763
2231
  activeAvailable: true,
1764
- errorCode: error instanceof Error ? error.name : 'catalogSourcePrimaryFailed',
2232
+ errorCode: typeof error === 'object' && error !== null
2233
+ && 'code' in error && typeof error.code === 'string'
2234
+ ? error.code
2235
+ : error instanceof Error ? error.name : 'catalogSourcePrimaryFailed',
1765
2236
  };
1766
2237
  }
1767
2238
  }
@@ -1827,11 +2298,18 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1827
2298
  cwd: context.cwd,
1828
2299
  env: context.env,
1829
2300
  expectedConfigDigest: expectedEvidence.sourceWrite?.configDigest,
2301
+ expectedProposedConfigDigest: expectedEvidence.sourceWrite?.proposedConfigDigest,
1830
2302
  expectedImpactDigest: expectedEvidence.sourceWrite?.impactDigest,
1831
2303
  runtime,
1832
2304
  };
1833
2305
  if (intent.action === 'add' && intent.source) {
1834
- return addCatalogSourceCommand({ ...common, source: intent.source, cliVersion: packageInfo.version });
2306
+ return addCatalogSourceCommand({
2307
+ ...common,
2308
+ source: intent.source,
2309
+ cliVersion: packageInfo.version,
2310
+ requireImpactConfirmation: true,
2311
+ confirmImpact: true,
2312
+ });
1835
2313
  }
1836
2314
  if (intent.action === 'update' && intent.update) {
1837
2315
  return updateCatalogSourceCommand({
@@ -1844,7 +2322,12 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1844
2322
  });
1845
2323
  }
1846
2324
  if (intent.action === 'setPrimary') {
1847
- return setPrimaryCatalogSourceCommand({ ...common, sourceId: intent.sourceId });
2325
+ return setPrimaryCatalogSourceCommand({
2326
+ ...common,
2327
+ sourceId: intent.sourceId,
2328
+ requireImpactConfirmation: true,
2329
+ confirmImpact: true,
2330
+ });
1848
2331
  }
1849
2332
  if (intent.action === 'remove') {
1850
2333
  return removeCatalogSourceCommand({
@@ -1856,12 +2339,165 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1856
2339
  }
1857
2340
  throw new TuiOperationBlockedError('invalidSourceMutation', '来源操作缺少候选输入。');
1858
2341
  }
2342
+ const codexObservationSession = intent.kind === 'pluginWrite'
2343
+ || intent.kind === 'pluginWriteBatch'
2344
+ ? await createCodexNativeObservationSession({
2345
+ exactCwd: context.cwd,
2346
+ env: context.env,
2347
+ runner: context.nativeCliRunner,
2348
+ manager: codexManagerByEvidence.get(expectedEvidence),
2349
+ })
2350
+ : undefined;
1859
2351
  return withAwHomeMutationLock({
1860
2352
  env: context.env,
1861
2353
  action: async (awHomeMutationToken) => {
1862
2354
  await tuiControllerRuntimeTestHooks?.beforeMutationEvidenceCapture?.();
1863
2355
  const operationInventory = createOperationInventoryReader();
1864
- const currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true);
2356
+ const currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
2357
+ ? expectedEvidence
2358
+ : undefined);
2359
+ if (intent.kind === 'pluginWriteBatch') {
2360
+ const expectedBatch = expectedEvidence;
2361
+ const currentBatch = currentEvidence;
2362
+ const reviewedGroups = expectedBatch.groups.map((group) => ({
2363
+ groupId: group.groupId,
2364
+ sourceId: group.sourceId,
2365
+ platform: group.platform,
2366
+ scope: group.scope,
2367
+ requestedPluginIds: group.requestedPluginIds,
2368
+ executable: group.executable,
2369
+ planFingerprint: group.preview.operationPlan
2370
+ ? createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2371
+ : undefined,
2372
+ }));
2373
+ const currentGroups = currentBatch.groups.map((group) => ({
2374
+ groupId: group.groupId,
2375
+ sourceId: group.sourceId,
2376
+ platform: group.platform,
2377
+ scope: group.scope,
2378
+ requestedPluginIds: group.requestedPluginIds,
2379
+ executable: expectedBatch.groups.find((reviewed) => reviewed.groupId === group.groupId)
2380
+ ?.executable === false ? false : group.executable,
2381
+ planFingerprint: expectedBatch.groups.find((reviewed) => reviewed.groupId === group.groupId)
2382
+ ?.executable === false
2383
+ ? undefined
2384
+ : group.preview.operationPlan
2385
+ ? createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
2386
+ : undefined,
2387
+ }));
2388
+ if (expectedBatch.catalog.configDigest !== currentBatch.catalog.configDigest
2389
+ || JSON.stringify(expectedBatch.requestedPluginIds)
2390
+ !== JSON.stringify(currentBatch.requestedPluginIds)
2391
+ || JSON.stringify(reviewedGroups) !== JSON.stringify(currentGroups)) {
2392
+ throw operationReviewStale('确认后批量目标或写入计划发生变化。');
2393
+ }
2394
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
2395
+ const groupResults = [];
2396
+ const completedTargetKeys = new Set();
2397
+ let executedGroupCount = 0;
2398
+ const appendStoppedGroups = (startIndex, staleMessage) => {
2399
+ for (const [offset, stopped] of expectedBatch.groups.slice(startIndex).entries()) {
2400
+ groupResults.push({
2401
+ groupId: stopped.groupId,
2402
+ sourceId: stopped.sourceId,
2403
+ platform: stopped.platform,
2404
+ scope: stopped.scope,
2405
+ requestedPluginIds: stopped.requestedPluginIds,
2406
+ result: !stopped.executable ? stopped.preview : failedWriteCommandResult(intent.action, stopped.requestedPluginIds, { cwd: context.cwd, platform: stopped.platform, scope: stopped.scope }, offset === 0
2407
+ ? new TuiOperationBlockedError('operationReviewStale', staleMessage)
2408
+ : new TuiOperationBlockedError('batchNotStarted', '前一个批量组的写入计划已变化,因此这个组没有开始。')),
2409
+ });
2410
+ }
2411
+ };
2412
+ for (const [groupIndex, reviewed] of expectedBatch.groups.entries()) {
2413
+ const current = currentBatch.groups.find((group) => group.groupId === reviewed.groupId);
2414
+ if (!reviewed.executable) {
2415
+ groupResults.push({
2416
+ groupId: reviewed.groupId,
2417
+ sourceId: reviewed.sourceId,
2418
+ platform: reviewed.platform,
2419
+ scope: reviewed.scope,
2420
+ requestedPluginIds: reviewed.requestedPluginIds,
2421
+ result: reviewed.preview,
2422
+ });
2423
+ continue;
2424
+ }
2425
+ const groupInventory = groupResults.length === 0
2426
+ ? operationInventory
2427
+ : createOperationInventoryReader();
2428
+ let executionPreview = current.preview;
2429
+ if (completedTargetKeys.size > 0) {
2430
+ executionPreview = await runBatchWriteGroup({
2431
+ intent,
2432
+ group: current,
2433
+ cwd: context.cwd,
2434
+ runtime: {
2435
+ ...context,
2436
+ awHomeMutationToken,
2437
+ inventoryCatalogSourceReferences: groupInventory,
2438
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2439
+ previewOperationPlan: true,
2440
+ ...(codexObservationSession ? { codexObservationSession } : {}),
2441
+ },
2442
+ });
2443
+ if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
2444
+ || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
2445
+ appendStoppedGroups(groupIndex, '前序批量结果改变了这个组的实际写入计划。');
2446
+ break;
2447
+ }
2448
+ }
2449
+ const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
2450
+ const result = await runBatchWriteGroup({
2451
+ intent,
2452
+ group: current,
2453
+ cwd: context.cwd,
2454
+ runtime: {
2455
+ ...context,
2456
+ awHomeMutationToken,
2457
+ preApplyConfirmed: true,
2458
+ inventoryCatalogSourceReferences: groupInventory,
2459
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
2460
+ ...(executionPreview.operationPlan ? {
2461
+ expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint,
2462
+ } : {}),
2463
+ ...(requiresManagedCodex ? {
2464
+ managedCodexAuthorizer: async () => true,
2465
+ } : {}),
2466
+ ...(codexObservationSession ? { codexObservationSession } : {}),
2467
+ },
2468
+ });
2469
+ if (writeReviewIsStale(result)) {
2470
+ if (executedGroupCount === 0)
2471
+ throwIfWriteReviewStale(result);
2472
+ appendStoppedGroups(groupIndex, '这个组的实际写入计划发生变化。');
2473
+ break;
2474
+ }
2475
+ executedGroupCount += 1;
2476
+ for (const target of result.targetResults) {
2477
+ if (target.state === 'completed') {
2478
+ completedTargetKeys.add([
2479
+ target.target.pluginId,
2480
+ target.target.platform,
2481
+ target.target.scope,
2482
+ ].join('\0'));
2483
+ }
2484
+ }
2485
+ groupResults.push({
2486
+ groupId: reviewed.groupId,
2487
+ sourceId: reviewed.sourceId,
2488
+ platform: reviewed.platform,
2489
+ scope: reviewed.scope,
2490
+ requestedPluginIds: reviewed.requestedPluginIds,
2491
+ result,
2492
+ });
2493
+ }
2494
+ const merged = mergeWriteCommandResults(groupResults.map((group) => group.result));
2495
+ return {
2496
+ ...merged,
2497
+ requestedPluginIds: expectedBatch.requestedPluginIds,
2498
+ groupResults,
2499
+ };
2500
+ }
1865
2501
  if (intent.kind !== 'pluginWrite'
1866
2502
  && !operationEvidenceMatches(expectedEvidence, currentEvidence)) {
1867
2503
  throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');
@@ -1877,6 +2513,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1877
2513
  cwd: context.cwd,
1878
2514
  platform: intent.platform,
1879
2515
  scope: intent.scope,
2516
+ ...(reviewedScriptTimeoutMs(intent) === undefined
2517
+ ? {}
2518
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1880
2519
  ...(intent.action === 'install' && intent.dependencyScopeOverrides
1881
2520
  ? { dependencyScopeOverrides: intent.dependencyScopeOverrides }
1882
2521
  : {}),
@@ -1895,12 +2534,21 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1895
2534
  }[intent.action];
1896
2535
  const currentSource = currentEvidence.source;
1897
2536
  const offlineCleanup = currentEvidence.offlineCleanup;
2537
+ const reviewedOfflineCleanup = expectedEvidence.offlineCleanup;
2538
+ if ((offlineCleanup || reviewedOfflineCleanup)
2539
+ && (!offlineCleanup
2540
+ || !reviewedOfflineCleanup
2541
+ || !offlineCleanupEvidenceMatches(reviewedOfflineCleanup, offlineCleanup))) {
2542
+ throw new TuiOperationBlockedError('operationReviewStale', '确认后离线清理证据发生变化。');
2543
+ }
1898
2544
  if (!offlineCleanup && (!expectedEvidence.source || !currentSource)) {
1899
2545
  throw new TuiOperationBlockedError('operationReviewStale', '确认信息缺少来源身份。');
1900
2546
  }
1901
2547
  const result = await command([intent.pluginId], options, {
1902
2548
  ...context,
2549
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1903
2550
  awHomeMutationToken,
2551
+ preApplyConfirmed: true,
1904
2552
  inventoryCatalogSourceReferences: operationInventory,
1905
2553
  ...(expectedEvidence.operationPlan
1906
2554
  ? { expectedPlanFingerprint: expectedEvidence.operationPlan.planFingerprint }
@@ -1913,14 +2561,18 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1913
2561
  ...(expectedEvidence.codexManager?.state === 'requires-install'
1914
2562
  ? { managedCodexAuthorizer: async () => true }
1915
2563
  : {}),
1916
- ...(offlineCleanup
1917
- ? { expectedOfflineCleanupEvidence: offlineCleanup }
2564
+ ...(reviewedOfflineCleanup
2565
+ ? { expectedOfflineCleanupEvidence: reviewedOfflineCleanup }
1918
2566
  : { reviewPreflight: (actual) => assertSourceEvidenceMatches(currentSource, actual) }),
1919
2567
  });
1920
2568
  throwIfWriteReviewStale(result);
1921
2569
  return result;
1922
2570
  }
1923
2571
  if (intent.kind === 'configSave' || intent.kind === 'configReset') {
2572
+ const operationRebuildResults = [];
2573
+ const operationDiagnosticSink = (results) => {
2574
+ operationRebuildResults.push(...structuredClone(results));
2575
+ };
1924
2576
  const currentSource = currentEvidence.source;
1925
2577
  if (intent.kind === 'configReset') {
1926
2578
  const parsed = parsePluginId(intent.pluginId);
@@ -1937,6 +2589,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1937
2589
  rebuildRuntime: { ...context, awHomeMutationToken },
1938
2590
  awHomeMutationToken,
1939
2591
  apply: false,
2592
+ sharedNativeApplyConfirmed: false,
1940
2593
  });
1941
2594
  }
1942
2595
  if (!currentSource) {
@@ -1950,7 +2603,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1950
2603
  ...(catalogOperation ? { operationContext: catalogOperation } : {}),
1951
2604
  consume: async ({ plugin, source }) => {
1952
2605
  assertSourceEvidenceMatches(currentSource, sourceEvidenceForPlugin(source, plugin.manifest.name));
1953
- const applyTargets = intent.apply
2606
+ const applyTargets = intent.retryApply || intent.apply
1954
2607
  ? currentEvidence.config?.affectedTargets.map((target) => ({
1955
2608
  pluginName: plugin.manifest.name,
1956
2609
  platform: target.platform,
@@ -1967,6 +2620,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1967
2620
  rebuildRuntime: {
1968
2621
  ...context,
1969
2622
  awHomeMutationToken,
2623
+ operationDiagnosticSink,
2624
+ ...(reviewedScriptTimeoutMs(intent) === undefined
2625
+ ? {}
2626
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1970
2627
  ...(catalogOperation ? {
1971
2628
  catalogOperationContext: catalogOperation,
1972
2629
  expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
@@ -1974,6 +2631,9 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1974
2631
  },
1975
2632
  awHomeMutationToken,
1976
2633
  applyTargets,
2634
+ sharedNativeApplyConfirmed: true,
2635
+ preApplyConfirmed: true,
2636
+ preApplyAuthorizedTargets: expectedEvidence.config?.preApplyAuthorization ?? [],
1977
2637
  })
1978
2638
  : await applyPluginConfigWrite({
1979
2639
  plugin,
@@ -1985,6 +2645,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1985
2645
  rebuildRuntime: {
1986
2646
  ...context,
1987
2647
  awHomeMutationToken,
2648
+ operationDiagnosticSink,
2649
+ ...(reviewedScriptTimeoutMs(intent) === undefined
2650
+ ? {}
2651
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1988
2652
  ...(catalogOperation ? {
1989
2653
  catalogOperationContext: catalogOperation,
1990
2654
  expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
@@ -1993,13 +2657,18 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1993
2657
  awHomeMutationToken,
1994
2658
  applyTargets,
1995
2659
  request: { updates: intent.updates, removeKeys: intent.removeKeys },
2660
+ sharedNativeApplyConfirmed: true,
2661
+ preApplyConfirmed: true,
2662
+ preApplyAuthorizedTargets: expectedEvidence.config?.preApplyAuthorization ?? [],
1996
2663
  });
1997
- return result;
2664
+ return { ...result, operationRebuildResults };
1998
2665
  },
1999
2666
  });
2000
2667
  }
2001
2668
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
2002
2669
  },
2670
+ }).finally(async () => {
2671
+ await codexObservationSession?.dispose();
2003
2672
  });
2004
2673
  },
2005
2674
  };