@lesliechan721/aw 0.1.0-beta.17 → 0.1.0-beta.19

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 (48) hide show
  1. package/dist/src/commands/operation-planner.d.ts +3 -3
  2. package/dist/src/commands/rebuild-command.js +4 -5
  3. package/dist/src/commands/rebuild-command.js.map +1 -1
  4. package/dist/src/commands/status-v6.d.ts +83 -33
  5. package/dist/src/commands/status-v6.js +531 -191
  6. package/dist/src/commands/status-v6.js.map +1 -1
  7. package/dist/src/commands/wizard.d.ts +2 -2
  8. package/dist/src/index.js +4 -4
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/tui/catalog-sources/projection.d.ts +2 -2
  11. package/dist/src/tui/catalog-sources/screens.js +17 -10
  12. package/dist/src/tui/catalog-sources/screens.js.map +1 -1
  13. package/dist/src/tui/console-app-v4.js +1296 -437
  14. package/dist/src/tui/console-app-v4.js.map +1 -1
  15. package/dist/src/tui/controller-runtime.d.ts +3 -3
  16. package/dist/src/tui/controller-runtime.js +85 -27
  17. package/dist/src/tui/controller-runtime.js.map +1 -1
  18. package/dist/src/tui/controller.d.ts +17 -7
  19. package/dist/src/tui/controller.js +265 -24
  20. package/dist/src/tui/controller.js.map +1 -1
  21. package/dist/src/tui/dependency-graph.d.ts +3 -0
  22. package/dist/src/tui/dependency-graph.js +37 -1
  23. package/dist/src/tui/dependency-graph.js.map +1 -1
  24. package/dist/src/tui/diagnostic-projection.d.ts +1 -0
  25. package/dist/src/tui/diagnostic-projection.js +51 -4
  26. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  27. package/dist/src/tui/model.d.ts +22 -12
  28. package/dist/src/tui/model.js +276 -89
  29. package/dist/src/tui/model.js.map +1 -1
  30. package/dist/src/tui/operation-history.d.ts +1 -0
  31. package/dist/src/tui/operation-history.js +5 -0
  32. package/dist/src/tui/operation-history.js.map +1 -1
  33. package/dist/src/tui/operation-review-planner.js +67 -2
  34. package/dist/src/tui/operation-review-planner.js.map +1 -1
  35. package/dist/src/tui/plugin-write-task.d.ts +2 -0
  36. package/dist/src/tui/plugin-write-task.js.map +1 -1
  37. package/dist/src/tui/router.d.ts +6 -0
  38. package/dist/src/tui/router.js +56 -12
  39. package/dist/src/tui/router.js.map +1 -1
  40. package/dist/src/tui/screen-registry.d.ts +7 -0
  41. package/dist/src/utils/codex-manager.d.ts +5 -7
  42. package/dist/src/utils/codex-manager.js +24 -97
  43. package/dist/src/utils/codex-manager.js.map +1 -1
  44. package/dist/src/utils/plugin-config-write.js +3 -3
  45. package/dist/src/utils/plugin-config-write.js.map +1 -1
  46. package/dist/src/utils/target-eligibility.d.ts +2 -2
  47. package/dist/src/utils/target-eligibility.js.map +1 -1
  48. package/package.json +3 -3
@@ -23,7 +23,26 @@ import { readPluginIntentState, } from '../utils/plugin-intent-state.js';
23
23
  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
- import { inspectSystemCodexFilesystem, } from '../utils/codex-manager.js';
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,20 +597,51 @@ 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'}`);
563
604
  }
