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

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 (66) hide show
  1. package/dist/src/commands/catalog-dev-source.d.ts +31 -0
  2. package/dist/src/commands/catalog-dev-source.js +165 -5
  3. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  4. package/dist/src/commands/catalog-generation.d.ts +5 -0
  5. package/dist/src/commands/catalog-generation.js.map +1 -1
  6. package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
  7. package/dist/src/commands/catalog-publication-journal.js +77 -0
  8. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  9. package/dist/src/commands/catalog-store.d.ts +5 -0
  10. package/dist/src/commands/catalog-store.js +19 -1
  11. package/dist/src/commands/catalog-store.js.map +1 -1
  12. package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
  13. package/dist/src/commands/dependency-operation-plan.js +63 -27
  14. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  15. package/dist/src/commands/rebuild-command.d.ts +22 -8
  16. package/dist/src/commands/rebuild-command.js +562 -250
  17. package/dist/src/commands/rebuild-command.js.map +1 -1
  18. package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
  19. package/dist/src/commands/reconcile-operation-plan.js +25 -6
  20. package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
  21. package/dist/src/commands/status-v6.d.ts +2 -1
  22. package/dist/src/commands/status-v6.js +7 -1
  23. package/dist/src/commands/status-v6.js.map +1 -1
  24. package/dist/src/commands/write-commands.d.ts +2 -1
  25. package/dist/src/commands/write-commands.js +24 -1
  26. package/dist/src/commands/write-commands.js.map +1 -1
  27. package/dist/src/tui/catalog-management-screens.d.ts +45 -0
  28. package/dist/src/tui/catalog-management-screens.js +25 -0
  29. package/dist/src/tui/catalog-management-screens.js.map +1 -0
  30. package/dist/src/tui/catalog-management.d.ts +240 -0
  31. package/dist/src/tui/catalog-management.js +856 -0
  32. package/dist/src/tui/catalog-management.js.map +1 -0
  33. package/dist/src/tui/catalog-sources/session.d.ts +5 -0
  34. package/dist/src/tui/catalog-sources/session.js +49 -1
  35. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  36. package/dist/src/tui/catalog-sources/types.d.ts +11 -0
  37. package/dist/src/tui/console-app-v4.d.ts +1 -1
  38. package/dist/src/tui/console-app-v4.js +2597 -587
  39. package/dist/src/tui/console-app-v4.js.map +1 -1
  40. package/dist/src/tui/controller-runtime.js +668 -50
  41. package/dist/src/tui/controller-runtime.js.map +1 -1
  42. package/dist/src/tui/controller.d.ts +136 -7
  43. package/dist/src/tui/controller.js +752 -19
  44. package/dist/src/tui/controller.js.map +1 -1
  45. package/dist/src/tui/dependency-graph.d.ts +67 -0
  46. package/dist/src/tui/dependency-graph.js +399 -0
  47. package/dist/src/tui/dependency-graph.js.map +1 -0
  48. package/dist/src/tui/lifecycle-action-view.js +4 -2
  49. package/dist/src/tui/lifecycle-action-view.js.map +1 -1
  50. package/dist/src/tui/model.d.ts +37 -0
  51. package/dist/src/tui/model.js +163 -7
  52. package/dist/src/tui/model.js.map +1 -1
  53. package/dist/src/tui/operation-review-planner.js +266 -9
  54. package/dist/src/tui/operation-review-planner.js.map +1 -1
  55. package/dist/src/tui/plugin-write-task.d.ts +67 -0
  56. package/dist/src/tui/plugin-write-task.js +282 -0
  57. package/dist/src/tui/plugin-write-task.js.map +1 -0
  58. package/dist/src/tui/router.d.ts +39 -2
  59. package/dist/src/tui/router.js +13 -5
  60. package/dist/src/tui/router.js.map +1 -1
  61. package/dist/src/tui/screen-registry.d.ts +125 -6
  62. package/dist/src/tui/screen-registry.js +35 -4
  63. package/dist/src/tui/screen-registry.js.map +1 -1
  64. package/dist/src/utils/catalog-generation-service.d.ts +4 -0
  65. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  66. package/package.json +3 -3
@@ -1,8 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
- import { statusCommandV7, statusTargetIntent, } from '../commands/status-v6.js';
3
+ import { statusCommandV7, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } from '../commands/status-v6.js';
4
4
  import { addCatalogSourceCommand, listCatalogSources, preflightCatalogSourceMutation, removeCatalogSourceCommand, setPrimaryCatalogSourceCommand, updateCatalogSourceCommand, } from '../commands/catalog-source.js';
5
- import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
5
+ import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, taskWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
6
6
  import { resolveSyncIntentSelection } from '../commands/sync-command.js';
7
7
  import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, } from '../commands/options.js';
8
8
  import { readCliPackageInfo } from '../utils/local-marketplace.js';
