agentwheel 0.18.1 → 0.18.2

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.
package/dist/index.js CHANGED
@@ -10483,7 +10483,7 @@ async function createGraphSourcePlan(options) {
10483
10483
  replaceConflict: options.replaceConflict,
10484
10484
  warn
10485
10485
  });
10486
- if (options.forceForeignState !== true || options.fleetId) {
10486
+ if ((options.forceForeignState !== true || options.fleetId) && options.deferForeignStateCheck !== true) {
10487
10487
  await assertNoForeignWorkspaceState({
10488
10488
  installRoot: resolvedInstallRoot,
10489
10489
  adapter: options.adapter.name,
@@ -10509,6 +10509,19 @@ async function createGraphSourcePlan(options) {
10509
10509
  recoveredPendingApply
10510
10510
  };
10511
10511
  }
10512
+ async function assertNoForeignWorkspaceStateForPlan(plan, options) {
10513
+ if (!plan.stateKey) throw new Error(`Foreign-state validation requires a state key for ${plan.adapter}.`);
10514
+ await assertNoForeignWorkspaceState({
10515
+ installRoot: plan.targetRoot,
10516
+ adapter: plan.adapter,
10517
+ transport: options.transport ?? localTransport,
10518
+ workspaceRoot: options.workspaceRoot,
10519
+ workspaceOwner: options.workspaceOwner,
10520
+ globalRoot: options.globalRoot,
10521
+ stateKey: plan.stateKey,
10522
+ plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath)
10523
+ });
10524
+ }
10512
10525
  function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2) {
10513
10526
  return pathForGraphLock(workspaceRoot, targetKey2, adapter, computeTargetFingerprint(targetFingerprintParts2));
10514
10527
  }
@@ -13280,7 +13293,10 @@ async function planFleetNormalization(request) {
13280
13293
  packages.push({ name, declarationDigest: sha2564(sourceDeclaration) });
13281
13294
  }
13282
13295
  if (!legacySelfNormalization) await assertSourceFleetPostcondition(normalizedRequest, sourceScope, candidates);
13283
- const installedState = legacySelfNormalization ? await inspectLegacySelfInstalledState(destinationScope, candidates) : await inspectInstalledState(sourceScope, destinationScope, candidates);
13296
+ if ((normalizedRequest.profile || normalizedRequest.artifacts) && !legacySelfNormalization) {
13297
+ throw new Error("--profile and --artifact are supported only for legacy same-fleet ownership normalization.");
13298
+ }
13299
+ const installedState = legacySelfNormalization ? await inspectLegacySelfInstalledState(destinationScope, candidates, normalizedRequest.profile, normalizedRequest.artifacts) : await inspectInstalledState(sourceScope, destinationScope, candidates);
13284
13300
  const planWithoutDigest = {
13285
13301
  version: 1,
13286
13302
  request: normalizedRequest,
@@ -13444,10 +13460,16 @@ async function assertSourceFleetPostcondition(request, source, selectedPackages)
13444
13460
  function normalizeRequest(request) {
13445
13461
  const packages = request.packages === void 0 ? void 0 : sortedUnique8(request.packages);
13446
13462
  if (request.packages !== void 0 && packages?.length === 0) throw new Error("--package requires at least one package name.");
13463
+ const artifacts = request.artifacts === void 0 ? void 0 : sortedUnique8(request.artifacts.map((artifact) => normalizeArtifactSelector(artifact)));
13464
+ if (request.artifacts !== void 0 && artifacts?.length === 0) throw new Error("--artifact requires at least one type/name selector.");
13465
+ const profile = request.profile?.trim();
13466
+ if (request.profile !== void 0 && !profile) throw new Error("--profile requires a profile name.");
13447
13467
  return {
13448
13468
  destinationFleet: request.destinationFleet.trim(),
13449
13469
  from: request.from,
13450
13470
  ...packages ? { packages } : {},
13471
+ ...artifacts ? { artifacts } : {},
13472
+ ...profile ? { profile } : {},
13451
13473
  ...request.globalRoot ? { globalRoot: resolve25(request.globalRoot) } : {}
13452
13474
  };
13453
13475
  }
@@ -13631,19 +13653,45 @@ async function inspectInstalledState(source, destination, packageNames) {
13631
13653
  transfers: [...transfers.values()].sort((a, b) => `${a.sourceManifestPath}:${a.destinationManifestPath}`.localeCompare(`${b.sourceManifestPath}:${b.destinationManifestPath}`))
13632
13654
  };
13633
13655
  }
13634
- async function inspectLegacySelfInstalledState(fleet, packageNames) {
13656
+ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName, artifactNames) {
13635
13657
  if (fleet.kind !== "fleet" || !fleet.fleetId) {
13636
13658
  throw new Error("Legacy self-normalization requires a registered named fleet destination.");
13637
13659
  }
13638
13660
  const selected = new Set(packageNames);
13639
- const [graphs, roots] = await Promise.all([
13640
- relevantGraphLocks(fleet.root, selected),
13641
- runtimeRoots(fleet)
13642
- ]);
13643
- const manifests = await collectManifests(roots);
13661
+ const selectedArtifacts = artifactNames ? new Set(artifactNames) : void 0;
13662
+ const targetKeys = profileName ? targetKeysForProfile(fleet, profileName) : void 0;
13663
+ const graphCandidates = await relevantGraphLocks(
13664
+ fleet.root,
13665
+ selected,
13666
+ targetKeys,
13667
+ async (graph) => hasRelevantLegacyManifestCandidate(fleet, graph, selected)
13668
+ );
13669
+ const graphs = [];
13644
13670
  const legacyOwner = workspaceOwnerForRoot(fleet.root);
13645
13671
  const fleetOwner = workspaceOwnerForRoot(fleet.root, fleet.fleetId);
13646
- const relevantEntries = manifests.flatMap((manifest) => manifest.manifest.entries.filter((entry) => entryMatchesPackages(entry, selected)).map((entry) => ({ manifest, entry, renderedPath: renderedEntryPath(manifest.manifest, entry) })));
13672
+ let normalizedGraphCount = 0;
13673
+ for (const graph of graphCandidates) {
13674
+ const legacyState = await legacyTargetStateForGraph(fleet, graph);
13675
+ const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
13676
+ const relevantManifestEntries2 = legacyManifest?.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected) && entryMatchesArtifacts(entry, selectedArtifacts)) ?? [];
13677
+ if (relevantManifestEntries2.length > 0) {
13678
+ graphs.push(graph);
13679
+ }
13680
+ const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
13681
+ if (destinationState.graphLockPath === graph.path && legacyManifest?.manifest.entries.some((entry) => entry.workspaceOwner === fleetOwner && entryMatchesPackages(entry, selected))) {
13682
+ normalizedGraphCount += 1;
13683
+ }
13684
+ }
13685
+ if (graphs.length === 0 && normalizedGraphCount > 0) {
13686
+ throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
13687
+ }
13688
+ const manifestPaths = /* @__PURE__ */ new Set();
13689
+ for (const graph of graphs) {
13690
+ manifestPaths.add((await legacyTargetStateForGraph(fleet, graph)).manifestPath);
13691
+ manifestPaths.add((await targetStateForGraph(fleet, graph, fleet.fleetId)).manifestPath);
13692
+ }
13693
+ const manifests = await collectManifestPaths([...manifestPaths]);
13694
+ const relevantEntries = manifests.flatMap((manifest) => manifest.manifest.entries.filter((entry) => graphs.some((graph) => entryMatchesGraphPackages(entry, graph.lock, selected)) && entryMatchesArtifacts(entry, selectedArtifacts)).map((entry) => ({ manifest, entry, renderedPath: renderedEntryPath(manifest.manifest, entry) })));
13647
13695
  const legacyEntries = relevantEntries.filter((entry) => entry.entry.workspaceOwner === legacyOwner);
