@lesliechan721/aw 0.1.0-beta.18 → 0.1.0-beta.20

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 (47) hide show
  1. package/dist/src/commands/operation-planner.d.ts +3 -3
  2. package/dist/src/commands/rebuild-command.js +0 -5
  3. package/dist/src/commands/rebuild-command.js.map +1 -1
  4. package/dist/src/commands/rebuild-integration.d.ts +0 -1
  5. package/dist/src/commands/rebuild-integration.js +3 -152
  6. package/dist/src/commands/rebuild-integration.js.map +1 -1
  7. package/dist/src/commands/status-v6.d.ts +81 -31
  8. package/dist/src/commands/status-v6.js +494 -183
  9. package/dist/src/commands/status-v6.js.map +1 -1
  10. package/dist/src/commands/wizard.d.ts +2 -2
  11. package/dist/src/index.js +2 -2
  12. package/dist/src/tui/catalog-sources/projection.d.ts +2 -2
  13. package/dist/src/tui/catalog-sources/screens.js +17 -10
  14. package/dist/src/tui/catalog-sources/screens.js.map +1 -1
  15. package/dist/src/tui/console-app-v4.js +1300 -437
  16. package/dist/src/tui/console-app-v4.js.map +1 -1
  17. package/dist/src/tui/controller-runtime.d.ts +3 -3
  18. package/dist/src/tui/controller-runtime.js +114 -36
  19. package/dist/src/tui/controller-runtime.js.map +1 -1
  20. package/dist/src/tui/controller.d.ts +17 -7
  21. package/dist/src/tui/controller.js +269 -24
  22. package/dist/src/tui/controller.js.map +1 -1
  23. package/dist/src/tui/dependency-graph.d.ts +3 -0
  24. package/dist/src/tui/dependency-graph.js +37 -1
  25. package/dist/src/tui/dependency-graph.js.map +1 -1
  26. package/dist/src/tui/diagnostic-projection.d.ts +1 -0
  27. package/dist/src/tui/diagnostic-projection.js +51 -4
  28. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  29. package/dist/src/tui/model.d.ts +22 -12
  30. package/dist/src/tui/model.js +251 -86
  31. package/dist/src/tui/model.js.map +1 -1
  32. package/dist/src/tui/operation-history.d.ts +1 -0
  33. package/dist/src/tui/operation-history.js +5 -0
  34. package/dist/src/tui/operation-history.js.map +1 -1
  35. package/dist/src/tui/operation-review-planner.js +67 -2
  36. package/dist/src/tui/operation-review-planner.js.map +1 -1
  37. package/dist/src/tui/plugin-write-task.d.ts +2 -0
  38. package/dist/src/tui/plugin-write-task.js.map +1 -1
  39. package/dist/src/tui/router.d.ts +6 -0
  40. package/dist/src/tui/router.js +56 -12
  41. package/dist/src/tui/router.js.map +1 -1
  42. package/dist/src/tui/screen-registry.d.ts +7 -0
  43. package/dist/src/utils/plugin-config-write.js +3 -3
  44. package/dist/src/utils/plugin-config-write.js.map +1 -1
  45. package/dist/src/utils/target-eligibility.d.ts +2 -2
  46. package/dist/src/utils/target-eligibility.js.map +1 -1
  47. package/package.json +3 -3
@@ -24,6 +24,25 @@ import { IMPLEMENTED_AW_CAPABILITIES } from './aw-capabilities.js';
24
24
  import { createDependencyPlacementPlan } from './dependency-operation-plan.js';
25
25
  import { getCodexOperationRequirement } from '../utils/agent-version.js';
26
26
  import { inspectSystemCodex, } from '../utils/codex-manager.js';
27
+ export function statusTargetPlacements(target) {
28
+ if (target.platform === 'claude')
29
+ return [target];
30
+ return target.placements.map((placement) => ({
31
+ pluginName: target.pluginName,
32
+ sourceId: target.sourceId,
33
+ sourceIdentity: target.sourceIdentity,
34
+ pluginId: target.pluginId,
35
+ ...(target.version ? { version: target.version } : {}),
36
+ platform: target.platform,
37
+ cwd: target.cwd,
38
+ ...placement,
39
+ sharedComponents: target.components.filter((component) => component.kind !== 'systemConsumer' || component.key === `codex:${placement.scope}`),
40
+ sharedReasons: target.reasons.filter((reason) => reason.code.startsWith('codexSharedNative') || reason.code === 'staleCacheObserved'),
41
+ }));
42
+ }
43
+ export function flattenStatusTargets(targets) {
44
+ return targets.flatMap(statusTargetPlacements);
45
+ }
27
46
  export function projectStatusSharedNativeReasons(sharedNative, existingReasonCodes = [], desiredEnabled = false) {
28
47
  if (!sharedNative)
29
48
  return [];
@@ -64,7 +83,7 @@ export function projectStatusSharedNativeReasons(sharedNative, existingReasonCod
64
83
  ];
65
84
  }
66
85
  function statusComponent(target, kind, key) {
67
- return target.components.find((component) => component.kind === kind && (key === undefined || component.key === key));
86
+ return [...target.components, ...(target.sharedComponents ?? [])].find((component) => component.kind === kind && (key === undefined || component.key === key));
68
87
  }