@@ -18,7 +18,9 @@ import { createBatchIntentCompatiblePlanFingerprint } from '../commands/operatio
18
18
  import { offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from '../commands/offline-cleanup-evidence.js';
19
19
  import { inventoryCatalogSourceReferences } from '../utils/catalog-source-references.js';
20
20
  import { operationEvidenceMatches, operationEvidenceFingerprint, TuiConfigInvalidStoreError, TuiController, TuiOperationBlockedError, } from './controller.js';
21
- import { buildConfigGroups, buildConfigPage, projectPlatformPlacement, } from './model.js';
21
+ import { buildConfigGroups, buildConfigPage, buildTuiViewModel, projectPlatformPlacement, } from './model.js';
22
+ import { buildPluginWriteTaskCandidates, buildPluginWriteTaskDraft, evaluatePluginWriteTaskRestoreVersion, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
23
+ import { createCatalogManagementService, } from './catalog-management.js';
22
24
  import { projectCatalogSourceLocation } from '../utils/catalog-source-presentation.js';
23
25
  import { calculateCatalogSourceConfigFingerprint, calculateCatalogSourceIdentity, } from '../utils/catalog-source-identity.js';
24
26
  import { checkAwCapabilitySupport, readPluginManifest } from '../utils/manifest.js';
@@ -208,6 +210,9 @@ function pluginIntent(intent) {
208
210
  return 'pluginId' in intent;
209
211
  }
210
212
  function reviewedScriptTimeoutMs(intent) {
213
+ if (intent.kind === 'pluginWriteTask') {
214
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
215
+ }
211
216
  if ((intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch')
212
217
  && (intent.action === 'install' || intent.action === 'refreshAndSync'
213
218
  || intent.action === 'reinstall' || intent.action === 'sync')) {
@@ -229,9 +234,9 @@ function selectedBatchGroups(intent) {
229
234
  }));
230
235
  const normalizedTargets = rawTargets.map((target) => {
231
236
  if ((target.platform !== 'codex' && target.platform !== 'claude' && target.platform !== 'both')
232
- || target.scope !== intent.scope
237
+ || (target.scope !== 'project' && target.scope !== 'user')
233
238
  || (intent.selection.kind === 'targets' && target.platform === 'both')) {
234
- throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用共同 scope 和单个平台。');
239
+ throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用有效 scope 和单个平台。');
235
240
  }
236
241
  const pluginId = target.pluginId;
237
242
  const parsed = parsePluginId(pluginId);
@@ -364,40 +369,6 @@ async function loadDependencyOverviews(options) {
364
369
  const unsupportedPluginIds = new Set(options.status.pluginSummaries
365
370
  .filter((summary) => (summary.missingCapabilities?.length ?? 0) > 0)
366
371
  .map((summary) => summary.pluginId));
367
- const placementReferences = new Map();
368
- for (const consumer of options.status.payload.targets.filter((target) => target.state !== 'absent')) {
369
- const consumerNode = nodes.get(consumer.pluginId);
370
- if (!consumerNode)
371
- continue;
372
- for (const dependencyPluginId of consumerNode.dependencies) {
373
- const dependencyTargets = (targetsByPlugin.get(dependencyPluginId) ?? [])
374
- .filter((target) => target.platform === consumer.platform);
375
- const projectTarget = dependencyTargets.find((target) => target.scope === 'project');
376
- const userTarget = dependencyTargets.find((target) => target.scope === 'user');
377
- const dependencyScope = consumer.scope === 'user'
378
- ? 'user'
379
- : userTarget && (!projectTarget
380
- || (userTarget.state !== 'absent' && projectTarget.state === 'absent'))
381
- ? 'user'
382
- : 'project';
383
- const reference = {
384
- consumer: {
385
- pluginId: consumer.pluginId,
386
- platform: consumer.platform,
387
- scope: consumer.scope,
388
- project: consumer.scope === 'project' ? 'current' : null,
389
- },
390
- dependency: {
391
- pluginId: dependencyPluginId,
392
- platform: consumer.platform,
393
- scope: dependencyScope,
394
- project: dependencyScope === 'project' ? 'current' : null,
395
- },
396
- };
397
- const key = `${consumer.pluginId}\0${dependencyPluginId}`;
398
- placementReferences.set(key, [...(placementReferences.get(key) ?? []), reference]);
399
- }
400
- }
401
372
  const placementsFor = (pluginId) => {
402
373
  const targets = targetsByPlugin.get(pluginId) ?? [];
403
374
  if (targets.length > 0) {
@@ -469,7 +440,7 @@ async function loadDependencyOverviews(options) {
469
440
  })),
470
441
  placements,
471
442
  aggregateStatus: aggregateFor(dependency.pluginId, placements),
472
- placementReferences: placementReferences.get(`${pluginId}\0${dependency.pluginId}`) ?? [],
443
+ placementReferences: [],
473
444
  };
474
445
  });
475
446
  const dependentIds = [...new Set(reverse.get(pluginId) ?? [])].sort();
@@ -483,7 +454,7 @@ async function loadDependencyOverviews(options) {
483
454
  requiredBy: [],
484
455
  placements,
485
456
  aggregateStatus: aggregateFor(dependentId, placements),
486
- placementReferences: placementReferences.get(`${dependentId}\0${pluginId}`) ?? [],
457
+ placementReferences: [],
487
458
  };
488
459
  });
489
460
  const targetBlockers = (targetsByPlugin.get(pluginId) ?? []).flatMap((target) => target.reasons.filter((error) => error.code.startsWith('dependency') || error.code === 'sourceChanged')
@@ -563,6 +534,7 @@ function createReviewStageReporter(options) {
563
534
  };
564
535
  }
565
536
  export function createTuiControllerDependencies(context, progressFeed, options = {}) {
537
+ const catalogManagement = createCatalogManagementService(context);
566
538
  const readInventory = options.inventoryCatalogSourceReferences
567
539
  ?? inventoryCatalogSourceReferences;
568
540
  const validatedStoreEntries = new Map();
@@ -597,6 +569,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
597
569
  };
598
570
  };
599
571
  const catalogOperationByEvidence = new WeakMap();
572
+ const taskCatalogOperationsByEvidence = new WeakMap();
600
573
  const codexManagerByEvidence = new WeakMap();
601
574
  let sessionCatalogOperationContext;
602
575
  let sessionStatusResult;