13648
13696
  const qualifiedEntries = relevantEntries.filter((entry) => entry.entry.workspaceOwner === fleetOwner);
13649
13697
  const foreignEntries = relevantEntries.filter((entry) => entry.entry.workspaceOwner !== legacyOwner && entry.entry.workspaceOwner !== fleetOwner);
@@ -13652,17 +13700,17 @@ async function inspectLegacySelfInstalledState(fleet, packageNames) {
13652
13700
  `Legacy self-normalization owner mismatch at ${foreignEntries[0].renderedPath}: expected ${legacyOwner}, found foreign owner ${foreignEntries[0].entry.workspaceOwner}.`
13653
13701
  );
13654
13702
  }
13655
- if (qualifiedEntries.length > 0) {
13656
- throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
13657
- }
13658
13703
  if (legacyEntries.length === 0) {
13704
+ if (qualifiedEntries.length > 0) {
13705
+ throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
13706
+ }
13659
13707
  throw new Error(`Fleet '${fleet.fleetId}' has no legacy same-root manifest ownership to normalize.`);
13660
13708
  }
13661
13709
  if (graphs.length === 0) {
13662
13710
  throw new Error("Legacy same-root install manifests exist without matching graph lock evidence; self-normalization is blocked.");
13663
13711
  }