69
88
  export function statusTargetIntent(target) {
70
89
  const metadata = statusComponent(target, 'intent', 'desired')?.metadata;
@@ -121,7 +140,7 @@ export function statusTargetSharedNative(target) {
121
140
  export function statusTargetActivation(target) {
122
141
  const user = statusComponent(target, 'activation', 'user');
123
142
  const project = statusComponent(target, 'activation', 'project');
124
- const consumer = statusComponent(target, 'systemConsumer', 'codex');
143
+ const consumer = statusComponent(target, 'systemConsumer');
125
144
  if (!user && !project && !consumer)
126
145
  return undefined;
127
146
  const activation = user ?? project ?? consumer;
@@ -181,7 +200,7 @@ export function setStatusDependencyInspectionHookForTests(hook) {
181
200
  dependencyInspectionHookForTests = hook;
182
201
  }
183
202
  export async function loadConsoleItemsResult(options = {}, runtime = {}) {
184
- const result = await statusCommandV7({
203
+ const result = await statusCommandV8({
185
204
  cwd: options.cwd ?? process.cwd(),
186
205
  env: runtime.env,
187
206
  platform: options.platform,
@@ -203,7 +222,7 @@ export async function loadConsoleItemsResult(options = {}, runtime = {}) {
203
222
  ...(pluginSummary ? { pluginSummary } : {}),
204
223
  targets: [],
205
224
  };
206
- current.targets.push(target);
225
+ current.targets.push(...statusTargetPlacements(target));
207
226
  groups.set(target.pluginId, current);
208
227
  }
209
228
  const allSourcesFailed = result.payload.catalog.sources.length > 0
@@ -251,6 +270,7 @@ function finalizeTargetError(error, targetState) {
251
270
  return {
252
271
  ...error,
253
272
  severity: error.code === 'artifactCollision'
273
+ || error.code === 'dependencyGraphInvalid'
254
274
  || (targetState === 'error'
255
275
  && error.dependencyPluginId === undefined)
256
276
  ? 'blocking'
@@ -416,7 +436,7 @@ function projectDependencyClosureForTarget(options) {
416
436
  visit(options.pluginId);
417
437
  return closure;
418
438
  }
419
- function statusRecovery(options) {
439
+ export function statusRecovery(options) {
420
440
  if (options.errorCodes?.some((code) => code === 'codexProjectPluginVersionTooLow'
421
441
  || code === 'codexProjectPluginUnsupported'
422
442
  || code === 'codexSystemCliUnsupported')) {
@@ -431,8 +451,29 @@ function statusRecovery(options) {
431
451
  description: '升级 AW CLI 到支持插件所需 capability 的版本后重试。',
432
452
  }];
433
453
  }
454
+ if (options.errorCodes?.includes('dependencyGraphInvalid')) {
455
+ return [{
456
+ command: 'manual',
457
+ description: '修复 Catalog 中的插件依赖循环后重试。',
458
+ }];
459
+ }
434
460
  if (options.state === 'ready' || options.state === 'absent')
435
461
  return [];
462
+ if (options.desired.scopes.length === 0
463
+ && options.sharedNative?.state === 'replaceable'
464
+ && options.sharedNative.reason === 'damaged') {
465
+ return [
466
+ ...(options.actualEnabled ? [{
467
+ command: 'cleanup',
468
+ ...(options.scope ? { scope: options.scope } : {}),
469
+ description: `仅清理 ${options.scope ?? '当前'} scope 的激活;不会修复或删除损坏的共享内容。`,
470
+ }] : []),
471
+ {
472
+ command: 'manual',
473
+ description: `共享内容已损坏。先明确选择 user 或 project 使用范围,再运行 aw install ${options.pluginId} --platform codex --scope <user|project> 重建。`,
474
+ },
475
+ ];
476
+ }
436
477
  if (options.state === 'orphaned') {
437
478
  return [{ command: 'cleanup', description: '清理可证明属于 AW 的遗留输出。' }];
438
479
  }
@@ -556,7 +597,7 @@ export function projectTargetRevision(availableVersion, installedVersion) {
556
597
  return { state: 'upgradeAvailable', availableVersion, installedVersion };
557
598
  return { state: 'unknown', availableVersion, installedVersion };
558
599
  }
559
- function renderStatusV7(payload, verbose) {
600
+ function renderStatusV8(payload, verbose) {
560
601
  const lines = [`Catalog sources: ${payload.catalog.sources.length}`];
561
602
  for (const source of payload.catalog.sources) {
562
603
  lines.push(`${source.primary ? '*' : '-'} ${source.id}: ${source.error ?? source.freshness ?? 'unavailable'}`);
@@ -566,11 +607,41 @@ function renderStatusV7(payload, verbose) {
566
607
  ? ` [${target.reasons.map((reason) => reason.message.startsWith(`${reason.code}:`)
567
608
  ? reason.message : `${reason.code}: ${reason.message}`).join('; ')}]`
568
609
  : '';
569
- lines.push(`${target.pluginId} ${target.platform} (${target.scope}): ${target.state}${reasons}`);
610
+ if (target.platform === 'claude') {
611
+ lines.push(`${target.pluginId} ${target.platform} (${target.scope}): ${target.state}${reasons}`);
612
+ }
613
+ else {
614
+ lines.push(`${target.pluginId} ${target.platform}: ${target.state}${reasons}`);
615
+ const desiredScopes = target.placements.filter((placement) => placement.components.some((component) => component.kind === 'intent' && component.metadata?.enabled === true))
616
+ .map((placement) => placement.scope);
617
+ const actualScopes = target.placements.filter((placement) => placement.components.some((component) => component.kind === 'activation'
618
+ && component.key === placement.scope
619
+ && component.state === 'enabled'))
620
+ .map((placement) => placement.scope);
621
+ const usage = (scopes) => scopes.length === 0
622
+ ? 'disabled'
623
+ : scopes.length === 2 ? 'user+project' : scopes[0];
624
+ lines.push(` desired use: ${usage(desiredScopes)}`);
625
+ lines.push(` actual use: ${usage(actualScopes)}`);
626
+ const shared = target.components.find((component) => component.kind === 'sharedNative');
627
+ if (shared)
628
+ lines.push(` shared content: ${shared.state} (${shared.key})`);
629
+ for (const placement of target.placements) {
630
+ lines.push(` ${placement.scope}: ${placement.state}`);
631
+ }
632
+ }
570
633
  if (verbose) {
571
634
  for (const component of target.components) {
572
635
  lines.push(` component ${component.kind}/${component.key}: ${component.state}`);
573
636
  }
637
+ if (target.platform === 'codex') {
638
+ for (const placement of target.placements) {
639
+ lines.push(` placement ${placement.scope}: ${placement.state}`);
640
+ for (const component of placement.components) {
641
+ lines.push(` component ${component.kind}/${component.key}: ${component.state}`);
642
+ }
643
+ }
644
+ }
574
645
  for (const action of target.recoveryActions) {
575
646
  lines.push(` recovery ${action.command}: ${action.description}`);
576
647
  }
@@ -601,7 +672,7 @@ function collisionRootIssueId(options) {
601
672
  options.collision.artifactKey,
602
673
  ].join(':');
603
674
  }
604
- async function statusCommandV7InSession(options) {
675
+ async function statusCommandV8InSession(options) {
605
676
  const env = options.env ?? process.env;
606
677
  const dependencyPluginCache = new Map();
607
678
  const dependencyInspectionCache = new Map();
@@ -732,9 +803,9 @@ async function statusCommandV7InSession(options) {
732
803
  if (!userCodexConfig || (probe.scope === 'project' && !projectCodexConfig)) {
733
804
  return 'partial';
734
805
  }
735
- const configEnabled = userCodexConfig.snapshot.plugins[`${probe.pluginName}@${probe.marketplaceName}`]?.enabled === true
736
- || (probe.scope === 'project'
737
- && projectCodexConfig?.snapshot.plugins[`${probe.pluginName}@${probe.marketplaceName}`]?.enabled === true);
806
+ const configEnabled = (probe.scope === 'user'
807
+ ? userCodexConfig
808
+ : projectCodexConfig)?.snapshot.plugins[`${probe.pluginName}@${probe.marketplaceName}`]?.enabled === true;
738
809
  return configEnabled
739
810
  ? 'present'
740
811
  : 'partial';
@@ -865,6 +936,35 @@ async function statusCommandV7InSession(options) {
865
936
  };
866
937
  }
867
938
  try {
939
+ const catalogEntriesByName = new Map(item.resolution.manifest.entries.map((entry) => [
940
+ entry.name,
941
+ entry,
942
+ ]));
943
+ const cyclicDependencyPluginNames = new Set();
944
+ const dependencyVisitState = new Map();
945
+ const dependencyVisitStack = [];
946
+ const visitDependencyGraph = (pluginName) => {
947
+ const state = dependencyVisitState.get(pluginName);
948
+ if (state === 'visited')
949
+ return;
950
+ if (state === 'visiting') {
951
+ const cycleStart = dependencyVisitStack.lastIndexOf(pluginName);
952
+ for (const name of dependencyVisitStack.slice(cycleStart)) {
953
+ cyclicDependencyPluginNames.add(name);
954
+ }
955
+ return;
956
+ }
957
+ dependencyVisitState.set(pluginName, 'visiting');
958
+ dependencyVisitStack.push(pluginName);
959
+ for (const dependencyName of catalogEntriesByName.get(pluginName)?.dependencies ?? []) {
960
+ if (catalogEntriesByName.has(dependencyName))
961
+ visitDependencyGraph(dependencyName);
962
+ }
963
+ dependencyVisitStack.pop();
964
+ dependencyVisitState.set(pluginName, 'visited');
965
+ };
966
+ for (const pluginName of catalogEntriesByName.keys())
967
+ visitDependencyGraph(pluginName);
868
968
  const placementEntries = item.resolution.manifest.entries.map((entry) => {
869
969
  const supportedPlatforms = resolveCatalogEntrySupportedPlatforms(entry.policy);
870
970
  return {
@@ -1125,8 +1225,238 @@ async function statusCommandV7InSession(options) {
1125
1225
  const dependencyErrors = [];
1126
1226
  const dependencyReadiness = [];
1127
1227
  const directDependencies = plugin.manifest.dependencies ?? [];
1228
+ const desiredInScope = sourceDesired.get(desiredTargetKey(plugin.pluginId, platform))?.scopes.includes(scope) === true;
1229
+ const codexPluginId = `${entry.name}@${marketplaceName}`;
1230
+ const actualCodexActivation = platform === 'codex'
1231
+ && (scope === 'user' ? userCodexConfig : projectCodexConfig)
1232
+ ?.snapshot.plugins[codexPluginId]?.enabled === true;
1233
+ const hasScopedExtensionResidue = platform === 'codex'
1234
+ && inspected.inspection.components.some((component) => component.component.kind !== 'native-plugin'
1235
+ && !component.component.identity.artifactKey.startsWith('activation-')
1236
+ && !component.component.identity.artifactKey.startsWith('marketplace-')
1237
+ && (component.state !== 'absent' || component.collision !== undefined));
1238
+ const dependencyPriority = {
1239
+ ready: 5,
1240
+ blockedByCollision: 4,
1241
+ incomplete: 3,
1242
+ error: 2,
1243
+ absent: 1,
1244
+ };
1245
+ function dependencyPlacementsFor(dependencyId, consumerScope) {
1246
+ const dependencyOverride = (consumerScope === 'project'
1247
+ ? projectIntent
1248
+ : userIntent).dependencyScopeOverrides.find((override) => override.pluginId === dependencyId
1249
+ && (override.platform === undefined || override.platform === platform));
1250
+ return dependencyOverride
1251
+ ? [dependencyOverride.scope]
1252
+ : consumerScope === 'project' ? ['project', 'user'] : ['user'];
1253
+ }
1254
+ async function inspectDependencyAtScope(dependencyName, dependencyScope) {
1255
+ const dependencyId = formatPluginId({
1256
+ sourceId: item.resolution.sourceId,
1257
+ pluginName: dependencyName,
1258
+ });
1259
+ const inspectionCacheKey = [
1260
+ item.resolution.sourceIdentity,
1261
+ dependencyId,
1262
+ platform,
1263
+ dependencyScope,
1264
+ dependencyScope === 'project' ? currentProjectKey : '',
1265
+ ].join('\0');
1266
+ if (cyclicDependencyPluginNames.has(dependencyName)) {
1267
+ return {
1268
+ state: 'error',
1269
+ dependencyScope,
1270
+ rootIssueId: `dependencyGraphInvalid:${dependencyId}`,
1271
+ rootIssueCode: 'dependencyGraphInvalid',
1272
+ issuePluginId: dependencyId,
1273
+ issueScope: dependencyScope,
1274
+ };
1275
+ }
1276
+ const cached = dependencyInspectionCache.get(inspectionCacheKey);
1277
+ if (cached)
1278
+ return cached;
1279
+ const inspection = (async () => {
1280
+ const dependencyEntry = item.resolution.manifest.entries.find((candidate) => candidate.name === dependencyName);
1281
+ if (!dependencyEntry) {
1282
+ return {
1283
+ state: 'absent',
1284
+ dependencyScope,
1285
+ rootIssueId: `dependencyMissing:${dependencyId}:${platform}:${dependencyScope}`,
1286
+ rootIssueCode: 'dependencyMissing',
1287
+ issuePluginId: dependencyId,
1288
+ issueScope: dependencyScope,
1289
+ };
1290
+ }
1291
+ const dependencySupport = checkAwCapabilitySupport(dependencyEntry.requires.aw.capabilities ?? [], IMPLEMENTED_AW_CAPABILITIES);
1292
+ if (!dependencySupport.supported) {
1293
+ return {
1294
+ state: 'error',
1295
+ dependencyScope,
1296
+ rootIssueId: `awCapabilityUnsupported:${dependencyId}`,
1297
+ rootIssueCode: 'awCapabilityUnsupported',
1298
+ issuePluginId: dependencyId,
1299
+ issueScope: dependencyScope,
1300
+ missingCapabilities: dependencySupport.missing,
1301
+ };
1302
+ }
1303
+ const pluginCacheKey = [item.resolution.sourceIdentity, dependencyId].join('\0');
1304
+ let dependencyPluginPromise = dependencyPluginCache.get(pluginCacheKey);
1305
+ if (!dependencyPluginPromise) {
1306
+ dependencyPluginPromise = readPluginManifest(path.join(item.resolution.rootDir, 'plugins', dependencyName), { mode: 'bundle' }).then((dependencyPlugin) => {
1307
+ dependencyPlugin.sourceId = item.resolution.sourceId;
1308
+ dependencyPlugin.sourceIdentity = item.resolution.sourceIdentity;
1309
+ dependencyPlugin.catalogLogicalIdentity = item.resolution.logicalIdentity;
1310
+ dependencyPlugin.pluginId = dependencyId;
1311
+ return dependencyPlugin;
1312
+ });
1313
+ dependencyPluginCache.set(pluginCacheKey, dependencyPluginPromise);
1314
+ }
1315
+ const dependencyPlugin = await dependencyPluginPromise;
1316
+ dependencyInspectionHookForTests?.();
1317
+ const dependencyMarketplaceName = marketplaceNameFor(dependencyName, platform, dependencyScope);
1318
+ const dependencyInspected = await inspectRebuildTarget({
1319
+ sourceId: item.resolution.sourceId,
1320
+ sourceIdentity: item.resolution.sourceIdentity,
1321
+ pluginId: dependencyId,
1322
+ marketplaceName: dependencyMarketplaceName,
1323
+ marketplaceRoot,
1324
+ plugin: dependencyPlugin,
1325
+ platform,
1326
+ scope: dependencyScope,
1327
+ cwd: options.cwd,
1328
+ catalog: { rootDir: item.resolution.rootDir, source: 'local' },
1329
+ context: {
1330
+ env,
1331
+ nativeCliRunner,
1332
+ reporter: createNoopReporter(),
1333
+ debugLogger: createNoopDebugLogger(),
1334
+ codexObservationSession: options.codexObservationSession,
1335
+ },
1336
+ userActivationDesired: codexDesiredUserActivationIds,
1337
+ nativeProbe: readOnlyNativeProbe({
1338
+ pluginName: dependencyName,
1339
+ sourceId: item.resolution.sourceId,
1340
+ sourceIdentity: item.resolution.sourceIdentity,
1341
+ platform,
1342
+ scope: dependencyScope,
1343
+ marketplaceName: dependencyMarketplaceName,
1344
+ marketplaceRoot,
1345
+ desired: sourceDesired.get(desiredTargetKey(dependencyId, platform))
1346
+ ?.scopes.includes(dependencyScope) === true,
1347
+ }),
1348
+ shallow: true,
1349
+ });
1350
+ const dependencyLiveState = projectedLiveState({
1351
+ inspection: dependencyInspected.inspection,
1352
+ required: dependencyInspected.target.desiredOwnedComponents
1353
+ ?? dependencyInspected.target.ownedComponents,
1354
+ optional: dependencyInspected.target.optionalOwnedComponents ?? [],
1355
+ });
1356
+ const collision = dependencyInspected.inspection.components.find((component) => component.collision)?.collision;
1357
+ if (collision) {
1358
+ return {
1359
+ state: 'blockedByCollision',
1360
+ dependencyScope,
1361
+ rootIssueId: collisionRootIssueId({
1362
+ pluginId: dependencyId,
1363
+ platform,
1364
+ scope: dependencyScope,
1365
+ collision,
1366
+ }),
1367
+ rootIssueCode: 'artifactCollision',
1368
+ issuePluginId: dependencyId,
1369
+ issueScope: dependencyScope,
1370
+ };
1371
+ }
1372
+ const requiredDependencyComponents = dependencyInspected.target
1373
+ .desiredOwnedComponents ?? dependencyInspected.target.ownedComponents;
1374
+ const desiredDependency = sourceDesired.get(desiredTargetKey(dependencyId, platform));
1375
+ const projectMigrationPending = dependencyScope === 'project'
1376
+ && projectIntent.dependencyScopeOverrides.some((override) => override.pluginId === dependencyId
1377
+ && override.scope === 'project'
1378
+ && (override.platform === undefined || override.platform === platform))
1379
+ && desiredDependency?.scopes.includes('project') === true
1380
+ && desiredDependency.scopes.includes('user') === false
1381
+ && ownerInventory.references.some((reference) => reference.kind === 'namespacedOwner'
1382
+ && reference.sourceId === item.resolution.sourceId
1383
+ && reference.pluginName === dependencyName
1384
+ && reference.platform === platform
1385
+ && reference.scope === 'user');
1386
+ const migrationOnlyPartial = dependencyLiveState === 'partial'
1387
+ && projectMigrationPending
1388
+ && dependencyInspected.inspection.components.every((component) => component.state === 'present' || component.state === 'absent')
1389
+ && requiredDependencyComponents.every((required) => dependencyInspected.inspection.components.some((component) => component.component === required
1390
+ && (component.state === 'present'
1391
+ || component.state === 'absent'
1392
+ && component.component.kind !== 'native-plugin'
1393
+ && 'locator' in component.component
1394
+ && component.component.locator.kind === 'codex-activation'
1395
+ && component.component.locator.desired === false)));
1396
+ const effectiveDependencyLiveState = migrationOnlyPartial
1397
+ ? 'present'
1398
+ : dependencyLiveState;
1399
+ if (effectiveDependencyLiveState !== 'present') {
1400
+ const state = effectiveDependencyLiveState === 'absent' ? 'absent'
1401
+ : effectiveDependencyLiveState === 'partial' ? 'incomplete'
1402
+ : 'error';
1403
+ return {
1404
+ state,
1405
+ dependencyScope,
1406
+ rootIssueId: state === 'absent'
1407
+ ? `dependencyMissing:${dependencyId}:${platform}:${dependencyScope}`
1408
+ : state === 'incomplete'
1409
+ ? `incompleteInstallation:${dependencyId}:${platform}:${dependencyScope}`
1410
+ : `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1411
+ rootIssueCode: state === 'absent' ? 'dependencyMissing'
1412
+ : state === 'incomplete' ? 'incompleteInstallation' : 'diagnostic',
1413
+ issuePluginId: dependencyId,
1414
+ issueScope: dependencyScope,
1415
+ };
1416
+ }
1417
+ for (const childName of dependencyPlugin.manifest.dependencies ?? []) {
1418
+ const childId = formatPluginId({
1419
+ sourceId: item.resolution.sourceId,
1420
+ pluginName: childName,
1421
+ });
1422
+ let childResult;
1423
+ for (const childScope of dependencyPlacementsFor(childId, dependencyScope)) {
1424
+ const candidate = await inspectDependencyAtScope(childName, childScope);
1425
+ if (candidate.state === 'ready') {
1426
+ childResult = candidate;
1427
+ break;
1428
+ }
1429
+ if (!childResult
1430
+ || dependencyPriority[candidate.state]
1431
+ > dependencyPriority[childResult.state]) {
1432
+ childResult = candidate;
1433
+ }
1434
+ }
1435
+ if (childResult?.state !== 'ready') {
1436
+ return {
1437
+ ...childResult,
1438
+ dependencyScope,
1439
+ };
1440
+ }
1441
+ }
1442
+ return { state: 'ready', dependencyScope };
1443
+ })().catch(() => ({
1444
+ state: 'error',
1445
+ dependencyScope,
1446
+ rootIssueId: `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1447
+ rootIssueCode: 'diagnostic',
1448
+ issuePluginId: dependencyId,
1449
+ issueScope: dependencyScope,
1450
+ }));
1451
+ dependencyInspectionCache.set(inspectionCacheKey, inspection);
1452
+ return inspection;
1453
+ }
1128
1454
  if (directDependencies.length > 0
1129
- && (liveState === 'present' || liveState === 'partial')) {
1455
+ && (desiredInScope
1456
+ || actualCodexActivation
1457
+ || hasScopedExtensionResidue
1458
+ || (platform !== 'codex'
1459
+ && (liveState === 'present' || liveState === 'partial')))) {
1130
1460
  for (const dependencyName of directDependencies) {
1131
1461
  const dependencyId = formatPluginId({
1132
1462
  sourceId: item.resolution.sourceId,
@@ -1180,120 +1510,12 @@ async function statusCommandV7InSession(options) {
1180
1510
  }, 'partial'));
1181
1511
  continue;
1182
1512
  }
1183
- const pluginCacheKey = [item.resolution.sourceIdentity, dependencyId].join('\0');
1184
- let dependencyPluginPromise = dependencyPluginCache.get(pluginCacheKey);
1185
- if (!dependencyPluginPromise) {
1186
- dependencyPluginPromise = readPluginManifest(path.join(item.resolution.rootDir, 'plugins', dependencyName), { mode: 'bundle' }).then((dependencyPlugin) => {
1187
- dependencyPlugin.sourceId = item.resolution.sourceId;
1188
- dependencyPlugin.sourceIdentity = item.resolution.sourceIdentity;
1189
- dependencyPlugin.catalogLogicalIdentity = item.resolution.logicalIdentity;
1190
- dependencyPlugin.pluginId = dependencyId;
1191
- return dependencyPlugin;
1192
- });
1193
- dependencyPluginCache.set(pluginCacheKey, dependencyPluginPromise);
1194
- }
1195
- const dependencyOverride = (scope === 'project'
1196
- ? projectIntent
1197
- : userIntent).dependencyScopeOverrides.find((override) => override.pluginId === dependencyId
1198
- && (override.platform === undefined || override.platform === platform));
1199
- const placements = dependencyOverride
1200
- ? [dependencyOverride.scope]
1201
- : scope === 'project' ? ['project', 'user'] : ['user'];
1513
+ const placements = dependencyPlacementsFor(dependencyId, scope);
1202
1514
  let dependencyResult = {
1203
1515
  state: 'absent',
1204
1516
  };
1205
1517
  for (const dependencyScope of placements) {
1206
- const inspectionCacheKey = [
1207
- item.resolution.sourceIdentity,
1208
- dependencyId,
1209
- platform,
1210
- dependencyScope,
1211
- dependencyScope === 'project' ? currentProjectKey : '',
1212
- ].join('\0');
1213
- let dependencyInspection = dependencyInspectionCache.get(inspectionCacheKey);
1214
- if (!dependencyInspection) {
1215
- dependencyInspection = dependencyPluginPromise.then(async (dependencyPlugin) => {
1216
- dependencyInspectionHookForTests?.();
1217
- const dependencyMarketplaceName = marketplaceNameFor(dependencyName, platform, dependencyScope);
1218
- const dependencyInspected = await inspectRebuildTarget({
1219
- sourceId: item.resolution.sourceId,
1220
- sourceIdentity: item.resolution.sourceIdentity,
1221
- pluginId: dependencyId,
1222
- marketplaceName: dependencyMarketplaceName,
1223
- marketplaceRoot,
1224
- plugin: dependencyPlugin,
1225
- platform,
1226
- scope: dependencyScope,
1227
- cwd: options.cwd,
1228
- catalog: { rootDir: item.resolution.rootDir, source: 'local' },
1229
- context: {
1230
- env,
1231
- nativeCliRunner,
1232
- reporter: createNoopReporter(),
1233
- debugLogger: createNoopDebugLogger(),
1234
- codexObservationSession: options.codexObservationSession,
1235
- },
1236
- userActivationDesired: codexDesiredUserActivationIds,
1237
- nativeProbe: readOnlyNativeProbe({
1238
- pluginName: dependencyName,
1239
- sourceId: item.resolution.sourceId,
1240
- sourceIdentity: item.resolution.sourceIdentity,
1241
- platform,
1242
- scope: dependencyScope,
1243
- marketplaceName: dependencyMarketplaceName,
1244
- marketplaceRoot,
1245
- desired: sourceDesired.get(desiredTargetKey(dependencyId, platform))
1246
- ?.scopes.includes(dependencyScope) === true,
1247
- }),
1248
- shallow: true,
1249
- });
1250
- const dependencyLiveState = projectedLiveState({
1251
- inspection: dependencyInspected.inspection,
1252
- required: dependencyInspected.target.desiredOwnedComponents
1253
- ?? dependencyInspected.target.ownedComponents,
1254
- optional: dependencyInspected.target.optionalOwnedComponents ?? [],
1255
- });
1256
- const collision = dependencyInspected.inspection.components.find((component) => component.collision)?.collision;
1257
- if (collision) {
1258
- return {
1259
- state: 'blockedByCollision',
1260
- dependencyScope,
1261
- rootIssueId: collisionRootIssueId({
1262
- pluginId: dependencyId,
1263
- platform,
1264
- scope: dependencyScope,
1265
- collision,
1266
- }),
1267
- rootIssueCode: 'artifactCollision',
1268
- };
1269
- }
1270
- return {
1271
- state: dependencyLiveState === 'present' ? 'ready'
1272
- : dependencyLiveState === 'absent' ? 'absent'
1273
- : dependencyLiveState === 'partial' ? 'incomplete'
1274
- : 'error',
1275
- dependencyScope,
1276
- ...(dependencyLiveState === 'partial'
1277
- ? {
1278
- rootIssueId: `incompleteInstallation:${dependencyId}:${platform}:${dependencyScope}`,
1279
- rootIssueCode: 'incompleteInstallation',
1280
- }
1281
- : dependencyLiveState === 'error'
1282
- ? {
1283
- rootIssueId: `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1284
- rootIssueCode: 'diagnostic',
1285
- }
1286
- : {}),
1287
- };
1288
- }).catch(() => ({
1289
- state: 'error',
1290
- dependencyScope,
1291
- rootIssueId: `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1292
- rootIssueCode: 'diagnostic',
1293
- }));
1294
- dependencyInspectionCache.set(inspectionCacheKey, dependencyInspection);
1295
- }
1296
- const candidate = await dependencyInspection;
1518
+ const candidate = await inspectDependencyAtScope(dependencyName, dependencyScope);
1297
1519
  if (candidate.state === 'ready') {
1298
1520
  dependencyResult = candidate;
1299
1521
  break;
@@ -1309,7 +1531,7 @@ async function statusCommandV7InSession(options) {
1309
1531
  dependencyResult = candidate;
1310
1532
  }
1311
1533
  }
1312
- if (dependencyResult.state === 'absent') {
1534
+ if (dependencyResult.state === 'absent' && !dependencyResult.rootIssueId) {
1313
1535
  const dependencyScope = dependencyResult.dependencyScope ?? placements[0];
1314
1536
  dependencyResult = {
1315
1537
  state: 'absent',
@@ -1324,28 +1546,46 @@ async function statusCommandV7InSession(options) {
1324
1546
  dependencyPlatform: platform,
1325
1547
  dependencyScope,
1326
1548
  dependencyProject: dependencyScope === 'project' ? 'current' : null,
1327
- ...dependencyResult,
1549
+ state: dependencyResult.state,
1550
+ ...(dependencyResult.rootIssueId
1551
+ ? { rootIssueId: dependencyResult.rootIssueId }
1552
+ : {}),
1553
+ ...(dependencyResult.rootIssueCode
1554
+ ? { rootIssueCode: dependencyResult.rootIssueCode }
1555
+ : {}),
1328
1556
  });
1557
+ const issuePluginId = dependencyResult.issuePluginId ?? dependencyId;
1558
+ const issueScope = dependencyResult.issueScope ?? dependencyScope;
1329
1559
  if (dependencyResult.state === 'absent') {
1330
1560
  dependencyErrors.push(finalizeTargetError({
1331
1561
  code: 'dependencyMissing',
1332
- message: `dependencyMissing: ${dependencyId} is absent from its required live placement.`,
1333
- dependencyPluginId: dependencyId,
1562
+ message: `dependencyMissing: ${issuePluginId} is absent from its required live placement.`,
1563
+ dependencyPluginId: issuePluginId,
1334
1564
  dependencyPlatform: platform,
1335
- dependencyScope,
1336
- dependencyProject: dependencyScope === 'project' ? 'current' : null,
1565
+ dependencyScope: issueScope,
1566
+ dependencyProject: issueScope === 'project' ? 'current' : null,
1337
1567
  rootIssueId: dependencyResult.rootIssueId,
1338
1568
  rootIssueCode: dependencyResult.rootIssueCode,
1339
1569
  }, 'partial'));
1340
1570
  }
1341
1571
  else if (dependencyResult.state !== 'ready') {
1572
+ const code = dependencyResult.rootIssueCode === 'awCapabilityUnsupported'
1573
+ ? 'awCapabilityUnsupported'
1574
+ : dependencyResult.rootIssueCode === 'dependencyGraphInvalid'
1575
+ ? 'dependencyGraphInvalid'
1576
+ : 'dependencyNotReady';
1577
+ const message = code === 'awCapabilityUnsupported'
1578
+ ? `${code}: ${issuePluginId} requires unsupported AW capabilities: ${(dependencyResult.missingCapabilities ?? []).join(', ')}.`
1579
+ : code === 'dependencyGraphInvalid'
1580
+ ? `${code}: dependency cycle includes ${issuePluginId}.`
1581
+ : `${code}: ${issuePluginId} is not ready at its required live placement.`;
1342
1582
  dependencyErrors.push(finalizeTargetError({
1343
- code: 'dependencyNotReady',
1344
- message: `dependencyNotReady: ${dependencyId} is not ready at its required live placement.`,
1345
- dependencyPluginId: dependencyId,
1583
+ code,
1584
+ message,
1585
+ dependencyPluginId: issuePluginId,
1346
1586
  dependencyPlatform: platform,
1347
- dependencyScope,
1348
- dependencyProject: dependencyScope === 'project' ? 'current' : null,
1587
+ dependencyScope: issueScope,
1588
+ dependencyProject: issueScope === 'project' ? 'current' : null,
1349
1589
  ...(dependencyResult.rootIssueId
1350
1590
  ? { rootIssueId: dependencyResult.rootIssueId }
1351
1591
  : {}),
@@ -1924,12 +2164,10 @@ async function statusCommandV7InSession(options) {
1924
2164
  const codex = target.platform === 'codex' ? entry?.codex : undefined;
1925
2165
  const desiredAll = desiredTargetStates.get(desiredTargetKey(target.pluginId, target.platform))
1926
2166
  ?? { scopes: [], relation: 'none' };
1927
- const desired = target.platform === 'codex'
1928
- ? desiredAll
1929
- : {
1930
- scopes: desiredAll.scopes.includes(target.scope) ? [target.scope] : [],
1931
- relation: desiredAll.relation,
1932
- };
2167
+ const desired = {
2168
+ scopes: desiredAll.scopes.includes(target.scope) ? [target.scope] : [],
2169
+ relation: desiredAll.relation,
2170
+ };
1933
2171
  const sharedNative = codex?.sharedNative ?? target.sharedNative;
1934
2172
  const baseActivation = codex?.activation ?? target.activation;
1935
2173
  const consumerScope = desired.scopes.includes('project')
@@ -1952,9 +2190,7 @@ async function statusCommandV7InSession(options) {
1952
2190
  systemConsumer,
1953
2191
  }
1954
2192
  : baseActivation;
1955
- const observationScopes = explicitScope
1956
- ? [explicitScope]
1957
- : desired.scopes.length > 0 ? desired.scopes : groupedScopes;
2193
+ const observationScopes = [target.scope];
1958
2194
  const scopedTargets = groupedTargets.filter((candidate) => observationScopes.includes(candidate.scope));
1959
2195
  const observedTargets = scopedTargets.length > 0 ? scopedTargets : groupedTargets;
1960
2196
  const ownershipState = observedTargets.some((candidate) => candidate.state === 'error')
@@ -1966,6 +2202,8 @@ async function statusCommandV7InSession(options) {
1966
2202
  : sharedNative?.staleCacheObserved
1967
2203
  ? 'partial'
1968
2204
  : 'absent';
2205
+ const actualScopeEnabled = target.platform === 'codex'
2206
+ && baseActivation?.[target.scope] === true;
1969
2207
  const hasSourceError = observedTargets.some((candidate) => candidate.errors.some((error) => error.code === 'sourceMissing'
1970
2208
  || error.code === 'catalogSourceUnavailable'
1971
2209
  || error.code === 'bundleUnavailable'
@@ -1974,18 +2212,16 @@ async function statusCommandV7InSession(options) {
1974
2212
  && (codex?.extensionKinds.length ?? 0) > 0
1975
2213
  && desired.scopes.includes('user')
1976
2214
  && desired.scopes.includes('project');
1977
- const expectedUser = desired.scopes.includes('user');
1978
- const expectedProject = desired.scopes.includes('project');
2215
+ const expectedEnabled = desired.scopes.includes(target.scope);
1979
2216
  const activationDrift = target.platform === 'codex'
1980
2217
  && (codexConfigErrors.some((error) => error.scope === 'user' || observationScopes.includes(error.scope))
1981
2218
  || ((codex?.nativeKinds.length ?? 0) > 0
1982
- && ((expectedUser && activation?.user !== true)
1983
- || (!expectedUser && activation?.user === true)
1984
- || (expectedProject && activation?.project !== true)
1985
- || (!expectedProject && activation?.project === true))));
2219
+ && (expectedEnabled && activation?.[target.scope] !== true
2220
+ || !expectedEnabled && activation?.[target.scope] === true)));
1986
2221
  const extensionComponents = observedTargets.flatMap((candidate) => candidate.components).filter((component) => component.key !== 'native'
1987
2222
  && !component.key.startsWith('activation-')
1988
2223
  && !component.key.startsWith('marketplace-'));
2224
+ const hasScopedExtensionResidue = extensionComponents.some((component) => component.state !== 'absent' || component.error !== undefined);
1989
2225
  const extensions = target.platform !== 'codex'
1990
2226
  || (codex?.extensionKinds.length ?? 0) === 0
1991
2227
  ? undefined
@@ -2041,6 +2277,7 @@ async function statusCommandV7InSession(options) {
2041
2277
  const baseTargetErrors = observedTargets.flatMap((candidate) => candidate.errors);
2042
2278
  const hasBlockingBaseReason = baseTargetErrors.some((reason) => reason.severity === 'blocking');
2043
2279
  const capabilityUnsupported = baseTargetErrors.some((reason) => reason.code === 'awCapabilityUnsupported');
2280
+ const dependencyNotReady = (target.dependencyReadiness ?? []).some((dependency) => dependency.state !== 'ready');
2044
2281
  const state = hasSourceError && desiredEnabled
2045
2282
  ? 'blocked'
2046
2283
  : consumerBlocking && desiredEnabled
@@ -2049,23 +2286,31 @@ async function statusCommandV7InSession(options) {
2049
2286
  ? 'blocked'
2050
2287
  : sharedNative?.state === 'blocked' || hasBlockingBaseReason || capabilityUnsupported
2051
2288
  ? 'blocked'
2052
- : !desiredEnabled
2053
- ? ownershipState !== 'absent' || desired.relation === 'managed'
2054
- ? 'orphaned'
2055
- : 'absent'
2056
- : dependencyMoves.length > 0
2057
- ? 'needsSync'
2058
- : sharedNative?.state === 'missing'
2059
- || sharedNative?.state === 'replaceable'
2060
- || sharedNative?.state === 'unobserved'
2289
+ : sharedNative?.state === 'replaceable' && sharedNative.reason === 'damaged'
2290
+ ? 'needsSync'
2291
+ : !desiredEnabled
2292
+ ? (target.platform === 'codex'
2293
+ ? actualScopeEnabled || hasScopedExtensionResidue
2294
+ : ownershipState !== 'absent')
2295
+ || desired.relation === 'managed'
2296
+ ? 'orphaned'
2297
+ : 'absent'
2298
+ : dependencyMoves.length > 0
2061
2299
  ? 'needsSync'
2062
- : activationDrift || extensions?.state === 'drifted'
2300
+ : dependencyNotReady
2063
2301
  ? 'needsSync'
2064
- : extensions?.state === 'missing' || ownershipState === 'absent'
2302
+ : sharedNative?.state === 'missing'
2303
+ || sharedNative?.state === 'replaceable'
2304
+ || sharedNative?.state === 'unobserved'
2065
2305
  ? 'needsSync'
2066
- : ownershipState === 'partial' || ownershipState === 'error'
2306
+ : activationDrift || extensions?.state === 'drifted'
2067
2307
  ? 'needsSync'
2068
- : 'ready';
2308
+ : extensions?.state === 'missing'
2309
+ || (target.platform !== 'codex' && ownershipState === 'absent')
2310
+ ? 'needsSync'
2311
+ : ownershipState === 'partial' || ownershipState === 'error'
2312
+ ? 'needsSync'
2313
+ : 'ready';
2069
2314
  const stableErrors = [
2070
2315
  ...projectStatusSharedNativeReasons(sharedNative, baseTargetErrors.map((error) => error.code), desiredEnabled),
2071
2316
  ...(desiredOverlapConflict
@@ -2073,8 +2318,8 @@ async function statusCommandV7InSession(options) {
2073
2318
  : []),
2074
2319
  ...(activationDrift && sharedNative?.state !== 'missing' && sharedNative?.state !== 'unobserved'
2075
2320
  ? [warning('codexActivationDrift', `${target.pluginId} activation differs from desired intent: `
2076
- + `user expected=${expectedUser}, actual=${activation?.user ?? 'unset'}; `
2077
- + `project expected=${expectedProject}, actual=${activation?.project ?? 'unset'}.`)]
2321
+ + `${target.scope} expected=${expectedEnabled}, `
2322
+ + `actual=${activation?.[target.scope] ?? 'unset'}.`)]
2078
2323
  : []),
2079
2324
  ...(extensions?.state === 'drifted'
2080
2325
  ? [warning('codexExtensionDrift', `${target.pluginId} extension projection has drifted.`)]
@@ -2100,11 +2345,7 @@ async function statusCommandV7InSession(options) {
2100
2345
  ])).values()];
2101
2346
  const ownershipComponents = [...new Map(observedTargets.flatMap((candidate) => candidate.components)
2102
2347
  .map((component) => [component.key, component])).values()];
2103
- const representativeScope = explicitScope
2104
- ?? (desired.scopes.length === 1
2105
- ? desired.scopes[0]
2106
- : observedTargets.find((candidate) => candidate.state !== 'absent')?.scope
2107
- ?? (groupedScopes.includes('user') ? 'user' : groupedScopes[0] ?? target.scope));
2348
+ const representativeScope = target.scope;
2108
2349
  const representativeTarget = observedTargets.find((candidate) => candidate.scope === representativeScope) ?? target;
2109
2350
  const components = [
2110
2351
  {
@@ -2151,7 +2392,7 @@ async function statusCommandV7InSession(options) {
2151
2392
  }]),
2152
2393
  ...(activation?.systemConsumer ? [{
2153
2394
  kind: 'systemConsumer',
2154
- key: 'codex',
2395
+ key: `codex:${representativeScope}`,
2155
2396
  state: activation.systemConsumer.state,
2156
2397
  metadata: {
2157
2398
  evidence: activation.evidence,
@@ -2198,6 +2439,8 @@ async function statusCommandV7InSession(options) {
2198
2439
  pluginId: target.pluginId,
2199
2440
  ...(dependencyMoves.length > 0 ? { dependencyMoves } : {}),
2200
2441
  ...(sharedNative ? { sharedNative } : {}),
2442
+ scope: target.scope,
2443
+ actualEnabled: actualScopeEnabled,
2201
2444
  conflictRoots: rootIntentsForTarget({
2202
2445
  pluginId: target.pluginId,
2203
2446
  platform: target.platform,
@@ -2206,7 +2449,9 @@ async function statusCommandV7InSession(options) {
2206
2449
  available,
2207
2450
  }),
2208
2451
  errorCodes: dedupedErrors.map((error) => error.code),
2209
- }),
2452
+ }).map((action) => action.command === 'manual' || action.scope
2453
+ ? action
2454
+ : { ...action, scope: representativeScope }),
2210
2455
  };
2211
2456
  };
2212
2457
  const codexGroups = new Map();
@@ -2221,18 +2466,84 @@ async function statusCommandV7InSession(options) {
2221
2466
  codexGroups.set(target.pluginId, group);
2222
2467
  }
2223
2468
  for (const group of codexGroups.values()) {
2224
- const scopes = [...new Set(group.map((target) => target.scope))].sort();
2225
- projectedTargets.push(await decorateTarget(group[0], scopes, group));
2469
+ const decorated = await Promise.all(group.map((target) => decorateTarget(target, [target.scope], [target])));
2470
+ const first = decorated[0];
2471
+ const entry = selectedByPluginId.get(first.pluginId);
2472
+ const desired = desiredTargetStates.get(desiredTargetKey(first.pluginId, 'codex'))
2473
+ ?? { scopes: [], relation: 'none' };
2474
+ const returnedScopes = new Set(decorated.map((target) => target.scope));
2475
+ const desiredOverlapConflict = (entry?.codex?.extensionKinds.length ?? 0) > 0
2476
+ && desired.scopes.includes('user')
2477
+ && desired.scopes.includes('project')
2478
+ && returnedScopes.has('user')
2479
+ && returnedScopes.has('project');
2480
+ const sharedComponents = [...new Map(decorated.flatMap((target) => target.components)
2481
+ .filter((component) => component.kind === 'sharedNative' || component.kind === 'systemConsumer')
2482
+ .map((component) => [`${component.kind}\0${component.key}`, component])).values()];
2483
+ const sharedReason = (reason) => reason.code.startsWith('codexSharedNative') || reason.code === 'staleCacheObserved';
2484
+ const sharedReasons = decorated.flatMap((target) => target.reasons.filter(sharedReason));
2485
+ const placements = decorated.map((target) => ({
2486
+ scope: target.scope,
2487
+ state: target.state,
2488
+ components: target.components.filter((component) => component.kind !== 'sharedNative' && component.kind !== 'systemConsumer'),
2489
+ reasons: target.reasons.filter((reason) => !sharedReason(reason)),
2490
+ recoveryActions: target.recoveryActions,
2491
+ })).sort((left, right) => left.scope.localeCompare(right.scope, 'en'));
2492
+ const reasons = [...new Map([
2493
+ ...placements.flatMap((placement) => placement.reasons),
2494
+ ...sharedReasons,
2495
+ ...(desiredOverlapConflict ? [blocking('codexExtensionScopeConflict', `${first.pluginId} has AW extensions desired at user and project scope.`)] : []),
2496
+ ]
2497
+ .map((reason) => [`${reason.code}\0${reason.message}`, reason])).values()];
2498
+ const conflictRecovery = desiredOverlapConflict ? statusRecovery({
2499
+ state: 'blocked',
2500
+ desired,
2501
+ platform: 'codex',
2502
+ pluginId: first.pluginId,
2503
+ conflictRoots: rootIntentsForTarget({
2504
+ pluginId: first.pluginId,
2505
+ platform: 'codex',
2506
+ userIntent,
2507
+ projectIntent,
2508
+ available,
2509
+ }),
2510
+ errorCodes: ['codexExtensionScopeConflict'],
2511
+ }) : [];
2512
+ const recoveryActions = [...new Map((desiredOverlapConflict
2513
+ ? conflictRecovery
2514
+ : placements.flatMap((placement) => placement.recoveryActions))
2515
+ .map((action) => [
2516
+ `${action.command}\0${action.scope ?? ''}\0${action.description}`,
2517
+ action,
2518
+ ])).values()];
2519
+ const state = desiredOverlapConflict ? 'blocked'
2520
+ : ['blocked', 'needsSync', 'orphaned', 'ready', 'absent']
2521
+ .find((candidate) => placements.some((placement) => placement.state === candidate));
2522
+ projectedTargets.push({
2523
+ pluginName: first.pluginName,
2524
+ sourceId: first.sourceId,
2525
+ sourceIdentity: first.sourceIdentity,
2526
+ pluginId: first.pluginId,
2527
+ ...(first.version ? { version: first.version } : {}),
2528
+ platform: 'codex',
2529
+ cwd: first.cwd,
2530
+ state,
2531
+ components: sharedComponents,
2532
+ reasons,
2533
+ recoveryActions,
2534
+ placements,
2535
+ });
2226
2536
  }
2227
- projectedTargets.sort((left, right) => `${left.pluginId}\0${left.platform}\0${left.scope}`.localeCompare(`${right.pluginId}\0${right.platform}\0${right.scope}`, 'en'));
2228
- const targetRevisions = projectedTargets.map((target) => {
2537
+ projectedTargets.sort((left, right) => `${left.pluginId}\0${left.platform}`.localeCompare(`${right.pluginId}\0${right.platform}`, 'en'));
2538
+ const targetRevisions = projectedTargets.flatMap((target) => {
2229
2539
  const entry = selectedByPluginId.get(target.pluginId);
2230
- return {
2540
+ const placements = target.platform === 'codex' ? target.placements : [target];
2541
+ return placements.map((placement) => ({
2231
2542
  pluginId: target.pluginId,
2232
2543
  platform: target.platform,
2233
- scope: target.scope,
2234
- revision: projectTargetRevision(entry?.version, entry?.states[target.platform]?.[target.scope]?.installedVersion),
2235
- };
2544
+ scope: placement.scope,
2545
+ revision: projectTargetRevision(entry?.version, entry?.states[target.platform]?.[placement.scope]?.installedVersion),
2546
+ }));
2236
2547
  });
2237
2548
  const publicConfigIssues = available.flatMap((entry) => (entry.configIssues ?? [])
2238
2549
  .filter((issue) => issue.type === 'configLayerUnsupported')
@@ -2242,7 +2553,7 @@ async function statusCommandV7InSession(options) {
2242
2553
  ...issue,
2243
2554
  })));
2244
2555
  const payload = {
2245
- schemaVersion: 7,
2556
+ schemaVersion: 8,
2246
2557
  catalog: {
2247
2558
  primarySourceId: catalog.primarySourceId,
2248
2559
  configDigest: catalog.configDigest,
@@ -2272,7 +2583,7 @@ async function statusCommandV7InSession(options) {
2272
2583
  || payload.targets.some((target) => target.reasons.some((reason) => reason.severity === 'blocking'));
2273
2584
  return {
2274
2585
  payload,
2275
- output: options.json ? JSON.stringify(payload) : renderStatusV7(payload, options.verbose === true),
2586
+ output: options.json ? JSON.stringify(payload) : renderStatusV8(payload, options.verbose === true),
2276
2587
  hasErrors,
2277
2588
  targetRevisions,
2278
2589
  pluginSummaries: available.map((entry) => {
@@ -2319,9 +2630,9 @@ async function statusCommandV7InSession(options) {
2319
2630
  }),
2320
2631
  };
2321
2632
  }
2322
- export async function statusCommandV7(options) {
2633
+ export async function statusCommandV8(options) {
2323
2634
  if (options.codexObservationSession) {
2324
- return statusCommandV7InSession({
2635
+ return statusCommandV8InSession({
2325
2636
  ...options,
2326
2637
  codexObservationSession: options.codexObservationSession,
2327
2638
  });
@@ -2333,7 +2644,7 @@ export async function statusCommandV7(options) {
2333
2644
  runner: options.nativeCliRunner ?? getDefaultNativeCliRunner(),
2334
2645
  });
2335
2646
  try {
2336
- return await statusCommandV7InSession({ ...options, codexObservationSession: session });
2647
+ return await statusCommandV8InSession({ ...options, codexObservationSession: session });
2337
2648
  }
2338
2649
  finally {
2339
2650
  await session.dispose();