564
605
  for (const target of payload.targets) {
565
606
  const reasons = target.reasons.length > 0
566
- ? ` [${target.reasons.map((reason) => `${reason.code}: ${reason.message}`).join('; ')}]`
607
+ ? ` [${target.reasons.map((reason) => reason.message.startsWith(`${reason.code}:`)
608
+ ? reason.message : `${reason.code}: ${reason.message}`).join('; ')}]`
567
609
  : '';
568
- lines.push(`${target.pluginId} ${target.platform}: ${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
+ }
569
633
  if (verbose) {
570
634
  for (const component of target.components) {
571
635
  lines.push(` component ${component.kind}/${component.key}: ${component.state}`);
572
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
+ }
573
645
  for (const action of target.recoveryActions) {
574
646
  lines.push(` recovery ${action.command}: ${action.description}`);
575
647
  }
@@ -600,7 +672,7 @@ function collisionRootIssueId(options) {
600
672
  options.collision.artifactKey,
601
673
  ].join(':');
602
674
  }
603
- async function statusCommandV7InSession(options) {
675
+ async function statusCommandV8InSession(options) {
604
676
  const env = options.env ?? process.env;
605
677
  const dependencyPluginCache = new Map();
606
678
  const dependencyInspectionCache = new Map();
@@ -637,13 +709,13 @@ async function statusCommandV7InSession(options) {
637
709
  return undefined;
638
710
  }
639
711
  };
712
+ const codexScopePathsAlias = codexProjectConfigAliasesUserHome(options.cwd, env);
640
713
  const [userCodexConfig, projectCodexConfig] = await Promise.all([
641
714
  readActivationConfig('user'),
642
- readActivationConfig('project'),
715
+ codexScopePathsAlias ? undefined : readActivationConfig('project'),
643
716
  ]);
644
717
  const currentProjectKey = await resolveCanonicalRepoProjectKey(options.cwd);
645
718
  const currentProjectLocationKey = createProjectLocationKey(currentProjectKey);
646
- const codexScopePathsAlias = codexProjectConfigAliasesUserHome(options.cwd, env);
647
719
  const inventoryErrors = [];
648
720
  const readInventory = options.inventoryCatalogSourceReferences
649
721
  ?? inventoryCatalogSourceReferences;
@@ -667,7 +739,8 @@ async function statusCommandV7InSession(options) {
667
739
  const inspectCodexSystemConsumer = (scope) => {
668
740
  let inspection = codexSystemConsumerInspections.get(scope);
669
741
  if (!inspection) {
670
- inspection = inspectSystemCodexFilesystem({
742
+ inspection = inspectSystemCodex({
743
+ runner: nativeCliRunner,
671
744
  cwd: options.cwd,
672
745
  env,
673
746
  operationRequirement: getCodexOperationRequirement(scope),
@@ -730,9 +803,9 @@ async function statusCommandV7InSession(options) {
730
803
  if (!userCodexConfig || (probe.scope === 'project' && !projectCodexConfig)) {
731
804
  return 'partial';
732
805
  }
733
- const configEnabled = userCodexConfig.snapshot.plugins[`${probe.pluginName}@${probe.marketplaceName}`]?.enabled === true
734
- || (probe.scope === 'project'
735
- && 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;
736
809
  return configEnabled
737
810
  ? 'present'
738
811
  : 'partial';
@@ -863,6 +936,35 @@ async function statusCommandV7InSession(options) {
863
936
  };
864
937
  }
865
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);
866
968
  const placementEntries = item.resolution.manifest.entries.map((entry) => {
867
969
  const supportedPlatforms = resolveCatalogEntrySupportedPlatforms(entry.policy);
868
970
  return {
@@ -1123,8 +1225,238 @@ async function statusCommandV7InSession(options) {
1123
1225
  const dependencyErrors = [];
1124
1226
  const dependencyReadiness = [];
1125
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
+ }
1126
1454
  if (directDependencies.length > 0
1127
- && (liveState === 'present' || liveState === 'partial')) {
1455
+ && (desiredInScope
1456
+ || actualCodexActivation
1457
+ || hasScopedExtensionResidue
1458
+ || (platform !== 'codex'
1459
+ && (liveState === 'present' || liveState === 'partial')))) {
1128
1460
  for (const dependencyName of directDependencies) {
1129
1461
  const dependencyId = formatPluginId({
1130
1462
  sourceId: item.resolution.sourceId,
@@ -1178,120 +1510,12 @@ async function statusCommandV7InSession(options) {
1178
1510
  }, 'partial'));
1179
1511
  continue;
1180
1512
  }
1181
- const pluginCacheKey = [item.resolution.sourceIdentity, dependencyId].join('\0');
1182
- let dependencyPluginPromise = dependencyPluginCache.get(pluginCacheKey);
1183
- if (!dependencyPluginPromise) {
1184
- dependencyPluginPromise = readPluginManifest(path.join(item.resolution.rootDir, 'plugins', dependencyName), { mode: 'bundle' }).then((dependencyPlugin) => {
1185
- dependencyPlugin.sourceId = item.resolution.sourceId;
1186
- dependencyPlugin.sourceIdentity = item.resolution.sourceIdentity;
1187
- dependencyPlugin.catalogLogicalIdentity = item.resolution.logicalIdentity;
1188
- dependencyPlugin.pluginId = dependencyId;
1189
- return dependencyPlugin;
1190
- });
1191
- dependencyPluginCache.set(pluginCacheKey, dependencyPluginPromise);
1192
- }
1193
- const dependencyOverride = (scope === 'project'
1194
- ? projectIntent
1195
- : userIntent).dependencyScopeOverrides.find((override) => override.pluginId === dependencyId
1196
- && (override.platform === undefined || override.platform === platform));
1197
- const placements = dependencyOverride
1198
- ? [dependencyOverride.scope]
1199
- : scope === 'project' ? ['project', 'user'] : ['user'];
1513
+ const placements = dependencyPlacementsFor(dependencyId, scope);
1200
1514
  let dependencyResult = {
1201
1515
  state: 'absent',
1202
1516
  };
1203
1517
  for (const dependencyScope of placements) {
1204
- const inspectionCacheKey = [
1205
- item.resolution.sourceIdentity,
1206
- dependencyId,
1207
- platform,
1208
- dependencyScope,
1209
- dependencyScope === 'project' ? currentProjectKey : '',
1210
- ].join('\0');
1211
- let dependencyInspection = dependencyInspectionCache.get(inspectionCacheKey);
1212
- if (!dependencyInspection) {
1213
- dependencyInspection = dependencyPluginPromise.then(async (dependencyPlugin) => {
1214
- dependencyInspectionHookForTests?.();
1215
- const dependencyMarketplaceName = marketplaceNameFor(dependencyName, platform, dependencyScope);
1216
- const dependencyInspected = await inspectRebuildTarget({
1217
- sourceId: item.resolution.sourceId,
1218
- sourceIdentity: item.resolution.sourceIdentity,
1219
- pluginId: dependencyId,
1220
- marketplaceName: dependencyMarketplaceName,
1221
- marketplaceRoot,
1222
- plugin: dependencyPlugin,
1223
- platform,
1224
- scope: dependencyScope,
1225
- cwd: options.cwd,
1226
- catalog: { rootDir: item.resolution.rootDir, source: 'local' },
1227
- context: {
1228
- env,
1229
- nativeCliRunner,
1230
- reporter: createNoopReporter(),
1231
- debugLogger: createNoopDebugLogger(),
1232
- codexObservationSession: options.codexObservationSession,
1233
- },
1234
- userActivationDesired: codexDesiredUserActivationIds,
1235
- nativeProbe: readOnlyNativeProbe({
1236
- pluginName: dependencyName,
1237
- sourceId: item.resolution.sourceId,
1238
- sourceIdentity: item.resolution.sourceIdentity,
1239
- platform,
1240
- scope: dependencyScope,
1241
- marketplaceName: dependencyMarketplaceName,
1242
- marketplaceRoot,
1243
- desired: sourceDesired.get(desiredTargetKey(dependencyId, platform))
1244
- ?.scopes.includes(dependencyScope) === true,
1245
- }),
1246
- shallow: true,
1247
- });
1248
- const dependencyLiveState = projectedLiveState({
1249
- inspection: dependencyInspected.inspection,
1250
- required: dependencyInspected.target.desiredOwnedComponents
1251
- ?? dependencyInspected.target.ownedComponents,
1252
- optional: dependencyInspected.target.optionalOwnedComponents ?? [],
1253
- });
1254
- const collision = dependencyInspected.inspection.components.find((component) => component.collision)?.collision;
1255
- if (collision) {
1256
- return {
1257
- state: 'blockedByCollision',
1258
- dependencyScope,
1259
- rootIssueId: collisionRootIssueId({
1260
- pluginId: dependencyId,
1261
- platform,
1262
- scope: dependencyScope,
1263
- collision,
1264
- }),
1265
- rootIssueCode: 'artifactCollision',
1266
- };
1267
- }
1268
- return {
1269
- state: dependencyLiveState === 'present' ? 'ready'
1270
- : dependencyLiveState === 'absent' ? 'absent'
1271
- : dependencyLiveState === 'partial' ? 'incomplete'
1272
- : 'error',
1273
- dependencyScope,
1274
- ...(dependencyLiveState === 'partial'
1275
- ? {
1276
- rootIssueId: `incompleteInstallation:${dependencyId}:${platform}:${dependencyScope}`,
1277
- rootIssueCode: 'incompleteInstallation',
1278
- }
1279
- : dependencyLiveState === 'error'
1280
- ? {
1281
- rootIssueId: `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1282
- rootIssueCode: 'diagnostic',
1283
- }
1284
- : {}),
1285
- };
1286
- }).catch(() => ({
1287
- state: 'error',
1288
- dependencyScope,
1289
- rootIssueId: `diagnostic:${dependencyId}:${platform}:${dependencyScope}`,
1290
- rootIssueCode: 'diagnostic',
1291
- }));
1292
- dependencyInspectionCache.set(inspectionCacheKey, dependencyInspection);
1293
- }
1294
- const candidate = await dependencyInspection;
1518
+ const candidate = await inspectDependencyAtScope(dependencyName, dependencyScope);
1295
1519
  if (candidate.state === 'ready') {
1296
1520
  dependencyResult = candidate;
1297
1521
  break;
@@ -1307,7 +1531,7 @@ async function statusCommandV7InSession(options) {
1307
1531
  dependencyResult = candidate;
1308
1532
  }
1309
1533
  }