13664
13712
  for (const graph of graphs) {
13665
- if (!graph.allRootsSelected) {
13713
+ if (!selectedArtifacts && !graph.allRootsSelected) {
13666
13714
  throw new Error(`Legacy graph lock is only partially selected and cannot be moved safely: ${graph.path}`);
13667
13715
  }
13668
13716
  }
@@ -13671,14 +13719,11 @@ async function inspectLegacySelfInstalledState(fleet, packageNames) {
13671
13719
  const coveredEntries = /* @__PURE__ */ new Set();
13672
13720
  const renderedPaths = /* @__PURE__ */ new Set();
13673
13721
  for (const graph of graphs) {
13674
- const legacyState = await targetStateForGraph(fleet, graph, null);
13722
+ const legacyState = await legacyTargetStateForGraph(fleet, graph);
13675
13723
  const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
13676
13724
  if (graph.path === destinationState.graphLockPath) {
13677
13725
  throw new Error(`Fleet '${fleet.fleetId}' graph state is already normalized to fleet-qualified identity.`);
13678
13726
  }
13679
- if (legacyState.graphLockPath !== graph.path) {
13680
- throw new Error(`Legacy graph lock target identity is stale or noncanonical: ${graph.path}`);
13681
- }
13682
13727
  const expectedEntries = legacyEntries.filter((entry) => entry.manifest.path === legacyState.manifestPath);
13683
13728
  if (expectedEntries.length === 0) {
13684
13729
  throw new Error(`Legacy graph lock is not covered by its canonical same-root install manifest: ${legacyState.manifestPath}`);
@@ -13716,18 +13761,22 @@ async function inspectLegacySelfInstalledState(fleet, packageNames) {
13716
13761
  renderedPaths: sortedUnique8(expectedEntries.map((entry) => entry.renderedPath)),
13717
13762
  destinationRenderedPaths: sortedUnique8(expectedEntries.map((entry) => entry.renderedPath))
13718
13763
  });
13719
- const fullDigest = sha2564(canonicalJson2(graph.lock));
13720
- const destinationGraph = destinationGraphFromSource(graph.lock, destinationState.targetFingerprint);
13721
- graphTransfers.push({
13722
- sourceGraphLockPath: graph.path,
13723
- sourceGraphLockDigest: fullDigest,
13724
- destinationGraphLockPath: destinationState.graphLockPath,
13725
- destinationGraphLockDigest: null,
13726
- destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraph)),
13727
- targetKey: graph.targetKey,
13728
- adapter: graph.adapter,
13729
- targetFingerprint: destinationState.targetFingerprint
13730
- });
13764
+ if (!selectedArtifacts) {
13765
+ const fullDigest = sha2564(canonicalJson2(graph.lock));
13766
+ const existingDestinationGraph = graphCandidates.find((candidate) => candidate.path === destinationState.graphLockPath);
13767
+ if (existingDestinationGraph) assertGraphIsSubset(graph, existingDestinationGraph);
13768
+ const destinationGraph = destinationGraphFromSource(graph.lock, destinationState.targetFingerprint);
13769
+ graphTransfers.push({
13770
+ sourceGraphLockPath: graph.path,
13771
+ sourceGraphLockDigest: fullDigest,
13772
+ destinationGraphLockPath: destinationState.graphLockPath,
13773
+ destinationGraphLockDigest: existingDestinationGraph ? sha2564(canonicalJson2(existingDestinationGraph.lock)) : null,
13774
+ destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraph)),
13775
+ targetKey: graph.targetKey,
13776
+ adapter: graph.adapter,
13777
+ targetFingerprint: destinationState.targetFingerprint
13778
+ });
13779
+ }
13731
13780
  }