@@ -676,6 +649,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
676
649
  const result = await statusCommandV7({
677
650
  cwd: context.cwd,
678
651
  env: context.env,
652
+ nativeCliRunner: context.nativeCliRunner,
679
653
  cliVersion: packageInfo.version,
680
654
  verbose: context.diagnostics.verbose,
681
655
  ...(catalogOperationContext ? { catalogOperationContext } : {}),
@@ -768,6 +742,37 @@ export function createTuiControllerDependencies(context, progressFeed, options =
768
742
  };
769
743
  const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
770
744
  reviewedScriptTimeoutMs(intent);
745
+ if ((intent.kind === 'pluginWrite'
746
+ && (intent.platform === 'codex' || intent.platform === 'both')
747
+ || intent.kind === 'pluginWriteTask'
748
+ && intent.targets.some((target) => target.platform === 'codex'))
749
+ && !codexObservationSession) {
750
+ const observationSession = await createCodexNativeObservationSession({
751
+ exactCwd: context.cwd,
752
+ env: context.env,
753
+ runner: context.nativeCliRunner,
754
+ });
755
+ try {
756
+ return await captureEvidence(intent, signal, preparation, operationInventory, freshCatalog, reusableCandidate, observationSession, frozenBatchEvidence);
757
+ }
758
+ finally {
759
+ await observationSession.dispose();
760
+ }
761
+ }
762
+ if (intent.kind === 'catalogManagement') {
763
+ signal?.throwIfAborted();
764
+ await preparation?.reporter.emit('validate');
765
+ const managementEvidence = await catalogManagement.captureEvidence(intent);
766
+ signal?.throwIfAborted();
767
+ await preparation?.reporter.emit('evidence');
768
+ return {
769
+ catalog: {
770
+ configDigest: managementEvidence.workspaceBinding ?? 'catalog-store',
771
+ compositeDigest: managementEvidence.evidenceDigest,
772
+ },
773
+ catalogManagement: managementEvidence,
774
+ };
775
+ }
771
776
  if (intent.kind === 'configReset') {
772
777
  const parsed = parsePluginId(intent.pluginId);
773
778
  if (!parsed.sourceId || !parsed.pluginName) {
@@ -1157,10 +1162,394 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1157
1162
  }
1158
1163
  return evidence;
1159
1164
  }
1160
- if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch') {
1165
+ if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch'
1166
+ && intent.kind !== 'pluginWriteTask') {
1161
1167
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
1162
1168
  }
1163
1169
  signal?.throwIfAborted();
1170
+ if (intent.kind === 'pluginWriteTask') {
1171
+ await preparation?.reporter.emit('status');
1172
+ const packageInfo = await readCliPackageInfo();
1173
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1174
+ const requestedBySource = new Map();
1175
+ for (const target of intent.targets) {
1176
+ const parsed = parsePluginId(target.pluginId);
1177
+ if (!parsed.sourceId || formatPluginId({
1178
+ sourceId: parsed.sourceId,
1179
+ pluginName: parsed.pluginName,
1180
+ }) !== target.pluginId) {
1181
+ throw new TuiOperationBlockedError('invalidPluginId', '任务目标必须使用 canonical pluginId。');
1182
+ }
1183
+ requestedBySource.set(parsed.sourceId, [
1184
+ ...(requestedBySource.get(parsed.sourceId) ?? []),
1185
+ target,
1186
+ ]);
1187
+ }
1188
+ const sources = [];
1189
+ const executableTargets = [];
1190
+ const causes = [];
1191
+ const blockedTargets = [];
1192
+ const observedTargets = [];
1193
+ const observedRevisions = [];
1194
+ const operationContexts = new Map();
1195
+ await preparation?.reporter.emit('catalog');
1196
+ for (const [sourceId, requestedTargets] of [...requestedBySource.entries()]
1197
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))) {
1198
+ signal?.throwIfAborted();
1199
+ let catalogOperation;
1200
+ try {
1201
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
1202
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [sourceId])
1203
+ : await resolveCatalogOperationContext({
1204
+ sourceIds: [sourceId],
1205
+ env: context.env,
1206
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
1207
+ ...(signal ? { signal } : {}),
1208
+ });
1209
+ }
1210
+ catch {
1211
+ blockedTargets.push(...requestedTargets.map((target) => ({
1212
+ target,
1213
+ reason: 'targetUnavailable',
1214
+ issueIds: [],
1215
+ })));
1216
+ continue;
1217
+ }
1218
+ operationContexts.set(sourceId, catalogOperation);
1219
+ const status = await statusCommandV7({
1220
+ plugins: [...new Set(requestedTargets.map((target) => target.pluginId))],
1221
+ cwd: context.cwd,
1222
+ env: context.env,
1223
+ nativeCliRunner: context.nativeCliRunner,
1224
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1225
+ cliVersion: packageInfo.version,
1226
+ catalogOperationContext: catalogOperation,
1227
+ inventoryCatalogSourceReferences: operationInventory,
1228
+ });
1229
+ observedTargets.push(...status.payload.targets);
1230
+ observedRevisions.push(...status.targetRevisions);
1231
+ const model = buildTuiViewModel({
1232
+ status: status.payload,
1233
+ pluginSummaries: status.pluginSummaries,
1234
+ targetRevisions: status.targetRevisions,
1235
+ });
1236
+ const runtimeCandidates = intent.goal === 'restore'
1237
+ ? buildPluginWriteTaskCandidates(model, 'restore')
1238
+ : requestedTargets.map((target) => {
1239
+ const expected = target.expectedUpdate;
1240
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
1241
+ && entry.scope === target.scope);
1242
+ const live = status.payload.targets.find((entry) => {
1243
+ if (entry.pluginId !== target.pluginId || entry.platform !== target.platform)
1244
+ return false;
1245
+ const desired = statusTargetIntent(entry);
1246
+ return entry.scope === target.scope || desired.scopes.includes(target.scope);
1247
+ });
1248
+ const generation = catalogOperation.generationSet.sources.find((entry) => entry.sourceId === sourceId);
1249
+ const eligible = Boolean(expected && live && revision
1250
+ && live.sourceIdentity === expected.sourceIdentity
1251
+ && generation?.storeKey === expected.generationKey
1252
+ && revision.revision.installedVersion === expected.installedVersion
1253
+ && revision.revision.availableVersion === expected.activeVersion
1254
+ && revision.revision.state === 'upgradeAvailable');
1255
+ return {
1256
+ target,
1257
+ displayName: model.plugins.find((plugin) => plugin.pluginId === target.pluginId)
1258
+ ?.displayName ?? parsePluginId(target.pluginId).pluginName,
1259
+ sourceId,
1260
+ eligible,
1261
+ ...(!eligible ? { blockReason: 'updateEvidenceIncomplete' } : {}),
1262
+ recommendedMethod: live?.state === 'ready' ? 'sync' : 'reinstall',
1263
+ issueIds: [],
1264
+ };
1265
+ });
1266
+ const draft = buildPluginWriteTaskDraft({ ...intent, targets: requestedTargets }, model, runtimeCandidates);
1267
+ blockedTargets.push(...draft.blockedTargets);
1268
+ if (draft.executableTargets.length === 0)
1269
+ continue;
1270
+ const methodsByTarget = new Map();
1271
+ for (const cause of draft.causes) {
1272
+ const key = pluginWriteTaskTargetKey(cause.repairTarget);
1273
+ const methods = methodsByTarget.get(key) ?? new Set();
1274
+ methods.add(cause.method);
1275
+ methodsByTarget.set(key, methods);
1276
+ }
1277
+ const conflicted = new Set([...methodsByTarget].filter(([, methods]) => methods.size > 1)
1278
+ .map(([key]) => key));
1279
+ if (conflicted.size > 0) {
1280
+ const blockedKeys = new Set(draft.causes.filter((cause) => conflicted.has(pluginWriteTaskTargetKey(cause.repairTarget)))
1281
+ .flatMap((cause) => cause.consumers.map(pluginWriteTaskTargetKey)));
1282
+ blockedTargets.push(...draft.executableTargets.filter((target) => blockedKeys.has(pluginWriteTaskTargetKey(target))).map((target) => ({
1283
+ target,
1284
+ reason: 'taskActionConflict',
1285
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1286
+ .map((cause) => cause.issueId),
1287
+ })));
1288
+ draft.executableTargets = draft.executableTargets.filter((target) => !blockedKeys.has(pluginWriteTaskTargetKey(target)));
1289
+ draft.causes = draft.causes.filter((cause) => cause.consumers.some((consumer) => !blockedKeys.has(pluginWriteTaskTargetKey(consumer))));
1290
+ }
1291
+ if (draft.executableTargets.length === 0)
1292
+ continue;
1293
+ const taskTargets = () => draft.executableTargets.map((target) => {
1294
+ const direct = draft.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
1295
+ return {
1296
+ pluginId: target.pluginId,
1297
+ platform: target.platform,
1298
+ scope: target.scope,
1299
+ method: direct.some((cause) => cause.method === 'reinstall')
1300
+ ? 'reinstall' : 'sync',
1301
+ };
1302
+ });
1303
+ const repairTargets = () => [...new Map(draft.causes.map((cause) => [
1304
+ pluginWriteTaskTargetKey(cause.repairTarget),
1305
+ { ...cause.repairTarget, method: cause.method },
1306
+ ])).values()];
1307
+ let preview;
1308
+ while (draft.executableTargets.length > 0) {
1309
+ preview = await taskWriteCommand(taskTargets(), {
1310
+ cwd: context.cwd,
1311
+ repairTargets: repairTargets(),
1312
+ ...(reviewedScriptTimeoutMs(intent) === undefined
1313
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1314
+ }, {
1315
+ ...context,
1316
+ catalogOperationContext: catalogOperation,
1317
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1318
+ expectedCatalogConfigDigest: configSnapshot.digest,
1319
+ inventoryCatalogSourceReferences: operationInventory,
1320
+ previewOperationPlan: true,
1321
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1322
+ ...(preparation ? {
1323
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1324
+ } : {}),
1325
+ });
1326
+ if (!preview.operationPlan) {
1327
+ blockedTargets.push(...draft.executableTargets.map((target) => ({
1328
+ target,
1329
+ reason: 'targetUnavailable',
1330
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1331
+ .map((cause) => cause.issueId),
1332
+ })));
1333
+ draft.executableTargets = [];
1334
+ draft.causes = [];
1335
+ break;
1336
+ }
1337
+ if (intent.goal === 'restore') {
1338
+ const grouped = new Map();
1339
+ const blockedByVersion = new Map();
1340
+ for (const cause of draft.causes) {
1341
+ const placements = (preview.dependencyPlan?.targets ?? []).filter((target) => target.pluginId === cause.repairTarget.pluginId
1342
+ && target.platform === cause.repairTarget.platform);
1343
+ const placement = placements.find((target) => target.requestedScope === cause.repairTarget.scope)
1344
+ ?? (placements.length === 1 ? placements[0] : undefined);
1345
+ if (!placement) {
1346
+ throw new TuiOperationBlockedError('taskRepairPlacementMissing', '任务修复目标无法绑定到依赖位置。');
1347
+ }
1348
+ const repairTarget = {
1349
+ pluginId: cause.repairTarget.pluginId,
1350
+ platform: cause.repairTarget.platform,
1351
+ scope: placement.requestedScope ?? placement.resolvedScope,
1352
+ };
1353
+ const version = evaluatePluginWriteTaskRestoreVersion(repairTarget, cause.restoreVersionEvidence?.state === 'absent', model);
1354
+ if (!version.evidence || version.blockReason) {
1355
+ for (const consumer of cause.consumers) {
1356
+ const key = pluginWriteTaskTargetKey(consumer);
1357
+ const current = blockedByVersion.get(key) ?? {
1358
+ reason: version.blockReason ?? 'restoreVersionUnverified',
1359
+ issueIds: new Set(),
1360
+ };
1361
+ if (version.blockReason === 'restoreVersionChanged') {
1362
+ current.reason = version.blockReason;
1363
+ }
1364
+ current.issueIds.add(cause.issueId);
1365
+ blockedByVersion.set(key, current);
1366
+ }
1367
+ continue;
1368
+ }
1369
+ const causeKey = ['restore', cause.sourceIdentity,
1370
+ pluginWriteTaskTargetKey(repairTarget)].join('\0');
1371
+ const current = grouped.get(causeKey);
1372
+ grouped.set(causeKey, {
1373
+ cause: {
1374
+ ...cause,
1375
+ causeKey,
1376
+ issueId: [cause.issueId, ...(current?.issueIds ?? [])]
1377
+ .sort((left, right) => left.localeCompare(right, 'en'))[0],
1378
+ repairTarget,
1379
+ consumers: [...new Map([
1380
+ ...(current?.cause.consumers ?? []),
1381
+ ...cause.consumers,
1382
+ ].map((consumer) => [pluginWriteTaskTargetKey(consumer), consumer])).values()]
1383
+ .sort((left, right) => pluginWriteTaskTargetKey(left)
1384
+ .localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1385
+ restoreVersionEvidence: version.evidence,
1386
+ },
1387
+ issueIds: new Set([...(current?.issueIds ?? []), cause.issueId]),
1388
+ methods: new Set([...(current?.methods ?? []), cause.method]),
1389
+ });
1390
+ }
1391
+ for (const group of grouped.values()) {
1392
+ if (group.methods.size < 2)
1393
+ continue;
1394
+ for (const consumer of group.cause.consumers) {
1395
+ const key = pluginWriteTaskTargetKey(consumer);
1396
+ const current = blockedByVersion.get(key) ?? {
1397
+ reason: 'taskActionConflict',
1398
+ issueIds: new Set(),
1399
+ };
1400
+ group.issueIds.forEach((issueId) => current.issueIds.add(issueId));
1401
+ blockedByVersion.set(key, current);
1402
+ }
1403
+ }
1404
+ const newlyBlockedKeys = new Set(blockedByVersion.keys());
1405
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1406
+ const blocked = blockedByVersion.get(pluginWriteTaskTargetKey(target));
1407
+ return blocked ? [{
1408
+ target,
1409
+ reason: blocked.reason,
1410
+ issueIds: [...blocked.issueIds].sort((left, right) => left.localeCompare(right, 'en')),
1411
+ }] : [];
1412
+ }));
1413
+ const normalizedCauses = [...grouped.values()]
1414
+ .filter((group) => group.methods.size === 1)
1415
+ .map((group) => ({
1416
+ ...group.cause,
1417
+ consumers: group.cause.consumers.filter((consumer) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(consumer))),
1418
+ }))
1419
+ .filter((cause) => cause.consumers.length > 0)
1420
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en'));
1421
+ const normalizedTargets = draft.executableTargets.filter((target) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(target)));
1422
+ const changed = JSON.stringify(normalizedCauses) !== JSON.stringify(draft.causes)
1423
+ || JSON.stringify(normalizedTargets) !== JSON.stringify(draft.executableTargets);
1424
+ draft.causes = normalizedCauses;
1425
+ draft.executableTargets = normalizedTargets;
1426
+ if (changed)
1427
+ continue;
1428
+ }
1429
+ const blockedPlanTargets = (preview.dependencyPlan?.targets ?? []).filter((target) => target.action === 'block' || target.errorCode !== undefined);
1430
+ if (blockedPlanTargets.length === 0)
1431
+ break;
1432
+ const requestedKeys = new Set(draft.executableTargets.map(pluginWriteTaskTargetKey));
1433
+ const errorCodesByRoot = new Map();
1434
+ for (const blockedPlanTarget of blockedPlanTargets) {
1435
+ const pending = [blockedPlanTarget];
1436
+ const visited = new Set();
1437
+ const mappedRoots = new Set();
1438
+ const repairKey = pluginWriteTaskTargetKey({
1439
+ pluginId: blockedPlanTarget.pluginId,
1440
+ platform: blockedPlanTarget.platform,
1441
+ scope: blockedPlanTarget.requestedScope ?? blockedPlanTarget.resolvedScope,
1442
+ });
1443
+ for (const cause of draft.causes) {
1444
+ if (pluginWriteTaskTargetKey(cause.repairTarget) !== repairKey)
1445
+ continue;
1446
+ cause.consumers.forEach((consumer) => mappedRoots.add(pluginWriteTaskTargetKey(consumer)));
1447
+ }
1448
+ while (pending.length > 0) {
1449
+ const current = pending.shift();
1450
+ const currentKey = pluginWriteTaskTargetKey({
1451
+ pluginId: current.pluginId,
1452
+ platform: current.platform,
1453
+ scope: current.requestedScope ?? current.resolvedScope,
1454
+ });
1455
+ if (visited.has(currentKey))
1456
+ continue;
1457
+ visited.add(currentKey);
1458
+ if (current.relation === 'requested' && requestedKeys.has(currentKey)) {
1459
+ mappedRoots.add(currentKey);
1460
+ }
1461
+ for (const consumer of current.requiredByTargets ?? []) {
1462
+ const consumerKey = pluginWriteTaskTargetKey(consumer);
1463
+ if (requestedKeys.has(consumerKey)) {
1464
+ mappedRoots.add(consumerKey);
1465
+ continue;
1466
+ }
1467
+ const placement = preview.dependencyPlan?.targets.find((target) => target.pluginId === consumer.pluginId && target.platform === consumer.platform
1468
+ && (target.requestedScope ?? target.resolvedScope) === consumer.scope);
1469
+ if (placement)
1470
+ pending.push(placement);
1471
+ }
1472
+ }
1473
+ if (mappedRoots.size === 0) {
1474
+ throw new TuiOperationBlockedError('taskPlanConsumerMissing', '任务计划中的阻止项无法映射到请求目标。');
1475
+ }
1476
+ for (const key of mappedRoots) {
1477
+ const codes = errorCodesByRoot.get(key) ?? new Set();
1478
+ codes.add(blockedPlanTarget.errorCode ?? 'writePlanBlocked');
1479
+ errorCodesByRoot.set(key, codes);
1480
+ }
1481
+ }
1482
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1483
+ const codes = errorCodesByRoot.get(pluginWriteTaskTargetKey(target));
1484
+ return codes ? [{
1485
+ target,
1486
+ reason: 'targetUnavailable',
1487
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1488
+ .map((cause) => cause.issueId),
1489
+ errorCodes: [...codes].sort((left, right) => left.localeCompare(right, 'en')),
1490
+ }] : [];
1491
+ }));
1492
+ draft.executableTargets = draft.executableTargets.filter((target) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(target)));
1493
+ draft.causes = draft.causes.map((cause) => ({
1494
+ ...cause,
1495
+ consumers: cause.consumers.filter((consumer) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(consumer))),
1496
+ })).filter((cause) => cause.consumers.length > 0);
1497
+ }
1498
+ if (!preview?.operationPlan || draft.executableTargets.length === 0)
1499
+ continue;
1500
+ const sourceIdentity = draft.causes[0]?.sourceIdentity
1501
+ ?? status.payload.catalog.sources.find((source) => source.id === sourceId)?.identity;
1502
+ if (!sourceIdentity) {
1503
+ throw new TuiOperationBlockedError('taskSourceIdentityMissing', '任务来源身份不可验证。');
1504
+ }
1505
+ const sourceEvidence = {
1506
+ sourceId,
1507
+ sourceIdentity,
1508
+ requestedTargets: draft.executableTargets,
1509
+ causes: draft.causes,
1510
+ preview,
1511
+ executable: true,
1512
+ };
1513
+ sources.push(sourceEvidence);
1514
+ executableTargets.push(...draft.executableTargets);
1515
+ causes.push(...draft.causes);
1516
+ }
1517
+ const canonicalBlocked = [...new Map(blockedTargets.map((target) => [
1518
+ pluginWriteTaskTargetKey(target.target), target,
1519
+ ])).values()].sort((left, right) => pluginWriteTaskTargetKey(left.target)
1520
+ .localeCompare(pluginWriteTaskTargetKey(right.target), 'en'));
1521
+ if (executableTargets.length === 0) {
1522
+ throw new TuiOperationBlockedError('taskNoExecutableTargets', '任务没有可安全执行的目标。');
1523
+ }
1524
+ if (intent.executionPolicy === 'all' && canonicalBlocked.length > 0) {
1525
+ throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理。请调整选择。');
1526
+ }
1527
+ await preparation?.reporter.emit('evidence');
1528
+ const evidence = {
1529
+ catalog: {
1530
+ configDigest: configSnapshot.digest,
1531
+ compositeDigest: digest(sources.map((source) => [
1532
+ source.sourceId,
1533
+ source.sourceIdentity,
1534
+ source.preview.operationPlan?.planFingerprint,
1535
+ ])),
1536
+ },
1537
+ taskPlan: {
1538
+ goal: intent.goal,
1539
+ requestedTargets: intent.targets,
1540
+ executableTargets: [...executableTargets].sort((left, right) => pluginWriteTaskTargetKey(left).localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1541
+ causes: [...new Map(causes.map((cause) => [cause.causeKey, cause])).values()]
1542
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en')),
1543
+ blockedTargets: canonicalBlocked,
1544
+ sources,
1545
+ targetFingerprint: statusTargetFingerprint(observedTargets, observedRevisions),
1546
+ planFingerprint: '',
1547
+ },
1548
+ };
1549
+ evidence.taskPlan.planFingerprint = operationEvidenceFingerprint(evidence);
1550
+ taskCatalogOperationsByEvidence.set(evidence, operationContexts);
1551
+ return evidence;
1552
+ }
1164
1553
  if (intent.kind === 'pluginWriteBatch') {
1165
1554
  await preparation?.reporter.emit('status');
1166
1555
  const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
@@ -1232,6 +1621,33 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1232
1621
  await preparation?.reporter.emit('evidence');
1233
1622
  signal?.throwIfAborted();
1234
1623
  const merged = mergeWriteCommandResults(groups.map((group) => group.preview));
1624
+ const targetVersions = groups.flatMap((group) => {
1625
+ if (group.platform === 'both')
1626
+ return [];
1627
+ const platform = group.platform;
1628
+ return group.requestedPluginIds.map((pluginId) => {
1629
+ const live = sessionStatusResult?.payload.targets.find((target) => target.pluginId === pluginId && target.platform === platform
1630
+ && (target.scope === group.scope || statusTargetIntent(target).scopes.includes(group.scope)
1631
+ || platform === 'codex' && statusTargetActivation(target)?.[group.scope] === true));
1632
+ const revision = sessionStatusResult?.targetRevisions?.find((target) => target.pluginId === pluginId && target.platform === platform
1633
+ && target.scope === group.scope)?.revision;
1634
+ const activation = live ? statusTargetActivation(live) : undefined;
1635
+ const ownership = live && live.scope === group.scope
1636
+ ? statusTargetOwnershipState(live) : 'absent';
1637
+ const present = ownership === 'present' || ownership === 'partial'
1638
+ || platform === 'codex' && activation?.[group.scope] === true;
1639
+ const planned = live ? statusTargetIntent(live).scopes.includes(group.scope) : false;
1640
+ return {
1641
+ pluginId,
1642
+ platform,
1643
+ scope: group.scope,
1644
+ presence: present ? 'installed'
1645
+ : ownership === 'absent' && planned ? 'new' : 'unknown',
1646
+ ...(revision?.installedVersion ? { installedVersion: revision.installedVersion } : {}),
1647
+ ...(revision?.availableVersion ? { availableVersion: revision.availableVersion } : {}),
1648
+ };
1649
+ });
1650
+ });
1235
1651
  const evidence = {
1236
1652
  catalog: {
1237
1653
  configDigest: configSnapshot.digest,
@@ -1240,6 +1656,13 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1240
1656
  },
1241
1657
  requestedPluginIds: selection.requestedPluginIds,
1242
1658
  groups,
1659
+ targetVersions: [...new Map(targetVersions.map((target) => [[
1660
+ target.pluginId, target.platform, target.scope,
1661
+ ].join('\0'), target])).values()].sort((left, right) => [
1662
+ left.pluginId, left.platform, left.scope,
1663
+ ].join('\0').localeCompare([
1664
+ right.pluginId, right.platform, right.scope,
1665
+ ].join('\0'), 'en')),
1243
1666
  ...(merged.dependencyPlan ? { dependencyPlan: merged.dependencyPlan } : {}),
1244
1667
  ...(merged.codexReconcilePlans ? { codexReconcilePlans: merged.codexReconcilePlans } : {}),
1245
1668
  ...(merged.operationPlan ? { operationPlan: merged.operationPlan } : {}),
@@ -1440,6 +1863,8 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1440
1863
  : {}),
1441
1864
  cwd: context.cwd,
1442
1865
  env: context.env,
1866
+ nativeCliRunner: context.nativeCliRunner,
1867
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1443
1868
  cliVersion: packageInfo.version,
1444
1869
  ...(statusCatalogOperation ? { catalogOperationContext: statusCatalogOperation } : {}),
1445
1870
  inventoryCatalogSourceReferences: operationInventory,
@@ -1580,6 +2005,21 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1580
2005
  };