1310
- if (dependencyResult.state === 'absent') {
1534
+ if (dependencyResult.state === 'absent' && !dependencyResult.rootIssueId) {
1311
1535
  const dependencyScope = dependencyResult.dependencyScope ?? placements[0];
1312
1536
  dependencyResult = {
1313
1537
  state: 'absent',
@@ -1322,28 +1546,46 @@ async function statusCommandV7InSession(options) {
1322
1546
  dependencyPlatform: platform,
1323
1547
  dependencyScope,
1324
1548
  dependencyProject: dependencyScope === 'project' ? 'current' : null,
1325
- ...dependencyResult,
1549
+ state: dependencyResult.state,
1550
+ ...(dependencyResult.rootIssueId
1551
+ ? { rootIssueId: dependencyResult.rootIssueId }
1552
+ : {}),
1553
+ ...(dependencyResult.rootIssueCode
1554
+ ? { rootIssueCode: dependencyResult.rootIssueCode }
1555
+ : {}),
1326
1556
  });
1557
+ const issuePluginId = dependencyResult.issuePluginId ?? dependencyId;
1558
+ const issueScope = dependencyResult.issueScope ?? dependencyScope;
1327
1559
  if (dependencyResult.state === 'absent') {
1328
1560
  dependencyErrors.push(finalizeTargetError({
1329
1561
  code: 'dependencyMissing',
1330
- message: `dependencyMissing: ${dependencyId} is absent from its required live placement.`,
1331
- dependencyPluginId: dependencyId,
1562
+ message: `dependencyMissing: ${issuePluginId} is absent from its required live placement.`,
1563
+ dependencyPluginId: issuePluginId,
1332
1564
  dependencyPlatform: platform,
1333
- dependencyScope,
1334
- dependencyProject: dependencyScope === 'project' ? 'current' : null,
1565
+ dependencyScope: issueScope,
1566
+ dependencyProject: issueScope === 'project' ? 'current' : null,
1335
1567
  rootIssueId: dependencyResult.rootIssueId,
1336
1568
  rootIssueCode: dependencyResult.rootIssueCode,
1337
1569
  }, 'partial'));
1338
1570
  }
1339
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.`;
1340
1582
  dependencyErrors.push(finalizeTargetError({
1341
- code: 'dependencyNotReady',
1342
- message: `dependencyNotReady: ${dependencyId} is not ready at its required live placement.`,
1343
- dependencyPluginId: dependencyId,
1583
+ code,
1584
+ message,
1585
+ dependencyPluginId: issuePluginId,
1344
1586
  dependencyPlatform: platform,
1345
- dependencyScope,
1346
- dependencyProject: dependencyScope === 'project' ? 'current' : null,
1587
+ dependencyScope: issueScope,
1588
+ dependencyProject: issueScope === 'project' ? 'current' : null,
1347
1589
  ...(dependencyResult.rootIssueId
1348
1590
  ? { rootIssueId: dependencyResult.rootIssueId }
1349
1591
  : {}),
@@ -1601,7 +1843,21 @@ async function statusCommandV7InSession(options) {
1601
1843
  scopes,
1602
1844
  currentProjectLocationKey,
1603
1845
  }));
1604
- if (!selectorHasInventoryError) {
1846
+ const selectorHasLiveOrIntent = ownerInventory.references.some((reference) => (reference.kind === 'namespacedOwner' || reference.kind === 'namespacedMarketplace')
1847
+ && reference.pluginName === selector.parsed.pluginName
1848
+ && reference.sourceId !== undefined
1849
+ && (selector.parsed.sourceId === undefined || reference.sourceId === selector.parsed.sourceId)
1850
+ && platforms.some((platform) => platform === reference.platform)
1851
+ && scopes.some((scope) => scope === reference.scope)
1852
+ && (reference.scope !== 'project' || reference.projectKey === currentProjectKey))
1853
+ || [userIntent, projectIntent].some((intent, index) => scopes.includes(index === 0 ? 'user' : 'project')
1854
+ && intent.roots.some((root) => {
1855
+ const parsed = parsePluginId(root.pluginId);
1856
+ return parsed.pluginName === selector.parsed.pluginName
1857
+ && (selector.parsed.sourceId === undefined || parsed.sourceId === selector.parsed.sourceId)
1858
+ && root.platforms.some((platform) => platforms.includes(platform));
1859
+ }));
1860
+ if (!selectorHasInventoryError && !selectorHasLiveOrIntent) {
1605
1861
  selectorErrors.push({
1606
1862
  code: 'pluginNotFound',
1607
1863
  message: `Plugin "${selector.raw}" was not found in the selected catalog source set.`,
@@ -1795,6 +2051,14 @@ async function statusCommandV7InSession(options) {
1795
2051
  sourceId: reference.sourceId,
1796
2052
  pluginName: reference.pluginName,
1797
2053
  })));
2054
+ const isDisabledCodexReference = (reference) => {
2055
+ if (reference.kind !== 'namespacedMarketplace' || reference.platform !== 'codex'
2056
+ || !reference.pluginName || !reference.marketplaceName)
2057
+ return false;
2058
+ const config = reference.scope === 'user' ? userCodexConfig : projectCodexConfig;
2059
+ return config?.snapshot.plugins[`${reference.pluginName}@${reference.marketplaceName}`]?.enabled === false;
2060
+ };
2061
+ unavailableBundleReferences.sort((left, right) => Number(isDisabledCodexReference(right)) - Number(isDisabledCodexReference(left)));
1798
2062
  const unavailableBundleTargets = [...new Map(unavailableBundleReferences.flatMap((reference) => {
1799
2063
  if (!reference.sourceId || !reference.pluginName
1800
2064
  || (reference.platform !== 'codex' && reference.platform !== 'claude')
@@ -1804,6 +2068,9 @@ async function statusCommandV7InSession(options) {
1804
2068
  if (requested.length > 0 && !plannedSelectors.some((selector) => selector.parsed.pluginName === reference.pluginName
1805
2069
  && (selector.parsed.sourceId === undefined || selector.parsed.sourceId === reference.sourceId)))
1806
2070
  return [];
2071
+ const disabled = isDisabledCodexReference(reference)
2072
+ && ![userIntent, projectIntent].some((intent) => intent.roots.some((root) => root.pluginId === pluginId && root.platforms.includes('codex')))
2073
+ && (desiredTargetStates.get(desiredTargetKey(pluginId, 'codex'))?.scopes.length ?? 0) === 0;
1807
2074
  const target = {
1808
2075
  pluginName: reference.pluginName,
1809
2076
  sourceId: reference.sourceId,
@@ -1812,9 +2079,9 @@ async function statusCommandV7InSession(options) {
1812
2079
  platform: reference.platform,
1813
2080
  scope: reference.scope,
1814
2081
  cwd: path.resolve(options.cwd),
1815
- state: 'error',
2082
+ state: disabled ? 'absent' : 'error',
1816
2083
  components: [],
1817
- errors: [finalizeTargetError({
2084
+ errors: disabled ? [] : [finalizeTargetError({
1818
2085
  code: 'bundleUnavailable',
1819
2086
  message: `bundleUnavailable: ${pluginId} has live state but is not present in the current source snapshot.`,
1820
2087
  }, 'error')],
@@ -1894,15 +2161,13 @@ async function statusCommandV7InSession(options) {
1894
2161
  });
1895
2162
  const decorateTarget = async (target, groupedScopes, groupedTargets) => {
1896
2163
  const entry = selectedByPluginId.get(target.pluginId);
1897
- const codex = entry?.codex;
2164
+ const codex = target.platform === 'codex' ? entry?.codex : undefined;
1898
2165
  const desiredAll = desiredTargetStates.get(desiredTargetKey(target.pluginId, target.platform))
1899
2166
  ?? { scopes: [], relation: 'none' };
1900
- const desired = target.platform === 'codex'
1901
- ? desiredAll
1902
- : {
1903
- scopes: desiredAll.scopes.includes(target.scope) ? [target.scope] : [],
1904
- relation: desiredAll.relation,
1905
- };
2167
+ const desired = {
2168
+ scopes: desiredAll.scopes.includes(target.scope) ? [target.scope] : [],
2169
+ relation: desiredAll.relation,
2170
+ };
1906
2171
  const sharedNative = codex?.sharedNative ?? target.sharedNative;
1907
2172
  const baseActivation = codex?.activation ?? target.activation;
1908
2173
  const consumerScope = desired.scopes.includes('project')
@@ -1925,9 +2190,7 @@ async function statusCommandV7InSession(options) {
1925
2190
  systemConsumer,
1926
2191
  }
1927
2192
  : baseActivation;
1928
- const observationScopes = explicitScope
1929
- ? [explicitScope]
1930
- : desired.scopes.length > 0 ? desired.scopes : groupedScopes;
2193
+ const observationScopes = [target.scope];
1931
2194
  const scopedTargets = groupedTargets.filter((candidate) => observationScopes.includes(candidate.scope));
1932
2195
  const observedTargets = scopedTargets.length > 0 ? scopedTargets : groupedTargets;
1933
2196
  const ownershipState = observedTargets.some((candidate) => candidate.state === 'error')
@@ -1939,6 +2202,8 @@ async function statusCommandV7InSession(options) {
1939
2202
  : sharedNative?.staleCacheObserved
1940
2203
  ? 'partial'
1941
2204
  : 'absent';
2205
+ const actualScopeEnabled = target.platform === 'codex'
2206
+ && baseActivation?.[target.scope] === true;
1942
2207
  const hasSourceError = observedTargets.some((candidate) => candidate.errors.some((error) => error.code === 'sourceMissing'
1943
2208
  || error.code === 'catalogSourceUnavailable'
1944
2209
  || error.code === 'bundleUnavailable'
@@ -1947,18 +2212,16 @@ async function statusCommandV7InSession(options) {
1947
2212
  && (codex?.extensionKinds.length ?? 0) > 0
1948
2213
  && desired.scopes.includes('user')
1949
2214
  && desired.scopes.includes('project');
1950
- const expectedUser = desired.scopes.includes('user');
1951
- const expectedProject = desired.scopes.includes('project');
2215
+ const expectedEnabled = desired.scopes.includes(target.scope);
1952
2216
  const activationDrift = target.platform === 'codex'
1953
2217
  && (codexConfigErrors.some((error) => error.scope === 'user' || observationScopes.includes(error.scope))
1954
2218
  || ((codex?.nativeKinds.length ?? 0) > 0
1955
- && ((expectedUser && activation?.user !== true)
1956
- || (!expectedUser && activation?.user === true)
1957
- || (expectedProject && activation?.project !== true)
1958
- || (!expectedProject && activation?.project === true))));
2219
+ && (expectedEnabled && activation?.[target.scope] !== true
2220
+ || !expectedEnabled && activation?.[target.scope] === true)));
1959
2221
  const extensionComponents = observedTargets.flatMap((candidate) => candidate.components).filter((component) => component.key !== 'native'
1960
2222
  && !component.key.startsWith('activation-')
1961
2223
  && !component.key.startsWith('marketplace-'));
2224
+ const hasScopedExtensionResidue = extensionComponents.some((component) => component.state !== 'absent' || component.error !== undefined);
1962
2225
  const extensions = target.platform !== 'codex'
1963
2226
  || (codex?.extensionKinds.length ?? 0) === 0
1964
2227
  ? undefined
@@ -2014,6 +2277,7 @@ async function statusCommandV7InSession(options) {
2014
2277
  const baseTargetErrors = observedTargets.flatMap((candidate) => candidate.errors);
2015
2278
  const hasBlockingBaseReason = baseTargetErrors.some((reason) => reason.severity === 'blocking');
2016
2279
  const capabilityUnsupported = baseTargetErrors.some((reason) => reason.code === 'awCapabilityUnsupported');
2280
+ const dependencyNotReady = (target.dependencyReadiness ?? []).some((dependency) => dependency.state !== 'ready');
2017
2281
  const state = hasSourceError && desiredEnabled
2018
2282
  ? 'blocked'
2019
2283
  : consumerBlocking && desiredEnabled
@@ -2022,30 +2286,40 @@ async function statusCommandV7InSession(options) {
2022
2286
  ? 'blocked'
2023
2287
  : sharedNative?.state === 'blocked' || hasBlockingBaseReason || capabilityUnsupported
2024
2288
  ? 'blocked'
2025
- : !desiredEnabled
2026
- ? ownershipState !== 'absent' || desired.relation === 'managed'
2027
- ? 'orphaned'
2028
- : 'absent'
2029
- : dependencyMoves.length > 0
2030
- ? 'needsSync'
2031
- : sharedNative?.state === 'missing'
2032
- || sharedNative?.state === 'replaceable'
2033
- || 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
2034
2299
  ? 'needsSync'
2035
- : activationDrift || extensions?.state === 'drifted'
2300
+ : dependencyNotReady
2036
2301
  ? 'needsSync'
2037
- : extensions?.state === 'missing' || ownershipState === 'absent'
2302
+ : sharedNative?.state === 'missing'
2303
+ || sharedNative?.state === 'replaceable'
2304
+ || sharedNative?.state === 'unobserved'
2038
2305
  ? 'needsSync'
2039
- : ownershipState === 'partial' || ownershipState === 'error'
2306
+ : activationDrift || extensions?.state === 'drifted'
2040
2307
  ? 'needsSync'
2041
- : 'ready';
2308
+ : extensions?.state === 'missing'
2309
+ || (target.platform !== 'codex' && ownershipState === 'absent')
2310
+ ? 'needsSync'
2311
+ : ownershipState === 'partial' || ownershipState === 'error'
2312
+ ? 'needsSync'
2313
+ : 'ready';
2042
2314
  const stableErrors = [
2043
2315
  ...projectStatusSharedNativeReasons(sharedNative, baseTargetErrors.map((error) => error.code), desiredEnabled),
2044
2316
  ...(desiredOverlapConflict
2045
2317
  ? [blocking('codexExtensionScopeConflict', `${target.pluginId} has AW extensions desired at user and project scope.`)]
2046
2318
  : []),
2047
2319
  ...(activationDrift && sharedNative?.state !== 'missing' && sharedNative?.state !== 'unobserved'
2048
- ? [warning('codexActivationDrift', `${target.pluginId} activation differs from desired intent.`)]
2320
+ ? [warning('codexActivationDrift', `${target.pluginId} activation differs from desired intent: `
2321
+ + `${target.scope} expected=${expectedEnabled}, `
2322
+ + `actual=${activation?.[target.scope] ?? 'unset'}.`)]
2049
2323
  : []),
2050
2324
  ...(extensions?.state === 'drifted'
2051
2325
  ? [warning('codexExtensionDrift', `${target.pluginId} extension projection has drifted.`)]
@@ -2071,11 +2345,7 @@ async function statusCommandV7InSession(options) {
2071
2345
  ])).values()];
2072
2346
  const ownershipComponents = [...new Map(observedTargets.flatMap((candidate) => candidate.components)
2073
2347
  .map((component) => [component.key, component])).values()];
2074
- const representativeScope = explicitScope
2075
- ?? (desired.scopes.length === 1
2076
- ? desired.scopes[0]
2077
- : observedTargets.find((candidate) => candidate.state !== 'absent')?.scope
2078
- ?? (groupedScopes.includes('user') ? 'user' : groupedScopes[0] ?? target.scope));
2348
+ const representativeScope = target.scope;
2079
2349
  const representativeTarget = observedTargets.find((candidate) => candidate.scope === representativeScope) ?? target;
2080
2350
  const components = [
2081
2351
  {
@@ -2122,7 +2392,7 @@ async function statusCommandV7InSession(options) {
2122
2392
  }]),
2123
2393
  ...(activation?.systemConsumer ? [{
2124
2394
  kind: 'systemConsumer',
2125
- key: 'codex',
2395
+ key: `codex:${representativeScope}`,
2126
2396
  state: activation.systemConsumer.state,
2127
2397
  metadata: {
2128
2398
  evidence: activation.evidence,
@@ -2169,6 +2439,8 @@ async function statusCommandV7InSession(options) {
2169
2439
  pluginId: target.pluginId,
2170
2440
  ...(dependencyMoves.length > 0 ? { dependencyMoves } : {}),
2171
2441
  ...(sharedNative ? { sharedNative } : {}),
2442
+ scope: target.scope,
2443
+ actualEnabled: actualScopeEnabled,
2172
2444
  conflictRoots: rootIntentsForTarget({
2173
2445
  pluginId: target.pluginId,
2174
2446
  platform: target.platform,
@@ -2177,7 +2449,9 @@ async function statusCommandV7InSession(options) {
2177
2449
  available,
2178
2450
  }),
2179
2451
  errorCodes: dedupedErrors.map((error) => error.code),
2180
- }),
2452
+ }).map((action) => action.command === 'manual' || action.scope
2453
+ ? action
2454
+ : { ...action, scope: representativeScope }),
2181
2455
  };
2182
2456
  };
2183
2457
  const codexGroups = new Map();
@@ -2192,18 +2466,84 @@ async function statusCommandV7InSession(options) {
2192
2466
  codexGroups.set(target.pluginId, group);
2193
2467
  }
2194
2468
  for (const group of codexGroups.values()) {
2195
- const scopes = [...new Set(group.map((target) => target.scope))].sort();
2196
- 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
+ });
2197
2536
  }
2198
- projectedTargets.sort((left, right) => `${left.pluginId}\0${left.platform}\0${left.scope}`.localeCompare(`${right.pluginId}\0${right.platform}\0${right.scope}`, 'en'));
2199
- 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) => {
2200
2539
  const entry = selectedByPluginId.get(target.pluginId);
2201
- return {
2540
+ const placements = target.platform === 'codex' ? target.placements : [target];
2541
+ return placements.map((placement) => ({
2202
2542
  pluginId: target.pluginId,
2203
2543
  platform: target.platform,
2204
- scope: target.scope,
2205
- revision: projectTargetRevision(entry?.version, entry?.states[target.platform]?.[target.scope]?.installedVersion),
2206
- };
2544
+ scope: placement.scope,
2545
+ revision: projectTargetRevision(entry?.version, entry?.states[target.platform]?.[placement.scope]?.installedVersion),
2546
+ }));
2207
2547
  });
2208
2548
  const publicConfigIssues = available.flatMap((entry) => (entry.configIssues ?? [])
2209
2549
  .filter((issue) => issue.type === 'configLayerUnsupported')
@@ -2213,7 +2553,7 @@ async function statusCommandV7InSession(options) {
2213
2553
  ...issue,
2214
2554
  })));
2215
2555
  const payload = {
2216
- schemaVersion: 7,
2556
+ schemaVersion: 8,
2217
2557
  catalog: {
2218
2558
  primarySourceId: catalog.primarySourceId,
2219
2559
  configDigest: catalog.configDigest,
@@ -2243,7 +2583,7 @@ async function statusCommandV7InSession(options) {
2243
2583
  || payload.targets.some((target) => target.reasons.some((reason) => reason.severity === 'blocking'));
2244
2584
  return {
2245
2585
  payload,
2246
- output: options.json ? JSON.stringify(payload) : renderStatusV7(payload, options.verbose === true),
2586
+ output: options.json ? JSON.stringify(payload) : renderStatusV8(payload, options.verbose === true),
2247
2587
  hasErrors,
2248
2588
  targetRevisions,
2249
2589
  pluginSummaries: available.map((entry) => {
@@ -2290,9 +2630,9 @@ async function statusCommandV7InSession(options) {
2290
2630
  }),
2291
2631
  };
2292
2632
  }
2293
- export async function statusCommandV7(options) {
2633
+ export async function statusCommandV8(options) {
2294
2634
  if (options.codexObservationSession) {
2295
- return statusCommandV7InSession({
2635
+ return statusCommandV8InSession({
2296
2636
  ...options,
2297
2637
  codexObservationSession: options.codexObservationSession,
2298
2638
  });
@@ -2304,7 +2644,7 @@ export async function statusCommandV7(options) {
2304
2644
  runner: options.nativeCliRunner ?? getDefaultNativeCliRunner(),
2305
2645
  });
2306
2646
  try {
2307
- return await statusCommandV7InSession({ ...options, codexObservationSession: session });
2647
+ return await statusCommandV8InSession({ ...options, codexObservationSession: session });
2308
2648
  }
2309
2649
  finally {
2310
2650
  await session.dispose();