agentwheel 0.20.8 → 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,10 @@ 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 compositionArtifacts = targetContext.noDeps ? filterArtifactsBySelection(runtimeArtifacts, rawNode.node.selected) : runtimeArtifacts;
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;
7231
7243
  const expandedArtifacts = await expandMarkdownIncludes(compositionArtifacts, staged.root, {
7232
7244
  nodeId: rawNode.node.id,
7233
7245
  originNodeId: rawNode.node.id,
@@ -7275,7 +7287,12 @@ async function renderGraphForTarget(graph, targetContext = {}) {
7275
7287
  };
7276
7288
  }
7277
7289
  });
7278
- 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
+ );
7279
7296
  const claudeRenderedArtifacts = await renderClaudeSubagents(selectedArtifacts, staged.root, targetContext.adapter);
7280
7297
  const codexRenderedArtifacts = await renderCodexSubagents(claudeRenderedArtifacts, staged.root, targetContext.adapter);
7281
7298
  const openClawRenderedArtifacts = await renderOpenClawSubagents(codexRenderedArtifacts, staged.root, targetContext.adapter);
@@ -7389,8 +7406,9 @@ function sha2563(content) {
7389
7406
  return createHash7("sha256").update(content).digest("hex");
7390
7407
  }
7391
7408
  function assignInstallNames(graph, artifacts) {
7392
- const superseded = supersededLogicalSelectors(artifacts);
7393
- 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));
7394
7412
  const aliases = workspaceAliases(graph);
7395
7413
  validateAliasScopes(graph, effectiveArtifacts, aliases);
7396
7414
  const decisions = /* @__PURE__ */ new Map();
@@ -7433,6 +7451,37 @@ function assignInstallNames(graph, artifacts) {
7433
7451
  }));
7434
7452
  return { artifacts: out, namespacing: [...decisions.values()], overrides: finalOverrides };