1581
2006
  },
1582
2007
  });
2008
+ if (intent.kind === 'pluginWrite' && intent.expectedUpdate) {
2009
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === intent.pluginId && entry.platform === intent.platform
2010
+ && entry.scope === intent.scope);
2011
+ const generation = catalogOperation?.generationSet.sources.find((entry) => entry.sourceId === selected.source.sourceId);
2012
+ if (intent.action !== 'sync'
2013
+ || !('pluginVersion' in selected.source)
2014
+ || selected.source.sourceIdentity !== intent.expectedUpdate.sourceIdentity
2015
+ || selected.source.pluginVersion !== intent.expectedUpdate.activeVersion
2016
+ || generation?.storeKey !== intent.expectedUpdate.generationKey
2017
+ || revision?.revision.installedVersion !== intent.expectedUpdate.installedVersion
2018
+ || revision?.revision.availableVersion !== intent.expectedUpdate.activeVersion
2019
+ || revision.revision.state !== 'upgradeAvailable') {
2020
+ throw operationReviewStale('插件更新版本或 Active Catalog 证据已变化。');
2021
+ }
2022
+ }
1583
2023
  let codexManager;
1584
2024
  let resolvedCodexManager;
1585
2025
  const hasDeclaredDependencies = (status.pluginSummaries.find((summary) => summary.pluginId === intent.pluginId)?.dependencyTotalCount ?? 0) > 0;
