agentwheel 0.20.7 → 0.20.9

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
@@ -12,10 +12,12 @@ import {
12
12
  commitManifestMetadataJournal,
13
13
  computeInstallManifestInventoryRevision,
14
14
  computeManifestRevision,
15
+ computeSourceLockRevision,
15
16
  computeTargetFingerprint,
16
17
  desiredManagedInstructionBlockHash,
17
18
  hasMergeRemovalContent,
18
19
  installManifestSchema,
20
+ installManifestV2Schema,
19
21
  legacyUnownedWorkspaceOwner,
20
22
  listInstallManifests,
21
23
  managedInstructionBlockMode,
@@ -29,11 +31,12 @@ import {
29
31
  readGraphLock,
30
32
  readInstallManifest,
31
33
  readManagedInstructionBlockState,
34
+ readSourceLock,
32
35
  recoverPendingApply,
33
36
  uninstall,
34
37
  withManifestRevision,
35
38
  writeInstallManifest
36
- } from "./chunk-4AM3LNUQ.js";
39
+ } from "./chunk-E7Z2YB2C.js";
37
40
  import {
38
41
  CURRENT_WORKSPACE_SCHEMA_VERSION,
39
42
  GovernedMutation,
@@ -94,7 +97,7 @@ import {
94
97
  workspaceSelectionImportSchema,
95
98
  writeApplyJournal,
96
99
  writeWorkspaceConfig
97
- } from "./chunk-JXX6EDIP.js";
100
+ } from "./chunk-SHPNLNBR.js";
98
101
  import {
99
102
  inferSourceDriverName
100
103
  } from "./chunk-EMDIG24I.js";
@@ -113,7 +116,7 @@ import { createHash as createHash14 } from "crypto";
113
116
  import { existsSync } from "fs";
114
117
  import { mkdir as mkdir16, rm as rm10, writeFile as writeFile13 } from "fs/promises";
115
118
  import { homedir as homedir12 } from "os";
116
- import { dirname as dirname22, join as join45, resolve as resolve24 } from "path";
119
+ import { dirname as dirname22, join as join45, resolve as resolve25 } from "path";
117
120
  import { fileURLToPath as fileURLToPath3 } from "url";
118
121
  import { Command } from "commander";
119
122
 
@@ -917,11 +920,11 @@ function normalizeArtifactSelectors(select, legacySkills) {
917
920
  if (selected.length === 0) return void 0;
918
921
  return [...new Set(selected.map(parseArtifactSelector))];
919
922
  }