13732
13781
  if (coveredEntries.size !== legacyEntries.length) {
13733
13782
  throw new Error("Legacy same-root manifest ownership is only partially covered by canonical graph locks.");
@@ -13735,13 +13784,21 @@ async function inspectLegacySelfInstalledState(fleet, packageNames) {
13735
13784
  return {
13736
13785
  graphLockDigests: graphs.map((graph) => graph.digest).sort(),
13737
13786
  sourceManifestCount: new Set(legacyEntries.map((entry) => entry.manifest.path)).size,
13738
- destinationManifestCount: 0,
13787
+ destinationManifestCount: new Set(qualifiedEntries.map((entry) => entry.manifest.path)).size,
13739
13788
  renderedPathCount: renderedPaths.size,
13740
13789
  sourceGraphLockPaths: graphs.map((graph) => graph.path).sort((a, b) => a.localeCompare(b)),
13741
13790
  graphTransfers: graphTransfers.sort((a, b) => a.sourceGraphLockPath.localeCompare(b.sourceGraphLockPath)),
13742
13791
  transfers: [...transfers.values()].sort((a, b) => a.sourceManifestPath.localeCompare(b.sourceManifestPath))
13743
13792
  };
13744
13793
  }
13794
+ function targetKeysForProfile(scope, name) {
13795
+ const profile = scope.config.profiles[name];
13796
+ if (!profile) throw new Error(`Unknown fleet profile '${name}'.`);
13797
+ if (isCompositeWorkspaceProfile(profile)) {
13798
+ throw new Error(`Fleet normalization requires a concrete local profile; '${name}' is composite.`);
13799
+ }
13800
+ return new Set(profile.runtimes.map((runtime) => runtime.agent ?? runtime.adapter));
13801
+ }
13745
13802
  async function runtimeRoots(scope) {
13746
13803
  const roots = /* @__PURE__ */ new Set([scope.root]);
13747
13804
  if (scope.config.packages.some((pkg) => pkg.installationType === "user") || Object.values(scope.config.agents).some((agent) => agent.installationType === "user")) {
@@ -13751,10 +13808,8 @@ async function runtimeRoots(scope) {
13751
13808
  if (agent.transport === "ssh") throw new Error(`Installed-state normalization cannot inspect SSH agent '${name}' locally.`);
13752
13809
  roots.add(resolveConfigPath(agent.root, scope.root));
13753
13810
  }
13754
- for (const [name, profile] of Object.entries(scope.config.profiles)) {
13755
- if (isCompositeWorkspaceProfile(profile)) {
13756
- throw new Error(`Installed-state normalization cannot prove composite profile '${name}' locally.`);
13757
- }
13811
+ for (const profile of Object.values(scope.config.profiles)) {
13812
+ if (isCompositeWorkspaceProfile(profile)) continue;
13758
13813
  for (const runtime of profile.runtimes) {
13759
13814
  if (runtime.agent) continue;
13760
13815
  roots.add(runtime.targetRoot ? resolveConfigPath(runtime.targetRoot, scope.root) : scope.root);
@@ -13799,34 +13854,74 @@ async function collectManifests(roots) {
13799
13854
  }
13800
13855
  return [...found.values()].sort((a, b) => a.path.localeCompare(b.path));
13801
13856
  }
13802
- async function relevantGraphLocks(workspaceRoot, selected) {
13857
+ async function collectManifestPaths(paths) {
13858
+ const found = [];
13859
+ for (const path of [...new Set(paths)].sort((a, b) => a.localeCompare(b))) {
13860
+ if (!await pathExists(path)) continue;
13861
+ const raw = JSON.parse(await readFile37(path, "utf8"));
13862
+ const parsed = installManifestSchema.parse(raw);
13863
+ if (parsed.version !== 2) {
13864
+ throw new Error(`Legacy v1 install manifest cannot prove fleet ownership: ${path}`);
13865
+ }
13866
+ const expectedPath = installManifestPath(parsed.targetRoot, parsed.adapter, {
13867
+ installationType: parsed.installationType,
13868
+ stateKey: parsed.stateKey
13869
+ });
13870
+ if (resolve25(expectedPath) !== resolve25(path)) {
13871
+ throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
13872
+ }
13873
+ found.push({ path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
13874
+ }
13875
+ return found;
13876
+ }
13877
+ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include) {
13803
13878
  const root = join50(workspaceRoot, ".agentwheel", "locks");
13804
13879
  if (!await pathExists(root)) return [];
13805
13880
  const results = [];
13806
13881
  for (const path of await listFiles(root)) {
13807
13882
  if (!path.endsWith(".graph-lock.json")) continue;
13808
13883
  const lock = await readGraphLock(path);
13809
- if (!lock.canonical.roots.some((candidate) => selected.has(candidate.rootId))) continue;
13884
+ if (!lock.canonical.roots.some((candidate2) => selected.has(candidate2.rootId))) continue;
13810
13885
  const parts = relative8(root, path).split(/[\\/]/);
13811
13886
  if (parts.length !== 3) throw new Error(`Graph lock path is not canonical: ${path}`);
13812
13887
  const [targetKey2, adapter, fileName] = parts;
13888
+ if (targetKeys && !targetKeys.has(targetKey2)) continue;
13813
13889
  const pathFingerprint = basename22(fileName, ".graph-lock.json");
13814
13890
  if (!lock.canonical.targetFingerprint || lock.canonical.targetFingerprint !== pathFingerprint) {
13815
13891
  throw new Error(`Graph lock fingerprint does not match its canonical path: ${path}`);
13816
13892
  }
13817
- const projection = relevantGraphProjection(lock, selected);
13818
- results.push({
13893
+ const candidate = {
13819
13894
  path,
13820
13895
  lock,
13821
- digest: sha2564(canonicalJson2(projection)),
13896
+ digest: "",
13822
13897
  allRootsSelected: lock.canonical.roots.every((rootEntry) => selected.has(rootEntry.rootId)),
13823
- artifactIdentities: lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => selected.has(owner))).map(graphArtifactIdentity).sort(),
13898
+ artifactIdentities: [],
13824
13899
  targetKey: targetKey2,
13825
13900
  adapter
13901
+ };
13902
+ if (include && !await include(candidate)) continue;
13903
+ const projection = relevantGraphProjection(lock, selected);
13904
+ results.push({
13905
+ ...candidate,
13906
+ digest: sha2564(canonicalJson2(projection)),
13907
+ artifactIdentities: lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => ownerMatchesGraphPackage(lock, owner, selected))).map(graphArtifactIdentity).sort()
13826
13908
  });
13827
13909
  }
13828
13910
  return results.sort((a, b) => a.path.localeCompare(b.path));
13829
13911
  }
13912
+ function isCurrentLocalTarget(scope, graph) {
13913
+ const agent = scope.config.agents[graph.targetKey];
13914
+ if (agent?.transport === "ssh") {
13915
+ throw new Error(`Installed-state normalization cannot hand off SSH target '${graph.targetKey}' locally.`);
13916
+ }
13917
+ return Boolean(agent) || graph.targetKey === graph.adapter;
13918
+ }
13919
+ async function hasRelevantLegacyManifestCandidate(scope, graph, selected) {
13920
+ if (!isCurrentLocalTarget(scope, graph)) return false;
13921
+ const state = await legacyTargetStateForGraph(scope, graph);
13922
+ const manifest = (await collectManifestPaths([state.manifestPath]))[0];
13923
+ return manifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected)) ?? false;
13924
+ }
13830
13925
  async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId ?? null) {
13831
13926
  const agent = scope.config.agents[graph.targetKey];
13832
13927
  if (agent?.transport === "ssh") {
@@ -13906,6 +14001,40 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
13906
14001
  manifestPath: installManifestPath(installRoot, adapter.name, { installationType, stateKey })
13907
14002
  };
13908
14003
  }
14004
+ async function legacyTargetStateForGraph(scope, graph) {
14005
+ const current = await targetStateForGraph(scope, graph, null);
14006
+ const targetFingerprint = graph.lock.canonical.targetFingerprint;
14007
+ const stateKey = stateKeyFor(current.adapter, {
14008
+ installationType: current.installationType,
14009
+ targetFingerprint
14010
+ });
14011
+ return {
14012
+ ...current,
14013
+ stateKey,
14014
+ targetFingerprint,
14015
+ graphLockPath: graph.path,
14016
+ manifestPath: installManifestPath(current.installRoot, current.adapter, {
14017
+ installationType: current.installationType,
14018
+ stateKey
14019
+ })
14020
+ };
14021
+ }
14022
+ function assertGraphIsSubset(source, destination) {
14023
+ const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson2(root)]));
14024
+ for (const root of destination.lock.canonical.roots) {
14025
+ if (sourceRoots.get(root.rootId) !== canonicalJson2(root)) {
14026
+ throw new Error(
14027
+ `Partially normalized destination graph diverges at root '${root.rootId}': ${destination.path}`
14028
+ );
14029
+ }
14030
+ }
14031
+ const sourceArtifacts = new Set(source.lock.canonical.artifacts.map(graphArtifactIdentity));
14032
+ for (const artifact of destination.lock.canonical.artifacts) {
14033
+ if (!sourceArtifacts.has(graphArtifactIdentity(artifact))) {
14034
+ throw new Error(`Partially normalized destination graph contains divergent artifact state: ${destination.path}`);
14035
+ }
14036
+ }
14037
+ }
13909
14038
  function destinationGraphFromSource(source, targetFingerprint) {
13910
14039
  const sourceFingerprint = source.canonical.targetFingerprint;
13911
14040
  return {
@@ -14003,9 +14132,9 @@ function relevantGraphProjection(lock, selected) {
14003
14132
  }
14004
14133
  }
14005
14134
  }