7435
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
+ }
7436
7485
  function supersededLogicalSelectors(artifacts) {
7437
7486
  const removed = /* @__PURE__ */ new Set();
7438
7487
  for (const replacement of artifacts) {
@@ -7595,7 +7644,8 @@ function reachableNodeIds(graph, rootNodeId) {
7595
7644
  return reachable;
7596
7645
  }
7597
7646
  function installNameCollisionError(group) {
7598
- 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}`);
7599
7649
  }
7600
7650
  function namespaceDecision(artifact, reason) {
7601
7651
  return {
@@ -8029,6 +8079,63 @@ function containsPath(parent, candidate) {
8029
8079
  return path === "" || !path.startsWith("..") && !isAbsolute2(path);
8030
8080
  }
8031
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
+
8032
8139
  // src/lifecycle/source-plan.ts
8033
8140
  async function createGraphSourcePlan(options) {
8034
8141
  if (options.roots.length === 0) {
@@ -8042,22 +8149,62 @@ async function createGraphSourcePlan(options) {
8042
8149
  options.warn?.(message);
8043
8150
  };
8044
8151
  const installationType = options.installationType ?? resolveInstallationTypeForAdapterTarget(options.adapter);
8045
- const targetFingerprint = computeTargetFingerprint(options.targetFingerprintParts ?? {
8152
+ const targetFingerprintParts2 = options.targetFingerprintParts ?? {
8046
8153
  adapter: options.adapter.name,
8047
8154
  fleetId: options.fleetId,
8048
8155
  installationType,
8049
8156
  targetRoot: options.targetRoot,
8050
8157
  transport: transport.kind,
8051
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
8052
8167
  });
8168
+ const targetFingerprint = targetIdentity.targetFingerprint;
8053
8169
  const stateKey = stateKeyFor(options.adapter.name, {
8054
8170
  installationType,
8055
8171
  stateKey: options.stateKey,
8056
- targetFingerprint,
8172
+ targetFingerprint: targetIdentity.stateFingerprint,
8057
8173
  fleetId: options.fleetId
8058
8174
  });
8059
- const installRoot = installRootForAdapterInstallationType(options.adapter, options.targetRoot, installationType, transport.kind === "ssh");
8060
- 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
+ }
8061
8208
  const workspaceConfig = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: options.globalRoot });
8062
8209
  const trustPolicy = {
8063
8210
  ...normalizeTrustPolicy(workspaceConfig.trust),
@@ -8065,8 +8212,29 @@ async function createGraphSourcePlan(options) {
8065
8212
  };
8066
8213
  const lockMode = options.frozenLock === true || options.offline === true;
8067
8214
  const lockLabel = options.offline === true ? "Offline" : options.frozenLock === true ? "Frozen lock" : options.lockedResolution === true ? "Locked install" : "Fresh resolve";
8068
- const graphLockPath = pathForGraphLock(workspaceRoot, options.targetKey ?? "default", options.adapter.name, targetFingerprint);
8069
- 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;
8070
8238
  const registryClient = new RegistryClient({ workspaceRoot, offline: lockMode, offlineLabel: lockLabel, warn });
8071
8239
  const graph = await resolveDependencyGraph(options.roots, {
8072
8240
  workspaceRoot,
@@ -8107,13 +8275,13 @@ async function createGraphSourcePlan(options) {
8107
8275
  const resolvedInstallRoot = installRootForArtifacts(options.adapter, options.targetRoot, resolvedInstallationType, desiredArtifacts.map((artifact) => artifact.type), transport.kind === "ssh");
8108
8276
  const graphLockDigest = digestGraphLock(bundle.graphLock);
8109
8277
  const graphDiff = diffGraphLocks(previousLock, bundle.graphLock);
8110
- const manifest = await readInstallManifest(resolvedInstallRoot, options.adapter.name, transport, { installationType: resolvedInstallationType, stateKey });
8111
- const ownership = await resolveWorkspaceOwnershipScope(workspaceRoot, {
8112
- fleetId: options.fleetId,
8113
- globalRoot: options.globalRoot
8114
- });
8115
- const workspaceOwner = workspaceOwnerForRoot(ownership.root, ownership.fleetId);
8116
- 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(
8117
8285
  desiredArtifacts,
8118
8286
  options.adapter,
8119
8287
  options.targetRoot,
@@ -8135,10 +8303,18 @@ async function createGraphSourcePlan(options) {
8135
8303
  forceDrift: options.forceDrift,
8136
8304
  forceConflict: options.forceConflict,
8137
8305
  replaceConflict: options.replaceConflict,
8306
+ stateMigration: priorState.migration,
8138
8307
  warn
8139
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 = [];
8140
8316
  if (options.forceForeignState !== true && options.deferForeignStateCheck !== true) {
8141
- await assertNoForeignWorkspaceState({
8317
+ foreignStateObservations = await assertNoForeignWorkspaceState({
8142
8318
  installRoot: resolvedInstallRoot,
8143
8319
  adapter: options.adapter.name,
8144
8320
  transport,
@@ -8146,9 +8322,11 @@ async function createGraphSourcePlan(options) {
8146
8322
  workspaceOwner,
8147
8323
  globalRoot: options.globalRoot,
8148
8324
  stateKey,
8325
+ migratingStateKey: priorState.migration?.fromStateKey,
8149
8326
  plannedPaths: plan.operations.map((operation) => operation.relativeDestPath),
8150
8327
  plannedOperations: plan.operations
8151
8328
  });
8329
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8152
8330
  }
8153
8331
  return {
8154
8332
  plan,
@@ -8161,7 +8339,10 @@ async function createGraphSourcePlan(options) {
8161
8339
  warnings,
8162
8340
  newTransitiveSources: trustEvaluation.promptSources,
8163
8341
  graphDiff,
8164
- recoveredPendingApply
8342
+ recoveredPendingApply,
8343
+ previousManifest: priorState.manifest,
8344
+ previousGraphLock: priorState.graphLock,
8345
+ foreignStateObservations
8165
8346
  };
8166
8347
  }
8167
8348
  async function assertNoForeignWorkspaceStateForPlan(plan, options) {
@@ -8174,12 +8355,15 @@ async function assertNoForeignWorkspaceStateForPlan(plan, options) {
8174
8355
  workspaceOwner: options.workspaceOwner,
8175
8356
  globalRoot: options.globalRoot,
8176
8357
  stateKey: plan.stateKey,
8358
+ migratingStateKey: plan.stateMigration?.fromStateKey,
8177
8359
  plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath),
8178
8360
  plannedOperations: plan.operations
8179
8361
  });
8362
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8180
8363
  }
8181
- function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2) {
8182
- 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);
8183
8367
  }
8184
8368
  function desiredArtifactsFromGraphBundle(bundle) {
8185
8369
  return bundle.artifacts.map((artifact) => desiredArtifactFromResolved(artifact));
@@ -8221,10 +8405,278 @@ async function readExistingGraphLock(path) {
8221
8405
  if (!await pathExists(path)) return void 0;
8222
8406
  return readGraphLock(path);
8223
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
+ }
8224
8676
  var workspaceOwnerPrefix2 = "workspace-root:";
8225
8677
  async function assertNoForeignWorkspaceState(check) {
8226
8678
  const planned = new Set(check.plannedPaths);
8227
- if (planned.size === 0) return;
8679
+ if (planned.size === 0) return [];
8228
8680
  const plannedOperations = /* @__PURE__ */ new Map();
8229
8681
  for (const operation of check.plannedOperations) {
8230
8682
  if (!planned.has(operation.relativeDestPath)) continue;
@@ -8233,18 +8685,22 @@ async function assertNoForeignWorkspaceState(check) {
8233
8685
  plannedOperations.set(operation.relativeDestPath, operations);
8234
8686
  }
8235
8687
  const manifests = await listInstallManifests(check.installRoot, check.adapter, check.transport);
8236
- const root = resolve15(check.workspaceRoot);
8688
+ const root = resolve16(check.workspaceRoot);
8237
8689
  const ownsSubWorkspaces = root !== globalConfigRoot(check.globalRoot);
8238
- const foreign = [];
8690
+ const collisions = /* @__PURE__ */ new Map();
8239
8691
  for (const entry of manifests) {
8240
- if (entry.stateKey === check.stateKey) continue;
8241
- 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
+ }
8242
8700
  for (const manifestEntry of entry.manifest.entries) {
8243
8701
  const owner = "workspaceOwner" in manifestEntry ? manifestEntry.workspaceOwner : void 0;
8244
8702
  if (typeof owner !== "string" || !owner.startsWith(workspaceOwnerPrefix2)) continue;
8245
8703
  if (ownedByWorkspace(owner, check.workspaceOwner, root, ownsSubWorkspaces)) continue;
8246
- const bucket = byOwner.get(owner) ?? { entryCount: 0, collidingPaths: [] };
8247
- bucket.entryCount += 1;
8248
8704
  if (planned.has(manifestEntry.path) && !await isDisjointVerifiedMergeContribution(
8249
8705
  check,
8250
8706
  manifestEntry.path,
@@ -8252,16 +8708,46 @@ async function assertNoForeignWorkspaceState(check) {
8252
8708
  manifestEntry.mergeRemoval,
8253
8709
  plannedOperations.get(manifestEntry.path) ?? []
8254
8710
  )) {
8255
- 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);
8256
8722
  }
8257
- byOwner.set(owner, bucket);
8258
8723
  }
8259
- for (const [owner, bucket] of byOwner) {
8260
- if (bucket.collidingPaths.length === 0) continue;
8261
- 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);
8262
8747
  }
8263
8748
  }
8264
- 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));
8265
8751
  throw new Error([
8266
8752
  `Refusing to plan ${check.adapter} at ${check.installRoot}.`,
8267
8753
  "This runtime root already carries Agentwheel state owned by another workspace, at paths this run would install:",
@@ -8275,6 +8761,36 @@ async function assertNoForeignWorkspaceState(check) {
8275
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."
8276
8762
  ].join("\n"));
8277
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
+ }
8278
8794
  async function isDisjointVerifiedMergeContribution(check, relativePath, foreignStrategy, foreignRemoval, operations) {
8279
8795
  if (!foreignStrategy || !hasMergeRemovalContent(foreignRemoval) || operations.length !== 1) return false;
8280
8796
  const operation = operations[0];
@@ -8446,7 +8962,7 @@ function targetLabel(target) {
8446
8962
  }
8447
8963
 
8448
8964
  // src/runtime/target.ts
8449
- 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";
8450
8966
  var runtimeMarkers = [
8451
8967
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
8452
8968
  { adapter: "claude", dirs: [".claude"] },
@@ -8455,9 +8971,9 @@ var runtimeMarkers = [
8455
8971
  { adapter: "copilot", dirs: [".github"] }
8456
8972
  ];
8457
8973
  async function resolveRuntimeTarget(request = {}) {
8458
- const cwd = resolve16(request.cwd ?? process.cwd());
8974
+ const cwd = resolve17(request.cwd ?? process.cwd());
8459
8975
  if (request.targetRoot) {
8460
- const targetRoot = resolve16(request.targetRoot);
8976
+ const targetRoot = resolve17(request.targetRoot);
8461
8977
  return {
8462
8978
  adapter: request.adapter ?? "openclaw",
8463
8979
  installationType: request.installationType,
@@ -8489,7 +9005,7 @@ async function resolveRuntimeTarget(request = {}) {
8489
9005
  async function resolveAllRuntimeTargets(request = {}) {
8490
9006
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
8491
9007
  if (request.agent) return [await resolveRuntimeTarget(request)];
8492
- const cwd = resolve16(request.cwd ?? process.cwd());
9008
+ const cwd = resolve17(request.cwd ?? process.cwd());
8493
9009
  const workspaceRoot = await findWorkspaceRoot(cwd);
8494
9010
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8495
9011
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot, request.installationType, request.fleetId));
@@ -8499,8 +9015,8 @@ async function resolveAllRuntimeTargets(request = {}) {
8499
9015
  return targets;
8500
9016
  }
8501
9017
  async function resolveProfileRuntimeTargets(request) {
8502
- const cwd = resolve16(request.cwd ?? process.cwd());
8503
- 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);
8504
9020
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8505
9021
  const profile = config.profiles[request.profile];
8506
9022
  if (!profile) {
@@ -8566,12 +9082,12 @@ async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
8566
9082
  return unique[0];
8567
9083
  }
8568
9084
  async function detectRuntimeTargets(cwd = process.cwd(), adapterFilter) {
8569
- const root = resolve16(cwd);
9085
+ const root = resolve17(cwd);
8570
9086
  const matches = [];
8571
9087
  for (const marker of runtimeMarkers) {
8572
9088
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
8573
9089
  for (const dir of marker.dirs) {
8574
- if (basename18(root) === dir) {
9090
+ if (basename19(root) === dir) {
8575
9091
  matches.push({ adapter: marker.adapter, targetRoot: dirname18(root) });
8576
9092
  } else if (await pathExists(join34(root, dir))) {
8577
9093
  matches.push({ adapter: marker.adapter, targetRoot: root });
@@ -8615,9 +9131,9 @@ function dedupeTargets(matches) {
8615
9131
  return [...byKey.values()];
8616
9132
  }
8617
9133
  function runtimeScanRoot(request) {
8618
- const root = resolve16(request.targetRoot ?? request.cwd ?? process.cwd());
9134
+ const root = resolve17(request.targetRoot ?? request.cwd ?? process.cwd());
8619
9135
  if (request.targetRoot) return root;
8620
- 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;
8621
9137
  }
8622
9138
 
8623
9139
  // src/lifecycle/profile.ts
@@ -9004,9 +9520,9 @@ function normalizeVersion(version) {
9004
9520
 
9005
9521
  // src/model/package-validate.ts
9006
9522
  import { stat as stat12 } from "fs/promises";
9007
- import { resolve as resolve17 } from "path";
9523
+ import { resolve as resolve18 } from "path";
9008
9524
  async function validatePackage(root) {
9009
- const packageRoot = resolve17(root);
9525
+ const packageRoot = resolve18(root);
9010
9526
  const findings = [];
9011
9527
  const manifestPath = await findPackageManifestPath(packageRoot);
9012
9528
  if (!manifestPath) {
@@ -9107,7 +9623,7 @@ async function validateManifestComposeInclude(packageRoot, selector, optional, f
9107
9623
  try {
9108
9624
  validateSelector(selector, "compose.include", findings, manifestPath, { fragmentsOnly: true, aliases });
9109
9625
  if (isCrossPackageSelector(selector)) return;
9110
- const full = resolve17(packageRoot, selector);
9626
+ const full = resolve18(packageRoot, selector);
9111
9627
  if (full !== packageRoot && !full.startsWith(`${packageRoot}/`)) {
9112
9628
  findings.push({ level: "error", message: `Compose include escapes package root: ${selector}`, path: manifestPath });
9113
9629
  return;
@@ -9161,10 +9677,10 @@ function isCrossPackageSelector(value) {
9161
9677
 
9162
9678
  // src/model/package-migrate.ts
9163
9679
  import { readFile as readFile22, rename as rename3, writeFile as writeFile12 } from "fs/promises";
9164
- import { join as join37, resolve as resolve18 } from "path";
9680
+ import { join as join37, resolve as resolve19 } from "path";
9165
9681
  import { applyEdits, modify, parse as parse2 } from "jsonc-parser";
9166
9682
  async function migratePackageManifest(root) {
9167
- const packageRoot = resolve18(root);
9683
+ const packageRoot = resolve19(root);
9168
9684
  for (const name of openPackManifestNames) {
9169
9685
  const path = join37(packageRoot, name);
9170
9686
  if (await pathExists(path)) {
@@ -9226,7 +9742,7 @@ function resolveCliVersion() {
9226
9742
  }
9227
9743
 
9228
9744
  // src/lifecycle/ownership.ts
9229
- import { resolve as resolve19 } from "path";
9745
+ import { resolve as resolve20 } from "path";
9230
9746
  async function planArtifactOwnershipHandoff(request) {
9231
9747
  return validateOwnershipHandoff(request, request.transport ?? localTransport);
9232
9748
  }
@@ -9352,8 +9868,8 @@ function containedArtifactPath(targetRoot, relativePath) {
9352
9868
  if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\0")) {
9353
9869
  throw new Error(`Unsafe managed artifact path: ${relativePath}`);
9354
9870
  }
9355
- const root = resolve19(targetRoot);
9356
- const candidate = resolve19(root, relativePath);
9871
+ const root = resolve20(targetRoot);
9872
+ const candidate = resolve20(root, relativePath);
9357
9873
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
9358
9874
  throw new Error(`Managed artifact path escapes target root: ${relativePath}`);
9359
9875
  }
@@ -9386,7 +9902,7 @@ function assertManifestIdentity(manifest, request) {
9386
9902
 
9387
9903
  // src/lifecycle/ownership-retire-stale.ts
9388
9904
  import { createHash as createHash9 } from "crypto";
9389
- import { resolve as resolve20 } from "path";
9905
+ import { resolve as resolve21 } from "path";
9390
9906
  async function planRetireStaleOwnership(request) {
9391
9907
  return observe(request, request.transport ?? localTransport);
9392
9908
  }
@@ -9519,7 +10035,7 @@ async function observe(request, transport) {
9519
10035
  return { ...withoutDigest, planDigest: digest(withoutDigest) };
9520
10036
  }
9521
10037
  function normalizeRequest(request) {
9522
- const targetRoot = resolve20(request.targetRoot);
10038
+ const targetRoot = resolve21(request.targetRoot);
9523
10039
  const sourceStateKey = exactStateKey(request.adapter, request.sourceStateKey, request.installationType, "source");
9524
10040
  const destinationStateKey = exactStateKey(request.adapter, request.destinationStateKey, request.installationType, "destination");
9525
10041
  if (sourceStateKey === destinationStateKey) throw new Error("Source and destination state keys must differ.");
@@ -9530,8 +10046,8 @@ function normalizeRequest(request) {
9530
10046
  installationType: request.installationType.trim(),
9531
10047
  sourceStateKey,
9532
10048
  destinationStateKey,
9533
- fromWorkspaceRoot: resolve20(request.fromWorkspaceRoot),
9534
- toWorkspaceRoot: resolve20(request.toWorkspaceRoot),
10049
+ fromWorkspaceRoot: resolve21(request.fromWorkspaceRoot),
10050
+ toWorkspaceRoot: resolve21(request.toWorkspaceRoot),
9535
10051
  toFleetId: request.toFleetId.trim()
9536
10052
  };
9537
10053
  }
@@ -9552,7 +10068,7 @@ async function requireManifest(request, stateKey, transport, label) {
9552
10068
  installationType: manifest.installationType,
9553
10069
  stateKey: manifest.stateKey
9554
10070
  });
9555
- 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) {
9556
10072
  throw new Error(`${label} install manifest identity does not match the requested adapter, target, installation type, and state key.`);
9557
10073
  }
9558
10074
  return manifest;
@@ -9636,7 +10152,7 @@ function canonicalJson(value) {
9636
10152
  // src/version/policy.ts
9637
10153
  import { execFile as execFile3 } from "child_process";
9638
10154
  import { readFile as readFile23 } from "fs/promises";
9639
- import { join as join39, resolve as resolve21 } from "path";
10155
+ import { join as join39, resolve as resolve22 } from "path";
9640
10156
  import { promisify as promisify3 } from "util";
9641
10157
  import { parse as parseJsonc } from "jsonc-parser";
9642
10158
  import { z as z5 } from "zod";
@@ -9730,7 +10246,7 @@ async function discoverVersionsFromSource(pkg, workspaceRoot) {
9730
10246
  if (tagged.length > 0) return tagged;
9731
10247
  }
9732
10248
  if (driverName === "local") {
9733
- const root = resolve21(workspaceRoot, pkg.source);
10249
+ const root = resolve22(workspaceRoot, pkg.source);
9734
10250
  const manifest2 = await readPackageManifest(root);
9735
10251
  const current = manifest2 ? [{ version: manifest2.version, ref: pkg.requestedRef ?? root }] : [];
9736
10252
  try {
@@ -9853,7 +10369,7 @@ async function readVersionCache(path) {
9853
10369
  // src/profile/members.ts
9854
10370
  import { execFile as execFile4 } from "child_process";
9855
10371
  import { readFile as readFile24 } from "fs/promises";
9856
- import { join as join40, resolve as resolve22 } from "path";
10372
+ import { join as join40, resolve as resolve23 } from "path";
9857
10373
  import { promisify as promisify4 } from "util";
9858
10374
  import { z as z7 } from "zod";
9859
10375
 
@@ -10028,7 +10544,7 @@ async function invokeMemberStatus(member, parentWorkspace, chain, options, cliEn
10028
10544
  let stderr = "";
10029
10545
  try {
10030
10546
  if (member.transport === "local") {
10031
- const workspace = resolve22(parentWorkspace, member.workspace);
10547
+ const workspace = resolve23(parentWorkspace, member.workspace);
10032
10548
  const result = await execFileAsync4(process.execPath, [cliEntry, ...args], {
10033
10549
  cwd: workspace,
10034
10550
  env: env2,
@@ -10074,7 +10590,7 @@ async function runMemberAgentwheel(member, parentWorkspace, args, chain) {
10074
10590
  process.execPath,
10075
10591
  [process.argv[1], ...memberCommandArgs(member, args)],
10076
10592
  {
10077
- cwd: resolve22(parentWorkspace, member.workspace),
10593
+ cwd: resolve23(parentWorkspace, member.workspace),
10078
10594
  env: env2,
10079
10595
  maxBuffer: 20 * 1024 * 1024
10080
10596
  }
@@ -10180,7 +10696,7 @@ function assertNoCompositeCycle(workspaceRoot, profileName, chain) {
10180
10696
  }
10181
10697
  }
10182
10698
  function compositeKey(workspaceRoot, profileName) {
10183
- return `${resolve22(workspaceRoot)}#${profileName}`;
10699
+ return `${resolve23(workspaceRoot)}#${profileName}`;
10184
10700
  }
10185
10701
 
10186
10702
  // src/status/repository.ts
@@ -11105,9 +11621,9 @@ function readSkillFrontmatter(content, artifact) {
11105
11621
 
11106
11622
  // src/lifecycle/fleet-normalize.ts
11107
11623
  import { createHash as createHash13 } from "crypto";
11108
- 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";
11109
11625
  import { homedir as homedir11 } from "os";
11110
- 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";
11111
11627
  async function planFleetNormalization(request) {
11112
11628
  const normalizedRequest = normalizeRequest2(request);
11113
11629
  const destinationScope = await resolveWorkspaceScope({
@@ -11378,17 +11894,17 @@ function normalizeRequest2(request) {
11378
11894
  ...profile ? { profile } : {},
11379
11895
  ...agent ? { agent } : {},
11380
11896
  ...orphanedOwnerRoots ? { orphanedOwnerRoots } : {},
11381
- ...request.globalRoot ? { globalRoot: resolve23(request.globalRoot) } : {}
11897
+ ...request.globalRoot ? { globalRoot: resolve24(request.globalRoot) } : {}
11382
11898
  };
11383
11899
  }
11384
11900
  function normalizeOrphanedOwnerRoot(value) {
11385
11901
  const root = value.trim();
11386
11902
  if (!root || !isAbsolute3(root)) throw new Error(`Invalid orphaned owner root '${value}'; expected an absolute workspace path.`);
11387
- return resolve23(root);
11903
+ return resolve24(root);
11388
11904
  }
11389
11905
  async function normalizationRoots(request) {
11390
11906
  const destination = await showRegisteredFleet(request.destinationFleet, { globalRoot: request.globalRoot });
11391
- 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;
11392
11908
  return { source, destination: destination.root };
11393
11909
  }
11394
11910
  async function withOrderedLocks(roots, fn) {
@@ -11454,11 +11970,28 @@ async function inspectInstalledState(source, destination, packageNames) {
11454
11970
  const coveredDestinationGraphs = /* @__PURE__ */ new Set();
11455
11971
  const plannedDestinationGraphs = /* @__PURE__ */ new Set();
11456
11972
  for (const sourceGraph of sourceGraphs) {
11457
- const sourceState = await targetStateForGraph(source, sourceGraph);
11458
- const destinationState = await targetStateForGraph(destination, sourceGraph);
11459
- if (sourceState.graphLockPath !== sourceGraph.path) {
11460
- 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
+ );
11461
11986
  }
11987
+ const destinationGraph = matchingDestinationGraphs[0];
11988
+ const destinationState = destinationGraph ? await installedTargetStateForGraph(
11989
+ destination,
11990
+ destinationGraph,
11991
+ manifests,
11992
+ destinationOwners,
11993
+ selected
11994
+ ) : destinationStableState;
11462
11995
  if (sourceState.adapter !== destinationState.adapter) {
11463
11996
  throw new Error(`Source and destination adapters diverge for target '${sourceGraph.targetKey}'.`);
11464
11997
  }
@@ -11467,7 +12000,6 @@ async function inspectInstalledState(source, destination, packageNames) {
11467
12000
  throw new Error(`Source graph lock is not covered by its canonical install manifest: ${sourceState.manifestPath}`);
11468
12001
  }
11469
12002
  const sourceGraphArtifacts = new Set(sourceGraph.artifactIdentities);
11470
- const destinationGraph = destinationGraphs.find((candidate) => candidate.path === destinationState.graphLockPath);
11471
12003
  if (destinationGraph && destinationGraph.digest !== sourceGraph.digest) {
11472
12004
  throw new Error("Source and destination graph declarations, selections, versions, identities, or rendered paths diverge.");
11473
12005
  }
@@ -11585,16 +12117,30 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11585
12117
  const transferableOwners = orphanedOwners.size > 0 ? orphanedOwners : /* @__PURE__ */ new Set([legacyOwner]);
11586
12118
  let normalizedGraphCount = 0;
11587
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
+ }
11588
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
+ }
11589
12139
  const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
11590
12140
  const relevantManifestEntries2 = legacyManifest?.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected) && entryMatchesArtifacts(entry, selectedArtifacts)) ?? [];
11591
12141
  if (relevantManifestEntries2.length > 0) {
11592
12142
  graphs.push(graph);
11593
12143
  }
11594
- const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
11595
- if (destinationState.graphLockPath === graph.path && legacyManifest?.manifest.entries.some((entry) => entry.workspaceOwner === fleetOwner && entryMatchesPackages(entry, selected))) {
11596
- normalizedGraphCount += 1;
11597
- }
11598
12144
  }
11599
12145
  if (graphs.length === 0 && normalizedGraphCount > 0) {
11600
12146
  throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
@@ -11762,7 +12308,7 @@ function targetKeysForAgent(scope, name) {
11762
12308
  async function runtimeRoots(scope) {
11763
12309
  const roots = /* @__PURE__ */ new Set([scope.root]);
11764
12310
  if (scope.config.packages.some((pkg) => pkg.installationType === "user") || Object.values(scope.config.agents).some((agent) => agent.installationType === "user")) {
11765
- 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()));
11766
12312
  }
11767
12313
  for (const [name, agent] of Object.entries(scope.config.agents)) {
11768
12314
  if (agent.transport === "ssh") throw new Error(`Installed-state normalization cannot inspect SSH agent '${name}' locally.`);
@@ -11783,7 +12329,7 @@ async function collectManifests(roots) {
11783
12329
  const dir = join44(root, ".agentwheel");
11784
12330
  let names;
11785
12331
  try {
11786
- names = await readdir9(dir);
12332
+ names = await readdir10(dir);
11787
12333
  } catch (error) {
11788
12334
  if (isMissing(error)) continue;
11789
12335
  throw error;
@@ -11799,14 +12345,14 @@ async function collectManifests(roots) {
11799
12345
  }
11800
12346
  continue;
11801
12347
  }
11802
- if (resolve23(parsed.targetRoot) !== resolve23(root)) {
12348
+ if (resolve24(parsed.targetRoot) !== resolve24(root)) {
11803
12349
  throw new Error(`Install manifest target root does not match its enumerated runtime root: ${path}`);
11804
12350
  }
11805
12351
  const expectedPath = installManifestPath(parsed.targetRoot, parsed.adapter, {
11806
12352
  installationType: parsed.installationType,
11807
12353
  stateKey: parsed.stateKey
11808
12354
  });
11809
- if (resolve23(expectedPath) !== resolve23(path)) {
12355
+ if (resolve24(expectedPath) !== resolve24(path)) {
11810
12356
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11811
12357
  }
11812
12358
  found.set(path, { path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11827,7 +12373,7 @@ async function collectManifestPaths(paths) {
11827
12373
  installationType: parsed.installationType,
11828
12374
  stateKey: parsed.stateKey
11829
12375
  });
11830
- if (resolve23(expectedPath) !== resolve23(path)) {
12376
+ if (resolve24(expectedPath) !== resolve24(path)) {
11831
12377
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11832
12378
  }
11833
12379
  found.push({ path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11855,12 +12401,8 @@ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include)
11855
12401
  if (!lock.canonical.roots.some((candidate2) => selected.has(candidate2.rootId))) continue;
11856
12402
  const parts = relative8(root, path).split(/[\\/]/);
11857
12403
  if (parts.length !== 3) throw new Error(`Graph lock path is not canonical: ${path}`);
11858
- const [targetKey2, adapter, fileName] = parts;
12404
+ const [targetKey2, adapter] = parts;
11859
12405
  if (targetKeys && !targetKeys.has(targetKey2)) continue;
11860
- const pathFingerprint = basename19(fileName, ".graph-lock.json");
11861
- if (!lock.canonical.targetFingerprint || lock.canonical.targetFingerprint !== pathFingerprint) {
11862
- throw new Error(`Graph lock fingerprint does not match its canonical path: ${path}`);
11863
- }
11864
12406
  const candidate = {
11865
12407
  path,
11866
12408
  lock,
@@ -11889,9 +12431,19 @@ function isCurrentLocalTarget(scope, graph) {
11889
12431
  }
11890
12432
  async function hasRelevantLegacyManifestCandidate(scope, graph, selected) {
11891
12433
  if (!isCurrentLocalTarget(scope, graph)) return false;
11892
- const manifestPath = await legacyManifestPathForConfiguredTarget(scope, graph) ?? (await legacyTargetStateForGraph(scope, graph)).manifestPath;
11893
- const manifest = (await collectManifestPaths([manifestPath]))[0];
11894
- 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;
11895
12447
  }
11896
12448
  async function legacyManifestPathForConfiguredTarget(scope, graph) {
11897
12449
  const agent = scope.config.agents[graph.targetKey];
@@ -11910,9 +12462,10 @@ async function legacyManifestPathForConfiguredTarget(scope, graph) {
11910
12462
  baseDir: scope.root
11911
12463
  });
11912
12464
  const targetRoot = resolveConfigPath(agent.root, scope.root);
11913
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
12465
+ const installRoot = resolve24(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
11914
12466
  const stateKey = stateKeyFor(adapter.name, {
11915
12467
  installationType: agent.installationType,
12468
+ stateKey: agent.stateKey,
11916
12469
  targetFingerprint: graph.lock.canonical.targetFingerprint
11917
12470
  });
11918
12471
  return installManifestPath(installRoot, adapter.name, { installationType: agent.installationType, stateKey });
@@ -11977,14 +12530,21 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11977
12530
  ssh: void 0,
11978
12531
  ...agent?.stateKey ? { stateKey: agent.stateKey } : {}
11979
12532
  };
11980
- 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;
11981
12542
  const stateKey = stateKeyFor(adapter.name, {
11982
12543
  installationType,
11983
12544
  stateKey: agent?.stateKey,
11984
- targetFingerprint,
12545
+ targetFingerprint: targetIdentity.stateFingerprint,
11985
12546
  ...identityFleetId ? { fleetId: identityFleetId } : {}
11986
12547
  });
11987
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
11988
12548
  return {
11989
12549
  adapter: adapter.name,
11990
12550
  installationType,
@@ -11992,28 +12552,81 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11992
12552
  installRoot,
11993
12553
  stateKey,
11994
12554
  targetFingerprint,
11995
- 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
+ }),
11996
12559
  manifestPath: installManifestPath(installRoot, adapter.name, { installationType, stateKey })
11997
12560
  };
11998
12561
  }
11999
- async function legacyTargetStateForGraph(scope, graph) {
12000
- 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);
12001
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];
12002
12588
  const stateKey = stateKeyFor(current.adapter, {
12003
12589
  installationType: current.installationType,
12004
- targetFingerprint
12590
+ stateKey: agent?.stateKey,
12591
+ targetFingerprint,
12592
+ ...identityFleetId ? { fleetId: identityFleetId } : {}
12005
12593
  });
12006
12594
  return {
12007
12595
  ...current,
12008
12596
  stateKey,
12009
12597
  targetFingerprint,
12010
- graphLockPath: graph.path,
12598
+ graphLockPath: join44(dirname21(graph.path), `${targetFingerprint}.graph-lock.json`),
12011
12599
  manifestPath: installManifestPath(current.installRoot, current.adapter, {
12012
12600
  installationType: current.installationType,
12013
12601
  stateKey
12014
12602
  })
12015
12603
  };
12016
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
+ }
12017
12630
  function assertGraphIsSubset(source, destination) {
12018
12631
  const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson2(root)]));
12019
12632
  for (const root of destination.lock.canonical.roots) {
@@ -12047,7 +12660,7 @@ function destinationGraphFromSource(source, targetFingerprint) {
12047
12660
  function mergeDestinationManifest(existing, state, generatedAt, desiredEntries, replaceOwners = /* @__PURE__ */ new Set()) {
12048
12661
  if (existing) {
12049
12662
  const manifest2 = existing.manifest;
12050
- 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) {
12051
12664
  throw new Error(`Destination manifest identity diverges from the derived target: ${existing.path}`);
12052
12665
  }
12053
12666
  }
@@ -12107,7 +12720,7 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12107
12720
  const content = await readFile27(path, "utf8");
12108
12721
  if (hasMergeRemovalContent(entry.mergeRemoval)) {
12109
12722
  assertExactMergeContribution(entry.mergeRemoval, entry.mergeStrategy, content);
12110
- } else if (resolve23(sourcePath) === resolve23(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12723
+ } else if (resolve24(sourcePath) === resolve24(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12111
12724
  assertExactMergeContribution({}, entry.mergeStrategy, content);
12112
12725
  } else {
12113
12726
  throw new Error(`Installed-state normalization cannot prove incomplete merge ownership at ${path}.`);
@@ -12118,8 +12731,8 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12118
12731
  await assertEquivalentRuntimeBytes(sourcePath, destinationPath, entry.hash);
12119
12732
  }
12120
12733
  function renderedEntryPathForRoot(root, entryPath) {
12121
- const normalizedRoot = resolve23(root);
12122
- const candidate = resolve23(normalizedRoot, entryPath);
12734
+ const normalizedRoot = resolve24(root);
12735
+ const candidate = resolve24(normalizedRoot, entryPath);
12123
12736
  if (candidate !== normalizedRoot && !candidate.startsWith(`${normalizedRoot}/`)) {
12124
12737
  throw new Error(`Install manifest entry escapes its target root: ${entryPath}`);
12125
12738
  }
@@ -12210,8 +12823,8 @@ function ownerMatchesGraphPackage(lock, owner, selected) {
12210
12823
  return ownerMatchesPackage(owner, selected) || lock.canonical.roots.some((root) => selected.has(root.rootId) && root.graphNodeId === owner);
12211
12824
  }
12212
12825
  function renderedEntryPath(manifest, entry) {
12213
- const root = resolve23(manifest.targetRoot);
12214
- const candidate = resolve23(root, entry.path);
12826
+ const root = resolve24(manifest.targetRoot);
12827
+ const candidate = resolve24(root, entry.path);
12215
12828
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
12216
12829
  throw new Error(`Install manifest entry escapes its target root: ${entry.path}`);
12217
12830
  }
@@ -12918,11 +13531,11 @@ program.command("deps").description("inspect the OpenPack dependency graph").add
12918
13531
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
12919
13532
  const targets = await resolveCliTargets(normalizedOptions);
12920
13533
  for (const target of targets) {
12921
- const { lock, adapter } = await readTargetGraphLock(target, normalizedOptions);
12922
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
12923
- const state = installStateForTarget(target, adapter, normalizedOptions, installationType);
12924
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transportForTarget(target), state);
12925
- 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));
12926
13539
  }
12927
13540
  })
12928
13541
  );
@@ -13013,19 +13626,23 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
13013
13626
  throw new Error(`Ownership handoff selector must be exactly type/name: ${selector}`);
13014
13627
  }
13015
13628
  const target = targets[0];
13016
- const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
13017
- const adapter = await resolveAdapterForTarget(target, adapterOptions);
13018
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13019
- 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
+ }
13020
13636
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13021
13637
  if (options.toFleet) {
13022
13638
  const fleet = await showRegisteredFleet(options.toFleet);
13023
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
13639
+ if (resolve25(fleet.root) !== resolve25(toWorkspaceRoot)) {
13024
13640
  throw new Error(`Destination fleet '${options.toFleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13025
13641
  }
13026
13642
  }
13027
13643
  const request = {
13028
- ...state,
13644
+ installationType,
13645
+ stateKey: state.stableStateKey,
13029
13646
  targetRoot: state.installRoot,
13030
13647
  adapter: adapter.name,
13031
13648
  artifactType,
@@ -13049,7 +13666,7 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
13049
13666
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
13050
13667
  const fleet = await showRegisteredFleet(options.fleet);
13051
13668
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13052
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
13669
+ if (resolve25(fleet.root) !== resolve25(toWorkspaceRoot)) {
13053
13670
  throw new Error(`Destination Fleet '${options.fleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13054
13671
  }
13055
13672
  const targets = await resolveCliTargets(normalizedOptions);
@@ -13057,19 +13674,22 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
13057
13674
  throw new Error(`Stale ownership retirement requires exactly one destination Fleet target, found ${targets.length}.`);
13058
13675
  }
13059
13676
  const target = targets[0];
13060
- if (target.fleetId !== options.fleet || resolve24(target.workspaceRoot) !== resolve24(fleet.root)) {
13677
+ if (target.fleetId !== options.fleet || resolve25(target.workspaceRoot) !== resolve25(fleet.root)) {
13061
13678
  throw new Error(`Resolved target does not belong to destination Fleet '${options.fleet}'.`);
13062
13679
  }
13063
- const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
13064
- const adapter = await resolveAdapterForTarget(target, adapterOptions);
13065
- const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13066
- 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
+ }
13067
13687
  const request = {
13068
13688
  targetRoot: state.installRoot,
13069
13689
  adapter: adapter.name,
13070
13690
  installationType,
13071
13691
  sourceStateKey: options.sourceStateKey,
13072
- destinationStateKey: state.stateKey,
13692
+ destinationStateKey: state.stableStateKey,
13073
13693
  fromWorkspaceRoot: normalizeCliPath(options.fromWorkspaceRoot),
13074
13694
  toWorkspaceRoot,
13075
13695
  toFleetId: fleet.id,
@@ -13154,13 +13774,18 @@ program.command("uninstall").description("remove configured packages or managed
13154
13774
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
13155
13775
  const transport = transportForTarget(target);
13156
13776
  const installationType = normalizedOptions.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
13157
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
13158
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
13777
+ const state = await resolveCliTargetState(target, normalizedOptions);
13778
+ const manifest = state.manifest;
13159
13779
  if (!manifest) {
13160
13780
  console.log(`No install manifest for ${adapter.name}/${installationType} at ${state.installRoot}`);
13161
13781
  continue;
13162
13782
  }
13163
- 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
+ };
13164
13789
  console.log(formatPlan(plan));
13165
13790
  const result = await uninstall(plan, { dryRun: normalizedOptions.dryRun, force: normalizedOptions.force, transport });
13166
13791
  if (!normalizedOptions.dryRun) {
@@ -13622,15 +14247,12 @@ async function packageSelectsSkillForTarget(target, pkg, selector, options) {
13622
14247
  if (explicitSelection && !explicitSelection.some((candidate) => candidate === selector)) return false;
13623
14248
  const groups = /* @__PURE__ */ new Map();
13624
14249
  const group = graphGroupForPackage(groups, target, pkg, options);
13625
- const adapter = await resolveAdapterForTarget(group.target, group.adapterOptions);
13626
- const graphLockPath = graphLockPathForTarget(
13627
- group.target.workspaceRoot,
13628
- targetKeyForTarget(group.target, adapter.name),
13629
- adapter.name,
13630
- targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
13631
- );
13632
- if (await pathExists(graphLockPath)) {
13633
- 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;
13634
14256
  const root = lock.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
13635
14257
  if (root?.selected.includes(selector)) return true;
13636
14258
  }
@@ -13933,13 +14555,30 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
13933
14555
  const allPackages = [...group.packages, ...group.extraPackages];
13934
14556
  const groupHasScope = !scopedRootId || allPackages.some((pkg) => pkg.name === scopedRootId || pkg.source === targetOptions.scope);
13935
14557
  if (behavior.mode === "install" && scopedRootId && !groupHasScope) continue;
13936
- const groupGraphLockPath = graphLockPathForTarget(
13937
- group.target.workspaceRoot,
13938
- targetKeyForTarget(group.target, adapter.name),
13939
- adapter.name,
13940
- targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
13941
- );
13942
- 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;
13943
14582
  const dependencyUpdateRootNames = /* @__PURE__ */ new Set();
13944
14583
  if (scopedDependencyUpdate) {
13945
14584
  for (const pkg of group.packages) {
@@ -14049,14 +14688,14 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14049
14688
  expectedFromWorkspaceOwner: targetOptions.expectedFromWorkspaceOwner
14050
14689
  });
14051
14690
  if ((behavior.mode === "install" || behavior.mode === "update") && scopedRootId) {
14052
- const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
14053
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14054
- 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);
14055
14694
  if (targetOptions.focusedArtifact && targetOptions.forceForeignState !== true) {
14056
14695
  const focusedPaths = scopedResult.plan.operations.filter((operation) => operationMatchesFocusedArtifact(
14057
14696
  operation,
14058
14697
  targetOptions.focusedArtifact,
14059
- focusedArtifactOwnerKeys(result.bundle.graphLock, previousGroupLock, scopedRootId, targetOptions.focusedArtifact)
14698
+ focusedArtifactOwnerKeys(result.bundle.graphLock, previousLock, scopedRootId, targetOptions.focusedArtifact)
14060
14699
  )).map((operation) => operation.relativeDestPath);
14061
14700
  const ownership = await resolveWorkspaceOwnershipScope(group.target.workspaceRoot, {
14062
14701
  fleetId: group.target.fleetId
@@ -14070,9 +14709,11 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14070
14709
  }
14071
14710
  results.push(scopedResult);
14072
14711
  } else if (scopedDependencyUpdate) {
14073
- const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
14074
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14075
- 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
+ }
14076
14717
  results.push(scopeUpdatePlanToDependencies(result, dependencyUpdateSelectors, previousLock, manifest));
14077
14718
  } else {
14078
14719
  results.push(result);
@@ -14665,8 +15306,12 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14665
15306
  const removedInstallationType = options.installationType ?? pkg.installationType ?? removedTarget.installationType ?? "local";
14666
15307
  const adapter = await resolveAdapterForTarget(removedTarget, removedAdapterOptions);
14667
15308
  const transport = transportForTarget(removedTarget);
14668
- const removedState = installStateForTarget(removedTarget, adapter, removedAdapterOptions, removedInstallationType);
14669
- 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;
14670
15315
  if (!manifest) {
14671
15316
  console.log(`No install manifest for ${adapter.name} at ${removedTarget.targetRoot}`);
14672
15317
  continue;
@@ -14714,25 +15359,24 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14714
15359
  remainingDesired = desiredArtifactsFromGraphBundle(result2.bundle);
14715
15360
  renderedRoot = result2.bundle.root;
14716
15361
  }
14717
- 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
+ };
14718
15372
  console.log(`Uninstall ${pkg.name} (${adapter.name} at ${removedTarget.targetRoot}):`);
14719
15373
  console.log(formatPlan(plan));
14720
15374
  const graphLockFinalState = remainingGraphPlan ? { graphLock: { path: remainingGraphPlan.graphLockPath, lock: remainingGraphPlan.bundle.graphLock } } : {
14721
- removeGraphLockPath: graphLockPathForTarget(
14722
- removedTarget.workspaceRoot,
14723
- targetKeyForTarget(removedTarget, adapter.name),
14724
- adapter.name,
14725
- targetFingerprintParts(removedTarget, adapter, removedAdapterOptions, removedInstallationType)
14726
- )
15375
+ removeGraphLockPath: removedState.graphLockPath ?? removedState.stableGraphLockPath
14727
15376
  };
14728
15377
  if (!options.dryRun) {
14729
15378
  declareMutationPath(workspaceConfigPath(target.workspaceRoot));
14730
- const declarativeGraphPath = remainingGraphPlan ? remainingGraphPlan.graphLockPath : graphLockPathForTarget(
14731
- removedTarget.workspaceRoot,
14732
- targetKeyForTarget(removedTarget, adapter.name),
14733
- adapter.name,
14734
- targetFingerprintParts(removedTarget, adapter, removedAdapterOptions, removedInstallationType)
14735
- );
15379
+ const declarativeGraphPath = remainingGraphPlan ? remainingGraphPlan.graphLockPath : removedState.graphLockPath ?? removedState.stableGraphLockPath;
14736
15380
  declareMutationPath(declarativeGraphPath);
14737
15381
  }
14738
15382
  const result = await uninstall(plan, {
@@ -14807,36 +15451,92 @@ function targetFingerprintParts(target, adapter, options, installationType) {
14807
15451
  };
14808
15452
  }
14809
15453
  function installStateForTarget(target, adapter, options, installationType) {
14810
- 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
+ });
14811
15462
  return {
14812
15463
  installationType,
14813
15464
  stateKey: stateKeyFor(adapter.name, {
14814
15465
  installationType,
14815
15466
  stateKey: target.stateKey,
14816
- targetFingerprint,
15467
+ targetFingerprint: targetIdentity.stateFingerprint,
14817
15468
  fleetId: target.fleetId
14818
15469
  }),
14819
- installRoot: installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, target.transport === "ssh")
15470
+ installRoot
14820
15471
  };
14821
15472
  }
14822
- function targetFingerprintDigest(target, adapter, options, installationType) {
14823
- const fingerprintInput = targetFingerprintParts(target, adapter, options, installationType);
14824
- 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
+ };
14825
15483
  }
14826
- async function readTargetGraphLock(target, options) {
15484
+ async function resolveCliTargetState(target, options) {
14827
15485
  const adapterOptions = adapterOptionsForTarget(target, options);
14828
15486
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15487
+ const transport = transportForTarget(target);
14829
15488
  const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
14830
- const path = graphLockPathForTarget(
15489
+ const stable = installStateForTarget(target, adapter, adapterOptions, installationType);
15490
+ const stableGraphLockPath = graphLockPathForTarget(
14831
15491
  target.workspaceRoot,
14832
15492
  targetKeyForTarget(target, adapter.name),
14833
15493
  adapter.name,
14834
- targetFingerprintParts(target, adapter, adapterOptions, installationType)
15494
+ targetFingerprintParts(target, adapter, adapterOptions, installationType),
15495
+ targetIdentityContext(target, adapter, installationType)
14835
15496
  );
14836
- if (!await pathExists(path)) {
14837
- 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}`);
14838
15538
  }
14839
- return { adapter, path, lock: await readGraphLock(path) };
15539
+ return { adapter: state.adapter, path: state.graphLockPath, lock: state.graphLock };
14840
15540
  }
14841
15541
  async function printStatus(target, options) {
14842
15542
  const report = await collectTargetStatus(target, options);
@@ -14844,30 +15544,24 @@ async function printStatus(target, options) {
14844
15544
  }
14845
15545
  async function collectTargetStatus(target, options) {
14846
15546
  const config = await readMergedWorkspaceConfig(target.workspaceRoot);
14847
- const adapterOptions = adapterOptionsForTarget(target, options);
14848
- const adapter = await resolveAdapterForTarget(target, { ...adapterOptions, warn: () => void 0 });
14849
- const transport = transportForTarget(target);
14850
- const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
14851
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
14852
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
14853
- let graphLockPath = null;
14854
- let graphLock;
14855
- try {
14856
- const result = await readTargetGraphLock(target, adapterOptions);
14857
- graphLockPath = result.path;
14858
- graphLock = result.lock;
14859
- } catch {
14860
- graphLock = void 0;
14861
- }
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;
14862
15556
  const packages = [];
14863
15557
  for (const pkg of config.packages) {
14864
- const root = graphLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
14865
- 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;
14866
15560
  const availability = await discoverPackageVersions(pkg, target.workspaceRoot, {
14867
15561
  forceRefresh: options.refresh,
14868
15562
  offline: options.offline
14869
15563
  });
14870
- 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;
14871
15565
  const locked = node?.version ?? null;
14872
15566
  const baseline = installed ?? locked;
14873
15567
  packages.push({
@@ -14890,14 +15584,13 @@ async function collectTargetStatus(target, options) {
14890
15584
  )
14891
15585
  });
14892
15586
  }
14893
- const pending = await collectPendingInstallWork(target, options);
14894
- const artifacts = (graphLock?.canonical.artifacts ?? []).map((artifact) => {
14895
- const node = graphLock?.canonical.nodes.find((candidate) => candidate.id === artifact.graphNodeId);
14896
- 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) => {
14897
15590
  if (!("graphNodeId" in entry) || entry.graphNodeId !== artifact.graphNodeId) return false;
14898
15591
  if ("logicalSelector" in entry && entry.logicalSelector) return entry.logicalSelector === artifact.logicalSelector;
14899
15592
  return entry.artifactType === artifact.type && entry.artifactName === artifact.name;
14900
- }) ?? false;
15593
+ }) ?? false) || foreignObservations.some((observation) => observation.graphNodeId === artifact.graphNodeId && (!observation.logicalSelector || observation.logicalSelector === artifact.logicalSelector));
14901
15594
  return {
14902
15595
  selector: artifact.logicalSelector,
14903
15596
  type: artifact.type,
@@ -14910,7 +15603,7 @@ async function collectTargetStatus(target, options) {
14910
15603
  };
14911
15604
  });
14912
15605
  const health = [];
14913
- if (!manifest || !graphLock) health.push("FAIL");
15606
+ if (!manifest && foreignObservations.length === 0 || !effectiveGraphLock) health.push("FAIL");
14914
15607
  if (pending.error) health.push("DEGRADED");
14915
15608
  if (pending.driftCount > 0 || pending.conflictCount > 0) health.push("FAIL");
14916
15609
  else if (pending.pendingCount > 0) health.push("WARN");
@@ -14921,11 +15614,11 @@ async function collectTargetStatus(target, options) {
14921
15614
  installationType,
14922
15615
  targetRoot: state.installRoot,
14923
15616
  health: worstStatusHealth(health),
14924
- manifestRevision: manifest?.revision ?? null,
14925
- manifestEntryCount: manifest?.entries.length ?? 0,
15617
+ manifestRevision: manifest?.revision ?? observedManifestRevision,
15618
+ manifestEntryCount: (manifest?.entries.length ?? 0) + foreignObservations.length,
14926
15619
  graphLockPath,
14927
15620
  packageCount: packages.length,
14928
- artifactCount: graphLock?.canonical.artifacts.length ?? 0,
15621
+ artifactCount: effectiveGraphLock?.canonical.artifacts.length ?? 0,
14929
15622
  pendingCount: pending.pendingCount,
14930
15623
  driftCount: pending.driftCount,
14931
15624
  conflictCount: pending.conflictCount,
@@ -15103,7 +15796,7 @@ async function assertCompositeMutationSupported(workspaceRoot, profile, readOnly
15103
15796
  `Composite-profile mutation is refused before member execution because ${member.id} uses ${member.transport}; durable remote policy and recovery preflight is not implemented.`
15104
15797
  );
15105
15798
  }
15106
- const memberRoot = resolve24(workspaceRoot, member.workspace);
15799
+ const memberRoot = resolve25(workspaceRoot, member.workspace);
15107
15800
  const memberPolicy = await mutationPolicyForWorkspace(memberRoot);
15108
15801
  if (memberPolicy?.revisioning.mode === "commit-after-verify") {
15109
15802
  throw new Error(
@@ -15260,9 +15953,34 @@ async function journalStateForTarget(target, options) {
15260
15953
  const adapterOptions = adapterOptionsForTarget(target, options);
15261
15954
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15262
15955
  const transport = transportForTarget(target);
15263
- const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
15264
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
15265
- 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
+ };
15266
15984
  }
15267
15985
  async function collectPendingInstallWork(target, options) {
15268
15986
  let results = [];
@@ -15283,11 +16001,20 @@ async function collectPendingInstallWork(target, options) {
15283
16001
  pendingCount: pending.length,
15284
16002
  driftCount: counts.drift ?? 0,
15285
16003
  conflictCount: counts.conflict ?? 0,
15286
- counts
16004
+ counts,
16005
+ foreignStateObservations: results.flatMap((result) => result.foreignStateObservations),
16006
+ observedGraphLock: results.length === 1 ? results[0].bundle.graphLock : void 0
15287
16007
  };
15288
16008
  } catch (error) {
15289
16009
  const message = error instanceof Error ? error.message : String(error);
15290
- 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
+ };
15291
16018
  } finally {
15292
16019
  await Promise.all(results.map((result) => rm10(result.bundle.root, { recursive: true, force: true })));
15293
16020
  }
@@ -15300,9 +16027,8 @@ async function printDoctor(target, options) {
15300
16027
  if (!targetMapping?.enabled) {
15301
16028
  throw new Error(`Adapter ${adapter.name} does not support skills for installation type '${installationType}'.`);
15302
16029
  }
15303
- const transport = transportForTarget(target);
15304
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
15305
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
16030
+ const state = await resolveCliTargetState(target, { ...options, installationType });
16031
+ const manifest = state.manifest;
15306
16032
  const requestedSkills = doctorSkillRequests(target, options);
15307
16033
  const skills = [];
15308
16034
  for (const request of requestedSkills) {
@@ -15554,14 +16280,14 @@ function looksLikeSourceSpecifier(value) {
15554
16280
  }
15555
16281
  function normalizeCliPath(value) {
15556
16282
  if (value === "~") return homedir12();
15557
- if (value.startsWith("~/")) return resolve24(homedir12(), value.slice(2));
15558
- return resolve24(value);
16283
+ if (value.startsWith("~/")) return resolve25(homedir12(), value.slice(2));
16284
+ return resolve25(value);
15559
16285
  }
15560
16286
  function shellQuoteArgument(value) {
15561
16287
  return /^[a-z0-9_./:@%+=,-]+$/i.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
15562
16288
  }
15563
16289
  function isHomePath(path) {
15564
- return resolve24(path) === resolve24(homedir12());
16290
+ return resolve25(path) === resolve25(homedir12());
15565
16291
  }
15566
16292
  function adapterListFromOption(adapter) {
15567
16293
  if (!adapter) return [];
@@ -15682,7 +16408,7 @@ function withFleetExample(config) {
15682
16408
  };
15683
16409
  }
15684
16410
  async function findAgentwheelPackageRoot(start) {
15685
- let current = resolve24(start);
16411
+ let current = resolve25(start);
15686
16412
  while (true) {
15687
16413
  if (await findPackageManifestPath(current, { warnLegacy: false })) return current;
15688
16414
  const parent = dirname22(current);