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
|
-
|
|
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
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
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:
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
13818
|
-
results.push({
|
|
13893
|
+
const candidate = {
|
|
13819
13894
|
path,
|
|
13820
13895
|
lock,
|
|
13821
|
-
digest:
|
|
13896
|
+
digest: "",
|
|
13822
13897
|
allRootsSelected: lock.canonical.roots.every((rootEntry) => selected.has(rootEntry.rootId)),
|
|
13823
|
-
artifactIdentities:
|
|
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) =>
|
|
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) => !
|
|
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) =>
|
|
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 --
|
|
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
|
-
|
|
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 =
|
|
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
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agentwheel-discovery
|
|
3
|
-
description: MUST use proactively when a request
|
|
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.
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
discovery, search,
|
|
32
|
-
|
|
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 &&
|
|
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, {
|