agentwheel 0.20.0 → 0.20.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
@@ -9,6 +9,8 @@ import {
9
9
  canonicalizeGraphLock,
10
10
  claudeInstructionBridgesAgents,
11
11
  combineMergeRemovals,
12
+ commitManifestMetadataJournal,
13
+ computeInstallManifestInventoryRevision,
12
14
  computeManifestRevision,
13
15
  computeTargetFingerprint,
14
16
  desiredManagedInstructionBlockHash,
@@ -19,6 +21,7 @@ import {
19
21
  managedInstructionBlockMode,
20
22
  managedInstructionPhysicalKey,
21
23
  managedInstructionSelector,
24
+ mergeContributionAbsent,
22
25
  mergeRemovalForInstall,
23
26
  normalizeImmutableCacheIdentity,
24
27
  normalizeOwners,
@@ -30,7 +33,7 @@ import {
30
33
  uninstall,
31
34
  withManifestRevision,
32
35
  writeInstallManifest
33
- } from "./chunk-USHT7FGV.js";
36
+ } from "./chunk-BAJEWUI6.js";
34
37
  import {
35
38
  CURRENT_WORKSPACE_SCHEMA_VERSION,
36
39
  GovernedMutation,
@@ -38,6 +41,7 @@ import {
38
41
  acquireApplyLock,
39
42
  adapterSchema,
40
43
  adapterTargetSupport,
44
+ applyLockPath,
41
45
  artifactFormatSchema,
42
46
  artifactTypeSchema,
43
47
  assertGovernedRuntimeTransportSupported,
@@ -53,9 +57,12 @@ import {
53
57
  installRootForAdapterInstallationType,
54
58
  installRootForArtifacts,
55
59
  isCompositeWorkspaceProfile,
60
+ listAllApplyJournals,
61
+ listApplyJournals,
56
62
  listMutationReceipts,
57
63
  loadAdapterConfig,
58
64
  localTransport,
65
+ mutationMetadataForApplyJournal,
59
66
  mutationPolicyForWorkspace,
60
67
  packageAssetSchema,
61
68
  packageComposeEntrySchema,
@@ -85,8 +92,9 @@ import {
85
92
  workspaceConfigSchema,
86
93
  workspaceExportsSchema,
87
94
  workspaceSelectionImportSchema,
95
+ writeApplyJournal,
88
96
  writeWorkspaceConfig
89
- } from "./chunk-HOYIIUL5.js";
97
+ } from "./chunk-Z4N7TB6Q.js";
90
98
  import {
91
99
  inferSourceDriverName
92
100
  } from "./chunk-EMDIG24I.js";
@@ -101,11 +109,11 @@ import {
101
109
  } from "./chunk-7VPI5J5Y.js";
102
110
 
103
111
  // src/cli/index.ts
104
- import { createHash as createHash13 } from "crypto";
112
+ import { createHash as createHash14 } from "crypto";
105
113
  import { existsSync } from "fs";
106
114
  import { mkdir as mkdir16, rm as rm10, writeFile as writeFile13 } from "fs/promises";
107
115
  import { homedir as homedir12 } from "os";
108
- import { dirname as dirname22, join as join45, resolve as resolve23 } from "path";
116
+ import { dirname as dirname22, join as join45, resolve as resolve24 } from "path";
109
117
  import { fileURLToPath as fileURLToPath3 } from "url";
110
118
  import { Command } from "commander";
111
119
 
@@ -1449,6 +1457,7 @@ async function createCombinedInstallPlan(desiredArtifacts, adapter, targetRoot,
1449
1457
  const installationType = resolveInstallationTypeForArtifacts(adapter, installableArtifacts.map((artifact) => artifact.type), requestedInstallationType);
1450
1458
  const installRoot = installRootForArtifacts(adapter, targetRoot, installationType, installableArtifacts.map((artifact) => artifact.type), transport.kind === "ssh");
1451
1459
  await validateArtifactsForInstall(installableArtifacts, adapter, installationType);
1460
+ const runtimeStateRevision = await computeInstallManifestInventoryRevision(installRoot, adapter.name, transport);
1452
1461
  for (const artifact of installableArtifacts) {
1453
1462
  if (artifact.meta.dependencyRole !== "root" && isGuardedMergeTarget(artifact.type)) {
1454
1463
  throw new Error(`Dependency-provided ${artifact.type} artifacts cannot be installed until per-subentry ownership exists: ${artifact.type}/${artifact.name}`);
@@ -1462,7 +1471,7 @@ async function createCombinedInstallPlan(desiredArtifacts, adapter, targetRoot,
1462
1471
  }
1463
1472
  }
1464
1473
  await addProgrammaticOperations(desired, adapter, installRoot);
1465
- return createPlanFromOperations(desired, adapter, installRoot, manifest, transport, { ...options, installationType });
1474
+ return createPlanFromOperations(desired, adapter, installRoot, manifest, transport, { ...options, installationType, runtimeStateRevision });
1466
1475
  }
1467
1476
  async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifest, transport, options) {
1468
1477
  const workspaceOwner = options.workspaceOwner;
@@ -1512,7 +1521,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1512
1521
  }
1513
1522
  }
1514
1523
  const incompleteMergeOwnership = existing2 && existing2.mergeStrategy && !("mergeCreatedDestination" in existing2 && existing2.mergeCreatedDestination === true) && !("mergeRemoval" in existing2 && hasMergeRemovalContent(existing2.mergeRemoval));
1515
- const adoptExisting = exists2 && (!existing2 || incompleteMergeOwnership) && options.forceConflict === true;
1524
+ const adoptExisting = exists2 && (incompleteMergeOwnership || !existing2 && options.forceConflict === true);
1516
1525
  let mergeRemoval;
1517
1526
  try {
1518
1527
  mergeRemoval = await mergeRemovalForInstall(op.sourcePath, op.mergeStrategy, currentContent, {
@@ -1544,7 +1553,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1544
1553
  mergeCreatedDestination: existing2?.mergeCreatedDestination,
1545
1554
  currentHash: currentHash2,
1546
1555
  manifestHash: existing2?.hash,
1547
- reason: existing2 ? "force repairing exact incomplete merge ownership" : "force adopting exact unmanaged merge contribution"
1556
+ reason: existing2 ? "repairing exact incomplete merge ownership" : "force adopting exact unmanaged merge contribution"
1548
1557
  });
1549
1558
  continue;
1550
1559
  }
@@ -1684,6 +1693,10 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1684
1693
  }
1685
1694
  for (const operation of operations) assertOperationContained(operation, targetRoot);
1686
1695
  operations.sort((a, b) => a.relativeDestPath.localeCompare(b.relativeDestPath));
1696
+ const runtimeStateRevision = await computeInstallManifestInventoryRevision(targetRoot, adapter.name, transport);
1697
+ if (options.runtimeStateRevision && runtimeStateRevision !== options.runtimeStateRevision) {
1698
+ throw new Error("Runtime manifest inventory changed during planning; replan needed.");
1699
+ }
1687
1700
  return {
1688
1701
  adapter: adapter.name,
1689
1702
  installationType: options.installationType ?? defaultInstallationType,
@@ -1692,6 +1705,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1692
1705
  operations,
1693
1706
  hasBlockingChanges: operations.some((op) => op.action === "drift" || op.action === "conflict"),
1694
1707
  baseRevision: options.baseRevision ?? manifest?.revision ?? null,
1708
+ runtimeStateRevision,
1695
1709
  migrationReport: migration.report,
1696
1710
  graphLockDigest: options.graphLockDigest,
1697
1711
  adapterCode: adapter.programmatic ? { modulePath: adapter.programmatic.modulePath, hash: adapter.programmatic.hash } : void 0,
@@ -2182,6 +2196,7 @@ function isPendingInstallOperation(operation) {
2182
2196
  // src/install/uninstall.ts
2183
2197
  import { join as join13 } from "path";
2184
2198
  async function createUninstallPlan(manifest, transport = localTransport) {
2199
+ const runtimeStateRevision = await computeInstallManifestInventoryRevision(manifest.targetRoot, manifest.adapter, transport);
2185
2200
  const operations = [];
2186
2201
  for (const entry of manifest.entries) {
2187
2202
  const semanticPlugin = "semanticPlugin" in entry ? entry.semanticPlugin : void 0;
@@ -2237,6 +2252,9 @@ async function createUninstallPlan(manifest, transport = localTransport) {
2237
2252
  }
2238
2253
  }
2239
2254
  operations.sort((a, b) => a.relativeDestPath.localeCompare(b.relativeDestPath));
2255
+ if (runtimeStateRevision !== await computeInstallManifestInventoryRevision(manifest.targetRoot, manifest.adapter, transport)) {
2256
+ throw new Error("Runtime manifest inventory changed during uninstall planning; replan needed.");
2257
+ }
2240
2258
  return {
2241
2259
  adapter: manifest.adapter,
2242
2260
  installationType: "installationType" in manifest ? manifest.installationType : defaultInstallationType,
@@ -2245,10 +2263,12 @@ async function createUninstallPlan(manifest, transport = localTransport) {
2245
2263
  operations,
2246
2264
  hasBlockingChanges: operations.some((operation) => operation.action === "conflict"),
2247
2265
  baseRevision: manifest.revision,
2266
+ runtimeStateRevision,
2248
2267
  adapterCode: manifest.adapterCode
2249
2268
  };
2250
2269
  }
2251
2270
  async function createOwnershipUninstallPlan(manifest, remainingDesired, adapter, transport = localTransport, options = {}) {
2271
+ const runtimeStateRevision = await computeInstallManifestInventoryRevision(manifest.targetRoot, manifest.adapter, transport);
2252
2272
  const installationType = "installationType" in manifest ? manifest.installationType : defaultInstallationType;
2253
2273
  const stateKey = "stateKey" in manifest ? manifest.stateKey : void 0;
2254
2274
  const desiredPlan = await createCombinedInstallPlan(remainingDesired, adapter, manifest.targetRoot, void 0, transport, { installationType, stateKey });
@@ -2338,6 +2358,9 @@ async function createOwnershipUninstallPlan(manifest, remainingDesired, adapter,
2338
2358
  }
2339
2359
  }
2340
2360
  operations.sort((a, b) => a.relativeDestPath.localeCompare(b.relativeDestPath));
2361
+ if (runtimeStateRevision !== await computeInstallManifestInventoryRevision(manifest.targetRoot, manifest.adapter, transport)) {
2362
+ throw new Error("Runtime manifest inventory changed during ownership uninstall planning; replan needed.");
2363
+ }
2341
2364
  return {
2342
2365
  adapter: manifest.adapter,
2343
2366
  installationType,
@@ -2346,6 +2369,7 @@ async function createOwnershipUninstallPlan(manifest, remainingDesired, adapter,
2346
2369
  operations,
2347
2370
  hasBlockingChanges: operations.some((operation) => operation.action === "conflict"),
2348
2371
  baseRevision: manifest.revision,
2372
+ runtimeStateRevision,
2349
2373
  adapterCode: manifest.adapterCode,
2350
2374
  graphLockDigest: options.graphLockDigest
2351
2375
  };
@@ -3083,14 +3107,14 @@ async function writeMermaidAsset(response) {
3083
3107
  "content-type": "application/javascript; charset=utf-8",
3084
3108
  "cache-control": "public, max-age=3600"
3085
3109
  });
3086
- await new Promise((resolve24) => {
3110
+ await new Promise((resolve25) => {
3087
3111
  const stream = createReadStream(assetPath);
3088
3112
  stream.on("error", () => {
3089
3113
  if (!response.headersSent) writeText(response, 404, "Mermaid asset unavailable.\n");
3090
3114
  else response.destroy();
3091
- resolve24();
3115
+ resolve25();
3092
3116
  });
3093
- stream.on("end", resolve24);
3117
+ stream.on("end", resolve25);
3094
3118
  stream.pipe(response);
3095
3119
  });
3096
3120
  }
@@ -3117,14 +3141,14 @@ function escapeHtml2(value) {
3117
3141
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3118
3142
  }
3119
3143
  function listen(server, port, bind) {
3120
- return new Promise((resolve24, reject) => {
3144
+ return new Promise((resolve25, reject) => {
3121
3145
  const onError = (error) => {
3122
3146
  server.off("listening", onListening);
3123
3147
  reject(error);
3124
3148
  };
3125
3149
  const onListening = () => {
3126
3150
  server.off("error", onError);
3127
- resolve24();
3151
+ resolve25();
3128
3152
  };
3129
3153
  server.once("error", onError);
3130
3154
  server.once("listening", onListening);
@@ -3132,7 +3156,7 @@ function listen(server, port, bind) {
3132
3156
  });
3133
3157
  }
3134
3158
  function waitForShutdown(server, beforeClose) {
3135
- return new Promise((resolve24) => {
3159
+ return new Promise((resolve25) => {
3136
3160
  let closing = false;
3137
3161
  const shutdown = () => {
3138
3162
  if (closing) return;
@@ -3140,7 +3164,7 @@ function waitForShutdown(server, beforeClose) {
3140
3164
  process.off("SIGINT", shutdown);
3141
3165
  process.off("SIGTERM", shutdown);
3142
3166
  beforeClose();
3143
- server.close(() => resolve24());
3167
+ server.close(() => resolve25());
3144
3168
  };
3145
3169
  process.once("SIGINT", shutdown);
3146
3170
  process.once("SIGTERM", shutdown);
@@ -3150,7 +3174,7 @@ function waitForShutdown(server, beforeClose) {
3150
3174
  process.off("SIGINT", shutdown);
3151
3175
  process.off("SIGTERM", shutdown);
3152
3176
  beforeClose();
3153
- resolve24();
3177
+ resolve25();
3154
3178
  });
3155
3179
  });
3156
3180
  }
@@ -3703,7 +3727,7 @@ async function fetchClawHubPackage(fetchImpl, packageName) {
3703
3727
  throw new Error(`ClawHub lookup failed for ${packageName}`);
3704
3728
  }
3705
3729
  async function delay(ms) {
3706
- await new Promise((resolve24) => setTimeout(resolve24, ms));
3730
+ await new Promise((resolve25) => setTimeout(resolve25, ms));
3707
3731
  }
3708
3732
  function parseClawHubSource(source) {
3709
3733
  if (!source.startsWith(sourcePrefix)) {
@@ -3874,7 +3898,7 @@ async function withGitCacheMaintenanceLock(cacheRoot, timeoutMs, fn) {
3874
3898
  if (Date.now() - started > timeoutMs) {
3875
3899
  throw new Error(`Timed out waiting for git cache maintenance lock at ${lockPath}`);
3876
3900
  }
3877
- await new Promise((resolve24) => setTimeout(resolve24, 50));
3901
+ await new Promise((resolve25) => setTimeout(resolve25, 50));
3878
3902
  }
3879
3903
  }
3880
3904
  try {
@@ -6665,8 +6689,8 @@ function verifyIntegrity(integrity, sourceHash, label) {
6665
6689
  async function withCachePathLock(path, fn) {
6666
6690
  const previous = cacheLocks.get(path) ?? Promise.resolve();
6667
6691
  let release = () => void 0;
6668
- const current = previous.then(() => new Promise((resolve24) => {
6669
- release = resolve24;
6692
+ const current = previous.then(() => new Promise((resolve25) => {
6693
+ release = resolve25;
6670
6694
  }));
6671
6695
  cacheLocks.set(path, current);
6672
6696
  await previous;
@@ -6742,8 +6766,8 @@ function detectDirectCollisions(nodes) {
6742
6766
  }
6743
6767
  }
6744
6768
  function graphNodeId(name, version, normalizedSource, resolvedCommit, sourceHash) {
6745
- const digest = createHash6("sha256").update(normalizedSource).update("\0").update(resolvedCommit ?? sourceHash).digest("hex").slice(0, 12);
6746
- return `${name}@${version}+${digest}`;
6769
+ const digest2 = createHash6("sha256").update(normalizedSource).update("\0").update(resolvedCommit ?? sourceHash).digest("hex").slice(0, 12);
6770
+ return `${name}@${version}+${digest2}`;
6747
6771
  }
6748
6772
  function sortedUnique2(values) {
6749
6773
  return [...new Set(values)].sort((a, b) => a.localeCompare(b));
@@ -6914,7 +6938,7 @@ import { rm as rm7 } from "fs/promises";
6914
6938
  // src/lifecycle/source-plan.ts
6915
6939
  import { createHash as createHash8 } from "crypto";
6916
6940
  import { mkdir as mkdir14 } from "fs/promises";
6917
- import { dirname as dirname17, join as join33, resolve as resolve14 } from "path";
6941
+ import { dirname as dirname17, join as join33, resolve as resolve15 } from "path";
6918
6942
 
6919
6943
  // src/resolve/graph-diff.ts
6920
6944
  function diffGraphLocks(previous, next) {
@@ -7735,6 +7759,185 @@ async function createExactMcpRetirementPlan(desiredArtifacts, adapter, targetRoo
7735
7759
  };
7736
7760
  }
7737
7761
 
7762
+ // src/model/fleet.ts
7763
+ import { lstat as lstat2, readFile as readFile20, realpath } from "fs/promises";
7764
+ import { homedir as homedir7 } from "os";
7765
+ import { isAbsolute as isAbsolute2, relative as relative7, resolve as resolve14 } from "path";
7766
+ async function resolveWorkspaceScope(request = {}) {
7767
+ const selected = [request.user === true, request.local === true, request.fleet !== void 0].filter(Boolean).length;
7768
+ if (selected > 1) {
7769
+ throw new Error("Choose exactly one workspace selector: --user, --local, or --fleet <id>.");
7770
+ }
7771
+ const globalRoot = resolve14(request.globalRoot ?? homedir7());
7772
+ if (request.user) {
7773
+ const config2 = await readWorkspaceConfig(globalRoot);
7774
+ assertNonFleetScope("user", config2);
7775
+ return { kind: "user", root: globalRoot, config: config2 };
7776
+ }
7777
+ if (request.fleet !== void 0) {
7778
+ const id = fleetIdSchema.parse(request.fleet);
7779
+ const registration = await showRegisteredFleet(id, { globalRoot });
7780
+ const root = await assertCanonicalDirectory(registration.root, `Registered fleet '${id}' root`);
7781
+ const config2 = await readRequiredConfig(root, `fleet '${id}'`);
7782
+ assertFleetContract(id, registration, config2);
7783
+ return { kind: "fleet", root, fleetId: id, config: config2 };
7784
+ }
7785
+ const cwd = resolve14(request.cwd ?? process.cwd());
7786
+ const discoveredRoot = await findExistingWorkspaceRoot(cwd);
7787
+ if (!discoveredRoot || discoveredRoot === globalRoot) {
7788
+ if (request.local === true && cwd !== globalRoot) {
7789
+ return { kind: "local", root: cwd, config: await readWorkspaceConfig(cwd) };
7790
+ }
7791
+ throw missingScopeError(request.local === true ? "local" : "implicit local");
7792
+ }
7793
+ const config = await readRequiredConfig(discoveredRoot, "local");
7794
+ assertNonFleetScope("local", config);
7795
+ return { kind: "local", root: discoveredRoot, config };
7796
+ }
7797
+ async function registerFleet(request) {
7798
+ const id = fleetIdSchema.parse(request.id);
7799
+ const requiredPackages = sortedUnique4(request.requiredPackages);
7800
+ if (requiredPackages.length === 0) throw new Error("Fleet registration requires at least one --required-package <name>.");
7801
+ const globalRoot = resolve14(request.globalRoot ?? homedir7());
7802
+ const home = await readWorkspaceConfig(globalRoot);
7803
+ if (supportsFleetConfig(home) && home.fleetId) {
7804
+ throw new Error(`The home config is fleet '${home.fleetId}', so it cannot own the global fleet registry.`);
7805
+ }
7806
+ const existing = supportsFleetConfig(home) ? await canonicalizeExistingFleetRoots(home.fleets, globalRoot) : {};
7807
+ if (existing[id]) throw new Error(`Fleet '${id}' is already registered.`);
7808
+ const root = await assertCanonicalDirectory(request.root, `Fleet '${id}' root`);
7809
+ const duplicateRoot = Object.entries(existing).find(([, value]) => value.root === root);
7810
+ if (duplicateRoot) throw new Error(`Fleet root ${root} is already registered as '${duplicateRoot[0]}'.`);
7811
+ if (root === globalRoot) throw new Error("A fleet root must be outside the user config root contract.");
7812
+ const target = await readRequiredConfig(root, `fleet '${id}'`);
7813
+ const registration = registeredFleetSchema.parse({ root, requiredPackages });
7814
+ assertFleetContract(id, registration, target);
7815
+ const upgraded = workspaceConfigSchema.parse({
7816
+ ...home,
7817
+ schemaVersion: CURRENT_WORKSPACE_SCHEMA_VERSION,
7818
+ exports: home.schemaVersion >= 2 ? home.exports : { selections: {} },
7819
+ fleets: { ...existing, [id]: registration }
7820
+ });
7821
+ const configPath = globalWorkspaceConfigPath(globalRoot);
7822
+ declareMutationPath(configPath);
7823
+ await writeJsonAtomic(configPath, upgraded);
7824
+ return { id, ...registration };
7825
+ }
7826
+ async function canonicalizeExistingFleetRoots(fleets, globalRoot) {
7827
+ const canonical = /* @__PURE__ */ new Map();
7828
+ const normalized = {};
7829
+ for (const [id, registration] of Object.entries(fleets).sort(([a], [b]) => a.localeCompare(b))) {
7830
+ const root = await canonicalizeDirectory(registration.root, `Registered fleet '${id}' root`);
7831
+ if (root === globalRoot) throw new Error(`Registered fleet '${id}' root must be outside the user config root contract.`);
7832
+ const incumbent = canonical.get(root);
7833
+ if (incumbent) {
7834
+ throw new Error(`Fleet root ${root} is registered more than once as '${incumbent}' and '${id}'.`);
7835
+ }
7836
+ canonical.set(root, id);
7837
+ normalized[id] = registeredFleetSchema.parse({ ...registration, root });
7838
+ }
7839
+ return normalized;
7840
+ }
7841
+ async function listRegisteredFleets(options = {}) {
7842
+ const globalRoot = resolve14(options.globalRoot ?? homedir7());
7843
+ const home = await readWorkspaceConfig(globalRoot);
7844
+ if (!supportsFleetConfig(home)) return [];
7845
+ return Object.entries(home.fleets).sort(([a], [b]) => a.localeCompare(b)).map(([id, registration]) => ({ id, ...registration }));
7846
+ }
7847
+ async function showRegisteredFleet(idInput, options = {}) {
7848
+ const id = fleetIdSchema.parse(idInput);
7849
+ const fleets = await listRegisteredFleets(options);
7850
+ const registration = fleets.find((candidate) => candidate.id === id);
7851
+ if (!registration) {
7852
+ throw new Error(`Unknown fleet '${id}'. Use 'agentwheel fleet list' or register it with 'agentwheel fleet register'.`);
7853
+ }
7854
+ return registration;
7855
+ }
7856
+ async function resolveWorkspaceOwnershipScope(workspaceRootInput, options = {}) {
7857
+ const workspaceRoot = await canonicalizeDirectory(resolve14(workspaceRootInput), "Workspace ownership root");
7858
+ if (options.fleetId) {
7859
+ return { root: workspaceRoot, fleetId: fleetIdSchema.parse(options.fleetId) };
7860
+ }
7861
+ const syntacticallyContaining = (await listRegisteredFleets({ globalRoot: options.globalRoot })).filter((fleet2) => containsPath(resolve14(fleet2.root), workspaceRoot));
7862
+ const registered = await Promise.all(syntacticallyContaining.map(async (fleet2) => ({
7863
+ ...fleet2,
7864
+ root: await canonicalizeDirectory(fleet2.root, `Registered fleet '${fleet2.id}' root`)
7865
+ })));
7866
+ const containing = registered.filter((fleet2) => containsPath(fleet2.root, workspaceRoot));
7867
+ if (containing.length > 1) {
7868
+ throw new Error(
7869
+ `Workspace ${workspaceRoot} is contained by multiple registered fleets: ` + containing.map((fleet2) => `'${fleet2.id}' at ${fleet2.root}`).join(", ") + ". Resolve the overlapping registrations before planning ownership."
7870
+ );
7871
+ }
7872
+ const fleet = containing[0];
7873
+ if (fleet) assertFleetContract(fleet.id, fleet, await readRequiredConfig(fleet.root, `fleet '${fleet.id}'`));
7874
+ return fleet ? { root: fleet.root, fleetId: fleet.id } : { root: workspaceRoot };
7875
+ }
7876
+ async function readRequiredConfig(root, label) {
7877
+ const path = workspaceConfigPath(root);
7878
+ if (!await pathExists(path)) throw missingScopeError(label);
7879
+ try {
7880
+ return workspaceConfigSchema.parse(JSON.parse(await readFile20(path, "utf8")));
7881
+ } catch (error) {
7882
+ throw new Error(`Invalid ${label} Agentwheel config at ${path}: ${error instanceof Error ? error.message : String(error)}`);
7883
+ }
7884
+ }
7885
+ function assertFleetContract(id, registration, config) {
7886
+ if (!supportsFleetConfig(config)) {
7887
+ throw new Error(`Fleet '${id}' config must use schemaVersion 3 or newer before registration or selection.`);
7888
+ }
7889
+ if (config.fleetId !== id) {
7890
+ throw new Error(`Fleet fleetId mismatch: expected '${id}', found '${config.fleetId ?? "missing"}'.`);
7891
+ }
7892
+ if (Object.keys(config.fleets).length > 0) {
7893
+ throw new Error(`Fleet '${id}' config may not contain the home fleets registry.`);
7894
+ }
7895
+ const names = new Set(config.packages.map((pkg) => pkg.name));
7896
+ for (const packageName of registration.requiredPackages) {
7897
+ if (!names.has(packageName)) throw new Error(`Fleet '${id}' is missing required package '${packageName}'.`);
7898
+ }
7899
+ }
7900
+ async function assertCanonicalDirectory(input, label) {
7901
+ const canonical = await canonicalizeDirectory(input, label);
7902
+ const normalized = resolve14(input);
7903
+ const stats = await lstat2(input);
7904
+ if (stats.isSymbolicLink() || canonical !== normalized || input !== normalized) {
7905
+ throw new Error(`${label} must be canonical and symlink-unambiguous: ${input} resolves to ${canonical}.`);
7906
+ }
7907
+ return canonical;
7908
+ }
7909
+ async function canonicalizeDirectory(input, label) {
7910
+ if (!isAbsolute2(input)) throw new Error(`${label} must be an absolute canonical path.`);
7911
+ let canonical;
7912
+ try {
7913
+ canonical = await realpath(input);
7914
+ } catch {
7915
+ throw new Error(`${label} does not exist: ${input}`);
7916
+ }
7917
+ const stats = await lstat2(canonical);
7918
+ if (!stats.isDirectory()) throw new Error(`${label} is not a directory: ${input}`);
7919
+ return canonical;
7920
+ }
7921
+ function missingScopeError(label) {
7922
+ return new Error(
7923
+ `Missing ${label} Agentwheel workspace config. Select --user, --local, or --fleet <id>, or run 'agentwheel init workspace' in the intended local root. Agentwheel never falls back to home desired state.`
7924
+ );
7925
+ }
7926
+ function assertNonFleetScope(kind, config) {
7927
+ if (supportsFleetConfig(config) && config.fleetId) {
7928
+ throw new Error(
7929
+ `The ${kind} config declares fleetId '${config.fleetId}'. Select it through the registered --fleet <id> scope.`
7930
+ );
7931
+ }
7932
+ }
7933
+ function sortedUnique4(values) {
7934
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
7935
+ }
7936
+ function containsPath(parent, candidate) {
7937
+ const path = relative7(parent, candidate);
7938
+ return path === "" || !path.startsWith("..") && !isAbsolute2(path);
7939
+ }
7940
+
7738
7941
  // src/lifecycle/source-plan.ts
7739
7942
  async function createGraphSourcePlan(options) {
7740
7943
  if (options.roots.length === 0) {
@@ -7813,7 +8016,11 @@ async function createGraphSourcePlan(options) {
7813
8016
  const graphLockDigest = digestGraphLock(bundle.graphLock);
7814
8017
  const graphDiff = diffGraphLocks(previousLock, bundle.graphLock);
7815
8018
  const manifest = await readInstallManifest(resolvedInstallRoot, options.adapter.name, transport, { installationType: resolvedInstallationType, stateKey });
7816
- const workspaceOwner = workspaceOwnerForRoot(workspaceRoot, options.fleetId);
8019
+ const ownership = await resolveWorkspaceOwnershipScope(workspaceRoot, {
8020
+ fleetId: options.fleetId,
8021
+ globalRoot: options.globalRoot
8022
+ });
8023
+ const workspaceOwner = workspaceOwnerForRoot(ownership.root, ownership.fleetId);
7817
8024
  const plan = options.retireExactMcp ? await createExactMcpRetirementPlan(
7818
8025
  desiredArtifacts,
7819
8026
  options.adapter,
@@ -7847,7 +8054,8 @@ async function createGraphSourcePlan(options) {
7847
8054
  workspaceOwner,
7848
8055
  globalRoot: options.globalRoot,
7849
8056
  stateKey,
7850
- plannedPaths: plan.operations.map((operation) => operation.relativeDestPath)
8057
+ plannedPaths: plan.operations.map((operation) => operation.relativeDestPath),
8058
+ plannedOperations: plan.operations
7851
8059
  });
7852
8060
  }
7853
8061
  return {
@@ -7874,7 +8082,8 @@ async function assertNoForeignWorkspaceStateForPlan(plan, options) {
7874
8082
  workspaceOwner: options.workspaceOwner,
7875
8083
  globalRoot: options.globalRoot,
7876
8084
  stateKey: plan.stateKey,
7877
- plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath)
8085
+ plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath),
8086
+ plannedOperations: plan.operations
7878
8087
  });
7879
8088
  }
7880
8089
  function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2) {
@@ -7924,8 +8133,15 @@ var workspaceOwnerPrefix2 = "workspace-root:";
7924
8133
  async function assertNoForeignWorkspaceState(check) {
7925
8134
  const planned = new Set(check.plannedPaths);
7926
8135
  if (planned.size === 0) return;
8136
+ const plannedOperations = /* @__PURE__ */ new Map();
8137
+ for (const operation of check.plannedOperations) {
8138
+ if (!planned.has(operation.relativeDestPath)) continue;
8139
+ const operations = plannedOperations.get(operation.relativeDestPath) ?? [];
8140
+ operations.push(operation);
8141
+ plannedOperations.set(operation.relativeDestPath, operations);
8142
+ }
7927
8143
  const manifests = await listInstallManifests(check.installRoot, check.adapter, check.transport);
7928
- const root = resolve14(check.workspaceRoot);
8144
+ const root = resolve15(check.workspaceRoot);
7929
8145
  const ownsSubWorkspaces = root !== globalConfigRoot(check.globalRoot);
7930
8146
  const foreign = [];
7931
8147
  for (const entry of manifests) {
@@ -7937,7 +8153,15 @@ async function assertNoForeignWorkspaceState(check) {
7937
8153
  if (ownedByWorkspace(owner, check.workspaceOwner, root, ownsSubWorkspaces)) continue;
7938
8154
  const bucket = byOwner.get(owner) ?? { entryCount: 0, collidingPaths: [] };
7939
8155
  bucket.entryCount += 1;
7940
- if (planned.has(manifestEntry.path)) bucket.collidingPaths.push(manifestEntry.path);
8156
+ if (planned.has(manifestEntry.path) && !await isDisjointVerifiedMergeContribution(
8157
+ check,
8158
+ manifestEntry.path,
8159
+ manifestEntry.mergeStrategy,
8160
+ manifestEntry.mergeRemoval,
8161
+ plannedOperations.get(manifestEntry.path) ?? []
8162
+ )) {
8163
+ bucket.collidingPaths.push(manifestEntry.path);
8164
+ }
7941
8165
  byOwner.set(owner, bucket);
7942
8166
  }
7943
8167
  for (const [owner, bucket] of byOwner) {
@@ -7959,6 +8183,34 @@ async function assertNoForeignWorkspaceState(check) {
7959
8183
  parseWorkspaceOwner(check.workspaceOwner)?.fleetId ? "Reconcile the owners with an explicit agentwheel fleet normalize operation before planning this fleet." : "Re-run from the owning workspace, or pass --force-foreign-state to plan against it anyway."
7960
8184
  ].join("\n"));
7961
8185
  }
8186
+ async function isDisjointVerifiedMergeContribution(check, relativePath, foreignStrategy, foreignRemoval, operations) {
8187
+ if (!foreignStrategy || !hasMergeRemovalContent(foreignRemoval) || operations.length !== 1) return false;
8188
+ const operation = operations[0];
8189
+ if (operation.mergeStrategy !== foreignStrategy || !hasMergeRemovalContent(operation.mergeRemoval)) return false;
8190
+ if (!disjointMcpMergeContributions(foreignStrategy, foreignRemoval, operation.mergeRemoval)) return false;
8191
+ try {
8192
+ const content = await check.transport.readFile(join33(check.installRoot, relativePath));
8193
+ assertExactMergeContribution(foreignRemoval, foreignStrategy, content);
8194
+ assertExactMergeContribution(operation.mergeRemoval, foreignStrategy, content);
8195
+ return true;
8196
+ } catch {
8197
+ return false;
8198
+ }
8199
+ }
8200
+ function disjointMcpMergeContributions(strategy, left, right) {
8201
+ const leftNames = mcpServerNames(strategy, left);
8202
+ const rightNames = mcpServerNames(strategy, right);
8203
+ if (!leftNames || !rightNames) return false;
8204
+ return [...leftNames].every((name) => !rightNames.has(name));
8205
+ }
8206
+ function mcpServerNames(strategy, removal) {
8207
+ if (strategy !== "codex-toml-mcp" && strategy !== "json-deep") return void 0;
8208
+ const keys = Object.keys(removal);
8209
+ if (strategy === "json-deep" && (keys.length !== 1 || keys[0] !== "mcpServers")) return void 0;
8210
+ const servers = removal.mcpServers ?? (strategy === "codex-toml-mcp" ? removal : void 0);
8211
+ if (!servers || Array.isArray(servers) || typeof servers !== "object") return void 0;
8212
+ return new Set(Object.keys(servers));
8213
+ }
7962
8214
  function globalConfigRoot(globalRoot) {
7963
8215
  return dirname17(dirname17(globalWorkspaceConfigPath(globalRoot)));
7964
8216
  }
@@ -8031,17 +8283,17 @@ function selectionImportKey2(selection) {
8031
8283
  exportHash: selection.exportHash,
8032
8284
  exportName: selection.exportName,
8033
8285
  extends: selection.extends,
8034
- inherited: sortedUnique4(selection.inherited),
8035
- additions: sortedUnique4(selection.additions),
8036
- exclusions: sortedUnique4(selection.exclusions),
8037
- effective: sortedUnique4(selection.effective)
8286
+ inherited: sortedUnique5(selection.inherited),
8287
+ additions: sortedUnique5(selection.additions),
8288
+ exclusions: sortedUnique5(selection.exclusions),
8289
+ effective: sortedUnique5(selection.effective)
8038
8290
  });
8039
8291
  }
8040
- function sortedUnique4(values) {
8292
+ function sortedUnique5(values) {
8041
8293
  return [...new Set(values)].sort((a, b) => a.localeCompare(b));
8042
8294
  }
8043
8295
  function selectorKey(selectors) {
8044
- return sortedUnique4(selectors).join("\0");
8296
+ return sortedUnique5(selectors).join("\0");
8045
8297
  }
8046
8298
  async function assertTrusted(sources, options) {
8047
8299
  if (sources.length === 0) return;
@@ -8102,7 +8354,7 @@ function targetLabel(target) {
8102
8354
  }
8103
8355
 
8104
8356
  // src/runtime/target.ts
8105
- import { basename as basename18, dirname as dirname18, join as join34, resolve as resolve15 } from "path";
8357
+ import { basename as basename18, dirname as dirname18, join as join34, resolve as resolve16 } from "path";
8106
8358
  var runtimeMarkers = [
8107
8359
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
8108
8360
  { adapter: "claude", dirs: [".claude"] },
@@ -8111,9 +8363,9 @@ var runtimeMarkers = [
8111
8363
  { adapter: "copilot", dirs: [".github"] }
8112
8364
  ];
8113
8365
  async function resolveRuntimeTarget(request = {}) {
8114
- const cwd = resolve15(request.cwd ?? process.cwd());
8366
+ const cwd = resolve16(request.cwd ?? process.cwd());
8115
8367
  if (request.targetRoot) {
8116
- const targetRoot = resolve15(request.targetRoot);
8368
+ const targetRoot = resolve16(request.targetRoot);
8117
8369
  return {
8118
8370
  adapter: request.adapter ?? "openclaw",
8119
8371
  installationType: request.installationType,
@@ -8145,7 +8397,7 @@ async function resolveRuntimeTarget(request = {}) {
8145
8397
  async function resolveAllRuntimeTargets(request = {}) {
8146
8398
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
8147
8399
  if (request.agent) return [await resolveRuntimeTarget(request)];
8148
- const cwd = resolve15(request.cwd ?? process.cwd());
8400
+ const cwd = resolve16(request.cwd ?? process.cwd());
8149
8401
  const workspaceRoot = await findWorkspaceRoot(cwd);
8150
8402
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8151
8403
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot, request.installationType, request.fleetId));
@@ -8155,8 +8407,8 @@ async function resolveAllRuntimeTargets(request = {}) {
8155
8407
  return targets;
8156
8408
  }
8157
8409
  async function resolveProfileRuntimeTargets(request) {
8158
- const cwd = resolve15(request.cwd ?? process.cwd());
8159
- const workspaceRoot = request.targetRoot ? resolve15(request.targetRoot) : await findWorkspaceRoot(cwd);
8410
+ const cwd = resolve16(request.cwd ?? process.cwd());
8411
+ const workspaceRoot = request.targetRoot ? resolve16(request.targetRoot) : await findWorkspaceRoot(cwd);
8160
8412
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8161
8413
  const profile = config.profiles[request.profile];
8162
8414
  if (!profile) {
@@ -8222,7 +8474,7 @@ async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
8222
8474
  return unique[0];
8223
8475
  }
8224
8476
  async function detectRuntimeTargets(cwd = process.cwd(), adapterFilter) {
8225
- const root = resolve15(cwd);
8477
+ const root = resolve16(cwd);
8226
8478
  const matches = [];
8227
8479
  for (const marker of runtimeMarkers) {
8228
8480
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
@@ -8271,7 +8523,7 @@ function dedupeTargets(matches) {
8271
8523
  return [...byKey.values()];
8272
8524
  }
8273
8525
  function runtimeScanRoot(request) {
8274
- const root = resolve15(request.targetRoot ?? request.cwd ?? process.cwd());
8526
+ const root = resolve16(request.targetRoot ?? request.cwd ?? process.cwd());
8275
8527
  if (request.targetRoot) return root;
8276
8528
  return runtimeMarkers.some((marker) => marker.dirs.includes(basename18(root))) ? dirname18(root) : root;
8277
8529
  }
@@ -8581,8 +8833,8 @@ function shellQuoteArg(value) {
8581
8833
  }
8582
8834
 
8583
8835
  // src/cli/update-check.ts
8584
- import { mkdir as mkdir15, readFile as readFile20, writeFile as writeFile11 } from "fs/promises";
8585
- import { homedir as homedir7 } from "os";
8836
+ import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
8837
+ import { homedir as homedir8 } from "os";
8586
8838
  import { dirname as dirname19, join as join35 } from "path";
8587
8839
  var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
8588
8840
  var DEFAULT_TIMEOUT_MS = 300;
@@ -8591,7 +8843,7 @@ async function maybeCheckForUpdate(options) {
8591
8843
  if (isDisabled(options)) return;
8592
8844
  const now = options.now?.() ?? /* @__PURE__ */ new Date();
8593
8845
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
8594
- const cachePath = options.cachePath ?? join35(homedir7(), ".agentwheel", "update-check.json");
8846
+ const cachePath = options.cachePath ?? join35(homedir8(), ".agentwheel", "update-check.json");
8595
8847
  try {
8596
8848
  const cached = await readCache(cachePath);
8597
8849
  if (cached && now.getTime() - Date.parse(cached.checkedAt) < ttlMs) {
@@ -8628,7 +8880,7 @@ async function fetchLatestVersion(fetchImpl, timeoutMs) {
8628
8880
  }
8629
8881
  async function readCache(path) {
8630
8882
  try {
8631
- const parsed = JSON.parse(await readFile20(path, "utf8"));
8883
+ const parsed = JSON.parse(await readFile21(path, "utf8"));
8632
8884
  if (typeof parsed.checkedAt !== "string" || typeof parsed.latest !== "string") return void 0;
8633
8885
  return { checkedAt: parsed.checkedAt, latest: parsed.latest };
8634
8886
  } catch {
@@ -8660,9 +8912,9 @@ function normalizeVersion(version) {
8660
8912
 
8661
8913
  // src/model/package-validate.ts
8662
8914
  import { stat as stat12 } from "fs/promises";
8663
- import { resolve as resolve16 } from "path";
8915
+ import { resolve as resolve17 } from "path";
8664
8916
  async function validatePackage(root) {
8665
- const packageRoot = resolve16(root);
8917
+ const packageRoot = resolve17(root);
8666
8918
  const findings = [];
8667
8919
  const manifestPath = await findPackageManifestPath(packageRoot);
8668
8920
  if (!manifestPath) {
@@ -8763,7 +9015,7 @@ async function validateManifestComposeInclude(packageRoot, selector, optional, f
8763
9015
  try {
8764
9016
  validateSelector(selector, "compose.include", findings, manifestPath, { fragmentsOnly: true, aliases });
8765
9017
  if (isCrossPackageSelector(selector)) return;
8766
- const full = resolve16(packageRoot, selector);
9018
+ const full = resolve17(packageRoot, selector);
8767
9019
  if (full !== packageRoot && !full.startsWith(`${packageRoot}/`)) {
8768
9020
  findings.push({ level: "error", message: `Compose include escapes package root: ${selector}`, path: manifestPath });
8769
9021
  return;
@@ -8816,11 +9068,11 @@ function isCrossPackageSelector(value) {
8816
9068
  }
8817
9069
 
8818
9070
  // src/model/package-migrate.ts
8819
- import { readFile as readFile21, rename as rename3, writeFile as writeFile12 } from "fs/promises";
8820
- import { join as join37, resolve as resolve17 } from "path";
9071
+ import { readFile as readFile22, rename as rename3, writeFile as writeFile12 } from "fs/promises";
9072
+ import { join as join37, resolve as resolve18 } from "path";
8821
9073
  import { applyEdits, modify, parse as parse2 } from "jsonc-parser";
8822
9074
  async function migratePackageManifest(root) {
8823
- const packageRoot = resolve17(root);
9075
+ const packageRoot = resolve18(root);
8824
9076
  for (const name of openPackManifestNames) {
8825
9077
  const path = join37(packageRoot, name);
8826
9078
  if (await pathExists(path)) {
@@ -8834,7 +9086,7 @@ async function migratePackageManifest(root) {
8834
9086
  const from = join37(packageRoot, legacyName);
8835
9087
  const toName = legacyName.endsWith(".jsonc") ? "openpack.jsonc" : "openpack.json";
8836
9088
  const to = join37(packageRoot, toName);
8837
- const content = await readFile21(from, "utf8");
9089
+ const content = await readFile22(from, "utf8");
8838
9090
  const updated = updateSchemaVersion(content);
8839
9091
  declareMutationPath(from);
8840
9092
  declareMutationPath(to);
@@ -8882,7 +9134,7 @@ function resolveCliVersion() {
8882
9134
  }
8883
9135
 
8884
9136
  // src/lifecycle/ownership.ts
8885
- import { resolve as resolve18 } from "path";
9137
+ import { resolve as resolve19 } from "path";
8886
9138
  async function planArtifactOwnershipHandoff(request) {
8887
9139
  return validateOwnershipHandoff(request, request.transport ?? localTransport);
8888
9140
  }
@@ -8895,7 +9147,9 @@ async function applyArtifactOwnershipHandoff(request) {
8895
9147
  const scope = { installationType: request.installationType, stateKey: request.stateKey };
8896
9148
  const lock = await acquireApplyLock(request.targetRoot, request.adapter, transport, {}, scope);
8897
9149
  try {
8898
- if (await readApplyJournal(request.targetRoot, request.adapter, transport, scope)) {
9150
+ if ((await listApplyJournals(request.targetRoot, request.adapter, transport, {
9151
+ installationType: request.installationType
9152
+ })).length > 0) {
8899
9153
  throw new Error("Cannot hand off ownership while an apply journal is pending. Recover or abort it first.");
8900
9154
  }
8901
9155
  const plan = await validateOwnershipHandoff(request, transport);
@@ -8997,12 +9251,17 @@ async function verifiedEntryHash(entry, destPath, transport) {
8997
9251
  }
8998
9252
  return currentHash;
8999
9253
  }
9254
+ async function verifyManifestEntryRuntime(targetRoot, entry, transport = localTransport) {
9255
+ const path = containedArtifactPath(targetRoot, entry.path);
9256
+ if (!await transport.pathExists(path)) throw new Error(`Managed artifact is missing: ${entry.path}`);
9257
+ return verifiedEntryHash(entry, path, transport);
9258
+ }
9000
9259
  function containedArtifactPath(targetRoot, relativePath) {
9001
9260
  if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\0")) {
9002
9261
  throw new Error(`Unsafe managed artifact path: ${relativePath}`);
9003
9262
  }
9004
- const root = resolve18(targetRoot);
9005
- const candidate = resolve18(root, relativePath);
9263
+ const root = resolve19(targetRoot);
9264
+ const candidate = resolve19(root, relativePath);
9006
9265
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
9007
9266
  throw new Error(`Managed artifact path escapes target root: ${relativePath}`);
9008
9267
  }
@@ -9033,10 +9292,246 @@ function assertManifestIdentity(manifest, request) {
9033
9292
  }
9034
9293
  }
9035
9294
 
9295
+ // src/lifecycle/ownership-retire-stale.ts
9296
+ import { createHash as createHash9 } from "crypto";
9297
+ import { resolve as resolve20 } from "path";
9298
+ async function planRetireStaleOwnership(request) {
9299
+ return observe(request, request.transport ?? localTransport);
9300
+ }
9301
+ async function applyRetireStaleOwnership(request) {
9302
+ assertApplyPreconditions(request);
9303
+ const transport = request.transport ?? localTransport;
9304
+ const lock = await acquireApplyLock(request.targetRoot, request.adapter, transport, {}, {
9305
+ installationType: request.installationType,
9306
+ stateKey: request.destinationStateKey
9307
+ });
9308
+ try {
9309
+ const pending = await listAllApplyJournals(request.targetRoot, request.adapter, transport);
9310
+ if (pending.length > 0) {
9311
+ throw new Error(`Cannot retire stale ownership while runtime apply journal(s) are pending: ${pending.map((item) => item.path).join(", ")}`);
9312
+ }
9313
+ const current = await observe(request, transport);
9314
+ assertExpected(request.planDigest, current.planDigest, "plan digest");
9315
+ assertExpected(request.expectedSourceRevision, current.source.revision, "source manifest revision");
9316
+ assertExpected(request.expectedDestinationRevision, current.destination.revision, "destination manifest revision");
9317
+ assertExpected(request.expectedInventoryRevision, current.manifestInventoryRevision, "manifest inventory revision");
9318
+ const source = await requireManifest(request, request.sourceStateKey, transport, "source");
9319
+ const selectedDigests = new Set(current.selected.map((entry) => entry.sourceEntryDigest));
9320
+ const retained = source.entries.filter((entry) => !selectedDigests.has(entryDigest(entry)));
9321
+ if (source.entries.length - retained.length !== current.selected.length) {
9322
+ throw new Error("Source manifest selection changed while locked; replan required.");
9323
+ }
9324
+ declareMutationPath(current.source.manifestPath);
9325
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9326
+ const mutation = mutationMetadataForApplyJournal();
9327
+ const journal = {
9328
+ version: mutation ? 2 : 1,
9329
+ ...mutation ? { mutation } : {},
9330
+ mode: "uninstall",
9331
+ adapter: request.adapter,
9332
+ installationType: request.installationType,
9333
+ stateKey: request.sourceStateKey,
9334
+ targetRoot: request.targetRoot,
9335
+ baseRevision: source.revision,
9336
+ createdAt: now,
9337
+ updatedAt: now,
9338
+ operations: [],
9339
+ completed: [],
9340
+ manifest: { ...source, entries: retained }
9341
+ };
9342
+ await writeApplyJournal(journal, transport);
9343
+ await commitManifestMetadataJournal(journal, transport);
9344
+ return {
9345
+ ...current,
9346
+ applied: true,
9347
+ sourceManifestRemoved: retained.length === 0,
9348
+ retainedSourceEntries: retained.length
9349
+ };
9350
+ } finally {
9351
+ await lock.release();
9352
+ }
9353
+ }
9354
+ async function observe(request, transport) {
9355
+ const normalized = normalizeRequest(request);
9356
+ const source = await requireManifest(normalized, normalized.sourceStateKey, transport, "source");
9357
+ const destination = await requireManifest(normalized, normalized.destinationStateKey, transport, "destination");
9358
+ const sourceOwner = workspaceOwnerForRoot(normalized.fromWorkspaceRoot);
9359
+ const destinationOwner = workspaceOwnerForRoot(normalized.toWorkspaceRoot, normalized.toFleetId);
9360
+ const destinationByPath = /* @__PURE__ */ new Map();
9361
+ for (const entry of destination.entries) {
9362
+ if (entry.workspaceOwner !== destinationOwner) continue;
9363
+ const entries = destinationByPath.get(entry.path) ?? [];
9364
+ entries.push(entry);
9365
+ destinationByPath.set(entry.path, entries);
9366
+ }
9367
+ const selected = [];
9368
+ for (const sourceEntry of source.entries) {
9369
+ if (sourceEntry.workspaceOwner !== sourceOwner) continue;
9370
+ const candidates = destinationByPath.get(sourceEntry.path) ?? [];
9371
+ if (candidates.length === 0) continue;
9372
+ if (candidates.length !== 1) {
9373
+ throw new Error(`Destination Fleet manifest has ambiguous ownership for ${sourceEntry.path}.`);
9374
+ }
9375
+ assertRetirableSourceEntry(sourceEntry);
9376
+ const destinationEntry = candidates[0];
9377
+ await assertContributionCoverage(sourceEntry, destinationEntry, normalized.targetRoot, transport);
9378
+ const runtimeHash = await verifyManifestEntryRuntime(normalized.targetRoot, destinationEntry, transport);
9379
+ selected.push({
9380
+ path: sourceEntry.path,
9381
+ sourceEntryDigest: entryDigest(sourceEntry),
9382
+ destinationEntryDigest: entryDigest(destinationEntry),
9383
+ runtimeHash
9384
+ });
9385
+ }
9386
+ selected.sort((a, b) => a.path.localeCompare(b.path) || a.sourceEntryDigest.localeCompare(b.sourceEntryDigest) || a.destinationEntryDigest.localeCompare(b.destinationEntryDigest));
9387
+ if (selected.length === 0) {
9388
+ throw new Error("No stale source ownership entries match an exact path in the destination Fleet manifest.");
9389
+ }
9390
+ const withoutDigest = {
9391
+ adapter: normalized.adapter,
9392
+ installationType: normalized.installationType,
9393
+ targetRoot: normalized.targetRoot,
9394
+ source: {
9395
+ stateKey: normalized.sourceStateKey,
9396
+ manifestPath: installManifestPath(normalized.targetRoot, normalized.adapter, {
9397
+ installationType: normalized.installationType,
9398
+ stateKey: normalized.sourceStateKey
9399
+ }),
9400
+ revision: source.revision,
9401
+ owner: sourceOwner
9402
+ },
9403
+ destination: {
9404
+ stateKey: normalized.destinationStateKey,
9405
+ manifestPath: installManifestPath(normalized.targetRoot, normalized.adapter, {
9406
+ installationType: normalized.installationType,
9407
+ stateKey: normalized.destinationStateKey
9408
+ }),
9409
+ revision: destination.revision,
9410
+ owner: destinationOwner,
9411
+ fleetId: normalized.toFleetId
9412
+ },
9413
+ manifestInventoryRevision: await computeInstallManifestInventoryRevision(
9414
+ normalized.targetRoot,
9415
+ normalized.adapter,
9416
+ transport
9417
+ ),
9418
+ selected
9419
+ };
9420
+ return { ...withoutDigest, planDigest: digest(withoutDigest) };
9421
+ }
9422
+ function normalizeRequest(request) {
9423
+ const targetRoot = resolve20(request.targetRoot);
9424
+ const sourceStateKey = exactStateKey(request.adapter, request.sourceStateKey, request.installationType, "source");
9425
+ const destinationStateKey = exactStateKey(request.adapter, request.destinationStateKey, request.installationType, "destination");
9426
+ if (sourceStateKey === destinationStateKey) throw new Error("Source and destination state keys must differ.");
9427
+ if (!request.toFleetId.trim()) throw new Error("Destination Fleet id is required.");
9428
+ return {
9429
+ ...request,
9430
+ targetRoot,
9431
+ installationType: request.installationType.trim(),
9432
+ sourceStateKey,
9433
+ destinationStateKey,
9434
+ fromWorkspaceRoot: resolve20(request.fromWorkspaceRoot),
9435
+ toWorkspaceRoot: resolve20(request.toWorkspaceRoot),
9436
+ toFleetId: request.toFleetId.trim()
9437
+ };
9438
+ }
9439
+ function exactStateKey(adapter, value, installationType, label) {
9440
+ if (!value || stateKeyFor(adapter, { installationType, stateKey: value }) !== value) {
9441
+ throw new Error(`The ${label} state key must be an exact canonical state key.`);
9442
+ }
9443
+ return value;
9444
+ }
9445
+ async function requireManifest(request, stateKey, transport, label) {
9446
+ const manifest = await readInstallManifest(request.targetRoot, request.adapter, transport, {
9447
+ installationType: request.installationType,
9448
+ stateKey
9449
+ });
9450
+ if (!manifest) throw new Error(`No ${label} install manifest for state key ${stateKey}.`);
9451
+ if (manifest.version !== 2) throw new Error(`${label} ownership retirement requires an Agentwheel v2 install manifest.`);
9452
+ const actualStateKey = stateKeyFor(manifest.adapter, {
9453
+ installationType: manifest.installationType,
9454
+ stateKey: manifest.stateKey
9455
+ });
9456
+ if (manifest.adapter !== request.adapter || manifest.installationType !== request.installationType || resolve20(manifest.targetRoot) !== request.targetRoot || actualStateKey !== stateKey) {
9457
+ throw new Error(`${label} install manifest identity does not match the requested adapter, target, installation type, and state key.`);
9458
+ }
9459
+ return manifest;
9460
+ }
9461
+ function assertRetirableSourceEntry(entry) {
9462
+ if (entry.semanticPlugin || entry.semanticCommand || entry.executed) {
9463
+ throw new Error(`Cannot retire semantic source ownership at ${entry.path}.`);
9464
+ }
9465
+ if (entry.mergeStrategy && !entry.mergeRemoval) {
9466
+ throw new Error(`Cannot retire incomplete merge ownership at ${entry.path}.`);
9467
+ }
9468
+ }
9469
+ async function assertContributionCoverage(source, destination, targetRoot, transport) {
9470
+ const sourceManagedBlock = source.mode === "managed-block";
9471
+ const destinationManagedBlock = destination.mode === "managed-block";
9472
+ const sourceMerge = source.mergeStrategy !== void 0;
9473
+ const destinationMerge = destination.mergeStrategy !== void 0;
9474
+ const sourceCategory = sourceManagedBlock ? "managed-block" : sourceMerge ? "merge" : "plain";
9475
+ const destinationCategory = destinationManagedBlock ? "managed-block" : destinationMerge ? "merge" : "plain";
9476
+ if (sourceCategory !== destinationCategory) {
9477
+ throw new Error(
9478
+ `Destination Fleet ownership category ${destinationCategory} cannot replace stale source category ${sourceCategory} at ${source.path}.`
9479
+ );
9480
+ }
9481
+ if (sourceCategory === "plain") return;
9482
+ if (sourceManagedBlock) {
9483
+ const sourceSelector = managedInstructionSelector(source.logicalSelector, source.artifactType, source.artifactName);
9484
+ const destinationSelector = managedInstructionSelector(destination.logicalSelector, destination.artifactType, destination.artifactName);
9485
+ if (!sourceManagedBlock || !destinationManagedBlock || sourceSelector !== destinationSelector) {
9486
+ throw new Error(`Destination Fleet does not exactly cover stale source managed block contribution at ${source.path}.`);
9487
+ }
9488
+ return;
9489
+ }
9490
+ if (source.mergeStrategy && destination.mergeStrategy) {
9491
+ if (!source.mergeRemoval) throw new Error(`Cannot retire incomplete merge ownership at ${source.path}.`);
9492
+ const exactlyCovered = destination.mergeStrategy === source.mergeStrategy && destination.mergeRemoval && contributionSelector(source) === contributionSelector(destination) && canonicalJson(source.mergeRemoval) === canonicalJson(destination.mergeRemoval);
9493
+ if (exactlyCovered) return;
9494
+ const current = await transport.readFile(containedArtifactPath(targetRoot, source.path));
9495
+ if (!mergeContributionAbsent(source.mergeRemoval, source.mergeStrategy, current)) {
9496
+ throw new Error(`Destination Fleet does not exactly cover or replace stale source merge contribution at ${source.path}.`);
9497
+ }
9498
+ }
9499
+ }
9500
+ function contributionSelector(entry) {
9501
+ return entry.logicalSelector ?? `${entry.artifactType}/${entry.artifactName}`;
9502
+ }
9503
+ function assertApplyPreconditions(request) {
9504
+ for (const [label, value] of [
9505
+ ["plan digest", request.planDigest],
9506
+ ["source manifest revision", request.expectedSourceRevision],
9507
+ ["destination manifest revision", request.expectedDestinationRevision],
9508
+ ["manifest inventory revision", request.expectedInventoryRevision]
9509
+ ]) {
9510
+ if (!value || !/^[a-f0-9]{64}$/.test(value)) {
9511
+ throw new Error(`Applying stale ownership retirement requires a reviewed lowercase SHA-256 ${label}.`);
9512
+ }
9513
+ }
9514
+ }
9515
+ function assertExpected(expected, current, label) {
9516
+ if (expected !== current) throw new Error(`Stale ${label}: expected ${expected}, found ${current}; replan required.`);
9517
+ }
9518
+ function entryDigest(entry) {
9519
+ return digest(entry);
9520
+ }
9521
+ function digest(value) {
9522
+ return createHash9("sha256").update(canonicalJson(value)).digest("hex");
9523
+ }
9524
+ function canonicalJson(value) {
9525
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
9526
+ if (!value || typeof value !== "object") return JSON.stringify(value);
9527
+ const record = value;
9528
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
9529
+ }
9530
+
9036
9531
  // src/version/policy.ts
9037
9532
  import { execFile as execFile3 } from "child_process";
9038
- import { readFile as readFile22 } from "fs/promises";
9039
- import { join as join39, resolve as resolve19 } from "path";
9533
+ import { readFile as readFile23 } from "fs/promises";
9534
+ import { join as join39, resolve as resolve21 } from "path";
9040
9535
  import { promisify as promisify3 } from "util";
9041
9536
  import { parse as parseJsonc } from "jsonc-parser";
9042
9537
  import { z as z5 } from "zod";
@@ -9130,7 +9625,7 @@ async function discoverVersionsFromSource(pkg, workspaceRoot) {
9130
9625
  if (tagged.length > 0) return tagged;
9131
9626
  }
9132
9627
  if (driverName === "local") {
9133
- const root = resolve19(workspaceRoot, pkg.source);
9628
+ const root = resolve21(workspaceRoot, pkg.source);
9134
9629
  const manifest2 = await readPackageManifest(root);
9135
9630
  const current = manifest2 ? [{ version: manifest2.version, ref: pkg.requestedRef ?? root }] : [];
9136
9631
  try {
@@ -9244,7 +9739,7 @@ function versionCachePath(workspaceRoot) {
9244
9739
  async function readVersionCache(path) {
9245
9740
  if (!await pathExists(path)) return { schemaVersion: 1, sources: {} };
9246
9741
  try {
9247
- return versionCacheSchema.parse(JSON.parse(await readFile22(path, "utf8")));
9742
+ return versionCacheSchema.parse(JSON.parse(await readFile23(path, "utf8")));
9248
9743
  } catch {
9249
9744
  return { schemaVersion: 1, sources: {} };
9250
9745
  }
@@ -9252,8 +9747,8 @@ async function readVersionCache(path) {
9252
9747
 
9253
9748
  // src/profile/members.ts
9254
9749
  import { execFile as execFile4 } from "child_process";
9255
- import { readFile as readFile23 } from "fs/promises";
9256
- import { join as join40, resolve as resolve20 } from "path";
9750
+ import { readFile as readFile24 } from "fs/promises";
9751
+ import { join as join40, resolve as resolve22 } from "path";
9257
9752
  import { promisify as promisify4 } from "util";
9258
9753
  import { z as z7 } from "zod";
9259
9754
 
@@ -9428,7 +9923,7 @@ async function invokeMemberStatus(member, parentWorkspace, chain, options, cliEn
9428
9923
  let stderr = "";
9429
9924
  try {
9430
9925
  if (member.transport === "local") {
9431
- const workspace = resolve20(parentWorkspace, member.workspace);
9926
+ const workspace = resolve22(parentWorkspace, member.workspace);
9432
9927
  const result = await execFileAsync4(process.execPath, [cliEntry, ...args], {
9433
9928
  cwd: workspace,
9434
9929
  env: env2,
@@ -9474,7 +9969,7 @@ async function runMemberAgentwheel(member, parentWorkspace, args, chain) {
9474
9969
  process.execPath,
9475
9970
  [process.argv[1], "--no-update-check", ...args],
9476
9971
  {
9477
- cwd: resolve20(parentWorkspace, member.workspace),
9972
+ cwd: resolve22(parentWorkspace, member.workspace),
9478
9973
  env: env2,
9479
9974
  maxBuffer: 20 * 1024 * 1024
9480
9975
  }
@@ -9554,7 +10049,7 @@ function memberCachePath(workspaceRoot, profileName, memberId) {
9554
10049
  async function readMemberCache(path) {
9555
10050
  if (!await pathExists(path)) return void 0;
9556
10051
  try {
9557
- return memberCacheSchema.parse(JSON.parse(await readFile23(path, "utf8")));
10052
+ return memberCacheSchema.parse(JSON.parse(await readFile24(path, "utf8")));
9558
10053
  } catch {
9559
10054
  return void 0;
9560
10055
  }
@@ -9575,7 +10070,7 @@ function assertNoCompositeCycle(workspaceRoot, profileName, chain) {
9575
10070
  }
9576
10071
  }
9577
10072
  function compositeKey(workspaceRoot, profileName) {
9578
- return `${resolve20(workspaceRoot)}#${profileName}`;
10073
+ return `${resolve22(workspaceRoot)}#${profileName}`;
9579
10074
  }
9580
10075
 
9581
10076
  // src/status/repository.ts
@@ -9622,9 +10117,9 @@ function valueAfter(lines, prefix) {
9622
10117
  }
9623
10118
 
9624
10119
  // src/catalogue/client.ts
9625
- import { createHash as createHash9 } from "crypto";
9626
- import { readFile as readFile24, rm as rm8 } from "fs/promises";
9627
- import { homedir as homedir8 } from "os";
10120
+ import { createHash as createHash10 } from "crypto";
10121
+ import { readFile as readFile25, rm as rm8 } from "fs/promises";
10122
+ import { homedir as homedir9 } from "os";
9628
10123
  import { join as join41 } from "path";
9629
10124
 
9630
10125
  // src/model/catalogue.ts
@@ -9841,7 +10336,7 @@ var CatalogueClient = class {
9841
10336
  async readCache() {
9842
10337
  if (!await pathExists(this.cachePath)) return void 0;
9843
10338
  try {
9844
- const value = JSON.parse(await readFile24(this.cachePath, "utf8"));
10339
+ const value = JSON.parse(await readFile25(this.cachePath, "utf8"));
9845
10340
  const envelope = catalogueCacheEnvelopeSchema.parse(value);
9846
10341
  if (envelope.contentHash) {
9847
10342
  const contentHash = catalogueContentHash(envelope.enriched, envelope.vercel);
@@ -9897,18 +10392,18 @@ var CatalogueClient = class {
9897
10392
  }
9898
10393
  return {
9899
10394
  value: schema.parse(value),
9900
- digest: createHash9("sha256").update(bytes).digest("hex")
10395
+ digest: createHash10("sha256").update(bytes).digest("hex")
9901
10396
  };
9902
10397
  }
9903
10398
  };
9904
10399
  function defaultCatalogueCachePath() {
9905
- return join41(homedir8(), ".agentwheel", "catalogue-cache.json");
10400
+ return join41(homedir9(), ".agentwheel", "catalogue-cache.json");
9906
10401
  }
9907
10402
  function sameSources2(a, b) {
9908
10403
  return a.length === b.length && a.every((source, index) => source === b[index]);
9909
10404
  }
9910
10405
  function catalogueContentHash(enriched, vercel) {
9911
- return createHash9("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
10406
+ return createHash10("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
9912
10407
  }
9913
10408
 
9914
10409
  // src/search/index.ts
@@ -10270,8 +10765,8 @@ function includesToken(normalizedText, token) {
10270
10765
  }
10271
10766
 
10272
10767
  // src/semantic/index.ts
10273
- import { createHash as createHash10 } from "crypto";
10274
- import { homedir as homedir9 } from "os";
10768
+ import { createHash as createHash11 } from "crypto";
10769
+ import { homedir as homedir10 } from "os";
10275
10770
  import { join as join42 } from "path";
10276
10771
  import { env, pipeline } from "@huggingface/transformers";
10277
10772
  var DEFAULT_SEMANTIC_INDEX_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-semantic-index/gte-v1/";
@@ -10360,13 +10855,13 @@ var SemanticSearchClient = class {
10360
10855
  if (!response.ok) throw new Error(`Semantic index file failed (${response.status}): ${descriptor.path}`);
10361
10856
  const bytes = new Uint8Array(await response.arrayBuffer());
10362
10857
  if (bytes.byteLength !== descriptor.bytes) throw new Error(`Semantic index file size does not match: ${descriptor.path}`);
10363
- const digest = createHash10("sha256").update(bytes).digest("hex");
10364
- if (digest !== descriptor.sha256) throw new Error(`Semantic index checksum does not match: ${descriptor.path}`);
10858
+ const digest2 = createHash11("sha256").update(bytes).digest("hex");
10859
+ if (digest2 !== descriptor.sha256) throw new Error(`Semantic index checksum does not match: ${descriptor.path}`);
10365
10860
  return bytes;
10366
10861
  }
10367
10862
  };
10368
10863
  async function embedQuery(query) {
10369
- env.cacheDir = join42(homedir9(), ".agentwheel", "semantic-models");
10864
+ env.cacheDir = join42(homedir10(), ".agentwheel", "semantic-models");
10370
10865
  const extractor = await pipeline("feature-extraction", CONTRACT.model.id, {
10371
10866
  revision: CONTRACT.model.revision,
10372
10867
  dtype: CONTRACT.model.dtype,
@@ -10450,8 +10945,8 @@ function ensureTrailingSlash(value) {
10450
10945
  }
10451
10946
 
10452
10947
  // src/trial/skill.ts
10453
- import { createHash as createHash11 } from "crypto";
10454
- import { readFile as readFile25, stat as stat13 } from "fs/promises";
10948
+ import { createHash as createHash12 } from "crypto";
10949
+ import { readFile as readFile26, stat as stat13 } from "fs/promises";
10455
10950
  import { join as join43 } from "path";
10456
10951
  import { parse as parseYaml2 } from "yaml";
10457
10952
  var MAX_TRIAL_SKILL_BYTES = 512 * 1024;
@@ -10469,7 +10964,7 @@ async function createSkillTrial(driver, resolved, selectors) {
10469
10964
  if (info.size > MAX_TRIAL_SKILL_BYTES) {
10470
10965
  throw new Error(`Skill trial exceeds the ${MAX_TRIAL_SKILL_BYTES / 1024} KiB content limit.`);
10471
10966
  }
10472
- const content = await readFile25(path, "utf8");
10967
+ const content = await readFile26(path, "utf8");
10473
10968
  const frontmatter = readSkillFrontmatter(content, artifact);
10474
10969
  return {
10475
10970
  schemaVersion: 1,
@@ -10480,7 +10975,7 @@ async function createSkillTrial(driver, resolved, selectors) {
10480
10975
  skill: {
10481
10976
  name: artifact.name,
10482
10977
  relativePath: artifact.relativePath,
10483
- sha256: createHash11("sha256").update(content).digest("hex"),
10978
+ sha256: createHash12("sha256").update(content).digest("hex"),
10484
10979
  frontmatter,
10485
10980
  content
10486
10981
  }
@@ -10498,168 +10993,13 @@ function readSkillFrontmatter(content, artifact) {
10498
10993
  return { name: value.name, description: value.description };
10499
10994
  }
10500
10995
 
10501
- // src/model/fleet.ts
10502
- import { lstat as lstat2, readFile as readFile26, realpath } from "fs/promises";
10503
- import { homedir as homedir10 } from "os";
10504
- import { isAbsolute as isAbsolute2, resolve as resolve21 } from "path";
10505
- async function resolveWorkspaceScope(request = {}) {
10506
- const selected = [request.user === true, request.local === true, request.fleet !== void 0].filter(Boolean).length;
10507
- if (selected > 1) {
10508
- throw new Error("Choose exactly one workspace selector: --user, --local, or --fleet <id>.");
10509
- }
10510
- const globalRoot = resolve21(request.globalRoot ?? homedir10());
10511
- if (request.user) {
10512
- const config2 = await readWorkspaceConfig(globalRoot);
10513
- assertNonFleetScope("user", config2);
10514
- return { kind: "user", root: globalRoot, config: config2 };
10515
- }
10516
- if (request.fleet !== void 0) {
10517
- const id = fleetIdSchema.parse(request.fleet);
10518
- const registration = await showRegisteredFleet(id, { globalRoot });
10519
- const root = await assertCanonicalDirectory(registration.root, `Registered fleet '${id}' root`);
10520
- const config2 = await readRequiredConfig(root, `fleet '${id}'`);
10521
- assertFleetContract(id, registration, config2);
10522
- return { kind: "fleet", root, fleetId: id, config: config2 };
10523
- }
10524
- const cwd = resolve21(request.cwd ?? process.cwd());
10525
- const discoveredRoot = await findExistingWorkspaceRoot(cwd);
10526
- if (!discoveredRoot || discoveredRoot === globalRoot) {
10527
- if (request.local === true && cwd !== globalRoot) {
10528
- return { kind: "local", root: cwd, config: await readWorkspaceConfig(cwd) };
10529
- }
10530
- throw missingScopeError(request.local === true ? "local" : "implicit local");
10531
- }
10532
- const config = await readRequiredConfig(discoveredRoot, "local");
10533
- assertNonFleetScope("local", config);
10534
- return { kind: "local", root: discoveredRoot, config };
10535
- }
10536
- async function registerFleet(request) {
10537
- const id = fleetIdSchema.parse(request.id);
10538
- const requiredPackages = sortedUnique5(request.requiredPackages);
10539
- if (requiredPackages.length === 0) throw new Error("Fleet registration requires at least one --required-package <name>.");
10540
- const globalRoot = resolve21(request.globalRoot ?? homedir10());
10541
- const home = await readWorkspaceConfig(globalRoot);
10542
- if (supportsFleetConfig(home) && home.fleetId) {
10543
- throw new Error(`The home config is fleet '${home.fleetId}', so it cannot own the global fleet registry.`);
10544
- }
10545
- const existing = supportsFleetConfig(home) ? await canonicalizeExistingFleetRoots(home.fleets, globalRoot) : {};
10546
- if (existing[id]) throw new Error(`Fleet '${id}' is already registered.`);
10547
- const root = await assertCanonicalDirectory(request.root, `Fleet '${id}' root`);
10548
- const duplicateRoot = Object.entries(existing).find(([, value]) => value.root === root);
10549
- if (duplicateRoot) throw new Error(`Fleet root ${root} is already registered as '${duplicateRoot[0]}'.`);
10550
- if (root === globalRoot) throw new Error("A fleet root must be outside the user config root contract.");
10551
- const target = await readRequiredConfig(root, `fleet '${id}'`);
10552
- const registration = registeredFleetSchema.parse({ root, requiredPackages });
10553
- assertFleetContract(id, registration, target);
10554
- const upgraded = workspaceConfigSchema.parse({
10555
- ...home,
10556
- schemaVersion: CURRENT_WORKSPACE_SCHEMA_VERSION,
10557
- exports: home.schemaVersion >= 2 ? home.exports : { selections: {} },
10558
- fleets: { ...existing, [id]: registration }
10559
- });
10560
- const configPath = globalWorkspaceConfigPath(globalRoot);
10561
- declareMutationPath(configPath);
10562
- await writeJsonAtomic(configPath, upgraded);
10563
- return { id, ...registration };
10564
- }
10565
- async function canonicalizeExistingFleetRoots(fleets, globalRoot) {
10566
- const canonical = /* @__PURE__ */ new Map();
10567
- const normalized = {};
10568
- for (const [id, registration] of Object.entries(fleets).sort(([a], [b]) => a.localeCompare(b))) {
10569
- const root = await canonicalizeDirectory(registration.root, `Registered fleet '${id}' root`);
10570
- if (root === globalRoot) throw new Error(`Registered fleet '${id}' root must be outside the user config root contract.`);
10571
- const incumbent = canonical.get(root);
10572
- if (incumbent) {
10573
- throw new Error(`Fleet root ${root} is registered more than once as '${incumbent}' and '${id}'.`);
10574
- }
10575
- canonical.set(root, id);
10576
- normalized[id] = registeredFleetSchema.parse({ ...registration, root });
10577
- }
10578
- return normalized;
10579
- }
10580
- async function listRegisteredFleets(options = {}) {
10581
- const globalRoot = resolve21(options.globalRoot ?? homedir10());
10582
- const home = await readWorkspaceConfig(globalRoot);
10583
- if (!supportsFleetConfig(home)) return [];
10584
- return Object.entries(home.fleets).sort(([a], [b]) => a.localeCompare(b)).map(([id, registration]) => ({ id, ...registration }));
10585
- }
10586
- async function showRegisteredFleet(idInput, options = {}) {
10587
- const id = fleetIdSchema.parse(idInput);
10588
- const fleets = await listRegisteredFleets(options);
10589
- const registration = fleets.find((candidate) => candidate.id === id);
10590
- if (!registration) {
10591
- throw new Error(`Unknown fleet '${id}'. Use 'agentwheel fleet list' or register it with 'agentwheel fleet register'.`);
10592
- }
10593
- return registration;
10594
- }
10595
- async function readRequiredConfig(root, label) {
10596
- const path = workspaceConfigPath(root);
10597
- if (!await pathExists(path)) throw missingScopeError(label);
10598
- try {
10599
- return workspaceConfigSchema.parse(JSON.parse(await readFile26(path, "utf8")));
10600
- } catch (error) {
10601
- throw new Error(`Invalid ${label} Agentwheel config at ${path}: ${error instanceof Error ? error.message : String(error)}`);
10602
- }
10603
- }
10604
- function assertFleetContract(id, registration, config) {
10605
- if (!supportsFleetConfig(config)) {
10606
- throw new Error(`Fleet '${id}' config must use schemaVersion 3 or newer before registration or selection.`);
10607
- }
10608
- if (config.fleetId !== id) {
10609
- throw new Error(`Fleet fleetId mismatch: expected '${id}', found '${config.fleetId ?? "missing"}'.`);
10610
- }
10611
- if (Object.keys(config.fleets).length > 0) {
10612
- throw new Error(`Fleet '${id}' config may not contain the home fleets registry.`);
10613
- }
10614
- const names = new Set(config.packages.map((pkg) => pkg.name));
10615
- for (const packageName of registration.requiredPackages) {
10616
- if (!names.has(packageName)) throw new Error(`Fleet '${id}' is missing required package '${packageName}'.`);
10617
- }
10618
- }
10619
- async function assertCanonicalDirectory(input, label) {
10620
- const canonical = await canonicalizeDirectory(input, label);
10621
- const normalized = resolve21(input);
10622
- const stats = await lstat2(input);
10623
- if (stats.isSymbolicLink() || canonical !== normalized || input !== normalized) {
10624
- throw new Error(`${label} must be canonical and symlink-unambiguous: ${input} resolves to ${canonical}.`);
10625
- }
10626
- return canonical;
10627
- }
10628
- async function canonicalizeDirectory(input, label) {
10629
- if (!isAbsolute2(input)) throw new Error(`${label} must be an absolute canonical path.`);
10630
- let canonical;
10631
- try {
10632
- canonical = await realpath(input);
10633
- } catch {
10634
- throw new Error(`${label} does not exist: ${input}`);
10635
- }
10636
- const stats = await lstat2(canonical);
10637
- if (!stats.isDirectory()) throw new Error(`${label} is not a directory: ${input}`);
10638
- return canonical;
10639
- }
10640
- function missingScopeError(label) {
10641
- return new Error(
10642
- `Missing ${label} Agentwheel workspace config. Select --user, --local, or --fleet <id>, or run 'agentwheel init workspace' in the intended local root. Agentwheel never falls back to home desired state.`
10643
- );
10644
- }
10645
- function assertNonFleetScope(kind, config) {
10646
- if (supportsFleetConfig(config) && config.fleetId) {
10647
- throw new Error(
10648
- `The ${kind} config declares fleetId '${config.fleetId}'. Select it through the registered --fleet <id> scope.`
10649
- );
10650
- }
10651
- }
10652
- function sortedUnique5(values) {
10653
- return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
10654
- }
10655
-
10656
10996
  // src/lifecycle/fleet-normalize.ts
10657
- import { createHash as createHash12 } from "crypto";
10997
+ import { createHash as createHash13 } from "crypto";
10658
10998
  import { readFile as readFile27, readdir as readdir9, rm as rm9 } from "fs/promises";
10659
10999
  import { homedir as homedir11 } from "os";
10660
- import { basename as basename19, isAbsolute as isAbsolute3, join as join44, relative as relative7, resolve as resolve22 } from "path";
11000
+ import { basename as basename19, isAbsolute as isAbsolute3, join as join44, relative as relative8, resolve as resolve23 } from "path";
10661
11001
  async function planFleetNormalization(request) {
10662
- const normalizedRequest = normalizeRequest(request);
11002
+ const normalizedRequest = normalizeRequest2(request);
10663
11003
  const destinationScope = await resolveWorkspaceScope({
10664
11004
  fleet: normalizedRequest.destinationFleet,
10665
11005
  globalRoot: normalizedRequest.globalRoot
@@ -10680,8 +11020,8 @@ async function planFleetNormalization(request) {
10680
11020
  if (!sourcePackage || !destinationPackage) {
10681
11021
  throw new Error(`Package '${name}' must be an existing duplicate in both source and destination fleets.`);
10682
11022
  }
10683
- const sourceDeclaration = canonicalJson(sourcePackage);
10684
- const destinationDeclaration = canonicalJson(destinationPackage);
11023
+ const sourceDeclaration = canonicalJson2(sourcePackage);
11024
+ const destinationDeclaration = canonicalJson2(destinationPackage);
10685
11025
  if (sourceDeclaration !== destinationDeclaration) {
10686
11026
  throw new Error(
10687
11027
  `Package '${name}' has divergent declarations. Source, driver, adapter, adapter configuration, installation type, version policy, selections, aliases, overrides, and suggestions must match before normalization.`
@@ -10718,13 +11058,13 @@ async function planFleetNormalization(request) {
10718
11058
  packages,
10719
11059
  installedState
10720
11060
  };
10721
- return { ...planWithoutDigest, planDigest: sha2564(canonicalJson(planWithoutDigest)) };
11061
+ return { ...planWithoutDigest, planDigest: sha2564(canonicalJson2(planWithoutDigest)) };
10722
11062
  }
10723
11063
  async function applyFleetNormalization(request, hooks = {}) {
10724
11064
  if (!request.apply) throw new Error("Fleet normalization is dry-run by default; pass --apply with --plan-digest <digest>.");
10725
11065
  if (!request.planDigest) throw new Error("Applying fleet normalization requires --plan-digest <digest> from a reviewed dry-run.");
10726
11066
  assertDigest(request.planDigest);
10727
- const normalizedRequest = normalizeRequest(request);
11067
+ const normalizedRequest = normalizeRequest2(request);
10728
11068
  const roots = await normalizationRoots(normalizedRequest);
10729
11069
  const ordered = [.../* @__PURE__ */ new Set([roots.source, roots.destination])].sort((a, b) => a.localeCompare(b));
10730
11070
  return withOrderedLocks(ordered, async () => {
@@ -10856,14 +11196,14 @@ async function assertNormalizationPostconditions(journal) {
10856
11196
  }
10857
11197
  for (const graphLock of journal.graphLocks) {
10858
11198
  const raw = await readOptionalRecord(graphLock.path);
10859
- const digest = raw ? sha2564(canonicalJson(await readGraphLock(graphLock.path))) : null;
10860
- if (digest !== graphLock.afterDigest) {
11199
+ const digest2 = raw ? sha2564(canonicalJson2(await readGraphLock(graphLock.path))) : null;
11200
+ if (digest2 !== graphLock.afterDigest) {
10861
11201
  throw new Error(`Fleet normalization graph-lock postcheck failed: ${graphLock.path}`);
10862
11202
  }
10863
11203
  }
10864
11204
  }
10865
11205
  async function recoverFleetNormalization(request) {
10866
- const normalizedRequest = normalizeRequest(request);
11206
+ const normalizedRequest = normalizeRequest2(request);
10867
11207
  const roots = await normalizationRoots(normalizedRequest);
10868
11208
  const ordered = [.../* @__PURE__ */ new Set([roots.source, roots.destination])].sort((a, b) => a.localeCompare(b));
10869
11209
  return withOrderedLocks(ordered, async () => {
@@ -10906,7 +11246,7 @@ async function assertSourceFleetPostcondition(request, source, selectedPackages)
10906
11246
  );
10907
11247
  }
10908
11248
  }
10909
- function normalizeRequest(request) {
11249
+ function normalizeRequest2(request) {
10910
11250
  const packages = request.packages === void 0 ? void 0 : sortedUnique6(request.packages);
10911
11251
  if (request.packages !== void 0 && packages?.length === 0) throw new Error("--package requires at least one package name.");
10912
11252
  const artifacts = request.artifacts === void 0 ? void 0 : sortedUnique6(request.artifacts.map((artifact) => normalizeArtifactSelector(artifact)));
@@ -10928,17 +11268,17 @@ function normalizeRequest(request) {
10928
11268
  ...profile ? { profile } : {},
10929
11269
  ...agent ? { agent } : {},
10930
11270
  ...orphanedOwnerRoots ? { orphanedOwnerRoots } : {},
10931
- ...request.globalRoot ? { globalRoot: resolve22(request.globalRoot) } : {}
11271
+ ...request.globalRoot ? { globalRoot: resolve23(request.globalRoot) } : {}
10932
11272
  };
10933
11273
  }
10934
11274
  function normalizeOrphanedOwnerRoot(value) {
10935
11275
  const root = value.trim();
10936
11276
  if (!root || !isAbsolute3(root)) throw new Error(`Invalid orphaned owner root '${value}'; expected an absolute workspace path.`);
10937
- return resolve22(root);
11277
+ return resolve23(root);
10938
11278
  }
10939
11279
  async function normalizationRoots(request) {
10940
11280
  const destination = await showRegisteredFleet(request.destinationFleet, { globalRoot: request.globalRoot });
10941
- const source = request.from === "user" ? resolve22(request.globalRoot ?? homedir11()) : (await showRegisteredFleet(request.from.slice("fleet:".length), { globalRoot: request.globalRoot })).root;
11281
+ const source = request.from === "user" ? resolve23(request.globalRoot ?? homedir11()) : (await showRegisteredFleet(request.from.slice("fleet:".length), { globalRoot: request.globalRoot })).root;
10942
11282
  return { source, destination: destination.root };
10943
11283
  }
10944
11284
  async function withOrderedLocks(roots, fn) {
@@ -11029,10 +11369,10 @@ async function inspectInstalledState(source, destination, packageNames) {
11029
11369
  const destinationGraphAfter = destinationGraph?.lock ?? destinationGraphFromSource(sourceGraph.lock, destinationState.targetFingerprint);
11030
11370
  graphTransfers.push({
11031
11371
  sourceGraphLockPath: sourceGraph.path,
11032
- sourceGraphLockDigest: sha2564(canonicalJson(sourceGraph.lock)),
11372
+ sourceGraphLockDigest: sha2564(canonicalJson2(sourceGraph.lock)),
11033
11373
  destinationGraphLockPath: destinationState.graphLockPath,
11034
- destinationGraphLockDigest: destinationGraph ? sha2564(canonicalJson(destinationGraph.lock)) : null,
11035
- destinationGraphLockAfterDigest: sha2564(canonicalJson(destinationGraphAfter)),
11374
+ destinationGraphLockDigest: destinationGraph ? sha2564(canonicalJson2(destinationGraph.lock)) : null,
11375
+ destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraphAfter)),
11036
11376
  targetKey: sourceGraph.targetKey,
11037
11377
  adapter: destinationState.adapter,
11038
11378
  targetFingerprint: destinationState.targetFingerprint
@@ -11245,7 +11585,7 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11245
11585
  )
11246
11586
  });
11247
11587
  if (!selectedArtifacts) {
11248
- const fullDigest = sha2564(canonicalJson(graph.lock));
11588
+ const fullDigest = sha2564(canonicalJson2(graph.lock));
11249
11589
  const existingDestinationGraph = graphCandidates.find((candidate) => candidate.path === destinationState.graphLockPath);
11250
11590
  if (existingDestinationGraph) assertGraphIsSubset(graph, existingDestinationGraph);
11251
11591
  const destinationGraph = destinationGraphFromSource(graph.lock, destinationState.targetFingerprint);
@@ -11253,8 +11593,8 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11253
11593
  sourceGraphLockPath: graph.path,
11254
11594
  sourceGraphLockDigest: fullDigest,
11255
11595
  destinationGraphLockPath: destinationState.graphLockPath,
11256
- destinationGraphLockDigest: existingDestinationGraph ? sha2564(canonicalJson(existingDestinationGraph.lock)) : null,
11257
- destinationGraphLockAfterDigest: sha2564(canonicalJson(destinationGraph)),
11596
+ destinationGraphLockDigest: existingDestinationGraph ? sha2564(canonicalJson2(existingDestinationGraph.lock)) : null,
11597
+ destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraph)),
11258
11598
  targetKey: graph.targetKey,
11259
11599
  adapter: graph.adapter,
11260
11600
  targetFingerprint: destinationState.targetFingerprint
@@ -11312,7 +11652,7 @@ function targetKeysForAgent(scope, name) {
11312
11652
  async function runtimeRoots(scope) {
11313
11653
  const roots = /* @__PURE__ */ new Set([scope.root]);
11314
11654
  if (scope.config.packages.some((pkg) => pkg.installationType === "user") || Object.values(scope.config.agents).some((agent) => agent.installationType === "user")) {
11315
- roots.add(resolve22(process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir11()));
11655
+ roots.add(resolve23(process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir11()));
11316
11656
  }
11317
11657
  for (const [name, agent] of Object.entries(scope.config.agents)) {
11318
11658
  if (agent.transport === "ssh") throw new Error(`Installed-state normalization cannot inspect SSH agent '${name}' locally.`);
@@ -11349,14 +11689,14 @@ async function collectManifests(roots) {
11349
11689
  }
11350
11690
  continue;
11351
11691
  }
11352
- if (resolve22(parsed.targetRoot) !== resolve22(root)) {
11692
+ if (resolve23(parsed.targetRoot) !== resolve23(root)) {
11353
11693
  throw new Error(`Install manifest target root does not match its enumerated runtime root: ${path}`);
11354
11694
  }
11355
11695
  const expectedPath = installManifestPath(parsed.targetRoot, parsed.adapter, {
11356
11696
  installationType: parsed.installationType,
11357
11697
  stateKey: parsed.stateKey
11358
11698
  });
11359
- if (resolve22(expectedPath) !== resolve22(path)) {
11699
+ if (resolve23(expectedPath) !== resolve23(path)) {
11360
11700
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11361
11701
  }
11362
11702
  found.set(path, { path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11377,7 +11717,7 @@ async function collectManifestPaths(paths) {
11377
11717
  installationType: parsed.installationType,
11378
11718
  stateKey: parsed.stateKey
11379
11719
  });
11380
- if (resolve22(expectedPath) !== resolve22(path)) {
11720
+ if (resolve23(expectedPath) !== resolve23(path)) {
11381
11721
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11382
11722
  }
11383
11723
  found.push({ path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11403,7 +11743,7 @@ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include)
11403
11743
  if (!path.endsWith(".graph-lock.json")) continue;
11404
11744
  const lock = await readGraphLock(path);
11405
11745
  if (!lock.canonical.roots.some((candidate2) => selected.has(candidate2.rootId))) continue;
11406
- const parts = relative7(root, path).split(/[\\/]/);
11746
+ const parts = relative8(root, path).split(/[\\/]/);
11407
11747
  if (parts.length !== 3) throw new Error(`Graph lock path is not canonical: ${path}`);
11408
11748
  const [targetKey2, adapter, fileName] = parts;
11409
11749
  if (targetKeys && !targetKeys.has(targetKey2)) continue;
@@ -11424,7 +11764,7 @@ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include)
11424
11764
  const projection = relevantGraphProjection(lock, selected);
11425
11765
  results.push({
11426
11766
  ...candidate,
11427
- digest: sha2564(canonicalJson(projection)),
11767
+ digest: sha2564(canonicalJson2(projection)),
11428
11768
  artifactIdentities: lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => ownerMatchesGraphPackage(lock, owner, selected))).map(graphArtifactIdentity).sort()
11429
11769
  });
11430
11770
  }
@@ -11460,7 +11800,7 @@ async function legacyManifestPathForConfiguredTarget(scope, graph) {
11460
11800
  baseDir: scope.root
11461
11801
  });
11462
11802
  const targetRoot = resolveConfigPath(agent.root, scope.root);
11463
- const installRoot = resolve22(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
11803
+ const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
11464
11804
  const stateKey = stateKeyFor(adapter.name, {
11465
11805
  installationType: agent.installationType,
11466
11806
  targetFingerprint: graph.lock.canonical.targetFingerprint
@@ -11534,7 +11874,7 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11534
11874
  targetFingerprint,
11535
11875
  ...identityFleetId ? { fleetId: identityFleetId } : {}
11536
11876
  });
11537
- const installRoot = resolve22(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
11877
+ const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
11538
11878
  return {
11539
11879
  adapter: adapter.name,
11540
11880
  installationType,
@@ -11565,9 +11905,9 @@ async function legacyTargetStateForGraph(scope, graph) {
11565
11905
  };
11566
11906
  }
11567
11907
  function assertGraphIsSubset(source, destination) {
11568
- const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson(root)]));
11908
+ const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson2(root)]));
11569
11909
  for (const root of destination.lock.canonical.roots) {
11570
- if (sourceRoots.get(root.rootId) !== canonicalJson(root)) {
11910
+ if (sourceRoots.get(root.rootId) !== canonicalJson2(root)) {
11571
11911
  throw new Error(
11572
11912
  `Partially normalized destination graph diverges at root '${root.rootId}': ${destination.path}`
11573
11913
  );
@@ -11597,7 +11937,7 @@ function destinationGraphFromSource(source, targetFingerprint) {
11597
11937
  function mergeDestinationManifest(existing, state, generatedAt, desiredEntries, replaceOwners = /* @__PURE__ */ new Set()) {
11598
11938
  if (existing) {
11599
11939
  const manifest2 = existing.manifest;
11600
- if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve22(manifest2.targetRoot) !== state.installRoot) {
11940
+ if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve23(manifest2.targetRoot) !== state.installRoot) {
11601
11941
  throw new Error(`Destination manifest identity diverges from the derived target: ${existing.path}`);
11602
11942
  }
11603
11943
  }
@@ -11657,7 +11997,7 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
11657
11997
  const content = await readFile27(path, "utf8");
11658
11998
  if (hasMergeRemovalContent(entry.mergeRemoval)) {
11659
11999
  assertExactMergeContribution(entry.mergeRemoval, entry.mergeStrategy, content);
11660
- } else if (resolve22(sourcePath) === resolve22(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12000
+ } else if (resolve23(sourcePath) === resolve23(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
11661
12001
  assertExactMergeContribution({}, entry.mergeStrategy, content);
11662
12002
  } else {
11663
12003
  throw new Error(`Installed-state normalization cannot prove incomplete merge ownership at ${path}.`);
@@ -11668,8 +12008,8 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
11668
12008
  await assertEquivalentRuntimeBytes(sourcePath, destinationPath, entry.hash);
11669
12009
  }
11670
12010
  function renderedEntryPathForRoot(root, entryPath) {
11671
- const normalizedRoot = resolve22(root);
11672
- const candidate = resolve22(normalizedRoot, entryPath);
12011
+ const normalizedRoot = resolve23(root);
12012
+ const candidate = resolve23(normalizedRoot, entryPath);
11673
12013
  if (candidate !== normalizedRoot && !candidate.startsWith(`${normalizedRoot}/`)) {
11674
12014
  throw new Error(`Install manifest entry escapes its target root: ${entryPath}`);
11675
12015
  }
@@ -11760,8 +12100,8 @@ function ownerMatchesGraphPackage(lock, owner, selected) {
11760
12100
  return ownerMatchesPackage(owner, selected) || lock.canonical.roots.some((root) => selected.has(root.rootId) && root.graphNodeId === owner);
11761
12101
  }
11762
12102
  function renderedEntryPath(manifest, entry) {
11763
- const root = resolve22(manifest.targetRoot);
11764
- const candidate = resolve22(root, entry.path);
12103
+ const root = resolve23(manifest.targetRoot);
12104
+ const candidate = resolve23(root, entry.path);
11765
12105
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
11766
12106
  throw new Error(`Install manifest entry escapes its target root: ${entry.path}`);
11767
12107
  }
@@ -11780,7 +12120,7 @@ function assertSimpleVerifiableEntry(entry, path) {
11780
12120
  if (entry.kind !== "file" && entry.kind !== "dir") throw new Error(`Unsupported installed entry kind at ${path}.`);
11781
12121
  }
11782
12122
  function entryIdentity(entry) {
11783
- return canonicalJson({
12123
+ return canonicalJson2({
11784
12124
  path: entry.path,
11785
12125
  artifactType: entry.artifactType,
11786
12126
  artifactName: entry.artifactName,
@@ -11798,7 +12138,7 @@ function entryIdentity(entry) {
11798
12138
  });
11799
12139
  }
11800
12140
  function graphArtifactIdentity(artifact) {
11801
- return canonicalJson({
12141
+ return canonicalJson2({
11802
12142
  graphNodeId: artifact.graphNodeId,
11803
12143
  type: artifact.type,
11804
12144
  name: artifact.name,
@@ -11812,7 +12152,7 @@ function graphArtifactIdentity(artifact) {
11812
12152
  });
11813
12153
  }
11814
12154
  function graphEntryIdentity(entry) {
11815
- return canonicalJson({
12155
+ return canonicalJson2({
11816
12156
  graphNodeId: entry.graphNodeId,
11817
12157
  type: entry.artifactType,
11818
12158
  name: entry.artifactName,
@@ -11916,7 +12256,7 @@ async function buildJournalGraphLockStates(plan) {
11916
12256
  for (const transfer of plan.installedState.graphTransfers) {
11917
12257
  const sourceBefore = JSON.parse(await readFile27(transfer.sourceGraphLockPath, "utf8"));
11918
12258
  const sourceLock = await readGraphLock(transfer.sourceGraphLockPath);
11919
- const sourceDigest = sha2564(canonicalJson(sourceLock));
12259
+ const sourceDigest = sha2564(canonicalJson2(sourceLock));
11920
12260
  if (sourceDigest !== transfer.sourceGraphLockDigest) {
11921
12261
  throw new Error(`Source graph lock changed after planning: ${transfer.sourceGraphLockPath}`);
11922
12262
  }
@@ -11934,9 +12274,9 @@ async function buildJournalGraphLockStates(plan) {
11934
12274
  continue;
11935
12275
  }
11936
12276
  const destinationBefore = await readOptionalRecord(transfer.destinationGraphLockPath);
11937
- const destinationBeforeDigest = destinationBefore ? sha2564(canonicalJson(await readGraphLock(transfer.destinationGraphLockPath))) : null;
12277
+ const destinationBeforeDigest = destinationBefore ? sha2564(canonicalJson2(await readGraphLock(transfer.destinationGraphLockPath))) : null;
11938
12278
  const destinationAfter = destinationBefore ? await readGraphLock(transfer.destinationGraphLockPath) : destinationGraphFromSource(sourceLock, transfer.targetFingerprint);
11939
- const destinationAfterDigest = sha2564(canonicalJson(destinationAfter));
12279
+ const destinationAfterDigest = sha2564(canonicalJson2(destinationAfter));
11940
12280
  if (destinationBeforeDigest !== transfer.destinationGraphLockDigest || destinationAfterDigest !== transfer.destinationGraphLockAfterDigest) {
11941
12281
  throw new Error(`Destination graph lock changed after planning: ${transfer.destinationGraphLockPath}`);
11942
12282
  }
@@ -11976,6 +12316,7 @@ async function acquireJournalManifestLocks(manifests) {
11976
12316
  try {
11977
12317
  const unique = /* @__PURE__ */ new Map();
11978
12318
  for (const item of manifests) unique.set(item.path, item);
12319
+ const acquiredPaths = /* @__PURE__ */ new Set();
11979
12320
  for (const item of [...unique.values()].sort((a, b) => a.path.localeCompare(b.path))) {
11980
12321
  const raw = await readOptionalRecord(item.path) ?? item.before ?? item.after;
11981
12322
  if (!raw && !(item.targetRoot && item.adapter && item.installationType && item.stateKey)) {
@@ -11988,9 +12329,13 @@ async function acquireJournalManifestLocks(manifests) {
11988
12329
  const installationType = manifest?.installationType ?? item.installationType;
11989
12330
  const stateKey = manifest?.stateKey ?? item.stateKey;
11990
12331
  const scope = { installationType, stateKey };
11991
- const lock = await acquireApplyLock(targetRoot, adapter, void 0, {}, scope);
11992
- locks.push(lock);
11993
- if (await readApplyJournal(targetRoot, adapter, void 0, scope)) {
12332
+ const lockPath = applyLockPath(targetRoot, adapter, scope);
12333
+ if (!acquiredPaths.has(lockPath)) {
12334
+ const lock = await acquireApplyLock(targetRoot, adapter, void 0, {}, scope);
12335
+ locks.push(lock);
12336
+ acquiredPaths.add(lockPath);
12337
+ }
12338
+ if ((await listApplyJournals(targetRoot, adapter, void 0, { installationType })).length > 0) {
11994
12339
  throw new Error(`Cannot normalize while an Agentwheel apply journal is pending for ${item.path}.`);
11995
12340
  }
11996
12341
  }
@@ -12041,8 +12386,8 @@ async function assertJournalRecoverable(journal) {
12041
12386
  }
12042
12387
  for (const graphLock of journal.graphLocks) {
12043
12388
  const raw = await readOptionalRecord(graphLock.path);
12044
- const digest = raw ? sha2564(canonicalJson(await readGraphLock(graphLock.path))) : null;
12045
- if (digest !== graphLock.beforeDigest && digest !== graphLock.afterDigest) {
12389
+ const digest2 = raw ? sha2564(canonicalJson2(await readGraphLock(graphLock.path))) : null;
12390
+ if (digest2 !== graphLock.beforeDigest && digest2 !== graphLock.afterDigest) {
12046
12391
  throw new Error(`Cannot recover fleet normalization because graph lock changed outside the transaction: ${graphLock.path}`);
12047
12392
  }
12048
12393
  }
@@ -12094,12 +12439,12 @@ function packageMap(config) {
12094
12439
  return result;
12095
12440
  }
12096
12441
  function configRevision(config) {
12097
- return sha2564(canonicalJson(config));
12442
+ return sha2564(canonicalJson2(config));
12098
12443
  }
12099
12444
  function normalizationJournalPath(sourceRoot, destinationFleet) {
12100
12445
  return join44(sourceRoot, ".agentwheel", `fleet-normalize-${destinationFleet}.journal.json`);
12101
12446
  }
12102
- function canonicalJson(value) {
12447
+ function canonicalJson2(value) {
12103
12448
  return JSON.stringify(sortValue(value));
12104
12449
  }
12105
12450
  function sortValue(value) {
@@ -12110,7 +12455,7 @@ function sortValue(value) {
12110
12455
  return value;
12111
12456
  }
12112
12457
  function sha2564(value) {
12113
- return createHash12("sha256").update(value).digest("hex");
12458
+ return createHash13("sha256").update(value).digest("hex");
12114
12459
  }
12115
12460
  function assertDigest(value) {
12116
12461
  if (!/^[a-f0-9]{64}$/.test(value)) throw new Error(`Invalid plan digest: ${value}`);
@@ -12148,6 +12493,7 @@ var commandGovernance = {
12148
12493
  "package migrate": "governed-always",
12149
12494
  remember: "governed-always",
12150
12495
  "ownership handoff": "governed-unless-dry-run",
12496
+ "ownership retire-stale": "governed-apply",
12151
12497
  "mcp retire": "governed-apply",
12152
12498
  eject: "governed-always",
12153
12499
  uninstall: "governed-unless-dry-run",
@@ -12564,7 +12910,7 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
12564
12910
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
12565
12911
  if (options.toFleet) {
12566
12912
  const fleet = await showRegisteredFleet(options.toFleet);
12567
- if (resolve23(fleet.root) !== resolve23(toWorkspaceRoot)) {
12913
+ if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
12568
12914
  throw new Error(`Destination fleet '${options.toFleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
12569
12915
  }
12570
12916
  }
@@ -12589,6 +12935,84 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
12589
12935
  console.log(`Manifest revision: ${result.manifestRevision}`);
12590
12936
  console.log(`Owner: ${result.fromOwner} -> ${result.toOwner}`);
12591
12937
  });
12938
+ ownershipCommand.command("retire-stale").description("retire stale source-manifest ownership already covered by one Fleet manifest").requiredOption("--from-workspace-root <path>", "exact stale workspace owner root").requiredOption("--to-workspace-root <path>", "registered destination Fleet root").requiredOption("--source-state-key <key>", "exact source install-manifest state key").requiredOption("--fleet <id>", "registered destination Fleet").option("--adapter <adapter>", "built-in adapter").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named destination Fleet agent").option("--profile <name>", "destination Fleet profile resolving to one target").option("--plan-digest <sha256>", "reviewed plan digest; required with --apply").option("--expected-source-revision <sha256>", "reviewed source manifest revision; required with --apply").option("--expected-destination-revision <sha256>", "reviewed destination manifest revision; required with --apply").option("--expected-inventory-revision <sha256>", "reviewed runtime manifest inventory revision; required with --apply").option("--apply", "apply the exact reviewed metadata-only retirement", false).option("--json", "print the complete deterministic plan as JSON", false).action(async (options) => {
12939
+ const normalizedOptions = normalizeRuntimeScopeOptions(options);
12940
+ const fleet = await showRegisteredFleet(options.fleet);
12941
+ const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
12942
+ if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
12943
+ throw new Error(`Destination Fleet '${options.fleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
12944
+ }
12945
+ const targets = await resolveCliTargets(normalizedOptions);
12946
+ if (targets.length !== 1) {
12947
+ throw new Error(`Stale ownership retirement requires exactly one destination Fleet target, found ${targets.length}.`);
12948
+ }
12949
+ const target = targets[0];
12950
+ if (target.fleetId !== options.fleet || resolve24(target.workspaceRoot) !== resolve24(fleet.root)) {
12951
+ throw new Error(`Resolved target does not belong to destination Fleet '${options.fleet}'.`);
12952
+ }
12953
+ const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
12954
+ const adapter = await resolveAdapterForTarget(target, adapterOptions);
12955
+ const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
12956
+ const state = installStateForTarget(target, adapter, adapterOptions, installationType);
12957
+ const request = {
12958
+ targetRoot: state.installRoot,
12959
+ adapter: adapter.name,
12960
+ installationType,
12961
+ sourceStateKey: options.sourceStateKey,
12962
+ destinationStateKey: state.stateKey,
12963
+ fromWorkspaceRoot: normalizeCliPath(options.fromWorkspaceRoot),
12964
+ toWorkspaceRoot,
12965
+ toFleetId: fleet.id,
12966
+ planDigest: options.planDigest,
12967
+ expectedSourceRevision: options.expectedSourceRevision,
12968
+ expectedDestinationRevision: options.expectedDestinationRevision,
12969
+ expectedInventoryRevision: options.expectedInventoryRevision,
12970
+ transport: transportForTarget(target)
12971
+ };
12972
+ const result = options.apply ? await applyRetireStaleOwnership(request) : await planRetireStaleOwnership(request);
12973
+ const targetArgs = options.profile ? ["--profile", options.profile] : target.agentName ? ["--agent", target.agentName] : ["--adapter", adapter.name, "--target-root", target.targetRoot];
12974
+ const applyArgs = [
12975
+ "agentwheel",
12976
+ "ownership",
12977
+ "retire-stale",
12978
+ "--from-workspace-root",
12979
+ request.fromWorkspaceRoot,
12980
+ "--to-workspace-root",
12981
+ request.toWorkspaceRoot,
12982
+ "--source-state-key",
12983
+ request.sourceStateKey,
12984
+ "--fleet",
12985
+ request.toFleetId,
12986
+ ...targetArgs,
12987
+ "--installation-type",
12988
+ installationType,
12989
+ ...options.adapterConfig ? ["--adapter-config", options.adapterConfig] : [],
12990
+ ...options.adapterModule ? ["--adapter-module", options.adapterModule] : [],
12991
+ ...options.allowAdapterCode ? ["--allow-adapter-code"] : [],
12992
+ "--plan-digest",
12993
+ result.planDigest,
12994
+ "--expected-source-revision",
12995
+ result.source.revision,
12996
+ "--expected-destination-revision",
12997
+ result.destination.revision,
12998
+ "--expected-inventory-revision",
12999
+ result.manifestInventoryRevision,
13000
+ "--apply"
13001
+ ];
13002
+ const applyCommand = applyArgs.map(shellQuoteArgument).join(" ");
13003
+ if (options.json) {
13004
+ console.log(JSON.stringify({ ...result, applyCommand }, null, 2));
13005
+ return;
13006
+ }
13007
+ console.log(`${options.apply ? "Retired stale ownership" : "Stale ownership retirement plan"}: ${result.selected.length} entr${result.selected.length === 1 ? "y" : "ies"}`);
13008
+ console.log(`Target: ${result.adapter}/${result.installationType} at ${result.targetRoot}`);
13009
+ console.log(`Source: ${result.source.stateKey} revision ${result.source.revision}`);
13010
+ console.log(`Destination: ${result.destination.stateKey} revision ${result.destination.revision}`);
13011
+ console.log(`Manifest inventory revision: ${result.manifestInventoryRevision}`);
13012
+ for (const entry of result.selected) console.log(`- ${entry.path}: ${entry.sourceEntryDigest} -> ${entry.destinationEntryDigest}`);
13013
+ console.log(`Plan digest: ${result.planDigest}`);
13014
+ if (!options.apply) console.log(`Apply: ${applyCommand}`);
13015
+ });
12592
13016
  var mcpCommand = program.command("mcp").description("operate on MCP runtime configuration");
12593
13017
  mcpCommand.command("retire").description("remove one exact legacy MCP contribution with explicit state ownership").argument("<package>", "configured package containing exactly one legacy MCP artifact").option("--user", "use the user workspace", false).option("--local", "use the nearest local workspace", false).option("--fleet <id>", "use one registered named fleet").option("-t, --target-root <path>", "explicit runtime/project root").option("--agent <name>", "one named agent from merged config").option("--profile <name>", "workspace runtime profile resolving to one target").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--from-workspace-root <path>", "required previous owner when managed legacy state exists").option("--dry-run", "show the retirement plan without writing (default)", false).option("--apply", "apply the exact reviewed retirement plan", false).option("--json", "print the install plan as JSON", false).action(async (packageName, options) => {
12594
13018
  if (options.apply && options.dryRun) throw new Error("--apply cannot be combined with --dry-run.");
@@ -13522,10 +13946,13 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
13522
13946
  targetOptions.focusedArtifact,
13523
13947
  focusedArtifactOwnerKeys(result.bundle.graphLock, previousGroupLock, scopedRootId, targetOptions.focusedArtifact)
13524
13948
  )).map((operation) => operation.relativeDestPath);
13949
+ const ownership = await resolveWorkspaceOwnershipScope(group.target.workspaceRoot, {
13950
+ fleetId: group.target.fleetId
13951
+ });
13525
13952
  await assertNoForeignWorkspaceStateForPlan(scopedResult.plan, {
13526
13953
  transport,
13527
13954
  workspaceRoot: group.target.workspaceRoot,
13528
- workspaceOwner: workspaceOwnerForRoot(group.target.workspaceRoot, group.target.fleetId),
13955
+ workspaceOwner: workspaceOwnerForRoot(ownership.root, ownership.fleetId),
13529
13956
  plannedPaths: focusedPaths
13530
13957
  });
13531
13958
  }
@@ -13576,7 +14003,7 @@ function scopeUpdatePlanToDependencies(result, selectors, previousLock, manifest
13576
14003
  selectedPreviousNodeIds,
13577
14004
  selectedRootIds
13578
14005
  );
13579
- const graphLockDigest = createHash13("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14006
+ const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
13580
14007
  return {
13581
14008
  ...result,
13582
14009
  bundle: { ...result.bundle, graphLock },
@@ -13702,7 +14129,7 @@ function operationBelongsToDependencyUpdate(operation, allowedNodeIds, allowedNa
13702
14129
  function scopeInstallPlanToArtifact(result, rootId, focused, manifest) {
13703
14130
  const scoped = scopePlanOperationsToArtifact(result, rootId, focused, manifest);
13704
14131
  const graphLock = focusedGraphLockForInstall(scoped.bundle.graphLock, rootId, focused);
13705
- const graphLockDigest = createHash13("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14132
+ const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
13706
14133
  return {
13707
14134
  ...scoped,
13708
14135
  bundle: { ...scoped.bundle, graphLock },
@@ -13713,7 +14140,7 @@ function scopeInstallPlanToArtifact(result, rootId, focused, manifest) {
13713
14140
  function scopeUpdatePlanToArtifact(result, rootId, focused, previousLock, manifest) {
13714
14141
  const scoped = scopePlanOperationsToArtifact(result, rootId, focused, manifest, previousLock);
13715
14142
  const graphLock = mergeFocusedArtifactGraphLock(scoped.bundle.graphLock, previousLock, rootId, focused);
13716
- const graphLockDigest = createHash13("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14143
+ const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
13717
14144
  return {
13718
14145
  ...scoped,
13719
14146
  bundle: { ...scoped.bundle, graphLock },
@@ -14008,7 +14435,7 @@ function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
14008
14435
  selectedPreviousNodeIds,
14009
14436
  /* @__PURE__ */ new Set([rootId])
14010
14437
  );
14011
- const graphLockDigest = createHash13("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14438
+ const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14012
14439
  return {
14013
14440
  ...scoped,
14014
14441
  bundle: { ...scoped.bundle, graphLock },
@@ -14092,6 +14519,8 @@ function keepManifestEntryOperation(entry, targetRoot, scopeDescription, operati
14092
14519
  semanticPlugin: entry.semanticPlugin,
14093
14520
  execute: entry.executed,
14094
14521
  mergeStrategy: entry.mergeStrategy,
14522
+ mergeRemoval: "mergeRemoval" in entry ? entry.mergeRemoval : void 0,
14523
+ mergeCreatedDestination: "mergeCreatedDestination" in entry ? entry.mergeCreatedDestination : void 0,
14095
14524
  composedFrom: entry.composedFrom,
14096
14525
  installName: fresh?.installName ?? ("installName" in entry ? entry.installName : entry.artifactName),
14097
14526
  logicalSelector: fresh?.logicalSelector ?? ("logicalSelector" in entry ? entry.logicalSelector : `${entry.artifactType}/${entry.artifactName}`),
@@ -14562,7 +14991,7 @@ async function assertCompositeMutationSupported(workspaceRoot, profile, readOnly
14562
14991
  `Composite-profile mutation is refused before member execution because ${member.id} uses ${member.transport}; durable remote policy and recovery preflight is not implemented.`
14563
14992
  );
14564
14993
  }
14565
- const memberRoot = resolve23(workspaceRoot, member.workspace);
14994
+ const memberRoot = resolve24(workspaceRoot, member.workspace);
14566
14995
  const memberPolicy = await mutationPolicyForWorkspace(memberRoot);
14567
14996
  if (memberPolicy?.revisioning.mode === "commit-after-verify") {
14568
14997
  throw new Error(
@@ -15013,11 +15442,14 @@ function looksLikeSourceSpecifier(value) {
15013
15442
  }
15014
15443
  function normalizeCliPath(value) {
15015
15444
  if (value === "~") return homedir12();
15016
- if (value.startsWith("~/")) return resolve23(homedir12(), value.slice(2));
15017
- return resolve23(value);
15445
+ if (value.startsWith("~/")) return resolve24(homedir12(), value.slice(2));
15446
+ return resolve24(value);
15447
+ }
15448
+ function shellQuoteArgument(value) {
15449
+ return /^[a-z0-9_./:@%+=,-]+$/i.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
15018
15450
  }
15019
15451
  function isHomePath(path) {
15020
- return resolve23(path) === resolve23(homedir12());
15452
+ return resolve24(path) === resolve24(homedir12());
15021
15453
  }
15022
15454
  function adapterListFromOption(adapter) {
15023
15455
  if (!adapter) return [];
@@ -15138,7 +15570,7 @@ function withFleetExample(config) {
15138
15570
  };
15139
15571
  }
15140
15572
  async function findAgentwheelPackageRoot(start) {
15141
- let current = resolve23(start);
15573
+ let current = resolve24(start);
15142
15574
  while (true) {
15143
15575
  if (await findPackageManifestPath(current, { warnLegacy: false })) return current;
15144
15576
  const parent = dirname22(current);