920
- function filterArtifactsBySelection(artifacts, selectors, legacySkills) {
923
+ function filterArtifactsBySelection(artifacts, selectors, legacySkills, options = {}) {
921
924
  const selected = normalizeArtifactSelectors(selectors, legacySkills);
922
925
  if (!selected?.length) return artifacts;
923
926
  const selectedSet = new Set(selected);
924
- const available = new Set(artifacts.flatMap(artifactSelectorAliases));
927
+ const available = new Set((options.validationArtifacts ?? artifacts).flatMap(artifactSelectorAliases));
925
928
  const missing = selected.filter((selector) => !available.has(selector));
926
929
  if (missing.length > 0) {
927
930
  throw new Error(`Selected artifact not found in package: ${missing.join(", ")}`);
@@ -1706,6 +1709,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1706
1709
  hasBlockingChanges: operations.some((op) => op.action === "drift" || op.action === "conflict"),
1707
1710
  baseRevision: options.baseRevision ?? manifest?.revision ?? null,
1708
1711
  runtimeStateRevision,
1712
+ stateMigration: options.stateMigration,
1709
1713
  migrationReport: migration.report,
1710
1714
  graphLockDigest: options.graphLockDigest,
1711
1715
  adapterCode: adapter.programmatic ? { modulePath: adapter.programmatic.modulePath, hash: adapter.programmatic.hash } : void 0,
@@ -2388,6 +2392,7 @@ function operationMetadataFromEntry2(entry, ownersOverride) {
2388
2392
  graphNodeId: entry.graphNodeId,
2389
2393
  dependencyRole: entry.dependencyRole,
2390
2394
  owners: ownersOverride ?? entry.owners,
2395
+ workspaceOwner: entry.workspaceOwner,
2391
2396
  graphLockDigest: entry.graphLockDigest,
2392
2397
  mergeRemoval: entry.mergeRemoval,
2393
2398
  mergeCreatedDestination: entry.mergeCreatedDestination
@@ -3107,14 +3112,14 @@ async function writeMermaidAsset(response) {
3107
3112
  "content-type": "application/javascript; charset=utf-8",
3108
3113
  "cache-control": "public, max-age=3600"
3109
3114
  });
3110
- await new Promise((resolve25) => {
3115
+ await new Promise((resolve26) => {
3111
3116
  const stream = createReadStream(assetPath);
3112
3117
  stream.on("error", () => {
3113
3118
  if (!response.headersSent) writeText(response, 404, "Mermaid asset unavailable.\n");
3114
3119
  else response.destroy();
3115
- resolve25();
3120
+ resolve26();
3116
3121
  });
3117
- stream.on("end", resolve25);
3122
+ stream.on("end", resolve26);
3118
3123
  stream.pipe(response);
3119
3124
  });
3120
3125
  }
@@ -3141,14 +3146,14 @@ function escapeHtml2(value) {
3141
3146
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3142
3147
  }
3143
3148
  function listen(server, port, bind) {
3144
- return new Promise((resolve25, reject) => {
3149
+ return new Promise((resolve26, reject) => {
3145
3150
  const onError = (error) => {
3146
3151
  server.off("listening", onListening);
3147
3152
  reject(error);
3148
3153
  };
3149
3154
  const onListening = () => {
3150
3155
  server.off("error", onError);
3151
- resolve25();
3156
+ resolve26();
3152
3157
  };
3153
3158
  server.once("error", onError);
3154
3159
  server.once("listening", onListening);
@@ -3156,7 +3161,7 @@ function listen(server, port, bind) {
3156
3161
  });
3157
3162
  }
3158
3163
  function waitForShutdown(server, beforeClose) {
3159
- return new Promise((resolve25) => {
3164
+ return new Promise((resolve26) => {
3160
3165
  let closing = false;
3161
3166
  const shutdown = () => {
3162
3167
  if (closing) return;
@@ -3164,7 +3169,7 @@ function waitForShutdown(server, beforeClose) {
3164
3169
  process.off("SIGINT", shutdown);
3165
3170
  process.off("SIGTERM", shutdown);
3166
3171
  beforeClose();
3167
- server.close(() => resolve25());
3172
+ server.close(() => resolve26());
3168
3173
  };
3169
3174
  process.once("SIGINT", shutdown);
3170
3175
  process.once("SIGTERM", shutdown);
@@ -3174,7 +3179,7 @@ function waitForShutdown(server, beforeClose) {
3174
3179
  process.off("SIGINT", shutdown);
3175
3180
  process.off("SIGTERM", shutdown);
3176
3181
  beforeClose();
3177
- resolve25();
3182
+ resolve26();
3178
3183
  });
3179
3184
  });
3180
3185
  }
@@ -3727,7 +3732,7 @@ async function fetchClawHubPackage(fetchImpl, packageName) {
3727
3732
  throw new Error(`ClawHub lookup failed for ${packageName}`);
3728
3733
  }
3729
3734
  async function delay(ms) {
3730
- await new Promise((resolve25) => setTimeout(resolve25, ms));
3735
+ await new Promise((resolve26) => setTimeout(resolve26, ms));
3731
3736
  }
3732
3737
  function parseClawHubSource(source) {
3733
3738
  if (!source.startsWith(sourcePrefix)) {
@@ -3904,7 +3909,7 @@ async function withGitCacheMaintenanceLock(cacheRoot, timeoutMs, fn) {
3904
3909
  if (Date.now() - started > timeoutMs) {
3905
3910
  throw new Error(`Timed out waiting for git cache maintenance lock at ${lockPath}`);
3906
3911
  }
3907
- await new Promise((resolve25) => setTimeout(resolve25, 50));
3912
+ await new Promise((resolve26) => setTimeout(resolve26, 50));
3908
3913
  }
3909
3914
  }
3910
3915
  try {
@@ -6570,7 +6575,7 @@ async function collectIncludeSelector(raw, optional, artifactsByRelativePath, sc
6570
6575
  }
6571
6576
  function artifactTargetsRuntime(runtimes, runtime, nodeId, selector, warn) {
6572
6577
  if (!runtimes?.length || !runtime || runtimes.includes(runtime)) return true;
6573
- warn?.(`skip artifact ${nodeId}:${selector} (not targeted: runtimes=[${runtimes.join(",")}])`);
6578
+ warn?.(`skip artifact ${nodeId}:${selector} (selected but not targeted: runtimes=[${runtimes.join(",")}])`);
6574
6579
  return false;
6575
6580
  }
6576
6581
  function requirementTargetsRuntime(runtimes, runtime, label, warn) {
@@ -6770,8 +6775,8 @@ function verifyIntegrity(integrity, sourceHash, label) {
6770
6775
  async function withCachePathLock(path, fn) {
6771
6776
  const previous = cacheLocks.get(path) ?? Promise.resolve();
6772
6777
  let release = () => void 0;
6773
- const current = previous.then(() => new Promise((resolve25) => {
6774
- release = resolve25;
6778
+ const current = previous.then(() => new Promise((resolve26) => {
6779
+ release = resolve26;
6775
6780
  }));
6776
6781
  cacheLocks.set(path, current);
6777
6782
  await previous;
@@ -6833,6 +6838,11 @@ function detectDirectCollisions(nodes) {
6833
6838
  for (const [selector, owners] of bySelector) {
6834
6839
  const uniqueOwners = [...new Map(owners.map((owner) => [owner.node.id, owner])).values()];
6835
6840
  if (uniqueOwners.length <= 1) continue;
6841
+ const selectedArtifacts = uniqueOwners.map((owner) => ({
6842
+ owner,
6843
+ artifact: owner.artifacts.find((candidate) => artifactSelectorKey(candidate) === selector)
6844
+ }));
6845
+ if (new Set(selectedArtifacts.map(({ artifact }) => artifact.hash)).size === 1) continue;
6836
6846
  const replacements = uniqueOwners.filter((owner) => {
6837
6847
  const artifact = owner.artifacts.find((candidate) => artifactSelectorKey(candidate) === selector);
6838
6848
  return uniqueOwners.every((other) => {
@@ -6842,7 +6852,7 @@ function detectDirectCollisions(nodes) {
6842
6852
  });
6843
6853
  if (replacements.length === 1) continue;
6844
6854
  throw new Error(
6845
- `Direct dependency artifact collision for ${selector}: ${uniqueOwners.map((owner) => `${owner.node.id} required by ${owner.node.requiredBy.join(", ")}`).join("; ")}. Resolve by aliasing, deselecting one artifact, or overriding the dependency selection.`
6855
+ `Direct dependency artifact collision for ${selector}: ${uniqueOwners.map((owner) => `${owner.node.id} required by ${owner.node.requiredBy.join(", ")}`).join("; ")}. Content hashes differ: ${selectedArtifacts.map(({ owner, artifact }) => `${owner.node.id}=sha256:${artifact.hash}`).join("; ")}. Resolve by aliasing, deselecting one artifact, or overriding the dependency selection.`
6846
6856
  );
6847
6857
  }
6848
6858
  }
@@ -7027,8 +7037,8 @@ import { rm as rm7 } from "fs/promises";
7027
7037
 
7028
7038
  // src/lifecycle/source-plan.ts
7029
7039
  import { createHash as createHash8 } from "crypto";
7030
- import { mkdir as mkdir14 } from "fs/promises";
7031
- import { dirname as dirname17, join as join33, resolve as resolve15 } from "path";
7040
+ import { mkdir as mkdir14, readdir as readdir9 } from "fs/promises";
7041
+ import { basename as basename18, dirname as dirname17, join as join33, posix as posix2, resolve as resolve16 } from "path";
7032
7042
 
7033
7043
  // src/resolve/graph-diff.ts
7034
7044
  function diffGraphLocks(previous, next) {
@@ -7226,8 +7236,11 @@ async function renderGraphForTarget(graph, targetContext = {}) {
7226
7236
  for (const staged of [...stagedNodes.values()].sort((a, b) => a.rawNode.node.id.localeCompare(b.rawNode.node.id))) {
7227
7237
  const rawNode = staged.rawNode;
7228
7238
  const runtimeSelectedSet = new Set(normalizeArtifactSelectors(rawNode.node.selected) ?? []);
7229
- const runtimeArtifacts = targetContext.adapter ? filterArtifactsByRuntime2(staged.artifacts, targetContext.adapter.name, runtimeSelectedSet) : staged.artifacts;
7230
- const expandedArtifacts = await expandMarkdownIncludes(runtimeArtifacts, staged.root, {
7239
+ const selectionValidatedArtifacts = filterArtifactsBySelection(staged.artifacts, rawNode.node.selected);
7240
+ const preRuntimeArtifacts = targetContext.noDeps ? selectionValidatedArtifacts : staged.artifacts;
7241
+ const runtimeArtifacts = targetContext.adapter ? filterArtifactsByRuntime2(preRuntimeArtifacts, targetContext.adapter.name, runtimeSelectedSet) : preRuntimeArtifacts;
7242
+ const compositionArtifacts = runtimeArtifacts;
7243
+ const expandedArtifacts = await expandMarkdownIncludes(compositionArtifacts, staged.root, {
7231
7244
  nodeId: rawNode.node.id,
7232
7245
  originNodeId: rawNode.node.id,
7233
7246
  additionalComposeEntries: (artifact) => matchingCompositionEntries(
@@ -7274,7 +7287,12 @@ async function renderGraphForTarget(graph, targetContext = {}) {
7274
7287
  };
7275
7288
  }
7276
7289
  });
7277
- const selectedArtifacts = filterArtifactsBySelection(expandedArtifacts, rawNode.node.selected);
7290
+ const selectedArtifacts = filterArtifactsBySelection(
7291
+ expandedArtifacts,
7292
+ rawNode.node.selected,
7293
+ void 0,
7294
+ { validationArtifacts: staged.artifacts }
7295
+ );
7278
7296
  const claudeRenderedArtifacts = await renderClaudeSubagents(selectedArtifacts, staged.root, targetContext.adapter);
7279
7297
  const codexRenderedArtifacts = await renderCodexSubagents(claudeRenderedArtifacts, staged.root, targetContext.adapter);
7280
7298
  const openClawRenderedArtifacts = await renderOpenClawSubagents(codexRenderedArtifacts, staged.root, targetContext.adapter);
@@ -7388,8 +7406,9 @@ function sha2563(content) {
7388
7406
  return createHash7("sha256").update(content).digest("hex");
7389
7407
  }
7390
7408
  function assignInstallNames(graph, artifacts) {
7391
- const superseded = supersededLogicalSelectors(artifacts);
7392
- const effectiveArtifacts = artifacts.filter((artifact) => !superseded.has(artifact.logicalSelector));
7409
+ const equivalentArtifacts = coalesceEquivalentDirectArtifacts(artifacts);
7410
+ const superseded = supersededLogicalSelectors(equivalentArtifacts);
7411
+ const effectiveArtifacts = equivalentArtifacts.filter((artifact) => !superseded.has(artifact.logicalSelector));
7393
7412
  const aliases = workspaceAliases(graph);
7394
7413
  validateAliasScopes(graph, effectiveArtifacts, aliases);
7395
7414
  const decisions = /* @__PURE__ */ new Map();
@@ -7432,6 +7451,37 @@ function assignInstallNames(graph, artifacts) {
7432
7451
  }));
7433
7452
  return { artifacts: out, namespacing: [...decisions.values()], overrides: finalOverrides };
7434
7453
  }
7454
+ function coalesceEquivalentDirectArtifacts(artifacts) {
7455
+ const equivalent = /* @__PURE__ */ new Map();
7456
+ const passthrough = [];
7457
+ for (const artifact of artifacts) {
7458
+ if (artifact.dependencyRole !== "direct") {
7459
+ passthrough.push(artifact);
7460
+ continue;
7461
+ }
7462
+ const key = `${artifact.type}\0${artifact.name}\0${artifact.hash}`;
7463
+ const group = equivalent.get(key) ?? [];
7464
+ group.push(artifact);
7465
+ equivalent.set(key, group);
7466
+ }
7467
+ for (const group of equivalent.values()) {
7468
+ const sorted = [...group].sort((left, right) => left.logicalSelector.localeCompare(right.logicalSelector));
7469
+ const canonical = sorted[0];
7470
+ passthrough.push({
7471
+ ...canonical,
7472
+ owners: [...new Set(sorted.flatMap((artifact) => artifact.owners))].sort((left, right) => left.localeCompare(right)),
7473
+ composedFrom: mergeComposedFrom(sorted)
7474
+ });
7475
+ }
7476
+ return passthrough;
7477
+ }
7478
+ function mergeComposedFrom(artifacts) {
7479
+ const entries = /* @__PURE__ */ new Map();
7480
+ for (const entry of artifacts.flatMap((artifact) => artifact.composedFrom ?? [])) {
7481
+ entries.set(`${entry.selector}\0${entry.hash}`, entry);
7482
+ }
7483
+ return entries.size > 0 ? [...entries.values()].sort((left, right) => `${left.selector}\0${left.hash}`.localeCompare(`${right.selector}\0${right.hash}`)) : void 0;
7484
+ }
7435
7485
  function supersededLogicalSelectors(artifacts) {
7436
7486
  const removed = /* @__PURE__ */ new Set();
7437
7487
  for (const replacement of artifacts) {
@@ -7594,7 +7644,8 @@ function reachableNodeIds(graph, rootNodeId) {
7594
7644
  return reachable;
7595
7645
  }
7596
7646
  function installNameCollisionError(group) {
7597
- return new Error(`Install name collision for ${group[0]?.type}/${group[0]?.installName}: ${group.map((artifact) => artifact.logicalSelector).sort().join(" vs ")}`);
7647
+ const details = group.map((artifact) => `${artifact.logicalSelector}=sha256:${artifact.hash}`).sort().join("; ");
7648
+ return new Error(`Install name collision for ${group[0]?.type}/${group[0]?.installName}. Artifact content hashes: ${details}`);
7598
7649
  }
7599
7650
  function namespaceDecision(artifact, reason) {
7600
7651
  return {
@@ -8028,6 +8079,63 @@ function containsPath(parent, candidate) {
8028
8079
  return path === "" || !path.startsWith("..") && !isAbsolute2(path);
8029
8080
  }
8030
8081
 
8082
+ // src/model/target-state.ts
8083
+ import { posix, resolve as resolve15 } from "path";
8084
+ var targetEvolutionFields = /* @__PURE__ */ new Set([
8085
+ "adapterCodeHash",
8086
+ "adapterConfig",
8087
+ "adapterModule",
8088
+ "targetRoot",
8089
+ "transportDescription",
8090
+ "ssh"
8091
+ ]);
8092
+ function resolveTargetStateIdentity(options) {
8093
+ return {
8094
+ targetFingerprint: computeTargetFingerprint(options.targetFingerprintParts),
8095
+ stateFingerprint: computeTargetFingerprint({
8096
+ identityVersion: 1,
8097
+ contributionScope: {
8098
+ workspaceRoot: resolve15(options.workspaceRoot),
8099
+ targetKey: options.targetKey
8100
+ },
8101
+ target: {
8102
+ ...persistentTargetParts(options.targetFingerprintParts),
8103
+ installRoot: normalizeInstallRoot(options.resolvedInstallRoot, options.transportKind),
8104
+ endpoint: targetEndpoint(options.targetFingerprintParts, options.transportKind)
8105
+ }
8106
+ })
8107
+ };
8108
+ }
8109
+ function persistentTargetParts(value) {
8110
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { value };
8111
+ return Object.fromEntries(
8112
+ Object.entries(value).filter(([key]) => !targetEvolutionFields.has(key))
8113
+ );
8114
+ }
8115
+ function normalizeInstallRoot(path, transportKind) {
8116
+ return transportKind === "ssh" ? posix.normalize(path) : resolve15(path);
8117
+ }
8118
+ function targetEndpoint(value, transportKind) {
8119
+ if (transportKind !== "ssh") return { kind: "local" };
8120
+ const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
8121
+ const ssh = record.ssh && typeof record.ssh === "object" && !Array.isArray(record.ssh) ? record.ssh : {};
8122
+ if (typeof ssh.host !== "string" || ssh.host.trim().length === 0) {
8123
+ throw new Error("SSH endpoint host is required for target state identity; transport description is not an identity source.");
8124
+ }
8125
+ if (ssh.user !== void 0 && (typeof ssh.user !== "string" || ssh.user.trim().length === 0)) {
8126
+ throw new Error("SSH target state identity requires a non-empty endpoint user when one is provided.");
8127
+ }
8128
+ if (ssh.port !== void 0 && (!Number.isInteger(ssh.port) || ssh.port <= 0)) {
8129
+ throw new Error("SSH target state identity requires a positive integer endpoint port.");
8130
+ }
8131
+ return {
8132
+ kind: "ssh",
8133
+ host: ssh.host.trim(),
8134
+ user: ssh.user,
8135
+ port: ssh.port ?? 22
8136
+ };
8137
+ }
8138
+
8031
8139
  // src/lifecycle/source-plan.ts
8032
8140
  async function createGraphSourcePlan(options) {
8033
8141
  if (options.roots.length === 0) {
@@ -8041,22 +8149,62 @@ async function createGraphSourcePlan(options) {
8041
8149
  options.warn?.(message);
8042
8150
  };
8043
8151
  const installationType = options.installationType ?? resolveInstallationTypeForAdapterTarget(options.adapter);
8044
- const targetFingerprint = computeTargetFingerprint(options.targetFingerprintParts ?? {
8152
+ const targetFingerprintParts2 = options.targetFingerprintParts ?? {
8045
8153
  adapter: options.adapter.name,
8046
8154
  fleetId: options.fleetId,
8047
8155
  installationType,
8048
8156
  targetRoot: options.targetRoot,
8049
8157
  transport: transport.kind,
8050
8158
  transportDescription: transport.description
8159
+ };
8160
+ const installRoot = installRootForAdapterInstallationType(options.adapter, options.targetRoot, installationType, transport.kind === "ssh");
8161
+ const targetIdentity = resolveTargetStateIdentity({
8162
+ targetFingerprintParts: targetFingerprintParts2,
8163
+ workspaceRoot,
8164
+ targetKey: options.targetKey ?? "default",
8165
+ resolvedInstallRoot: installRoot,
8166
+ transportKind: transport.kind
8051
8167
  });
8168
+ const targetFingerprint = targetIdentity.targetFingerprint;
8052
8169
  const stateKey = stateKeyFor(options.adapter.name, {
8053
8170
  installationType,
8054
8171
  stateKey: options.stateKey,
8055
- targetFingerprint,
8172
+ targetFingerprint: targetIdentity.stateFingerprint,
8056
8173
  fleetId: options.fleetId
8057
8174
  });
8058
- const installRoot = installRootForAdapterInstallationType(options.adapter, options.targetRoot, installationType, transport.kind === "ssh");
8059
- const recoveredPendingApply = options.readOnly === true ? false : await recoverPendingApplyIfSafe(installRoot, options.adapter.name, transport, { installationType, stateKey });
8175
+ const graphLockPath = pathForGraphLock(
8176
+ workspaceRoot,
8177
+ options.targetKey ?? "default",
8178
+ options.adapter.name,
8179
+ targetIdentity.stateFingerprint
8180
+ );
8181
+ const ownership = await resolveWorkspaceOwnershipScope(workspaceRoot, {
8182
+ fleetId: options.fleetId,
8183
+ globalRoot: options.globalRoot
8184
+ });
8185
+ const workspaceOwner = workspaceOwnerForRoot(ownership.root, ownership.fleetId);
8186
+ let recoveredPendingApply = false;
8187
+ if (options.readOnly !== true) {
8188
+ const pendingJournal = await discoverTargetApplyJournal({
8189
+ adapter: options.adapter.name,
8190
+ installationType,
8191
+ stateKey,
8192
+ explicitStateKey: options.stateKey,
8193
+ fleetId: options.fleetId,
8194
+ installRoot,
8195
+ workspaceOwner,
8196
+ graphLockPath,
8197
+ transport
8198
+ });
8199
+ if (pendingJournal) {
8200
+ recoveredPendingApply = await recoverPendingApplyIfSafe(
8201
+ installRoot,
8202
+ options.adapter.name,
8203
+ transport,
8204
+ { installationType, stateKey: pendingJournal.stateKey }
8205
+ );
8206
+ }
8207
+ }
8060
8208
  const workspaceConfig = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: options.globalRoot });
8061
8209
  const trustPolicy = {
8062
8210
  ...normalizeTrustPolicy(workspaceConfig.trust),
@@ -8064,8 +8212,29 @@ async function createGraphSourcePlan(options) {
8064
8212
  };
8065
8213
  const lockMode = options.frozenLock === true || options.offline === true;
8066
8214
  const lockLabel = options.offline === true ? "Offline" : options.frozenLock === true ? "Frozen lock" : options.lockedResolution === true ? "Locked install" : "Fresh resolve";
8067
- const graphLockPath = pathForGraphLock(workspaceRoot, options.targetKey ?? "default", options.adapter.name, targetFingerprint);
8068
- const previousLock = await readExistingGraphLock(graphLockPath);
8215
+ const readPriorState = async () => {
8216
+ const [stableManifest, stableLock2, stableSourceLock2] = await Promise.all([
8217
+ readInstallManifest(installRoot, options.adapter.name, transport, { installationType, stateKey }),
8218
+ readExistingGraphLock(graphLockPath),
8219
+ readSourceLock(installRoot, options.adapter.name, transport, { installationType, stateKey })
8220
+ ]);
8221
+ const priorState2 = await resolvePriorTargetState({
8222
+ adapter: options.adapter.name,
8223
+ installationType,
8224
+ stateKey,
8225
+ explicitStateKey: options.stateKey,
8226
+ fleetId: options.fleetId,
8227
+ installRoot,
8228
+ workspaceOwner,
8229
+ graphLockPath,
8230
+ stableManifest,
8231
+ stableLock: stableLock2,
8232
+ transport
8233
+ });
8234
+ return { priorState: priorState2, stableLock: stableLock2, stableSourceLock: stableSourceLock2 };
8235
+ };
8236
+ const { priorState, stableLock, stableSourceLock } = await readPriorState();
8237
+ const previousLock = priorState.graphLock;
8069
8238
  const registryClient = new RegistryClient({ workspaceRoot, offline: lockMode, offlineLabel: lockLabel, warn });
8070
8239
  const graph = await resolveDependencyGraph(options.roots, {
8071
8240
  workspaceRoot,
@@ -8093,6 +8262,7 @@ async function createGraphSourcePlan(options) {
8093
8262
  adapter: options.adapter,
8094
8263
  installationType,
8095
8264
  targetFingerprint,
8265
+ noDeps: options.noDeps,
8096
8266
  warn
8097
8267
  });
8098
8268
  const desiredArtifacts = filterArtifactsByAdapterTargets(
@@ -8105,13 +8275,13 @@ async function createGraphSourcePlan(options) {
8105
8275
  const resolvedInstallRoot = installRootForArtifacts(options.adapter, options.targetRoot, resolvedInstallationType, desiredArtifacts.map((artifact) => artifact.type), transport.kind === "ssh");
8106
8276
  const graphLockDigest = digestGraphLock(bundle.graphLock);
8107
8277
  const graphDiff = diffGraphLocks(previousLock, bundle.graphLock);
8108
- const manifest = await readInstallManifest(resolvedInstallRoot, options.adapter.name, transport, { installationType: resolvedInstallationType, stateKey });
8109
- const ownership = await resolveWorkspaceOwnershipScope(workspaceRoot, {
8110
- fleetId: options.fleetId,
8111
- globalRoot: options.globalRoot
8112
- });
8113
- const workspaceOwner = workspaceOwnerForRoot(ownership.root, ownership.fleetId);
8114
- const plan = options.retireExactMcp ? await createExactMcpRetirementPlan(
8278
+ if (normalizeInstallRoot2(resolvedInstallRoot, transport.kind) !== normalizeInstallRoot2(installRoot, transport.kind)) {
8279
+ throw new Error(
8280
+ `Adapter ${options.adapter.name} resolved a different install root after rendering; target state identity cannot be proven.`
8281
+ );
8282
+ }
8283
+ const manifest = priorState.manifest;
8284
+ const basePlan = options.retireExactMcp ? await createExactMcpRetirementPlan(
8115
8285
  desiredArtifacts,
8116
8286
  options.adapter,
8117
8287
  options.targetRoot,
@@ -8133,10 +8303,18 @@ async function createGraphSourcePlan(options) {
8133
8303
  forceDrift: options.forceDrift,
8134
8304
  forceConflict: options.forceConflict,
8135
8305
  replaceConflict: options.replaceConflict,
8306
+ stateMigration: priorState.migration,
8136
8307
  warn
8137
8308
  });
8309
+ const plan = priorState.migration && !basePlan.stateMigration ? { ...basePlan, stateMigration: priorState.migration } : basePlan;
8310
+ plan.targetStateFilePreconditions = {
8311
+ graphLockPath,
8312
+ graphLockRevision: stableLock ? digestGraphLock(stableLock) : null,
8313
+ sourceLockRevision: computeSourceLockRevision(stableSourceLock)
8314
+ };
8315
+ let foreignStateObservations = [];
8138
8316
  if (options.forceForeignState !== true && options.deferForeignStateCheck !== true) {
8139
- await assertNoForeignWorkspaceState({
8317
+ foreignStateObservations = await assertNoForeignWorkspaceState({
8140
8318
  installRoot: resolvedInstallRoot,
8141
8319
  adapter: options.adapter.name,
8142
8320
  transport,
@@ -8144,9 +8322,11 @@ async function createGraphSourcePlan(options) {
8144
8322
  workspaceOwner,
8145
8323
  globalRoot: options.globalRoot,
8146
8324
  stateKey,
8325
+ migratingStateKey: priorState.migration?.fromStateKey,
8147
8326
  plannedPaths: plan.operations.map((operation) => operation.relativeDestPath),
8148
8327
  plannedOperations: plan.operations
8149
8328
  });
8329
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8150
8330
  }
8151
8331
  return {
8152
8332
  plan,
@@ -8159,7 +8339,10 @@ async function createGraphSourcePlan(options) {
8159
8339
  warnings,
8160
8340
  newTransitiveSources: trustEvaluation.promptSources,
8161
8341
  graphDiff,
8162
- recoveredPendingApply
8342
+ recoveredPendingApply,
8343
+ previousManifest: priorState.manifest,
8344
+ previousGraphLock: priorState.graphLock,
8345
+ foreignStateObservations
8163
8346
  };
8164
8347
  }
8165
8348
  async function assertNoForeignWorkspaceStateForPlan(plan, options) {
@@ -8172,12 +8355,15 @@ async function assertNoForeignWorkspaceStateForPlan(plan, options) {
8172
8355
  workspaceOwner: options.workspaceOwner,
8173
8356
  globalRoot: options.globalRoot,
8174
8357
  stateKey: plan.stateKey,
8358
+ migratingStateKey: plan.stateMigration?.fromStateKey,
8175
8359
  plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath),
8176
8360
  plannedOperations: plan.operations
8177
8361
  });
8362
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8178
8363
  }
8179
- function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2) {
8180
- return pathForGraphLock(workspaceRoot, targetKey2, adapter, computeTargetFingerprint(targetFingerprintParts2));
8364
+ function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2, context) {
8365
+ const identity = resolveTargetStateIdentity({ targetFingerprintParts: targetFingerprintParts2, workspaceRoot, targetKey: targetKey2, ...context });
8366
+ return pathForGraphLock(workspaceRoot, targetKey2, adapter, identity.stateFingerprint);
8181
8367
  }
8182
8368
  function desiredArtifactsFromGraphBundle(bundle) {
8183
8369
  return bundle.artifacts.map((artifact) => desiredArtifactFromResolved(artifact));
@@ -8219,10 +8405,278 @@ async function readExistingGraphLock(path) {
8219
8405
  if (!await pathExists(path)) return void 0;
8220
8406
  return readGraphLock(path);
8221
8407
  }
8408
+ async function discoverTargetApplyJournal(options) {
8409
+ const journals = await listApplyJournals(
8410
+ options.installRoot,
8411
+ options.adapter,
8412
+ options.transport,
8413
+ { installationType: options.installationType }
8414
+ );
8415
+ const correlated = [];
8416
+ for (const item of journals) {
8417
+ const journal = item.journal;
8418
+ const journalStateKey = journal.stateKey ?? stateKeyFor(options.adapter, { installationType: options.installationType });
8419
+ if (normalizeInstallRoot2(journal.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8420
+ continue;
8421
+ }
8422
+ if (journalStateKey === options.stateKey) {
8423
+ validateJournalManifestEnvelope(journal.manifest, options, journalStateKey, item.path);
8424
+ correlated.push({ path: item.path, stateKey: journalStateKey });
8425
+ continue;
8426
+ }
8427
+ if (!journal.graphLockPath || !journal.graphLock || !journal.graphLockDigest) continue;
8428
+ if (resolve16(dirname17(journal.graphLockPath)) !== resolve16(dirname17(options.graphLockPath))) continue;
8429
+ if (options.transport.kind === "ssh") {
8430
+ throw new Error(
8431
+ `Legacy SSH apply journal at ${item.path} does not record its original endpoint; Agentwheel cannot prove endpoint identity, so the journal was preserved.`
8432
+ );
8433
+ }
8434
+ const suffix = ".graph-lock.json";
8435
+ const graphName = basename18(journal.graphLockPath);
8436
+ if (!graphName.endsWith(suffix)) {
8437
+ throw new Error(`Legacy apply journal at ${item.path} has an invalid graph-lock path.`);
8438
+ }
8439
+ const fingerprint = graphName.slice(0, -suffix.length);
8440
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint) || journal.graphLock.canonical.targetFingerprint !== fingerprint) {
8441
+ throw new Error(`Legacy apply journal graph-lock fingerprint does not match its path at ${item.path}.`);
8442
+ }
8443
+ const expectedStateKey = stateKeyFor(options.adapter, {
8444
+ installationType: options.installationType,
8445
+ stateKey: options.explicitStateKey,
8446
+ targetFingerprint: fingerprint,
8447
+ fleetId: options.fleetId
8448
+ });
8449
+ if (journalStateKey !== expectedStateKey) {
8450
+ throw new Error(`Legacy apply journal state key does not match its graph lock at ${item.path}.`);
8451
+ }
8452
+ let actualGraphLock;
8453
+ try {
8454
+ actualGraphLock = await readGraphLock(journal.graphLockPath);
8455
+ } catch (error) {
8456
+ throw new Error(
8457
+ `Legacy apply journal graph lock is missing or invalid at ${journal.graphLockPath}: ` + (error instanceof Error ? error.message : String(error))
8458
+ );
8459
+ }
8460
+ const embeddedDigest = digestGraphLock(journal.graphLock);
8461
+ const actualDigest = digestGraphLock(actualGraphLock);
8462
+ if (embeddedDigest !== journal.graphLockDigest || actualDigest !== journal.graphLockDigest) {
8463
+ throw new Error(`Legacy apply journal graph-lock digest mismatch at ${item.path}.`);
8464
+ }
8465
+ const manifest = validateJournalManifestEnvelope(journal.manifest, options, journalStateKey, item.path);
8466
+ validateLegacyManifestCandidate(manifest, {
8467
+ ...options,
8468
+ candidateStateKey: journalStateKey,
8469
+ graphLockDigest: journal.graphLockDigest,
8470
+ graphLockPath: journal.graphLockPath
8471
+ });
8472
+ correlated.push({ path: item.path, stateKey: journalStateKey });
8473
+ }
8474
+ if (correlated.length > 1) {
8475
+ throw new Error(`Ambiguous apply journals for ${options.adapter}: multiple journals match the requested target state.`);
8476
+ }
8477
+ return correlated[0];
8478
+ }
8479
+ function validateJournalManifestEnvelope(value, options, stateKey, journalPath) {
8480
+ let manifest;
8481
+ try {
8482
+ manifest = installManifestV2Schema.parse(value);
8483
+ } catch (error) {
8484
+ throw new Error(
8485
+ `Apply journal at ${journalPath} has an invalid install manifest: ` + (error instanceof Error ? error.message : String(error))
8486
+ );
8487
+ }
8488
+ if (manifest.adapter !== options.adapter || manifest.installationType !== options.installationType || manifest.stateKey !== stateKey || normalizeInstallRoot2(manifest.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8489
+ throw new Error(`Apply journal target identity does not match its manifest at ${journalPath}.`);
8490
+ }
8491
+ return manifest;
8492
+ }
8493
+ async function resolvePriorTargetState(options) {
8494
+ const candidates = await discoverLegacyStateCandidates(options);
8495
+ const correlated = candidates.filter((candidate) => candidate.manifest);
8496
+ if (options.stableManifest) {
8497
+ for (const candidate of correlated) {
8498
+ if (!sameManifestContributions(options.stableManifest, candidate.manifest)) {
8499
+ throw new Error(
8500
+ `Stable and legacy target states coexist with disagreeing contributions for ${options.adapter}; reconcile them explicitly.`
8501
+ );
8502
+ }
8503
+ }
8504
+ const sameKeyCandidates = correlated.filter((candidate) => candidate.stateKey === options.stateKey);
8505
+ if (!options.stableLock && sameKeyCandidates.length > 1) {
8506
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple correlated graph locks use the explicit state key.`);
8507
+ }
8508
+ if (!options.stableLock && sameKeyCandidates.length === 1) {
8509
+ const candidate = sameKeyCandidates[0];
8510
+ return {
8511
+ graphLock: candidate.graphLock,
8512
+ manifest: options.stableManifest,
8513
+ migration: migrationFromCandidate(candidate)
8514
+ };
8515
+ }
8516
+ return { graphLock: options.stableLock, manifest: options.stableManifest };
8517
+ }
8518
+ if (options.stableLock && correlated.length > 0) {
8519
+ throw new Error(
8520
+ `Stable graph state and legacy manifest state coexist for ${options.adapter} without a stable manifest; reconcile them explicitly.`
8521
+ );
8522
+ }
8523
+ if (correlated.length > 1) {
8524
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple correlated manifests were found.`);
8525
+ }
8526
+ if (correlated.length === 1) {
8527
+ const candidate = correlated[0];
8528
+ return {
8529
+ graphLock: candidate.graphLock,
8530
+ manifest: candidate.manifest,
8531
+ migration: migrationFromCandidate(candidate)
8532
+ };
8533
+ }
8534
+ if (options.stableLock) return { graphLock: options.stableLock };
8535
+ if (candidates.length > 1) {
8536
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple graph-only candidates were found.`);
8537
+ }
8538
+ if (candidates.length === 1) {
8539
+ throw new Error(
8540
+ `Legacy graph-only target state for ${options.adapter} has no correlated install manifest; runtime identity and ownership cannot be proven, so the graph lock was preserved.`
8541
+ );
8542
+ }
8543
+ return {};
8544
+ }
8545
+ async function discoverLegacyStateCandidates(options) {
8546
+ const directory = dirname17(options.graphLockPath);
8547
+ let names;
8548
+ try {
8549
+ names = await readdir9(directory);
8550
+ } catch (error) {
8551
+ if (error.code === "ENOENT") return [];
8552
+ throw error;
8553
+ }
8554
+ const stableName = basename18(options.graphLockPath);
8555
+ const suffix = ".graph-lock.json";
8556
+ const candidates = [];
8557
+ for (const name of names.sort((left, right) => left.localeCompare(right))) {
8558
+ if (name === stableName || !name.endsWith(suffix)) continue;
8559
+ const fingerprint = name.slice(0, -suffix.length);
8560
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint)) continue;
8561
+ const graphLockPath = join33(directory, name);
8562
+ let graphLock;
8563
+ try {
8564
+ graphLock = await readGraphLock(graphLockPath);
8565
+ } catch (error) {
8566
+ throw new Error(
8567
+ `Legacy graph lock at ${graphLockPath} is invalid or unsupported: ` + (error instanceof Error ? error.message : String(error))
8568
+ );
8569
+ }
8570
+ if (graphLock.canonical.targetFingerprint !== fingerprint) {
8571
+ throw new Error(
8572
+ `Legacy graph lock fingerprint does not match its filename at ${graphLockPath}.`
8573
+ );
8574
+ }
8575
+ if (options.transport.kind === "ssh") {
8576
+ throw new Error(
8577
+ `Legacy SSH target state at ${graphLockPath} does not record its original endpoint; Agentwheel cannot prove endpoint identity, so the graph lock was preserved.`
8578
+ );
8579
+ }
8580
+ const graphLockDigest = digestGraphLock(graphLock);
8581
+ const candidateStateKey = stateKeyFor(options.adapter, {
8582
+ installationType: options.installationType,
8583
+ stateKey: options.explicitStateKey,
8584
+ targetFingerprint: fingerprint,
8585
+ fleetId: options.fleetId
8586
+ });
8587
+ let manifest;
8588
+ let sourceLock;
8589
+ try {
8590
+ manifest = await readInstallManifest(options.installRoot, options.adapter, options.transport, {
8591
+ installationType: options.installationType,
8592
+ stateKey: candidateStateKey
8593
+ });
8594
+ sourceLock = await readSourceLock(options.installRoot, options.adapter, options.transport, {
8595
+ installationType: options.installationType,
8596
+ stateKey: candidateStateKey
8597
+ });
8598
+ } catch (error) {
8599
+ throw new Error(
8600
+ `Legacy target state for ${graphLockPath} is invalid: ` + (error instanceof Error ? error.message : String(error))
8601
+ );
8602
+ }
8603
+ if (!manifest) {
8604
+ if (sourceLock) {
8605
+ throw new Error(`Legacy source lock for ${graphLockPath} has no correlated install manifest.`);
8606
+ }
8607
+ candidates.push({ stateKey: candidateStateKey, graphLockPath, graphLockDigest, graphLock });
8608
+ continue;
8609
+ }
8610
+ const validated = validateLegacyManifestCandidate(manifest, {
8611
+ ...options,
8612
+ candidateStateKey,
8613
+ graphLockDigest,
8614
+ graphLockPath
8615
+ });
8616
+ candidates.push({
8617
+ stateKey: candidateStateKey,
8618
+ graphLockPath,
8619
+ graphLockDigest,
8620
+ graphLock,
8621
+ manifest: validated,
8622
+ sourceLock
8623
+ });
8624
+ }
8625
+ return candidates;
8626
+ }
8627
+ function validateLegacyManifestCandidate(manifest, options) {
8628
+ if (manifest.version !== 2) {
8629
+ throw new Error(`Legacy target state for ${options.graphLockPath} requires an install manifest v2.`);
8630
+ }
8631
+ if (manifest.adapter !== options.adapter) {
8632
+ throw new Error(`Legacy target state adapter mismatch at ${options.graphLockPath}.`);
8633
+ }
8634
+ if (manifest.installationType !== options.installationType) {
8635
+ throw new Error(`Legacy target state installation type mismatch at ${options.graphLockPath}.`);
8636
+ }
8637
+ if (manifest.stateKey !== options.candidateStateKey) {
8638
+ throw new Error(`Legacy target state key mismatch at ${options.graphLockPath}.`);
8639
+ }
8640
+ if (normalizeInstallRoot2(manifest.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8641
+ throw new Error(`Legacy target state target root mismatch at ${options.graphLockPath}.`);
8642
+ }
8643
+ const correlated = manifest.entries.filter((entry) => entry.graphLockDigest === options.graphLockDigest);
8644
+ if (correlated.length === 0) {
8645
+ throw new Error(`Legacy install manifest graph-lock digest does not match ${options.graphLockPath}.`);
8646
+ }
8647
+ if (correlated.some((entry) => entry.workspaceOwner !== options.workspaceOwner)) {
8648
+ throw new Error(`Legacy target state contribution scope does not match ${options.graphLockPath}.`);
8649
+ }
8650
+ return manifest;
8651
+ }
8652
+ function migrationFromCandidate(candidate) {
8653
+ return {
8654
+ fromStateKey: candidate.manifest ? candidate.stateKey : void 0,
8655
+ fromGraphLockPath: candidate.graphLockPath,
8656
+ fromGraphLockDigest: candidate.graphLockDigest,
8657
+ sourceLock: candidate.manifest ? candidate.sourceLock ?? null : void 0
8658
+ };
8659
+ }
8660
+ function sameManifestContributions(left, right) {
8661
+ if (left.version !== 2) return false;
8662
+ return stableJson2(manifestContributionState(left)) === stableJson2(manifestContributionState(right));
8663
+ }
8664
+ function manifestContributionState(manifest) {
8665
+ return manifest.entries.map(({ updatedAt: _updatedAt, graphLockDigest: _graphLockDigest, ...entry }) => entry).sort((left, right) => left.path.localeCompare(right.path));
8666
+ }
8667
+ function stableJson2(value) {
8668
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
8669
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
8670
+ const record = value;
8671
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort((left, right) => left.localeCompare(right)).map((key) => `${JSON.stringify(key)}:${stableJson2(record[key])}`).join(",")}}`;
8672
+ }
8673
+ function normalizeInstallRoot2(path, transportKind) {
8674
+ return transportKind === "ssh" ? posix2.normalize(path) : resolve16(path);
8675
+ }
8222
8676
  var workspaceOwnerPrefix2 = "workspace-root:";
8223
8677
  async function assertNoForeignWorkspaceState(check) {
8224
8678
  const planned = new Set(check.plannedPaths);
8225
- if (planned.size === 0) return;
8679
+ if (planned.size === 0) return [];
8226
8680
  const plannedOperations = /* @__PURE__ */ new Map();
8227
8681
  for (const operation of check.plannedOperations) {
8228
8682
  if (!planned.has(operation.relativeDestPath)) continue;
@@ -8231,18 +8685,22 @@ async function assertNoForeignWorkspaceState(check) {
8231
8685
  plannedOperations.set(operation.relativeDestPath, operations);
8232
8686
  }
8233
8687
  const manifests = await listInstallManifests(check.installRoot, check.adapter, check.transport);
8234
- const root = resolve15(check.workspaceRoot);
8688
+ const root = resolve16(check.workspaceRoot);
8235
8689
  const ownsSubWorkspaces = root !== globalConfigRoot(check.globalRoot);
8236
- const foreign = [];
8690
+ const collisions = /* @__PURE__ */ new Map();
8237
8691
  for (const entry of manifests) {
8238
- if (entry.stateKey === check.stateKey) continue;
8239
- const byOwner = /* @__PURE__ */ new Map();
8692
+ if (entry.stateKey === check.stateKey || entry.stateKey === check.migratingStateKey) continue;
8693
+ const entryCounts = /* @__PURE__ */ new Map();
8694
+ for (const manifestEntry of entry.manifest.entries) {
8695
+ const owner = "workspaceOwner" in manifestEntry ? manifestEntry.workspaceOwner : void 0;
8696
+ if (typeof owner === "string" && owner.startsWith(workspaceOwnerPrefix2)) {
8697
+ entryCounts.set(owner, (entryCounts.get(owner) ?? 0) + 1);
8698
+ }
8699
+ }
8240
8700
  for (const manifestEntry of entry.manifest.entries) {
8241
8701
  const owner = "workspaceOwner" in manifestEntry ? manifestEntry.workspaceOwner : void 0;
8242
8702
  if (typeof owner !== "string" || !owner.startsWith(workspaceOwnerPrefix2)) continue;
8243
8703
  if (ownedByWorkspace(owner, check.workspaceOwner, root, ownsSubWorkspaces)) continue;
8244
- const bucket = byOwner.get(owner) ?? { entryCount: 0, collidingPaths: [] };
8245
- bucket.entryCount += 1;
8246
8704
  if (planned.has(manifestEntry.path) && !await isDisjointVerifiedMergeContribution(
8247
8705
  check,
8248
8706
  manifestEntry.path,
@@ -8250,16 +8708,46 @@ async function assertNoForeignWorkspaceState(check) {
8250
8708
  manifestEntry.mergeRemoval,
8251
8709
  plannedOperations.get(manifestEntry.path) ?? []
8252
8710
  )) {
8253
- bucket.collidingPaths.push(manifestEntry.path);
8711
+ const ownedEntry = manifestEntry;
8712
+ const pathCollisions = collisions.get(manifestEntry.path) ?? [];
8713
+ pathCollisions.push({
8714
+ owner,
8715
+ stateKey: entry.stateKey,
8716
+ fileName: entry.fileName,
8717
+ manifestRevision: entry.manifest.revision,
8718
+ entryCount: entryCounts.get(owner) ?? 1,
8719
+ entry: ownedEntry
8720
+ });
8721
+ collisions.set(manifestEntry.path, pathCollisions);
8254
8722
  }
8255
- byOwner.set(owner, bucket);
8256
8723
  }
8257
- for (const [owner, bucket] of byOwner) {
8258
- if (bucket.collidingPaths.length === 0) continue;
8259
- foreign.push({ owner, fileName: entry.fileName, ...bucket });
8724
+ }
8725
+ const foreignByOwner = /* @__PURE__ */ new Map();
8726
+ const observations = [];
8727
+ for (const [path, pathCollisions] of collisions) {
8728
+ const operations = plannedOperations.get(path) ?? [];
8729
+ const exact = pathCollisions.length === 1 ? await exactForeignKeepOperation(check, pathCollisions[0], operations) : void 0;
8730
+ if (exact) {
8731
+ const index = check.plannedOperations.indexOf(operations[0]);
8732
+ if (index < 0) throw new Error(`Foreign-state reconciliation lost planned operation for ${path}.`);
8733
+ check.plannedOperations[index] = exact.operation;
8734
+ observations.push(exact.observation);
8735
+ continue;
8736
+ }
8737
+ for (const collision of pathCollisions) {
8738
+ const key = `${collision.owner}\0${collision.fileName}`;
8739
+ const bucket = foreignByOwner.get(key) ?? {
8740
+ owner: collision.owner,
8741
+ fileName: collision.fileName,
8742
+ entryCount: collision.entryCount,
8743
+ collidingPaths: []
8744
+ };
8745
+ if (!bucket.collidingPaths.includes(path)) bucket.collidingPaths.push(path);
8746
+ foreignByOwner.set(key, bucket);
8260
8747
  }
8261
8748
  }
8262
- if (foreign.length === 0) return;
8749
+ const foreign = [...foreignByOwner.values()];
8750
+ if (foreign.length === 0) return observations.sort((left, right) => left.path.localeCompare(right.path));
8263
8751
  throw new Error([
8264
8752
  `Refusing to plan ${check.adapter} at ${check.installRoot}.`,
8265
8753
  "This runtime root already carries Agentwheel state owned by another workspace, at paths this run would install:",
@@ -8273,6 +8761,36 @@ async function assertNoForeignWorkspaceState(check) {
8273
8761
  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."
8274
8762
  ].join("\n"));
8275
8763
  }
8764
+ async function exactForeignKeepOperation(check, collision, operations) {
8765
+ if (operations.length !== 1) return void 0;
8766
+ const operation = operations[0];
8767
+ const entry = collision.entry;
8768
+ if (operation.semanticPlugin || operation.programmaticOperation || operation.mergeStrategy || operation.mode || entry.semanticPlugin || entry.mergeStrategy || entry.mode) return void 0;
8769
+ if (!operation.desiredHash || operation.artifactType !== entry.artifactType || operation.artifactName !== entry.artifactName || operation.kind !== entry.kind || operation.desiredHash !== entry.sourceHash) return void 0;
8770
+ const path = join33(check.installRoot, entry.path);
8771
+ if (!await check.transport.pathExists(path)) return void 0;
8772
+ const currentHash = await check.transport.hashPath(path);
8773
+ if (currentHash !== entry.hash || currentHash !== operation.desiredHash || operation.currentHash !== void 0 && operation.currentHash !== currentHash) return void 0;
8774
+ return {
8775
+ operation: {
8776
+ ...operation,
8777
+ action: "keep",
8778
+ currentHash,
8779
+ manifestHash: entry.hash,
8780
+ reason: `exact foreign artifact owned by ${collision.owner}; kept outside workspace ${check.workspaceOwner}`,
8781
+ preserveInManifest: false
8782
+ },
8783
+ observation: {
8784
+ stateKey: collision.stateKey,
8785
+ fileName: collision.fileName,
8786
+ manifestRevision: collision.manifestRevision,
8787
+ workspaceOwner: collision.owner,
8788
+ path: entry.path,
8789
+ graphNodeId: entry.graphNodeId,
8790
+ logicalSelector: entry.logicalSelector
8791
+ }
8792
+ };
8793
+ }
8276
8794
  async function isDisjointVerifiedMergeContribution(check, relativePath, foreignStrategy, foreignRemoval, operations) {
8277
8795
  if (!foreignStrategy || !hasMergeRemovalContent(foreignRemoval) || operations.length !== 1) return false;
8278
8796
  const operation = operations[0];
@@ -8444,7 +8962,7 @@ function targetLabel(target) {
8444
8962
  }
8445
8963
 
8446
8964
  // src/runtime/target.ts
8447
- import { basename as basename18, dirname as dirname18, join as join34, resolve as resolve16 } from "path";
8965
+ import { basename as basename19, dirname as dirname18, join as join34, resolve as resolve17 } from "path";
8448
8966
  var runtimeMarkers = [
8449
8967
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
8450
8968
  { adapter: "claude", dirs: [".claude"] },
@@ -8453,9 +8971,9 @@ var runtimeMarkers = [
8453
8971
  { adapter: "copilot", dirs: [".github"] }
8454
8972
  ];
8455
8973
  async function resolveRuntimeTarget(request = {}) {
8456
- const cwd = resolve16(request.cwd ?? process.cwd());
8974
+ const cwd = resolve17(request.cwd ?? process.cwd());
8457
8975
  if (request.targetRoot) {
8458
- const targetRoot = resolve16(request.targetRoot);
8976
+ const targetRoot = resolve17(request.targetRoot);
8459
8977
  return {
8460
8978
  adapter: request.adapter ?? "openclaw",
8461
8979
  installationType: request.installationType,
@@ -8487,7 +9005,7 @@ async function resolveRuntimeTarget(request = {}) {
8487
9005
  async function resolveAllRuntimeTargets(request = {}) {
8488
9006
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
8489
9007
  if (request.agent) return [await resolveRuntimeTarget(request)];
8490
- const cwd = resolve16(request.cwd ?? process.cwd());
9008
+ const cwd = resolve17(request.cwd ?? process.cwd());
8491
9009
  const workspaceRoot = await findWorkspaceRoot(cwd);
8492
9010
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8493
9011
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot, request.installationType, request.fleetId));
@@ -8497,8 +9015,8 @@ async function resolveAllRuntimeTargets(request = {}) {
8497
9015
  return targets;
8498
9016
  }
8499
9017
  async function resolveProfileRuntimeTargets(request) {
8500
- const cwd = resolve16(request.cwd ?? process.cwd());
8501
- const workspaceRoot = request.targetRoot ? resolve16(request.targetRoot) : await findWorkspaceRoot(cwd);
9018
+ const cwd = resolve17(request.cwd ?? process.cwd());
9019
+ const workspaceRoot = request.targetRoot ? resolve17(request.targetRoot) : await findWorkspaceRoot(cwd);
8502
9020
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8503
9021
  const profile = config.profiles[request.profile];
8504
9022
  if (!profile) {
@@ -8564,12 +9082,12 @@ async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
8564
9082
  return unique[0];
8565
9083
  }
8566
9084
  async function detectRuntimeTargets(cwd = process.cwd(), adapterFilter) {
8567
- const root = resolve16(cwd);
9085
+ const root = resolve17(cwd);
8568
9086
  const matches = [];
8569
9087
  for (const marker of runtimeMarkers) {
8570
9088
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
8571
9089
  for (const dir of marker.dirs) {
8572
- if (basename18(root) === dir) {
9090
+ if (basename19(root) === dir) {
8573
9091
  matches.push({ adapter: marker.adapter, targetRoot: dirname18(root) });
8574
9092
  } else if (await pathExists(join34(root, dir))) {
8575
9093
  matches.push({ adapter: marker.adapter, targetRoot: root });
@@ -8613,9 +9131,9 @@ function dedupeTargets(matches) {
8613
9131
  return [...byKey.values()];
8614
9132
  }
8615
9133
  function runtimeScanRoot(request) {
8616
- const root = resolve16(request.targetRoot ?? request.cwd ?? process.cwd());
9134
+ const root = resolve17(request.targetRoot ?? request.cwd ?? process.cwd());
8617
9135
  if (request.targetRoot) return root;
8618
- return runtimeMarkers.some((marker) => marker.dirs.includes(basename18(root))) ? dirname18(root) : root;
9136
+ return runtimeMarkers.some((marker) => marker.dirs.includes(basename19(root))) ? dirname18(root) : root;
8619
9137
  }
8620
9138
 
8621
9139
  // src/lifecycle/profile.ts
@@ -9002,9 +9520,9 @@ function normalizeVersion(version) {
9002
9520
 
9003
9521
  // src/model/package-validate.ts
9004
9522
  import { stat as stat12 } from "fs/promises";
9005
- import { resolve as resolve17 } from "path";
9523
+ import { resolve as resolve18 } from "path";
9006
9524
  async function validatePackage(root) {
9007
- const packageRoot = resolve17(root);
9525
+ const packageRoot = resolve18(root);
9008
9526
  const findings = [];
9009
9527
  const manifestPath = await findPackageManifestPath(packageRoot);
9010
9528
  if (!manifestPath) {
@@ -9105,7 +9623,7 @@ async function validateManifestComposeInclude(packageRoot, selector, optional, f
9105
9623
  try {
9106
9624
  validateSelector(selector, "compose.include", findings, manifestPath, { fragmentsOnly: true, aliases });
9107
9625
  if (isCrossPackageSelector(selector)) return;
9108
- const full = resolve17(packageRoot, selector);
9626
+ const full = resolve18(packageRoot, selector);
9109
9627
  if (full !== packageRoot && !full.startsWith(`${packageRoot}/`)) {
9110
9628
  findings.push({ level: "error", message: `Compose include escapes package root: ${selector}`, path: manifestPath });
9111
9629
  return;
@@ -9159,10 +9677,10 @@ function isCrossPackageSelector(value) {
9159
9677
 
9160
9678
  // src/model/package-migrate.ts
9161
9679
  import { readFile as readFile22, rename as rename3, writeFile as writeFile12 } from "fs/promises";
9162
- import { join as join37, resolve as resolve18 } from "path";
9680
+ import { join as join37, resolve as resolve19 } from "path";
9163
9681
  import { applyEdits, modify, parse as parse2 } from "jsonc-parser";
9164
9682
  async function migratePackageManifest(root) {
9165
- const packageRoot = resolve18(root);
9683
+ const packageRoot = resolve19(root);
9166
9684
  for (const name of openPackManifestNames) {
9167
9685
  const path = join37(packageRoot, name);
9168
9686
  if (await pathExists(path)) {
@@ -9224,7 +9742,7 @@ function resolveCliVersion() {
9224
9742
  }
9225
9743
 
9226
9744
  // src/lifecycle/ownership.ts
9227
- import { resolve as resolve19 } from "path";
9745
+ import { resolve as resolve20 } from "path";
9228
9746
  async function planArtifactOwnershipHandoff(request) {
9229
9747
  return validateOwnershipHandoff(request, request.transport ?? localTransport);
9230
9748
  }
@@ -9350,8 +9868,8 @@ function containedArtifactPath(targetRoot, relativePath) {
9350
9868
  if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\0")) {
9351
9869
  throw new Error(`Unsafe managed artifact path: ${relativePath}`);
9352
9870
  }
9353
- const root = resolve19(targetRoot);
9354
- const candidate = resolve19(root, relativePath);
9871
+ const root = resolve20(targetRoot);
9872
+ const candidate = resolve20(root, relativePath);
9355
9873
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
9356
9874
  throw new Error(`Managed artifact path escapes target root: ${relativePath}`);
9357
9875
  }
@@ -9384,7 +9902,7 @@ function assertManifestIdentity(manifest, request) {
9384
9902
 
9385
9903
  // src/lifecycle/ownership-retire-stale.ts
9386
9904
  import { createHash as createHash9 } from "crypto";
9387
- import { resolve as resolve20 } from "path";
9905
+ import { resolve as resolve21 } from "path";
9388
9906
  async function planRetireStaleOwnership(request) {
9389
9907
  return observe(request, request.transport ?? localTransport);
9390
9908
  }
@@ -9517,7 +10035,7 @@ async function observe(request, transport) {
9517
10035
  return { ...withoutDigest, planDigest: digest(withoutDigest) };
9518
10036
  }
9519
10037
  function normalizeRequest(request) {
9520
- const targetRoot = resolve20(request.targetRoot);
10038
+ const targetRoot = resolve21(request.targetRoot);
9521
10039
  const sourceStateKey = exactStateKey(request.adapter, request.sourceStateKey, request.installationType, "source");
9522
10040
  const destinationStateKey = exactStateKey(request.adapter, request.destinationStateKey, request.installationType, "destination");
9523
10041
  if (sourceStateKey === destinationStateKey) throw new Error("Source and destination state keys must differ.");
@@ -9528,8 +10046,8 @@ function normalizeRequest(request) {
9528
10046
  installationType: request.installationType.trim(),
9529
10047
  sourceStateKey,
9530
10048
  destinationStateKey,
9531
- fromWorkspaceRoot: resolve20(request.fromWorkspaceRoot),
9532
- toWorkspaceRoot: resolve20(request.toWorkspaceRoot),
10049
+ fromWorkspaceRoot: resolve21(request.fromWorkspaceRoot),
10050
+ toWorkspaceRoot: resolve21(request.toWorkspaceRoot),
9533
10051
  toFleetId: request.toFleetId.trim()
9534
10052
  };
9535
10053
  }
@@ -9550,7 +10068,7 @@ async function requireManifest(request, stateKey, transport, label) {
9550
10068
  installationType: manifest.installationType,
9551
10069
  stateKey: manifest.stateKey
9552
10070
  });
9553
- if (manifest.adapter !== request.adapter || manifest.installationType !== request.installationType || resolve20(manifest.targetRoot) !== request.targetRoot || actualStateKey !== stateKey) {
10071
+ if (manifest.adapter !== request.adapter || manifest.installationType !== request.installationType || resolve21(manifest.targetRoot) !== request.targetRoot || actualStateKey !== stateKey) {
9554
10072
  throw new Error(`${label} install manifest identity does not match the requested adapter, target, installation type, and state key.`);
9555
10073
  }
9556
10074
  return manifest;
@@ -9634,7 +10152,7 @@ function canonicalJson(value) {
9634
10152
  // src/version/policy.ts
9635
10153
  import { execFile as execFile3 } from "child_process";
9636
10154
  import { readFile as readFile23 } from "fs/promises";
9637
- import { join as join39, resolve as resolve21 } from "path";
10155
+ import { join as join39, resolve as resolve22 } from "path";
9638
10156
  import { promisify as promisify3 } from "util";
9639
10157
  import { parse as parseJsonc } from "jsonc-parser";
9640
10158
  import { z as z5 } from "zod";
@@ -9728,7 +10246,7 @@ async function discoverVersionsFromSource(pkg, workspaceRoot) {
9728
10246
  if (tagged.length > 0) return tagged;
9729
10247
  }
9730
10248
  if (driverName === "local") {
9731
- const root = resolve21(workspaceRoot, pkg.source);
10249
+ const root = resolve22(workspaceRoot, pkg.source);
9732
10250
  const manifest2 = await readPackageManifest(root);
9733
10251
  const current = manifest2 ? [{ version: manifest2.version, ref: pkg.requestedRef ?? root }] : [];
9734
10252
  try {
@@ -9851,7 +10369,7 @@ async function readVersionCache(path) {
9851
10369
  // src/profile/members.ts
9852
10370
  import { execFile as execFile4 } from "child_process";
9853
10371
  import { readFile as readFile24 } from "fs/promises";
9854
- import { join as join40, resolve as resolve22 } from "path";
10372
+ import { join as join40, resolve as resolve23 } from "path";
9855
10373
  import { promisify as promisify4 } from "util";
9856
10374
  import { z as z7 } from "zod";
9857
10375
 
@@ -10026,7 +10544,7 @@ async function invokeMemberStatus(member, parentWorkspace, chain, options, cliEn
10026
10544
  let stderr = "";
10027
10545
  try {
10028
10546
  if (member.transport === "local") {
10029
- const workspace = resolve22(parentWorkspace, member.workspace);
10547
+ const workspace = resolve23(parentWorkspace, member.workspace);
10030
10548
  const result = await execFileAsync4(process.execPath, [cliEntry, ...args], {
10031
10549
  cwd: workspace,
10032
10550
  env: env2,
@@ -10072,7 +10590,7 @@ async function runMemberAgentwheel(member, parentWorkspace, args, chain) {
10072
10590
  process.execPath,
10073
10591
  [process.argv[1], ...memberCommandArgs(member, args)],
10074
10592
  {
10075
- cwd: resolve22(parentWorkspace, member.workspace),
10593
+ cwd: resolve23(parentWorkspace, member.workspace),
10076
10594
  env: env2,
10077
10595
  maxBuffer: 20 * 1024 * 1024
10078
10596
  }
@@ -10178,7 +10696,7 @@ function assertNoCompositeCycle(workspaceRoot, profileName, chain) {
10178
10696
  }
10179
10697
  }
10180
10698
  function compositeKey(workspaceRoot, profileName) {
10181
- return `${resolve22(workspaceRoot)}#${profileName}`;
10699
+ return `${resolve23(workspaceRoot)}#${profileName}`;
10182
10700
  }
10183
10701
 
10184
10702
  // src/status/repository.ts
@@ -11103,9 +11621,9 @@ function readSkillFrontmatter(content, artifact) {
11103
11621
 
11104
11622
  // src/lifecycle/fleet-normalize.ts
11105
11623
  import { createHash as createHash13 } from "crypto";
11106
- import { readFile as readFile27, readdir as readdir9, rm as rm9 } from "fs/promises";
11624
+ import { readFile as readFile27, readdir as readdir10, rm as rm9 } from "fs/promises";
11107
11625
  import { homedir as homedir11 } from "os";
11108
- import { basename as basename19, isAbsolute as isAbsolute3, join as join44, relative as relative8, resolve as resolve23 } from "path";
11626
+ import { dirname as dirname21, isAbsolute as isAbsolute3, join as join44, relative as relative8, resolve as resolve24 } from "path";
11109
11627
  async function planFleetNormalization(request) {
11110
11628
  const normalizedRequest = normalizeRequest2(request);
11111
11629
  const destinationScope = await resolveWorkspaceScope({
@@ -11376,17 +11894,17 @@ function normalizeRequest2(request) {
11376
11894
  ...profile ? { profile } : {},
11377
11895
  ...agent ? { agent } : {},
11378
11896
  ...orphanedOwnerRoots ? { orphanedOwnerRoots } : {},
11379
- ...request.globalRoot ? { globalRoot: resolve23(request.globalRoot) } : {}
11897
+ ...request.globalRoot ? { globalRoot: resolve24(request.globalRoot) } : {}
11380
11898
  };
11381
11899
  }
11382
11900
  function normalizeOrphanedOwnerRoot(value) {
11383
11901
  const root = value.trim();
11384
11902
  if (!root || !isAbsolute3(root)) throw new Error(`Invalid orphaned owner root '${value}'; expected an absolute workspace path.`);
11385
- return resolve23(root);
11903
+ return resolve24(root);
11386
11904
  }
11387
11905
  async function normalizationRoots(request) {
11388
11906
  const destination = await showRegisteredFleet(request.destinationFleet, { globalRoot: request.globalRoot });
11389
- const source = request.from === "user" ? resolve23(request.globalRoot ?? homedir11()) : (await showRegisteredFleet(request.from.slice("fleet:".length), { globalRoot: request.globalRoot })).root;
11907
+ const source = request.from === "user" ? resolve24(request.globalRoot ?? homedir11()) : (await showRegisteredFleet(request.from.slice("fleet:".length), { globalRoot: request.globalRoot })).root;
11390
11908
  return { source, destination: destination.root };
11391
11909
  }
11392
11910
  async function withOrderedLocks(roots, fn) {
@@ -11452,11 +11970,28 @@ async function inspectInstalledState(source, destination, packageNames) {
11452
11970
  const coveredDestinationGraphs = /* @__PURE__ */ new Set();
11453
11971
  const plannedDestinationGraphs = /* @__PURE__ */ new Set();
11454
11972
  for (const sourceGraph of sourceGraphs) {
11455
- const sourceState = await targetStateForGraph(source, sourceGraph);
11456
- const destinationState = await targetStateForGraph(destination, sourceGraph);
11457
- if (sourceState.graphLockPath !== sourceGraph.path) {
11458
- throw new Error(`Source graph lock target identity is stale or noncanonical: ${sourceGraph.path}`);
11973
+ const sourceState = await installedTargetStateForGraph(
11974
+ source,
11975
+ sourceGraph,
11976
+ manifests,
11977
+ sourceOwners,
11978
+ selected
11979
+ );
11980
+ const destinationStableState = await targetStateForGraph(destination, sourceGraph);
11981
+ const matchingDestinationGraphs = destinationGraphs.filter((candidate) => candidate.targetKey === sourceGraph.targetKey && candidate.adapter === sourceGraph.adapter);
11982
+ if (matchingDestinationGraphs.length > 1) {
11983
+ throw new Error(
11984
+ `Ambiguous destination graph state for target '${sourceGraph.targetKey}': ` + matchingDestinationGraphs.map((candidate) => candidate.path).join(", ")
11985
+ );
11459
11986
  }
11987
+ const destinationGraph = matchingDestinationGraphs[0];
11988
+ const destinationState = destinationGraph ? await installedTargetStateForGraph(
11989
+ destination,
11990
+ destinationGraph,
11991
+ manifests,
11992
+ destinationOwners,
11993
+ selected
11994
+ ) : destinationStableState;
11460
11995
  if (sourceState.adapter !== destinationState.adapter) {
11461
11996
  throw new Error(`Source and destination adapters diverge for target '${sourceGraph.targetKey}'.`);
11462
11997
  }
@@ -11465,7 +12000,6 @@ async function inspectInstalledState(source, destination, packageNames) {
11465
12000
  throw new Error(`Source graph lock is not covered by its canonical install manifest: ${sourceState.manifestPath}`);
11466
12001
  }
11467
12002
  const sourceGraphArtifacts = new Set(sourceGraph.artifactIdentities);
11468
- const destinationGraph = destinationGraphs.find((candidate) => candidate.path === destinationState.graphLockPath);
11469
12003
  if (destinationGraph && destinationGraph.digest !== sourceGraph.digest) {
11470
12004
  throw new Error("Source and destination graph declarations, selections, versions, identities, or rendered paths diverge.");
11471
12005
  }
@@ -11583,16 +12117,30 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11583
12117
  const transferableOwners = orphanedOwners.size > 0 ? orphanedOwners : /* @__PURE__ */ new Set([legacyOwner]);
11584
12118
  let normalizedGraphCount = 0;
11585
12119
  for (const graph of graphCandidates) {
12120
+ const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
12121
+ if (resolve24(destinationState.graphLockPath) === resolve24(graph.path)) {
12122
+ const destinationManifest = (await collectManifestPaths([destinationState.manifestPath]))[0];
12123
+ if (destinationManifest?.manifest.entries.some((entry) => entry.workspaceOwner === fleetOwner && entryMatchesPackages(entry, selected))) {
12124
+ assertCorrelatedGraphManifest(
12125
+ graph,
12126
+ destinationState,
12127
+ destinationManifest,
12128
+ /* @__PURE__ */ new Set([fleetOwner]),
12129
+ selected
12130
+ );
12131
+ normalizedGraphCount += 1;
12132
+ continue;
12133
+ }
12134
+ }
11586
12135
  const legacyState = await legacyTargetStateForGraph(fleet, graph);
12136
+ if (resolve24(legacyState.graphLockPath) !== resolve24(graph.path)) {
12137
+ throw new Error(`Legacy graph lock target identity is stale or noncanonical: ${graph.path}`);
12138
+ }
11587
12139
  const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
11588
12140
  const relevantManifestEntries2 = legacyManifest?.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected) && entryMatchesArtifacts(entry, selectedArtifacts)) ?? [];
11589
12141
  if (relevantManifestEntries2.length > 0) {
11590
12142
  graphs.push(graph);
11591
12143
  }
11592
- const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
11593
- if (destinationState.graphLockPath === graph.path && legacyManifest?.manifest.entries.some((entry) => entry.workspaceOwner === fleetOwner && entryMatchesPackages(entry, selected))) {
11594
- normalizedGraphCount += 1;
11595
- }
11596
12144
  }
11597
12145
  if (graphs.length === 0 && normalizedGraphCount > 0) {
11598
12146
  throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
@@ -11760,7 +12308,7 @@ function targetKeysForAgent(scope, name) {
11760
12308
  async function runtimeRoots(scope) {
11761
12309
  const roots = /* @__PURE__ */ new Set([scope.root]);
11762
12310
  if (scope.config.packages.some((pkg) => pkg.installationType === "user") || Object.values(scope.config.agents).some((agent) => agent.installationType === "user")) {
11763
- roots.add(resolve23(process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir11()));
12311
+ roots.add(resolve24(process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir11()));
11764
12312
  }
11765
12313
  for (const [name, agent] of Object.entries(scope.config.agents)) {
11766
12314
  if (agent.transport === "ssh") throw new Error(`Installed-state normalization cannot inspect SSH agent '${name}' locally.`);
@@ -11781,7 +12329,7 @@ async function collectManifests(roots) {
11781
12329
  const dir = join44(root, ".agentwheel");
11782
12330
  let names;
11783
12331
  try {
11784
- names = await readdir9(dir);
12332
+ names = await readdir10(dir);
11785
12333
  } catch (error) {
11786
12334
  if (isMissing(error)) continue;
11787
12335
  throw error;
@@ -11797,14 +12345,14 @@ async function collectManifests(roots) {
11797
12345
  }
11798
12346
  continue;
11799
12347
  }
11800
- if (resolve23(parsed.targetRoot) !== resolve23(root)) {
12348
+ if (resolve24(parsed.targetRoot) !== resolve24(root)) {
11801
12349
  throw new Error(`Install manifest target root does not match its enumerated runtime root: ${path}`);
11802
12350
  }
11803
12351
  const expectedPath = installManifestPath(parsed.targetRoot, parsed.adapter, {
11804
12352
  installationType: parsed.installationType,
11805
12353
  stateKey: parsed.stateKey
11806
12354
  });
11807
- if (resolve23(expectedPath) !== resolve23(path)) {
12355
+ if (resolve24(expectedPath) !== resolve24(path)) {
11808
12356
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11809
12357
  }
11810
12358
  found.set(path, { path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11825,7 +12373,7 @@ async function collectManifestPaths(paths) {
11825
12373
  installationType: parsed.installationType,
11826
12374
  stateKey: parsed.stateKey
11827
12375
  });
11828
- if (resolve23(expectedPath) !== resolve23(path)) {
12376
+ if (resolve24(expectedPath) !== resolve24(path)) {
11829
12377
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11830
12378
  }
11831
12379
  found.push({ path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11853,12 +12401,8 @@ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include)
11853
12401
  if (!lock.canonical.roots.some((candidate2) => selected.has(candidate2.rootId))) continue;
11854
12402
  const parts = relative8(root, path).split(/[\\/]/);
11855
12403
  if (parts.length !== 3) throw new Error(`Graph lock path is not canonical: ${path}`);
11856
- const [targetKey2, adapter, fileName] = parts;
12404
+ const [targetKey2, adapter] = parts;
11857
12405
  if (targetKeys && !targetKeys.has(targetKey2)) continue;
11858
- const pathFingerprint = basename19(fileName, ".graph-lock.json");
11859
- if (!lock.canonical.targetFingerprint || lock.canonical.targetFingerprint !== pathFingerprint) {
11860
- throw new Error(`Graph lock fingerprint does not match its canonical path: ${path}`);
11861
- }
11862
12406
  const candidate = {
11863
12407
  path,
11864
12408
  lock,
@@ -11887,9 +12431,19 @@ function isCurrentLocalTarget(scope, graph) {
11887
12431
  }
11888
12432
  async function hasRelevantLegacyManifestCandidate(scope, graph, selected) {
11889
12433
  if (!isCurrentLocalTarget(scope, graph)) return false;
11890
- const manifestPath = await legacyManifestPathForConfiguredTarget(scope, graph) ?? (await legacyTargetStateForGraph(scope, graph)).manifestPath;
11891
- const manifest = (await collectManifestPaths([manifestPath]))[0];
11892
- return manifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected)) ?? false;
12434
+ const configuredLegacyPath = await legacyManifestPathForConfiguredTarget(scope, graph);
12435
+ if (configuredLegacyPath) {
12436
+ const manifest = (await collectManifestPaths([configuredLegacyPath]))[0];
12437
+ if (manifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected))) return true;
12438
+ }
12439
+ if (graph.lock.canonical.roots.some((root) => !scope.config.packages.some((pkg) => pkg.name === root.rootId))) return false;
12440
+ const legacyState = await legacyTargetStateForGraph(scope, graph);
12441
+ const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
12442
+ if (legacyManifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected))) return true;
12443
+ const stableState = await targetStateForGraph(scope, graph, scope.fleetId ?? null);
12444
+ if (resolve24(stableState.graphLockPath) !== resolve24(graph.path)) return false;
12445
+ const stableManifest = (await collectManifestPaths([stableState.manifestPath]))[0];
12446
+ return stableManifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected)) ?? false;
11893
12447
  }
11894
12448
  async function legacyManifestPathForConfiguredTarget(scope, graph) {
11895
12449
  const agent = scope.config.agents[graph.targetKey];
@@ -11908,9 +12462,10 @@ async function legacyManifestPathForConfiguredTarget(scope, graph) {
11908
12462
  baseDir: scope.root
11909
12463
  });
11910
12464
  const targetRoot = resolveConfigPath(agent.root, scope.root);
11911
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
12465
+ const installRoot = resolve24(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
11912
12466
  const stateKey = stateKeyFor(adapter.name, {
11913
12467
  installationType: agent.installationType,
12468
+ stateKey: agent.stateKey,
11914
12469
  targetFingerprint: graph.lock.canonical.targetFingerprint
11915
12470
  });
11916
12471
  return installManifestPath(installRoot, adapter.name, { installationType: agent.installationType, stateKey });
@@ -11975,14 +12530,21 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11975
12530
  ssh: void 0,
11976
12531
  ...agent?.stateKey ? { stateKey: agent.stateKey } : {}
11977
12532
  };
11978
- const targetFingerprint = computeTargetFingerprint(fingerprintParts);
12533
+ const installRoot = resolve24(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
12534
+ const targetIdentity = resolveTargetStateIdentity({
12535
+ targetFingerprintParts: fingerprintParts,
12536
+ workspaceRoot: scope.root,
12537
+ targetKey: graph.targetKey,
12538
+ resolvedInstallRoot: installRoot,
12539
+ transportKind: "local"
12540
+ });
12541
+ const targetFingerprint = targetIdentity.targetFingerprint;
11979
12542
  const stateKey = stateKeyFor(adapter.name, {
11980
12543
  installationType,
11981
12544
  stateKey: agent?.stateKey,
11982
- targetFingerprint,
12545
+ targetFingerprint: targetIdentity.stateFingerprint,
11983
12546
  ...identityFleetId ? { fleetId: identityFleetId } : {}
11984
12547
  });
11985
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
11986
12548
  return {
11987
12549
  adapter: adapter.name,
11988
12550
  installationType,
@@ -11990,28 +12552,81 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11990
12552
  installRoot,
11991
12553
  stateKey,
11992
12554
  targetFingerprint,
11993
- graphLockPath: graphLockPathForTarget(scope.root, graph.targetKey, adapter.name, fingerprintParts),
12555
+ graphLockPath: graphLockPathForTarget(scope.root, graph.targetKey, adapter.name, fingerprintParts, {
12556
+ resolvedInstallRoot: installRoot,
12557
+ transportKind: "local"
12558
+ }),
11994
12559
  manifestPath: installManifestPath(installRoot, adapter.name, { installationType, stateKey })
11995
12560
  };
11996
12561
  }
11997
- async function legacyTargetStateForGraph(scope, graph) {
11998
- const current = await targetStateForGraph(scope, graph, null);
12562
+ async function installedTargetStateForGraph(scope, graph, manifests, owners, selected, identityFleetId = scope.fleetId ?? null) {
12563
+ const stable = await targetStateForGraph(scope, graph, identityFleetId);
12564
+ let state;
12565
+ if (resolve24(graph.path) === resolve24(stable.graphLockPath)) {
12566
+ state = stable;
12567
+ } else {
12568
+ const released = await releasedTargetStateForGraph(scope, graph, identityFleetId);
12569
+ if (resolve24(graph.path) !== resolve24(released.graphLockPath)) {
12570
+ throw new Error(`Graph lock target identity is stale or noncanonical: ${graph.path}`);
12571
+ }
12572
+ state = released;
12573
+ }
12574
+ const manifest = manifests.find((candidate) => resolve24(candidate.path) === resolve24(state.manifestPath));
12575
+ if (!manifest) {
12576
+ throw new Error(`Graph lock is not covered by its canonical install manifest: ${state.manifestPath}`);
12577
+ }
12578
+ assertCorrelatedGraphManifest(graph, state, manifest, owners, selected);
12579
+ return state;
12580
+ }
12581
+ async function releasedTargetStateForGraph(scope, graph, identityFleetId = scope.fleetId ?? null) {
12582
+ const current = await targetStateForGraph(scope, graph, identityFleetId);
11999
12583
  const targetFingerprint = graph.lock.canonical.targetFingerprint;
12584
+ if (!targetFingerprint || !/^[a-f0-9]{64}$/u.test(targetFingerprint)) {
12585
+ throw new Error(`Released graph lock has an invalid target fingerprint: ${graph.path}`);
12586
+ }
12587
+ const agent = scope.config.agents[graph.targetKey];
12000
12588
  const stateKey = stateKeyFor(current.adapter, {
12001
12589
  installationType: current.installationType,
12002
- targetFingerprint
12590
+ stateKey: agent?.stateKey,
12591
+ targetFingerprint,
12592
+ ...identityFleetId ? { fleetId: identityFleetId } : {}
12003
12593
  });
12004
12594
  return {
12005
12595
  ...current,
12006
12596
  stateKey,
12007
12597
  targetFingerprint,
12008
- graphLockPath: graph.path,
12598
+ graphLockPath: join44(dirname21(graph.path), `${targetFingerprint}.graph-lock.json`),
12009
12599
  manifestPath: installManifestPath(current.installRoot, current.adapter, {
12010
12600
  installationType: current.installationType,
12011
12601
  stateKey
12012
12602
  })
12013
12603
  };
12014
12604
  }
12605
+ function assertCorrelatedGraphManifest(graph, state, manifest, owners, selected) {
12606
+ if (manifest.manifest.adapter !== state.adapter || manifest.manifest.installationType !== state.installationType || manifest.manifest.stateKey !== state.stateKey || resolve24(manifest.manifest.targetRoot) !== resolve24(state.installRoot)) {
12607
+ throw new Error(`Install manifest identity does not match graph target state: ${manifest.path}`);
12608
+ }
12609
+ const entries = manifest.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected));
12610
+ if (entries.length === 0) {
12611
+ throw new Error(`Graph lock is not covered by selected install-manifest contributions: ${graph.path}`);
12612
+ }
12613
+ const graphArtifacts = new Set(graph.artifactIdentities);
12614
+ const graphLockDigest = sha2564(canonicalJson2(graph.lock));
12615
+ for (const entry of entries) {
12616
+ if (!owners.has(entry.workspaceOwner)) {
12617
+ throw new Error(`Graph lock manifest owner mismatch at ${manifest.path}: ${entry.workspaceOwner}.`);
12618
+ }
12619
+ if (!graphArtifacts.has(graphEntryIdentity(entry))) {
12620
+ throw new Error(`Install manifest entry is not covered by its graph lock: ${renderedEntryPath(manifest.manifest, entry)}`);
12621
+ }
12622
+ if (entry.graphLockDigest !== void 0 && entry.graphLockDigest !== graphLockDigest) {
12623
+ throw new Error(`Install manifest graph-lock digest does not match ${graph.path}.`);
12624
+ }
12625
+ }
12626
+ }
12627
+ async function legacyTargetStateForGraph(scope, graph) {
12628
+ return releasedTargetStateForGraph(scope, graph, null);
12629
+ }
12015
12630
  function assertGraphIsSubset(source, destination) {
12016
12631
  const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson2(root)]));
12017
12632
  for (const root of destination.lock.canonical.roots) {
@@ -12045,7 +12660,7 @@ function destinationGraphFromSource(source, targetFingerprint) {
12045
12660
  function mergeDestinationManifest(existing, state, generatedAt, desiredEntries, replaceOwners = /* @__PURE__ */ new Set()) {
12046
12661
  if (existing) {
12047
12662
  const manifest2 = existing.manifest;
12048
- if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve23(manifest2.targetRoot) !== state.installRoot) {
12663
+ if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve24(manifest2.targetRoot) !== state.installRoot) {
12049
12664
  throw new Error(`Destination manifest identity diverges from the derived target: ${existing.path}`);
12050
12665
  }
12051
12666
  }
@@ -12105,7 +12720,7 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12105
12720
  const content = await readFile27(path, "utf8");
12106
12721
  if (hasMergeRemovalContent(entry.mergeRemoval)) {
12107
12722
  assertExactMergeContribution(entry.mergeRemoval, entry.mergeStrategy, content);
12108
- } else if (resolve23(sourcePath) === resolve23(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12723
+ } else if (resolve24(sourcePath) === resolve24(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12109
12724
  assertExactMergeContribution({}, entry.mergeStrategy, content);
12110
12725
  } else {
12111
12726
  throw new Error(`Installed-state normalization cannot prove incomplete merge ownership at ${path}.`);
@@ -12116,8 +12731,8 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12116
12731
  await assertEquivalentRuntimeBytes(sourcePath, destinationPath, entry.hash);
12117
12732
  }
12118
12733
  function renderedEntryPathForRoot(root, entryPath) {
12119
- const normalizedRoot = resolve23(root);
12120
- const candidate = resolve23(normalizedRoot, entryPath);
12734
+ const normalizedRoot = resolve24(root);
12735
+ const candidate = resolve24(normalizedRoot, entryPath);
12121
12736
  if (candidate !== normalizedRoot && !candidate.startsWith(`${normalizedRoot}/`)) {
12122
12737
  throw new Error(`Install manifest entry escapes its target root: ${entryPath}`);
12123
12738
  }
@@ -12208,8 +12823,8 @@ function ownerMatchesGraphPackage(lock, owner, selected) {
12208
12823
  return ownerMatchesPackage(owner, selected) || lock.canonical.roots.some((root) => selected.has(root.rootId) && root.graphNodeId === owner);
12209
12824
  }
12210
12825
  function renderedEntryPath(manifest, entry) {
12211
- const root = resolve23(manifest.targetRoot);
12212
- const candidate = resolve23(root, entry.path);
12826
+ const root = resolve24(manifest.targetRoot);
12827
+ const candidate = resolve24(root, entry.path);
12213
12828
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
12214
12829
  throw new Error(`Install manifest entry escapes its target root: ${entry.path}`);
12215
12830
  }
@@ -12916,11 +13531,11 @@ program.command("deps").description("inspect the OpenPack dependency graph").add
12916
13531
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
12917
13532
  const targets = await resolveCliTargets(normalizedOptions);
12918
13533
  for (const target of targets) {
12919
- const { lock, adapter } = await readTargetGraphLock(target, normalizedOptions);
12920
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
12921
- const state = installStateForTarget(target, adapter, normalizedOptions, installationType);
12922
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transportForTarget(target), state);
12923
- console.log(formatDepsWhy(lock, manifest, selector));
13534
+ const state = await resolveCliTargetState(target, normalizedOptions);
13535
+ if (!state.graphLock) {
13536
+ throw new Error(`No graph lock for ${state.adapter.name} at ${target.targetRoot}: ${state.stableGraphLockPath}`);
13537
+ }
13538
+ console.log(formatDepsWhy(state.graphLock, state.manifest, selector));
12924
13539
  }
12925
13540
  })
12926
13541
  );
@@ -13011,19 +13626,23 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
13011
13626
  throw new Error(`Ownership handoff selector must be exactly type/name: ${selector}`);
13012
13627
  }
13013
13628
  const target = targets[0];
13014
- const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
13015
- const adapter = await resolveAdapterForTarget(target, adapterOptions);
13016
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13017
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
13629
+ const state = await resolveCliTargetState(target, normalizedOptions);
13630
+ const { adapter, installationType } = state;
13631
+ if (state.migration) {
13632
+ throw new Error(
13633
+ "Migrate the target state before ownership handoff by running a full agentwheel install for this target."
13634
+ );
13635
+ }
13018
13636
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13019
13637
  if (options.toFleet) {
13020
13638
  const fleet = await showRegisteredFleet(options.toFleet);
13021
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
13639
+ if (resolve25(fleet.root) !== resolve25(toWorkspaceRoot)) {
13022
13640
  throw new Error(`Destination fleet '${options.toFleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13023
13641
  }
13024
13642
  }
13025
13643
  const request = {
13026
- ...state,
13644
+ installationType,
13645
+ stateKey: state.stableStateKey,
13027
13646
  targetRoot: state.installRoot,
13028
13647
  adapter: adapter.name,
13029
13648
  artifactType,
@@ -13047,7 +13666,7 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
13047
13666
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
13048
13667
  const fleet = await showRegisteredFleet(options.fleet);
13049
13668
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13050
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
13669
+ if (resolve25(fleet.root) !== resolve25(toWorkspaceRoot)) {
13051
13670
  throw new Error(`Destination Fleet '${options.fleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13052
13671
  }
13053
13672
  const targets = await resolveCliTargets(normalizedOptions);
@@ -13055,19 +13674,22 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
13055
13674
  throw new Error(`Stale ownership retirement requires exactly one destination Fleet target, found ${targets.length}.`);
13056
13675
  }
13057
13676
  const target = targets[0];
13058
- if (target.fleetId !== options.fleet || resolve24(target.workspaceRoot) !== resolve24(fleet.root)) {
13677
+ if (target.fleetId !== options.fleet || resolve25(target.workspaceRoot) !== resolve25(fleet.root)) {
13059
13678
  throw new Error(`Resolved target does not belong to destination Fleet '${options.fleet}'.`);
13060
13679
  }
13061
- const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
13062
- const adapter = await resolveAdapterForTarget(target, adapterOptions);
13063
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13064
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
13680
+ const state = await resolveCliTargetState(target, normalizedOptions);
13681
+ const { adapter, installationType } = state;
13682
+ if (state.migration) {
13683
+ throw new Error(
13684
+ "Migrate the target state before ownership retirement by running a full agentwheel install for this target."
13685
+ );
13686
+ }
13065
13687
  const request = {
13066
13688
  targetRoot: state.installRoot,
13067
13689
  adapter: adapter.name,
13068
13690
  installationType,
13069
13691
  sourceStateKey: options.sourceStateKey,
13070
- destinationStateKey: state.stateKey,
13692
+ destinationStateKey: state.stableStateKey,
13071
13693
  fromWorkspaceRoot: normalizeCliPath(options.fromWorkspaceRoot),
13072
13694
  toWorkspaceRoot,
13073
13695
  toFleetId: fleet.id,
@@ -13152,13 +13774,18 @@ program.command("uninstall").description("remove configured packages or managed
13152
13774
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
13153
13775
  const transport = transportForTarget(target);
13154
13776
  const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13155
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
13156
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
13777
+ const state = await resolveCliTargetState(target, normalizedOptions);
13778
+ const manifest = state.manifest;
13157
13779
  if (!manifest) {
13158
13780
  console.log(`No install manifest for ${adapter.name}/${installationType} at ${state.installRoot}`);
13159
13781
  continue;
13160
13782
  }
13161
- const plan = filterUninstallPlanBySelection(await createUninstallPlan(manifest), selectedArtifactsFromOptions(options));
13783
+ const plan = {
13784
+ ...filterUninstallPlanBySelection(await createUninstallPlan(manifest), selectedArtifactsFromOptions(options)),
13785
+ stateKey: state.stableStateKey,
13786
+ stateMigration: state.migration,
13787
+ targetStateFilePreconditions: state.targetStateFilePreconditions
13788
+ };
13162
13789
  console.log(formatPlan(plan));
13163
13790
  const result = await uninstall(plan, { dryRun: normalizedOptions.dryRun, force: normalizedOptions.force, transport });
13164
13791
  if (!normalizedOptions.dryRun) {
@@ -13620,15 +14247,12 @@ async function packageSelectsSkillForTarget(target, pkg, selector, options) {
13620
14247
  if (explicitSelection && !explicitSelection.some((candidate) => candidate === selector)) return false;
13621
14248
  const groups = /* @__PURE__ */ new Map();
13622
14249
  const group = graphGroupForPackage(groups, target, pkg, options);
13623
- const adapter = await resolveAdapterForTarget(group.target, group.adapterOptions);
13624
- const graphLockPath = graphLockPathForTarget(
13625
- group.target.workspaceRoot,
13626
- targetKeyForTarget(group.target, adapter.name),
13627
- adapter.name,
13628
- targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
13629
- );
13630
- if (await pathExists(graphLockPath)) {
13631
- const lock = await readGraphLock(graphLockPath);
14250
+ const state = await resolveCliTargetState(group.target, {
14251
+ ...options,
14252
+ installationType: group.installationType
14253
+ });
14254
+ if (state.graphLock) {
14255
+ const lock = state.graphLock;
13632
14256
  const root = lock.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
13633
14257
  if (root?.selected.includes(selector)) return true;
13634
14258
  }
@@ -13931,13 +14555,30 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
13931
14555
  const allPackages = [...group.packages, ...group.extraPackages];
13932
14556
  const groupHasScope = !scopedRootId || allPackages.some((pkg) => pkg.name === scopedRootId || pkg.source === targetOptions.scope);
13933
14557
  if (behavior.mode === "install" && scopedRootId && !groupHasScope) continue;
13934
- const groupGraphLockPath = graphLockPathForTarget(
13935
- group.target.workspaceRoot,
13936
- targetKeyForTarget(group.target, adapter.name),
13937
- adapter.name,
13938
- targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
13939
- );
13940
- const previousGroupLock = await (await pathExists(groupGraphLockPath) ? readGraphLock(groupGraphLockPath) : void 0);
14558
+ if (behavior.readOnly !== true && targetOptions.dryRun !== true) {
14559
+ const pendingState = await journalStateForTarget(group.target, {
14560
+ ...targetOptions,
14561
+ installationType: group.installationType
14562
+ });
14563
+ if (await readApplyJournal(
14564
+ pendingState.installRoot,
14565
+ pendingState.adapter.name,
14566
+ pendingState.transport,
14567
+ pendingState.state
14568
+ )) {
14569
+ await recoverPendingApply(
14570
+ pendingState.installRoot,
14571
+ pendingState.adapter.name,
14572
+ pendingState.transport,
14573
+ pendingState.state
14574
+ );
14575
+ }
14576
+ }
14577
+ const priorGroupState = await resolveCliTargetState(group.target, {
14578
+ ...targetOptions,
14579
+ installationType: group.installationType
14580
+ });
14581
+ const previousGroupLock = priorGroupState.graphLock;
13941
14582
  const dependencyUpdateRootNames = /* @__PURE__ */ new Set();
13942
14583
  if (scopedDependencyUpdate) {
13943
14584
  for (const pkg of group.packages) {
@@ -14047,14 +14688,14 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14047
14688
  expectedFromWorkspaceOwner: targetOptions.expectedFromWorkspaceOwner
14048
14689
  });
14049
14690
  if ((behavior.mode === "install" || behavior.mode === "update") && scopedRootId) {
14050
- const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
14051
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14052
- const scopedResult = targetOptions.focusedArtifact ? previousGroupLock ? scopeUpdatePlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, previousGroupLock, manifest) : scopeInstallPlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, manifest) : previousGroupLock ? scopeUpdatePlanToRoot(result, scopedRootId, previousGroupLock, manifest) : scopeInstallPlanToRoot(result, scopedRootId, manifest);
14691
+ const manifest = result.previousManifest;
14692
+ const previousLock = result.previousGraphLock;
14693
+ const scopedResult = targetOptions.focusedArtifact ? previousLock ? scopeUpdatePlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, previousLock, manifest) : scopeInstallPlanToArtifact(result, scopedRootId, targetOptions.focusedArtifact, manifest) : previousLock ? scopeUpdatePlanToRoot(result, scopedRootId, previousLock, manifest) : scopeInstallPlanToRoot(result, scopedRootId, manifest);
14053
14694
  if (targetOptions.focusedArtifact && targetOptions.forceForeignState !== true) {
14054
14695
  const focusedPaths = scopedResult.plan.operations.filter((operation) => operationMatchesFocusedArtifact(
14055
14696
  operation,
14056
14697
  targetOptions.focusedArtifact,
14057
- focusedArtifactOwnerKeys(result.bundle.graphLock, previousGroupLock, scopedRootId, targetOptions.focusedArtifact)
14698
+ focusedArtifactOwnerKeys(result.bundle.graphLock, previousLock, scopedRootId, targetOptions.focusedArtifact)
14058
14699
  )).map((operation) => operation.relativeDestPath);
14059
14700
  const ownership = await resolveWorkspaceOwnershipScope(group.target.workspaceRoot, {
14060
14701
  fleetId: group.target.fleetId
@@ -14068,9 +14709,11 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14068
14709
  }
14069
14710
  results.push(scopedResult);
14070
14711
  } else if (scopedDependencyUpdate) {
14071
- const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
14072
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14073
- const previousLock = await readGraphLock(result.graphLockPath);
14712
+ const manifest = result.previousManifest;
14713
+ const previousLock = result.previousGraphLock;
14714
+ if (!previousLock) {
14715
+ throw new Error(`Dependency update requires an existing graph lock for ${adapter.name} at ${group.target.targetRoot}.`);
14716
+ }
14074
14717
  results.push(scopeUpdatePlanToDependencies(result, dependencyUpdateSelectors, previousLock, manifest));
14075
14718
  } else {
14076
14719
  results.push(result);
@@ -14663,8 +15306,12 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14663
15306
  const removedInstallationType = options.installationType ?? pkg.installationType ?? removedTarget.installationType ?? "local";
14664
15307
  const adapter = await resolveAdapterForTarget(removedTarget, removedAdapterOptions);
14665
15308
  const transport = transportForTarget(removedTarget);
14666
- const removedState = installStateForTarget(removedTarget, adapter, removedAdapterOptions, removedInstallationType);
14667
- const manifest = await readInstallManifest(removedState.installRoot, adapter.name, transport, removedState);
15309
+ const removedState = await resolveCliTargetState(removedTarget, {
15310
+ ...options,
15311
+ ...removedAdapterOptions,
15312
+ installationType: removedInstallationType
15313
+ });
15314
+ const manifest = removedState.manifest;
14668
15315
  if (!manifest) {
14669
15316
  console.log(`No install manifest for ${adapter.name} at ${removedTarget.targetRoot}`);
14670
15317
  continue;
@@ -14712,25 +15359,24 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14712
15359
  remainingDesired = desiredArtifactsFromGraphBundle(result2.bundle);
14713
15360
  renderedRoot = result2.bundle.root;
14714
15361
  }
14715
- const plan = await createOwnershipUninstallPlan(manifest, remainingDesired, adapter, transport, { graphLockDigest: remainingGraphPlan?.graphLockDigest });
15362
+ const uninstallPlan = await createOwnershipUninstallPlan(manifest, remainingDesired, adapter, transport, {
15363
+ graphLockDigest: remainingGraphPlan?.graphLockDigest
15364
+ });
15365
+ const transitionPlan = remainingGraphPlan?.plan;
15366
+ const plan = {
15367
+ ...uninstallPlan,
15368
+ stateKey: transitionPlan?.stateKey ?? removedState.stableStateKey,
15369
+ stateMigration: transitionPlan?.stateMigration ?? removedState.migration,
15370
+ targetStateFilePreconditions: transitionPlan?.targetStateFilePreconditions ?? removedState.targetStateFilePreconditions
15371
+ };
14716
15372
  console.log(`Uninstall ${pkg.name} (${adapter.name} at ${removedTarget.targetRoot}):`);
14717
15373
  console.log(formatPlan(plan));
14718
15374
  const graphLockFinalState = remainingGraphPlan ? { graphLock: { path: remainingGraphPlan.graphLockPath, lock: remainingGraphPlan.bundle.graphLock } } : {
14719
- removeGraphLockPath: graphLockPathForTarget(
14720
- removedTarget.workspaceRoot,
14721
- targetKeyForTarget(removedTarget, adapter.name),
14722
- adapter.name,
14723
- targetFingerprintParts(removedTarget, adapter, removedAdapterOptions, removedInstallationType)
14724
- )
15375
+ removeGraphLockPath: removedState.graphLockPath ?? removedState.stableGraphLockPath
14725
15376
  };
14726
15377
  if (!options.dryRun) {
14727
15378
  declareMutationPath(workspaceConfigPath(target.workspaceRoot));
14728
- const declarativeGraphPath = remainingGraphPlan ? remainingGraphPlan.graphLockPath : graphLockPathForTarget(
14729
- removedTarget.workspaceRoot,
14730
- targetKeyForTarget(removedTarget, adapter.name),
14731
- adapter.name,
14732
- targetFingerprintParts(removedTarget, adapter, removedAdapterOptions, removedInstallationType)
14733
- );
15379
+ const declarativeGraphPath = remainingGraphPlan ? remainingGraphPlan.graphLockPath : removedState.graphLockPath ?? removedState.stableGraphLockPath;
14734
15380
  declareMutationPath(declarativeGraphPath);
14735
15381
  }
14736
15382
  const result = await uninstall(plan, {
@@ -14805,36 +15451,92 @@ function targetFingerprintParts(target, adapter, options, installationType) {
14805
15451
  };
14806
15452
  }
14807
15453
  function installStateForTarget(target, adapter, options, installationType) {
14808
- const targetFingerprint = targetFingerprintDigest(target, adapter, options, installationType);
15454
+ const installRoot = installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, target.transport === "ssh");
15455
+ const targetIdentity = resolveTargetStateIdentity({
15456
+ targetFingerprintParts: targetFingerprintParts(target, adapter, options, installationType),
15457
+ workspaceRoot: target.workspaceRoot,
15458
+ targetKey: targetKeyForTarget(target, adapter.name),
15459
+ resolvedInstallRoot: installRoot,
15460
+ transportKind: target.transport
15461
+ });
14809
15462
  return {
14810
15463
  installationType,
14811
15464
  stateKey: stateKeyFor(adapter.name, {
14812
15465
  installationType,
14813
15466
  stateKey: target.stateKey,
14814
- targetFingerprint,
15467
+ targetFingerprint: targetIdentity.stateFingerprint,
14815
15468
  fleetId: target.fleetId
14816
15469
  }),
14817
- installRoot: installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, target.transport === "ssh")
15470
+ installRoot
14818
15471
  };
14819
15472
  }
14820
- function targetFingerprintDigest(target, adapter, options, installationType) {
14821
- const fingerprintInput = targetFingerprintParts(target, adapter, options, installationType);
14822
- return computeTargetFingerprint(fingerprintInput);
15473
+ function targetIdentityContext(target, adapter, installationType) {
15474
+ return {
15475
+ resolvedInstallRoot: installRootForAdapterInstallationType(
15476
+ adapter,
15477
+ target.targetRoot,
15478
+ installationType,
15479
+ target.transport === "ssh"
15480
+ ),
15481
+ transportKind: target.transport
15482
+ };
14823
15483
  }
14824
- async function readTargetGraphLock(target, options) {
15484
+ async function resolveCliTargetState(target, options) {
14825
15485
  const adapterOptions = adapterOptionsForTarget(target, options);
14826
15486
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15487
+ const transport = transportForTarget(target);
14827
15488
  const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
14828
- const path = graphLockPathForTarget(
15489
+ const stable = installStateForTarget(target, adapter, adapterOptions, installationType);
15490
+ const stableGraphLockPath = graphLockPathForTarget(
14829
15491
  target.workspaceRoot,
14830
15492
  targetKeyForTarget(target, adapter.name),
14831
15493
  adapter.name,
14832
- targetFingerprintParts(target, adapter, adapterOptions, installationType)
15494
+ targetFingerprintParts(target, adapter, adapterOptions, installationType),
15495
+ targetIdentityContext(target, adapter, installationType)
14833
15496
  );
14834
- if (!await pathExists(path)) {
14835
- throw new Error(`No graph lock for ${adapter.name} at ${target.targetRoot}: ${path}`);
15497
+ const [stableManifest, stableLock, stableSourceLock, ownership] = await Promise.all([
15498
+ readInstallManifest(stable.installRoot, adapter.name, transport, stable),
15499
+ pathExists(stableGraphLockPath).then((exists) => exists ? readGraphLock(stableGraphLockPath) : void 0),
15500
+ readSourceLock(stable.installRoot, adapter.name, transport, stable),
15501
+ resolveWorkspaceOwnershipScope(target.workspaceRoot, { fleetId: target.fleetId })
15502
+ ]);
15503
+ const prior = await resolvePriorTargetState({
15504
+ adapter: adapter.name,
15505
+ installationType,
15506
+ stateKey: stable.stateKey,
15507
+ explicitStateKey: target.stateKey,
15508
+ fleetId: target.fleetId,
15509
+ installRoot: stable.installRoot,
15510
+ workspaceOwner: workspaceOwnerForRoot(ownership.root, ownership.fleetId),
15511
+ graphLockPath: stableGraphLockPath,
15512
+ stableManifest,
15513
+ stableLock,
15514
+ transport
15515
+ });
15516
+ return {
15517
+ adapter,
15518
+ installationType,
15519
+ installRoot: stable.installRoot,
15520
+ stateKey: prior.migration?.fromStateKey ?? stable.stateKey,
15521
+ stableStateKey: stable.stateKey,
15522
+ stableGraphLockPath,
15523
+ manifest: prior.manifest,
15524
+ graphLock: prior.graphLock,
15525
+ graphLockPath: prior.graphLock ? prior.migration?.fromGraphLockPath ?? stableGraphLockPath : null,
15526
+ migration: prior.migration,
15527
+ targetStateFilePreconditions: {
15528
+ graphLockPath: stableGraphLockPath,
15529
+ graphLockRevision: stableLock ? createHash14("sha256").update(canonicalGraphLockJson(stableLock)).digest("hex") : null,
15530
+ sourceLockRevision: computeSourceLockRevision(stableSourceLock)
15531
+ }
15532
+ };
15533
+ }
15534
+ async function readTargetGraphLock(target, options) {
15535
+ const state = await resolveCliTargetState(target, options);
15536
+ if (!state.graphLock || !state.graphLockPath) {
15537
+ throw new Error(`No graph lock for ${state.adapter.name} at ${target.targetRoot}: ${state.stableGraphLockPath}`);
14836
15538
  }
14837
- return { adapter, path, lock: await readGraphLock(path) };
15539
+ return { adapter: state.adapter, path: state.graphLockPath, lock: state.graphLock };
14838
15540
  }
14839
15541
  async function printStatus(target, options) {
14840
15542
  const report = await collectTargetStatus(target, options);
@@ -14842,30 +15544,24 @@ async function printStatus(target, options) {
14842
15544
  }
14843
15545
  async function collectTargetStatus(target, options) {
14844
15546
  const config = await readMergedWorkspaceConfig(target.workspaceRoot);
14845
- const adapterOptions = adapterOptionsForTarget(target, options);
14846
- const adapter = await resolveAdapterForTarget(target, { ...adapterOptions, warn: () => void 0 });
14847
- const transport = transportForTarget(target);
14848
- const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
14849
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
14850
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14851
- let graphLockPath = null;
14852
- let graphLock;
14853
- try {
14854
- const result = await readTargetGraphLock(target, adapterOptions);
14855
- graphLockPath = result.path;
14856
- graphLock = result.lock;
14857
- } catch {
14858
- graphLock = void 0;
14859
- }
15547
+ const state = await resolveCliTargetState(target, { ...options, warn: () => void 0 });
15548
+ const { adapter, installationType, manifest, graphLock, graphLockPath } = state;
15549
+ const pending = await collectPendingInstallWork(target, options);
15550
+ const foreignObservations = pending.foreignStateObservations;
15551
+ const effectiveGraphLock = graphLock ?? pending.observedGraphLock;
15552
+ const observedManifestRevision = foreignObservations.length > 0 ? createHash14("sha256").update(JSON.stringify(foreignObservations.map((observation) => [
15553
+ observation.fileName,
15554
+ observation.manifestRevision
15555
+ ]).sort())).digest("hex") : null;
14860
15556
  const packages = [];
14861
15557
  for (const pkg of config.packages) {
14862
- const root = graphLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
14863
- const node = root ? graphLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId) : void 0;
15558
+ const root = effectiveGraphLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
15559
+ const node = root ? effectiveGraphLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId) : void 0;
14864
15560
  const availability = await discoverPackageVersions(pkg, target.workspaceRoot, {
14865
15561
  forceRefresh: options.refresh,
14866
15562
  offline: options.offline
14867
15563
  });
14868
- const installed = node && manifest?.entries.some((entry) => "graphNodeId" in entry && entry.graphNodeId === node.id) ? node.version : null;
15564
+ const installed = node && (manifest?.entries.some((entry) => "graphNodeId" in entry && entry.graphNodeId === node.id) || foreignObservations.some((observation) => observation.graphNodeId === node.id)) ? node.version : null;
14869
15565
  const locked = node?.version ?? null;
14870
15566
  const baseline = installed ?? locked;
14871
15567
  packages.push({
@@ -14888,14 +15584,13 @@ async function collectTargetStatus(target, options) {
14888
15584
  )
14889
15585
  });
14890
15586
  }
14891
- const pending = await collectPendingInstallWork(target, options);
14892
- const artifacts = (graphLock?.canonical.artifacts ?? []).map((artifact) => {
14893
- const node = graphLock?.canonical.nodes.find((candidate) => candidate.id === artifact.graphNodeId);
14894
- const installed = manifest?.entries.some((entry) => {
15587
+ const artifacts = (effectiveGraphLock?.canonical.artifacts ?? []).map((artifact) => {
15588
+ const node = effectiveGraphLock?.canonical.nodes.find((candidate) => candidate.id === artifact.graphNodeId);
15589
+ const installed = (manifest?.entries.some((entry) => {
14895
15590
  if (!("graphNodeId" in entry) || entry.graphNodeId !== artifact.graphNodeId) return false;
14896
15591
  if ("logicalSelector" in entry && entry.logicalSelector) return entry.logicalSelector === artifact.logicalSelector;
14897
15592
  return entry.artifactType === artifact.type && entry.artifactName === artifact.name;
14898
- }) ?? false;
15593
+ }) ?? false) || foreignObservations.some((observation) => observation.graphNodeId === artifact.graphNodeId && (!observation.logicalSelector || observation.logicalSelector === artifact.logicalSelector));
14899
15594
  return {
14900
15595
  selector: artifact.logicalSelector,
14901
15596
  type: artifact.type,
@@ -14908,7 +15603,7 @@ async function collectTargetStatus(target, options) {
14908
15603
  };
14909
15604
  });
14910
15605
  const health = [];
14911
- if (!manifest || !graphLock) health.push("FAIL");
15606
+ if (!manifest && foreignObservations.length === 0 || !effectiveGraphLock) health.push("FAIL");
14912
15607
  if (pending.error) health.push("DEGRADED");
14913
15608
  if (pending.driftCount > 0 || pending.conflictCount > 0) health.push("FAIL");
14914
15609
  else if (pending.pendingCount > 0) health.push("WARN");
@@ -14919,11 +15614,11 @@ async function collectTargetStatus(target, options) {
14919
15614
  installationType,
14920
15615
  targetRoot: state.installRoot,
14921
15616
  health: worstStatusHealth(health),
14922
- manifestRevision: manifest?.revision ?? null,
14923
- manifestEntryCount: manifest?.entries.length ?? 0,
15617
+ manifestRevision: manifest?.revision ?? observedManifestRevision,
15618
+ manifestEntryCount: (manifest?.entries.length ?? 0) + foreignObservations.length,
14924
15619
  graphLockPath,
14925
15620
  packageCount: packages.length,
14926
- artifactCount: graphLock?.canonical.artifacts.length ?? 0,
15621
+ artifactCount: effectiveGraphLock?.canonical.artifacts.length ?? 0,
14927
15622
  pendingCount: pending.pendingCount,
14928
15623
  driftCount: pending.driftCount,
14929
15624
  conflictCount: pending.conflictCount,
@@ -15101,7 +15796,7 @@ async function assertCompositeMutationSupported(workspaceRoot, profile, readOnly
15101
15796
  `Composite-profile mutation is refused before member execution because ${member.id} uses ${member.transport}; durable remote policy and recovery preflight is not implemented.`
15102
15797
  );
15103
15798
  }
15104
- const memberRoot = resolve24(workspaceRoot, member.workspace);
15799
+ const memberRoot = resolve25(workspaceRoot, member.workspace);
15105
15800
  const memberPolicy = await mutationPolicyForWorkspace(memberRoot);
15106
15801
  if (memberPolicy?.revisioning.mode === "commit-after-verify") {
15107
15802
  throw new Error(
@@ -15258,9 +15953,34 @@ async function journalStateForTarget(target, options) {
15258
15953
  const adapterOptions = adapterOptionsForTarget(target, options);
15259
15954
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15260
15955
  const transport = transportForTarget(target);
15261
- const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
15262
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
15263
- return { adapter, transport, installationType, installRoot: state.installRoot, state };
15956
+ const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
15957
+ const stable = installStateForTarget(target, adapter, adapterOptions, installationType);
15958
+ const graphLockPath = graphLockPathForTarget(
15959
+ target.workspaceRoot,
15960
+ targetKeyForTarget(target, adapter.name),
15961
+ adapter.name,
15962
+ targetFingerprintParts(target, adapter, adapterOptions, installationType),
15963
+ targetIdentityContext(target, adapter, installationType)
15964
+ );
15965
+ const ownership = await resolveWorkspaceOwnershipScope(target.workspaceRoot, { fleetId: target.fleetId });
15966
+ const pending = await discoverTargetApplyJournal({
15967
+ adapter: adapter.name,
15968
+ installationType,
15969
+ stateKey: stable.stateKey,
15970
+ explicitStateKey: target.stateKey,
15971
+ fleetId: target.fleetId,
15972
+ installRoot: stable.installRoot,
15973
+ workspaceOwner: workspaceOwnerForRoot(ownership.root, ownership.fleetId),
15974
+ graphLockPath,
15975
+ transport
15976
+ });
15977
+ return {
15978
+ adapter,
15979
+ transport,
15980
+ installationType,
15981
+ installRoot: stable.installRoot,
15982
+ state: { installationType, stateKey: pending?.stateKey ?? stable.stateKey }
15983
+ };
15264
15984
  }
15265
15985
  async function collectPendingInstallWork(target, options) {
15266
15986
  let results = [];
@@ -15281,11 +16001,20 @@ async function collectPendingInstallWork(target, options) {
15281
16001
  pendingCount: pending.length,
15282
16002
  driftCount: counts.drift ?? 0,
15283
16003
  conflictCount: counts.conflict ?? 0,
15284
- counts
16004
+ counts,
16005
+ foreignStateObservations: results.flatMap((result) => result.foreignStateObservations),
16006
+ observedGraphLock: results.length === 1 ? results[0].bundle.graphLock : void 0
15285
16007
  };
15286
16008
  } catch (error) {
15287
16009
  const message = error instanceof Error ? error.message : String(error);
15288
- return { pendingCount: 0, driftCount: 0, conflictCount: 0, counts: {}, error: message };
16010
+ return {
16011
+ pendingCount: 0,
16012
+ driftCount: 0,
16013
+ conflictCount: 0,
16014
+ counts: {},
16015
+ foreignStateObservations: [],
16016
+ error: message
16017
+ };
15289
16018
  } finally {
15290
16019
  await Promise.all(results.map((result) => rm10(result.bundle.root, { recursive: true, force: true })));
15291
16020
  }
@@ -15298,9 +16027,8 @@ async function printDoctor(target, options) {
15298
16027
  if (!targetMapping?.enabled) {
15299
16028
  throw new Error(`Adapter ${adapter.name} does not support skills for installation type '${installationType}'.`);
15300
16029
  }
15301
- const transport = transportForTarget(target);
15302
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
15303
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
16030
+ const state = await resolveCliTargetState(target, { ...options, installationType });
16031
+ const manifest = state.manifest;
15304
16032
  const requestedSkills = doctorSkillRequests(target, options);
15305
16033
  const skills = [];
15306
16034
  for (const request of requestedSkills) {
@@ -15552,14 +16280,14 @@ function looksLikeSourceSpecifier(value) {
15552
16280
  }
15553
16281
  function normalizeCliPath(value) {
15554
16282
  if (value === "~") return homedir12();
15555
- if (value.startsWith("~/")) return resolve24(homedir12(), value.slice(2));
15556
- return resolve24(value);
16283
+ if (value.startsWith("~/")) return resolve25(homedir12(), value.slice(2));
16284
+ return resolve25(value);
15557
16285
  }
15558
16286
  function shellQuoteArgument(value) {
15559
16287
  return /^[a-z0-9_./:@%+=,-]+$/i.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
15560
16288
  }
15561
16289
  function isHomePath(path) {
15562
- return resolve24(path) === resolve24(homedir12());
16290
+ return resolve25(path) === resolve25(homedir12());
15563
16291
  }
15564
16292
  function adapterListFromOption(adapter) {
15565
16293
  if (!adapter) return [];
@@ -15680,7 +16408,7 @@ function withFleetExample(config) {
15680
16408
  };
15681
16409
  }
15682
16410
  async function findAgentwheelPackageRoot(start) {
15683
- let current = resolve24(start);
16411
+ let current = resolve25(start);
15684
16412
  while (true) {
15685
16413
  if (await findPackageManifestPath(current, { warnLegacy: false })) return current;
15686
16414
  const parent = dirname22(current);