14006
- const artifacts = lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => selected.has(owner)));
14135
+ const artifacts = lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => ownerMatchesGraphPackage(lock, owner, selected)));
14007
14136
  for (const artifact of artifacts) {
14008
- if (artifact.owners.some((owner) => !selected.has(owner))) {
14137
+ if (artifact.owners.some((owner) => !ownerMatchesGraphPackage(lock, owner, selected))) {
14009
14138
  throw new Error(`Graph artifact ${artifact.logicalSelector} has partial ownership outside the selected normalization packages.`);
14010
14139
  }
14011
14140
  nodeIds.add(artifact.graphNodeId);
@@ -14036,7 +14165,27 @@ function relevantManifestEntries(manifests, selected, owners) {
14036
14165
  return manifests.flatMap((manifest) => manifest.manifest.entries.filter((entry) => entryMatchesPackages(entry, selected) && owners.has(entry.workspaceOwner)).map((entry) => ({ manifest, entry, renderedPath: renderedEntryPath(manifest.manifest, entry) })));
14037
14166
  }
14038
14167
  function entryMatchesPackages(entry, selected) {
14039
- return entry.owners.some((owner) => selected.has(owner)) || (entry.packageName ? selected.has(entry.packageName) : false);
14168
+ return entry.owners.some((owner) => ownerMatchesPackage(owner, selected)) || (entry.packageName ? selected.has(entry.packageName) : false);
14169
+ }
14170
+ function entryMatchesGraphPackages(entry, lock, selected) {
14171
+ return entry.owners.some((owner) => ownerMatchesGraphPackage(lock, owner, selected)) || (entry.packageName ? selected.has(entry.packageName) : false);
14172
+ }
14173
+ function entryMatchesArtifacts(entry, selected) {
14174
+ if (!selected) return true;
14175
+ return selected.has(entry.logicalSelector ?? `${entry.artifactType}/${entry.artifactName}`) || selected.has(`${entry.artifactType}/${entry.artifactName}`);
14176
+ }
14177
+ function normalizeArtifactSelector(value) {
14178
+ const selector = value.trim();
14179
+ if (!/^[^/\s]+\/[^/\s]+$/.test(selector)) {
14180
+ throw new Error(`Invalid --artifact selector '${value}'; expected type/name.`);
14181
+ }
14182
+ return selector;
14183
+ }
14184
+ function ownerMatchesPackage(owner, selected) {
14185
+ return selected.has(owner) || owner.startsWith("workspace:") && selected.has(owner.slice("workspace:".length));
14186
+ }
14187
+ function ownerMatchesGraphPackage(lock, owner, selected) {
14188
+ return ownerMatchesPackage(owner, selected) || lock.canonical.roots.some((root) => selected.has(root.rootId) && root.graphNodeId === owner);
14040
14189
  }
14041
14190
  function renderedEntryPath(manifest, entry) {
14042
14191
  const root = resolve25(manifest.targetRoot);
@@ -14438,14 +14587,16 @@ fleetCommand.command("show").description("show one registered fleet").argument("
14438
14587
  Root: ${fleet.root}
14439
14588
  Required packages: ${fleet.requiredPackages.join(", ")}`);
14440
14589
  });
14441
- fleetCommand.command("normalize").description("plan or apply duplicate desired-state ownership normalization").argument("<destinationFleet>", "destination fleet id").requiredOption("--from <scope>", "user or fleet:<sourceFleet>").option("--package <name>", "limit to one duplicate package (repeatable)", collectValueOption, []).option("--apply", "apply a reviewed plan", false).option("--recover", "restore source state from a pending normalization journal", false).option("--plan-digest <sha256>", "exact digest from the reviewed dry-run").option("--json", "print the plan or result as JSON", false).action(async (destinationFleet, options) => {
14590
+ fleetCommand.command("normalize").description("plan or apply duplicate desired-state ownership normalization").argument("<destinationFleet>", "destination fleet id").requiredOption("--from <scope>", "user or fleet:<sourceFleet>").option("--package <name>", "limit to one duplicate package (repeatable)", collectValueOption, []).option("--artifact <type/name>", "limit same-fleet ownership normalization to one artifact (repeatable)", collectValueOption, []).option("--profile <name>", "limit same-fleet installed-state normalization to one concrete profile").option("--apply", "apply a reviewed plan", false).option("--recover", "restore source state from a pending normalization journal", false).option("--plan-digest <sha256>", "exact digest from the reviewed dry-run").option("--json", "print the plan or result as JSON", false).action(async (destinationFleet, options) => {
14442
14591
  const request = {
14443
14592
  destinationFleet,
14444
14593
  from: options.from,
14445
- ...options.package.length > 0 ? { packages: options.package } : {}
14594
+ ...options.package.length > 0 ? { packages: options.package } : {},
14595
+ ...options.artifact.length > 0 ? { artifacts: options.artifact } : {},
14596
+ ...options.profile ? { profile: options.profile } : {}
14446
14597
  };
14447
- if (options.recover && (options.apply || options.planDigest || options.package.length > 0)) {
14448
- throw new Error("--recover cannot be combined with --apply, --plan-digest, or --package.");
14598
+ if (options.recover && (options.apply || options.planDigest || options.package.length > 0 || options.artifact.length > 0 || options.profile)) {
14599
+ throw new Error("--recover cannot be combined with --apply, --plan-digest, --package, --artifact, or --profile.");
14449
14600
  }
14450
14601
  const result = options.recover ? await recoverFleetNormalization(request) : options.apply ? await applyFleetNormalization({ ...request, apply: true, planDigest: options.planDigest }) : await planFleetNormalization(request);
14451
14602
  console.log(options.json ? JSON.stringify(result, null, 2) : formatFleetNormalization(result));
@@ -15243,7 +15394,8 @@ async function packageSelectsSkillForTarget(target, pkg, selector, options) {
15243
15394
  scope: pkg.name,
15244
15395
  onlySource: true,
15245
15396
  dryRun: true,
15246
- suppressEmptyMessage: true
15397
+ suppressEmptyMessage: true,
15398
+ deferForeignStateCheck: true
15247
15399
  }, { mode: "install" });
15248
15400
  try {
15249
15401
  return results.some((result) => result.bundle.graphLock.canonical.roots.some(
@@ -15644,6 +15796,7 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
15644
15796
  forceDrift: targetOptions.forceDrift,
15645
15797
  forceConflict: targetOptions.forceConflict,
15646
15798
  forceForeignState: targetOptions.forceForeignState,
15799
+ deferForeignStateCheck: targetOptions.deferForeignStateCheck === true || targetOptions.focusedArtifact !== void 0,
15647
15800
  replaceConflict: targetOptions.replaceConflict,
15648
15801
  retireExactMcp: targetOptions.retireExactMcp,
15649
15802
  expectedFromWorkspaceOwner: targetOptions.expectedFromWorkspaceOwner
@@ -15651,7 +15804,21 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
15651
15804
  if ((behavior.mode === "install" || behavior.mode === "update") && scopedRootId) {
15652
15805
  const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
15653
15806
  const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
15654
- results.push(targetOptions.focusedArtifact ? previousGroupLock ? scopeUpdatePlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, previousGroupLock, manifest) : scopeInstallPlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, manifest) : previousGroupLock ? scopeUpdatePlanToRoot(result, scopedRootId, previousGroupLock, manifest) : scopeInstallPlanToRoot(result, scopedRootId, manifest));
15807
+ const scopedResult = targetOptions.focusedArtifact ? previousGroupLock ? scopeUpdatePlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, previousGroupLock, manifest) : scopeInstallPlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, manifest) : previousGroupLock ? scopeUpdatePlanToRoot(result, scopedRootId, previousGroupLock, manifest) : scopeInstallPlanToRoot(result, scopedRootId, manifest);
15808
+ if (targetOptions.focusedArtifact && (targetOptions.forceForeignState !== true || group.target.fleetId)) {
15809
+ const focusedPaths = scopedResult.plan.operations.filter((operation) => operationMatchesFocusedArtifact(
15810
+ operation,
15811
+ targetOptions.focusedArtifact,
15812
+ focusedArtifactOwnerKeys(result.bundle.graphLock, previousGroupLock, scopedRootId, targetOptions.focusedArtifact)
15813
+ )).map((operation) => operation.relativeDestPath);
15814
+ await assertNoForeignWorkspaceStateForPlan(scopedResult.plan, {
15815
+ transport,
15816
+ workspaceRoot: group.target.workspaceRoot,
15817
+ workspaceOwner: workspaceOwnerForRoot(group.target.workspaceRoot, group.target.fleetId),
15818
+ plannedPaths: focusedPaths
15819
+ });
15820
+ }
15821
+ results.push(scopedResult);
15655
15822
  } else if (scopedDependencyUpdate) {
15656
15823
  const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
15657
15824
  const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
@@ -15847,11 +16014,7 @@ function scopeUpdatePlanToArtifact(result, rootId, focused, previousLock, manife
15847
16014
  function scopePlanOperationsToArtifact(result, rootId, focused, manifest, previousLock) {
15848
16015
  const currentArtifacts = focusedArtifactsForRoot(result.bundle.graphLock, rootId, focused);
15849
16016
  const previousArtifacts = previousLock ? focusedArtifactsForRoot(previousLock, rootId, focused) : [];
15850
- const focusedOwnerKeys = /* @__PURE__ */ new Set([
15851
- `workspace:${rootId}`,
15852
- ...currentArtifacts.map((artifact) => artifact.graphNodeId),
15853
- ...previousArtifacts.map((artifact) => artifact.graphNodeId)
15854
- ]);
16017
+ const focusedOwnerKeys = focusedArtifactOwnerKeys(result.bundle.graphLock, previousLock, rootId, focused);
15855
16018
  const manifestByPath = new Map((manifest?.entries ?? []).map((entry) => [entry.path, entry]));
15856
16019
  const plannedPaths = /* @__PURE__ */ new Set();
15857
16020
  const preservedPaths = /* @__PURE__ */ new Set();
@@ -15889,6 +16052,15 @@ function scopePlanOperationsToArtifact(result, rootId, focused, manifest, previo
15889
16052
  }
15890
16053
  };
15891
16054
  }
16055
+ function focusedArtifactOwnerKeys(currentLock, previousLock, rootId, focused) {
16056
+ const currentArtifacts = focusedArtifactsForRoot(currentLock, rootId, focused);
16057
+ const previousArtifacts = previousLock ? focusedArtifactsForRoot(previousLock, rootId, focused) : [];
16058
+ return /* @__PURE__ */ new Set([
16059
+ `workspace:${rootId}`,
16060
+ ...currentArtifacts.map((artifact) => artifact.graphNodeId),
16061
+ ...previousArtifacts.map((artifact) => artifact.graphNodeId)
16062
+ ]);
16063
+ }
15892
16064
  function operationMatchesFocusedArtifact(operation, focused, ownerKeys) {
15893
16065
  if (!matchesCanonicalFocusedArtifact(operation.artifactType, operation.artifactName, focused)) {
15894
16066
  return false;
@@ -17066,11 +17238,13 @@ function formatFleetNormalization(result) {
17066
17238
  if ("recovered" in result) return `Recovered fleet normalization source state and removed journal: ${result.journalPath}`;
17067
17239
  if ("applied" in result) return `Applied fleet normalization ${result.planDigest}: ${result.packages.join(", ")}`;
17068
17240
  const packageArgs = result.packages.map((pkg) => `--package ${shellQuoteArg2(pkg.name)}`).join(" ");
17241
+ const artifactArgs = result.request.artifacts?.map((artifact) => ` --artifact ${shellQuoteArg2(artifact)}`).join("") ?? "";
17242
+ const profileArg = result.request.profile ? ` --profile ${shellQuoteArg2(result.request.profile)}` : "";
17069
17243
  return [
17070
17244
  `Fleet normalization plan: ${result.source.root} -> ${result.destination.root}`,
17071
17245
  `Packages: ${result.packages.map((pkg) => pkg.name).join(", ")}`,
17072
17246
  `Plan digest: ${result.planDigest}`,
17073
- `Apply: agentwheel fleet normalize ${result.destination.fleetId} --from ${result.request.from} ${packageArgs} --apply --plan-digest ${result.planDigest}`
17247
+ `Apply: agentwheel fleet normalize ${result.destination.fleetId} --from ${result.request.from} ${packageArgs}${artifactArgs}${profileArg} --apply --plan-digest ${result.planDigest}`
17074
17248
  ].join("\n");
17075
17249
  }
17076
17250
  function shouldDefaultUserInstall(nameOrSource, options) {
package/openpack.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "name": "NestDevLab/agentwheel",
4
- "version": "0.18.1",
4
+ "version": "0.18.2",
5
5
  "provides": [
6
6
  { "type": "skills", "path": "skills" }
7
7
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ allowed-tools: [Bash]
5
5
  license: MIT
6
6
  metadata:
7
7
  author: NestDevLab
8
- version: "0.18.1"
8
+ version: "0.18.2"
9
9
  ---
10
10
 
11
11
  # agentwheel
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: agentwheel-discovery
3
- description: MUST use proactively when a request describes repeated manual work, inconsistent results, lost information, a missing capability, or an unavailable integration—even if the user did not ask for a skill. Run Agentwheel semantic search before generic brainstorming or workflow advice, suggest up to three evidence-backed reusable skills or integrations, and offer a read-only trial without installing or changing anything.
3
+ description: MUST use proactively when a request expresses an operational intent that an existing reusable skill or integration could plausibly fulfill—even if the user did not ask for a skill or describe a capability gap. Inspect installed artifacts, then run Agentwheel semantic search before generic or manual solution design when none already covers the intent. Suggest up to three evidence-backed matches and offer a read-only trial without installing or changing anything.
4
4
  allowed-tools: [Bash]
5
5
  license: MIT
6
6
  metadata:
7
7
  author: NestDevLab
8
- version: "0.18.1"
8
+ version: "0.18.2"
9
9
  ---
10
10
 
11
11
  # Agentwheel Discovery
@@ -25,11 +25,12 @@ This skill is optional: install it only when proactive recommendations are wante
25
25
 
26
26
  ## When To Search
27
27
 
28
- Use this skill before generic brainstorming, planning, or workflow advice whenever the request
29
- describes repeated manual work, inconsistent outputs, lost information, a missing operational
30
- capability, or an unavailable integration. The user does not need to mention Agentwheel, skills,
31
- discovery, search, or a catalogue. Run the first semantic search in the same turn; do not merely
32
- describe a workflow and defer discovery.
28
+ Use this skill whenever a request expresses an operational outcome that an existing reusable skill
29
+ or integration could plausibly deliver. This includes repeated manual work, inconsistent outputs,
30
+ lost information, missing capabilities, and unavailable integrations. The user does not need to
31
+ mention Agentwheel, skills, discovery, search, a catalogue, or a capability gap. Search before
32
+ generic brainstorming, planning, manual workflow advice, or an unsolicited custom solution. Run
33
+ the first semantic search in the same turn; do not merely describe a workflow and defer discovery.
33
34
 
34
35
  This is a per-turn judgment, not a background monitor. Delegated agents follow the same trigger
35
36
  when this skill is available.
@@ -20,7 +20,7 @@ if (parsed.error) {
20
20
  const config = findConfigs(cwd, home);
21
21
  const harness = detectHarness(skillRoot, cwd, home);
22
22
  const envHint = detectHarnessFromEnv();
23
- const agentwheel = inspectAgentwheel(parsed.statusArgs, envHint);
23
+ const agentwheel = inspectAgentwheel(parsed.statusArgs, envHint, harness);
24
24
  const manifests = findStateFiles(harness.runtimeRoot ?? projectRootFromConfig(config.projectConfig) ?? cwd);
25
25
  const assessment = assess({ harness, agentwheel, manifests, config });
26
26
  const report = {
@@ -50,11 +50,11 @@ function parseArgs(args) {
50
50
  json = true;
51
51
  continue;
52
52
  }
53
- if (["--all", "--all-detected"].includes(arg)) {
53
+ if (["--all", "--all-detected", "--user", "--local"].includes(arg)) {
54
54
  statusArgs.push(arg);
55
55
  continue;
56
56
  }
57
- if (["--agent", "--profile", "--target-root", "--adapter", "--installation-type"].includes(arg)) {
57
+ if (["--agent", "--profile", "--target-root", "--adapter", "--installation-type", "--fleet"].includes(arg)) {
58
58
  const value = args[index + 1];
59
59
  if (!value || value.startsWith("--")) return { error: `${arg} requires a value.` };
60
60
  statusArgs.push(arg, value);
@@ -66,12 +66,14 @@ function parseArgs(args) {
66
66
  return { json, statusArgs };
67
67
  }
68
68
 
69
- function inspectAgentwheel(statusArgs, hint) {
69
+ function inspectAgentwheel(statusArgs, hint, harness) {
70
70
  const which = run("command", ["-v", "agentwheel"], { shell: true });
71
71
  const version = which.ok ? run("agentwheel", ["--version"]) : { ok: false, code: null, stdout: "", stderr: "agentwheel not found" };
72
- let effectiveStatusArgs = statusArgs;
72
+ let effectiveStatusArgs = hasWorkspaceScope(statusArgs)
73
+ ? statusArgs
74
+ : [...defaultWorkspaceScope(harness), ...statusArgs];
73
75
  let status = which.ok ? run("agentwheel", ["status", ...effectiveStatusArgs]) : { ok: false, code: null, stdout: "", stderr: "agentwheel not found" };
74
- if (!status.ok && statusArgs.length === 0 && /Multiple runtime directories detected/i.test(status.stderr) && hint?.adapter) {
76
+ if (!status.ok && effectiveStatusArgs.length === 0 && /Multiple runtime directories detected/i.test(status.stderr) && hint?.adapter) {
75
77
  effectiveStatusArgs = ["--adapter", hint.adapter, "--installation-type", hint.installationType ?? "local"];
76
78
  status = run("agentwheel", ["status", ...effectiveStatusArgs]);
77
79
  }
@@ -90,6 +92,16 @@ function inspectAgentwheel(statusArgs, hint) {
90
92
  };
91
93
  }
92
94
 
95
+ function hasWorkspaceScope(args) {
96
+ return args.some((arg) => ["--user", "--local", "--fleet", "--target-root"].includes(arg));
97
+ }
98
+
99
+ function defaultWorkspaceScope(harness) {
100
+ if (harness?.installationType === "user") return ["--user"];
101
+ if (harness?.installationType === "local") return ["--local"];
102
+ return [];
103
+ }
104
+
93
105
  function run(command, args, options = {}) {
94
106
  try {
95
107
  const stdout = execFileSync(command, args, {