@@ -1743,6 +2183,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1743
2183
  return evidence;
1744
2184
  };
1745
2185
  const dependencies = {
2186
+ catalogManagement,
1746
2187
  beginCatalogSession(sessionOptions) {
1747
2188
  sessionCatalogOperationContext = undefined;
1748
2189
  if (sessionOptions?.reuseValidatedGenerations !== true) {
@@ -1817,11 +2258,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1817
2258
  validatedStoreEntries.clear();
1818
2259
  validatingStoreEntries.clear();
1819
2260
  }
1820
- const evidence = await captureEvidence(intent, signal, {
1821
- reporter,
1822
- validateIntent: true,
1823
- }, createOperationInventoryReader(), intent.kind === 'pluginWrite'
1824
- && intent.action === 'refreshAndSync', reusableCandidate);
2261
+ const taskObservationSession = intent.kind === 'pluginWriteTask'
2262
+ && intent.targets.some((target) => target.platform === 'codex')
2263
+ ? await createCodexNativeObservationSession({
2264
+ exactCwd: context.cwd,
2265
+ env: context.env,
2266
+ runner: context.nativeCliRunner,
2267
+ })
2268
+ : undefined;
2269
+ let evidence;
2270
+ try {
2271
+ evidence = await captureEvidence(intent, signal, {
2272
+ reporter,
2273
+ validateIntent: true,
2274
+ }, createOperationInventoryReader(), intent.kind === 'pluginWrite'
2275
+ && intent.action === 'refreshAndSync', reusableCandidate, taskObservationSession);
2276
+ }
2277
+ finally {
2278
+ await taskObservationSession?.dispose();
2279
+ }
1825
2280
  if (preparationWarnings.length > 0)
1826
2281
  evidence.preparationWarnings = preparationWarnings;
1827
2282
  return evidence;
@@ -1835,6 +2290,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1835
2290
  return evidence.dependencyPlan;
1836
2291
  },
1837
2292
  async validateIntent(intent, signal) {
2293
+ if (intent.kind === 'catalogManagement') {
2294
+ signal.throwIfAborted();
2295
+ return;
2296
+ }
1838
2297
  if (intent.kind === 'catalogSourcesRecovery') {
1839
2298
  signal.throwIfAborted();
1840
2299
  return;
@@ -2082,6 +2541,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2082
2541
  };
2083
2542
  },
2084
2543
  async executeIntent(intent, expectedEvidence, signal) {
2544
+ if (intent.kind === 'catalogManagement') {
2545
+ signal?.throwIfAborted();
2546
+ if (!expectedEvidence.catalogManagement) {
2547
+ throw new TuiOperationBlockedError('operationReviewStale', 'Catalog management Review evidence is missing.');
2548
+ }
2549
+ const progressLabels = {
2550
+ validate: '验证 Catalog 状态',
2551
+ build: '构建 Catalog snapshot',
2552
+ source: '更新开发来源',
2553
+ sync: '同步开发来源',
2554
+ prune: '清理 Catalog store',
2555
+ prepare: '准备发布计划',
2556
+ remote: '写入 Catalog 远端',
2557
+ verify: '验证操作结果',
2558
+ };
2559
+ return catalogManagement.execute(intent, expectedEvidence.catalogManagement, (stage) => {
2560
+ progressFeed?.setCurrent({ target: 'Catalog 管理', phase: progressLabels[stage] }, `Catalog 管理:${progressLabels[stage]}`);
2561
+ });
2562
+ }
2085
2563
  if (intent.kind === 'catalogSourcesRecovery') {
2086
2564
  signal?.throwIfAborted();
2087
2565
  return recoverInvalidCatalogSourcesConfig({ env: context.env });
@@ -2341,6 +2819,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2341
2819
  }
2342
2820
  const codexObservationSession = intent.kind === 'pluginWrite'
2343
2821
  || intent.kind === 'pluginWriteBatch'
2822
+ || intent.kind === 'pluginWriteTask'
2344
2823
  ? await createCodexNativeObservationSession({
2345
2824
  exactCwd: context.cwd,
2346
2825
  env: context.env,
@@ -2353,9 +2832,20 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2353
2832
  action: async (awHomeMutationToken) => {
2354
2833
  await tuiControllerRuntimeTestHooks?.beforeMutationEvidenceCapture?.();
2355
2834
  const operationInventory = createOperationInventoryReader();
2356
- const currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
2357
- ? expectedEvidence
2358
- : undefined);
2835
+ let currentEvidence;
2836
+ try {
2837
+ currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
2838
+ ? expectedEvidence
2839
+ : undefined);
2840
+ }
2841
+ catch (error) {
2842
+ const code = error instanceof TuiOperationBlockedError ? error.code : undefined;
2843
+ if (intent.kind === 'pluginWriteTask'
2844
+ && (code === 'taskHasBlockedTargets' || code === 'taskNoExecutableTargets')) {
2845
+ throw operationReviewStale('确认后任务可执行目标发生变化。');
2846
+ }
2847
+ throw error;
2848
+ }
2359
2849
  if (intent.kind === 'pluginWriteBatch') {
2360
2850
  const expectedBatch = expectedEvidence;
2361
2851
  const currentBatch = currentEvidence;
@@ -2498,6 +2988,134 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2498
2988
  groupResults,
2499
2989
  };
2500
2990
  }
2991
+ if (intent.kind === 'pluginWriteTask') {
2992
+ const expectedTask = expectedEvidence;
2993
+ const currentTask = currentEvidence;
2994
+ if (!operationEvidenceMatches(expectedTask, currentTask)) {
2995
+ throw operationReviewStale('确认后任务目标、根因、版本或写入计划发生变化。');
2996
+ }
2997
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
2998
+ const operationContexts = taskCatalogOperationsByEvidence.get(currentEvidence)
2999
+ ?? new Map();
3000
+ const sourceResults = [];
3001
+ const completedTargetKeys = new Set();
3002
+ const preApplyExecutionCache = new Map();
3003
+ const commandTargets = (source) => source.requestedTargets.map((target) => {
3004
+ const direct = source.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
3005
+ return {
3006
+ pluginId: target.pluginId,
3007
+ platform: target.platform,
3008
+ scope: target.scope,
3009
+ method: direct.some((cause) => cause.method === 'reinstall')
3010
+ ? 'reinstall' : 'sync',
3011
+ };
3012
+ });
3013
+ const repairTargets = (source) => [...new Map(source.causes.map((cause) => [
3014
+ pluginWriteTaskTargetKey(cause.repairTarget),
3015
+ { ...cause.repairTarget, method: cause.method },
3016
+ ])).values()];
3017
+ const stoppedResult = (source, error) => mergeWriteCommandResults(source.requestedTargets.map((target) => failedWriteCommandResult('sync', [target.pluginId], {
3018
+ cwd: context.cwd,
3019
+ platform: target.platform,
3020
+ scope: target.scope,
3021
+ }, error)));
3022
+ const appendStoppedSources = (startIndex, message) => {
3023
+ for (const [offset, source] of expectedTask.taskPlan.sources.slice(startIndex).entries()) {
3024
+ sourceResults.push({
3025
+ sourceId: source.sourceId,
3026
+ sourceIdentity: source.sourceIdentity,
3027
+ requestedTargets: source.requestedTargets,
3028
+ result: stoppedResult(source, offset === 0
3029
+ ? new TuiOperationBlockedError('operationReviewStale', message)
3030
+ : new TuiOperationBlockedError('taskNotStarted', '前一个来源计划已变化,因此这个来源没有开始。')),
3031
+ });
3032
+ }
3033
+ };
3034
+ for (const [sourceIndex, reviewed] of expectedTask.taskPlan.sources.entries()) {
3035
+ const current = currentTask.taskPlan.sources.find((source) => source.sourceId === reviewed.sourceId);
3036
+ const catalogOperation = operationContexts.get(reviewed.sourceId);
3037
+ if (!current || !catalogOperation || current.sourceIdentity !== reviewed.sourceIdentity) {
3038
+ if (sourceResults.length === 0) {
3039
+ throw operationReviewStale('确认后任务来源身份发生变化。');
3040
+ }
3041
+ appendStoppedSources(sourceIndex, '确认后任务来源身份发生变化。');
3042
+ break;
3043
+ }
3044
+ const sourceInventory = sourceResults.length === 0
3045
+ ? operationInventory : createOperationInventoryReader();
3046
+ let executionPreview = current.preview;
3047
+ if (completedTargetKeys.size > 0) {
3048
+ executionPreview = await taskWriteCommand(commandTargets(current), {
3049
+ cwd: context.cwd,
3050
+ repairTargets: repairTargets(current),
3051
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3052
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3053
+ }, {
3054
+ ...context,
3055
+ awHomeMutationToken,
3056
+ catalogOperationContext: catalogOperation,
3057
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3058
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3059
+ inventoryCatalogSourceReferences: sourceInventory,
3060
+ previewOperationPlan: true,
3061
+ preApplyExecutionCache,
3062
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3063
+ });
3064
+ if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
3065
+ || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
3066
+ appendStoppedSources(sourceIndex, '前序结果改变了这个来源的实际写入计划。');
3067
+ break;
3068
+ }
3069
+ }
3070
+ const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
3071
+ const result = await taskWriteCommand(commandTargets(current), {
3072
+ cwd: context.cwd,
3073
+ repairTargets: repairTargets(current),
3074
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3075
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3076
+ }, {
3077
+ ...context,
3078
+ awHomeMutationToken,
3079
+ catalogOperationContext: catalogOperation,
3080
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3081
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3082
+ inventoryCatalogSourceReferences: sourceInventory,
3083
+ preApplyConfirmed: true,
3084
+ preApplyExecutionCache,
3085
+ ...(executionPreview.operationPlan
3086
+ ? { expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint } : {}),
3087
+ ...(requiresManagedCodex ? { managedCodexAuthorizer: async () => true } : {}),
3088
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3089
+ });
3090
+ if (writeReviewIsStale(result)) {
3091
+ if (sourceResults.length === 0)
3092
+ throwIfWriteReviewStale(result);
3093
+ appendStoppedSources(sourceIndex, '这个来源的实际写入计划发生变化。');
3094
+ break;
3095
+ }
3096
+ for (const target of result.targetResults) {
3097
+ if (target.state === 'completed') {
3098
+ completedTargetKeys.add([
3099
+ target.target.pluginId,
3100
+ target.target.platform,
3101
+ target.target.scope,
3102
+ ].join('\0'));
3103
+ }
3104
+ }
3105
+ sourceResults.push({
3106
+ sourceId: reviewed.sourceId,
3107
+ sourceIdentity: reviewed.sourceIdentity,
3108
+ requestedTargets: reviewed.requestedTargets,
3109
+ result,
3110
+ });
3111
+ }
3112
+ const merged = mergeWriteCommandResults(sourceResults.map((source) => source.result));
3113
+ return {
3114
+ ...merged,
3115
+ taskPlan: expectedTask.taskPlan,
3116
+ sourceResults,
3117
+ };
3118
+ }
2501
3119
  if (intent.kind !== 'pluginWrite'
2502
3120
  && !operationEvidenceMatches(expectedEvidence, currentEvidence)) {
2503
3121
  throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');