agentwheel 0.20.8 → 0.20.10

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
@@ -3,6 +3,7 @@ import {
3
3
  MergeAdoptionMismatchError,
4
4
  applyCombinedInstallPlan,
5
5
  assertExactMergeContribution,
6
+ assertMergedSourceContribution,
6
7
  assertOperationContained,
7
8
  assertSafeInstallName,
8
9
  canonicalGraphLockJson,
@@ -12,10 +13,12 @@ import {
12
13
  commitManifestMetadataJournal,
13
14
  computeInstallManifestInventoryRevision,
14
15
  computeManifestRevision,
16
+ computeSourceLockRevision,
15
17
  computeTargetFingerprint,
16
18
  desiredManagedInstructionBlockHash,
17
19
  hasMergeRemovalContent,
18
20
  installManifestSchema,
21
+ installManifestV2Schema,
19
22
  legacyUnownedWorkspaceOwner,
20
23
  listInstallManifests,
21
24
  managedInstructionBlockMode,
@@ -29,11 +32,12 @@ import {
29
32
  readGraphLock,
30
33
  readInstallManifest,
31
34
  readManagedInstructionBlockState,
35
+ readSourceLock,
32
36
  recoverPendingApply,
33
37
  uninstall,
34
38
  withManifestRevision,
35
39
  writeInstallManifest
36
- } from "./chunk-4AM3LNUQ.js";
40
+ } from "./chunk-P2JK34W7.js";
37
41
  import {
38
42
  CURRENT_WORKSPACE_SCHEMA_VERSION,
39
43
  GovernedMutation,
@@ -94,7 +98,7 @@ import {
94
98
  workspaceSelectionImportSchema,
95
99
  writeApplyJournal,
96
100
  writeWorkspaceConfig
97
- } from "./chunk-JXX6EDIP.js";
101
+ } from "./chunk-J7DKPDS7.js";
98
102
  import {
99
103
  inferSourceDriverName
100
104
  } from "./chunk-EMDIG24I.js";
@@ -109,11 +113,11 @@ import {
109
113
  } from "./chunk-7VPI5J5Y.js";
110
114
 
111
115
  // src/cli/index.ts
112
- import { createHash as createHash14 } from "crypto";
116
+ import { createHash as createHash15 } from "crypto";
113
117
  import { existsSync } from "fs";
114
- import { mkdir as mkdir16, rm as rm10, writeFile as writeFile13 } from "fs/promises";
115
- import { homedir as homedir12 } from "os";
116
- import { dirname as dirname22, join as join45, resolve as resolve24 } from "path";
118
+ import { mkdir as mkdir16, mkdtemp as mkdtemp4, realpath as realpath2, rm as rm10, writeFile as writeFile13 } from "fs/promises";
119
+ import { homedir as homedir12, tmpdir as tmpdir5 } from "os";
120
+ import { basename as basename21, dirname as dirname23, isAbsolute as isAbsolute4, join as join46, relative as relative10, resolve as resolve26 } from "path";
117
121
  import { fileURLToPath as fileURLToPath3 } from "url";
118
122
  import { Command } from "commander";
119
123
 
@@ -917,11 +921,11 @@ function normalizeArtifactSelectors(select, legacySkills) {
917
921
  if (selected.length === 0) return void 0;
918
922
  return [...new Set(selected.map(parseArtifactSelector))];
919
923
  }
920
- function filterArtifactsBySelection(artifacts, selectors, legacySkills) {
924
+ function filterArtifactsBySelection(artifacts, selectors, legacySkills, options = {}) {
921
925
  const selected = normalizeArtifactSelectors(selectors, legacySkills);
922
926
  if (!selected?.length) return artifacts;
923
927
  const selectedSet = new Set(selected);
924
- const available = new Set(artifacts.flatMap(artifactSelectorAliases));
928
+ const available = new Set((options.validationArtifacts ?? artifacts).flatMap(artifactSelectorAliases));
925
929
  const missing = selected.filter((selector) => !available.has(selector));
926
930
  if (missing.length > 0) {
927
931
  throw new Error(`Selected artifact not found in package: ${missing.join(", ")}`);
@@ -1706,6 +1710,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
1706
1710
  hasBlockingChanges: operations.some((op) => op.action === "drift" || op.action === "conflict"),
1707
1711
  baseRevision: options.baseRevision ?? manifest?.revision ?? null,
1708
1712
  runtimeStateRevision,
1713
+ stateMigration: options.stateMigration,
1709
1714
  migrationReport: migration.report,
1710
1715
  graphLockDigest: options.graphLockDigest,
1711
1716
  adapterCode: adapter.programmatic ? { modulePath: adapter.programmatic.modulePath, hash: adapter.programmatic.hash } : void 0,
@@ -2388,6 +2393,7 @@ function operationMetadataFromEntry2(entry, ownersOverride) {
2388
2393
  graphNodeId: entry.graphNodeId,
2389
2394
  dependencyRole: entry.dependencyRole,
2390
2395
  owners: ownersOverride ?? entry.owners,
2396
+ workspaceOwner: entry.workspaceOwner,
2391
2397
  graphLockDigest: entry.graphLockDigest,
2392
2398
  mergeRemoval: entry.mergeRemoval,
2393
2399
  mergeCreatedDestination: entry.mergeCreatedDestination
@@ -3107,14 +3113,14 @@ async function writeMermaidAsset(response) {
3107
3113
  "content-type": "application/javascript; charset=utf-8",
3108
3114
  "cache-control": "public, max-age=3600"
3109
3115
  });
3110
- await new Promise((resolve25) => {
3116
+ await new Promise((resolve27) => {
3111
3117
  const stream = createReadStream(assetPath);
3112
3118
  stream.on("error", () => {
3113
3119
  if (!response.headersSent) writeText(response, 404, "Mermaid asset unavailable.\n");
3114
3120
  else response.destroy();
3115
- resolve25();
3121
+ resolve27();
3116
3122
  });
3117
- stream.on("end", resolve25);
3123
+ stream.on("end", resolve27);
3118
3124
  stream.pipe(response);
3119
3125
  });
3120
3126
  }
@@ -3141,14 +3147,14 @@ function escapeHtml2(value) {
3141
3147
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3142
3148
  }
3143
3149
  function listen(server, port, bind) {
3144
- return new Promise((resolve25, reject) => {
3150
+ return new Promise((resolve27, reject) => {
3145
3151
  const onError = (error) => {
3146
3152
  server.off("listening", onListening);
3147
3153
  reject(error);
3148
3154
  };
3149
3155
  const onListening = () => {
3150
3156
  server.off("error", onError);
3151
- resolve25();
3157
+ resolve27();
3152
3158
  };
3153
3159
  server.once("error", onError);
3154
3160
  server.once("listening", onListening);
@@ -3156,7 +3162,7 @@ function listen(server, port, bind) {
3156
3162
  });
3157
3163
  }
3158
3164
  function waitForShutdown(server, beforeClose) {
3159
- return new Promise((resolve25) => {
3165
+ return new Promise((resolve27) => {
3160
3166
  let closing = false;
3161
3167
  const shutdown = () => {
3162
3168
  if (closing) return;
@@ -3164,7 +3170,7 @@ function waitForShutdown(server, beforeClose) {
3164
3170
  process.off("SIGINT", shutdown);
3165
3171
  process.off("SIGTERM", shutdown);
3166
3172
  beforeClose();
3167
- server.close(() => resolve25());
3173
+ server.close(() => resolve27());
3168
3174
  };
3169
3175
  process.once("SIGINT", shutdown);
3170
3176
  process.once("SIGTERM", shutdown);
@@ -3174,7 +3180,7 @@ function waitForShutdown(server, beforeClose) {
3174
3180
  process.off("SIGINT", shutdown);
3175
3181
  process.off("SIGTERM", shutdown);
3176
3182
  beforeClose();
3177
- resolve25();
3183
+ resolve27();
3178
3184
  });
3179
3185
  });
3180
3186
  }
@@ -3727,7 +3733,7 @@ async function fetchClawHubPackage(fetchImpl, packageName) {
3727
3733
  throw new Error(`ClawHub lookup failed for ${packageName}`);
3728
3734
  }
3729
3735
  async function delay(ms) {
3730
- await new Promise((resolve25) => setTimeout(resolve25, ms));
3736
+ await new Promise((resolve27) => setTimeout(resolve27, ms));
3731
3737
  }
3732
3738
  function parseClawHubSource(source) {
3733
3739
  if (!source.startsWith(sourcePrefix)) {
@@ -3904,7 +3910,7 @@ async function withGitCacheMaintenanceLock(cacheRoot, timeoutMs, fn) {
3904
3910
  if (Date.now() - started > timeoutMs) {
3905
3911
  throw new Error(`Timed out waiting for git cache maintenance lock at ${lockPath}`);
3906
3912
  }
3907
- await new Promise((resolve25) => setTimeout(resolve25, 50));
3913
+ await new Promise((resolve27) => setTimeout(resolve27, 50));
3908
3914
  }
3909
3915
  }
3910
3916
  try {
@@ -6570,7 +6576,7 @@ async function collectIncludeSelector(raw, optional, artifactsByRelativePath, sc
6570
6576
  }
6571
6577
  function artifactTargetsRuntime(runtimes, runtime, nodeId, selector, warn) {
6572
6578
  if (!runtimes?.length || !runtime || runtimes.includes(runtime)) return true;
6573
- warn?.(`skip artifact ${nodeId}:${selector} (not targeted: runtimes=[${runtimes.join(",")}])`);
6579
+ warn?.(`skip artifact ${nodeId}:${selector} (selected but not targeted: runtimes=[${runtimes.join(",")}])`);
6574
6580
  return false;
6575
6581
  }
6576
6582
  function requirementTargetsRuntime(runtimes, runtime, label, warn) {
@@ -6770,8 +6776,8 @@ function verifyIntegrity(integrity, sourceHash, label) {
6770
6776
  async function withCachePathLock(path, fn) {
6771
6777
  const previous = cacheLocks.get(path) ?? Promise.resolve();
6772
6778
  let release = () => void 0;
6773
- const current = previous.then(() => new Promise((resolve25) => {
6774
- release = resolve25;
6779
+ const current = previous.then(() => new Promise((resolve27) => {
6780
+ release = resolve27;
6775
6781
  }));
6776
6782
  cacheLocks.set(path, current);
6777
6783
  await previous;
@@ -6833,6 +6839,11 @@ function detectDirectCollisions(nodes) {
6833
6839
  for (const [selector, owners] of bySelector) {
6834
6840
  const uniqueOwners = [...new Map(owners.map((owner) => [owner.node.id, owner])).values()];
6835
6841
  if (uniqueOwners.length <= 1) continue;
6842
+ const selectedArtifacts = uniqueOwners.map((owner) => ({
6843
+ owner,
6844
+ artifact: owner.artifacts.find((candidate) => artifactSelectorKey(candidate) === selector)
6845
+ }));
6846
+ if (new Set(selectedArtifacts.map(({ artifact }) => artifact.hash)).size === 1) continue;
6836
6847
  const replacements = uniqueOwners.filter((owner) => {
6837
6848
  const artifact = owner.artifacts.find((candidate) => artifactSelectorKey(candidate) === selector);
6838
6849
  return uniqueOwners.every((other) => {
@@ -6842,7 +6853,7 @@ function detectDirectCollisions(nodes) {
6842
6853
  });
6843
6854
  if (replacements.length === 1) continue;
6844
6855
  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.`
6856
+ `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
6857
  );
6847
6858
  }
6848
6859
  }
@@ -7027,8 +7038,8 @@ import { rm as rm7 } from "fs/promises";
7027
7038
 
7028
7039
  // src/lifecycle/source-plan.ts
7029
7040
  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";
7041
+ import { mkdir as mkdir14, readdir as readdir9 } from "fs/promises";
7042
+ import { basename as basename18, dirname as dirname17, join as join33, posix as posix2, resolve as resolve16 } from "path";
7032
7043
 
7033
7044
  // src/resolve/graph-diff.ts
7034
7045
  function diffGraphLocks(previous, next) {
@@ -7226,8 +7237,10 @@ async function renderGraphForTarget(graph, targetContext = {}) {
7226
7237
  for (const staged of [...stagedNodes.values()].sort((a, b) => a.rawNode.node.id.localeCompare(b.rawNode.node.id))) {
7227
7238
  const rawNode = staged.rawNode;
7228
7239
  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;
7240
+ const selectionValidatedArtifacts = filterArtifactsBySelection(staged.artifacts, rawNode.node.selected);
7241
+ const preRuntimeArtifacts = targetContext.noDeps ? selectionValidatedArtifacts : staged.artifacts;
7242
+ const runtimeArtifacts = targetContext.adapter ? filterArtifactsByRuntime2(preRuntimeArtifacts, targetContext.adapter.name, runtimeSelectedSet) : preRuntimeArtifacts;
7243
+ const compositionArtifacts = runtimeArtifacts;
7231
7244
  const expandedArtifacts = await expandMarkdownIncludes(compositionArtifacts, staged.root, {
7232
7245
  nodeId: rawNode.node.id,
7233
7246
  originNodeId: rawNode.node.id,
@@ -7275,7 +7288,12 @@ async function renderGraphForTarget(graph, targetContext = {}) {
7275
7288
  };
7276
7289
  }
7277
7290
  });
7278
- const selectedArtifacts = filterArtifactsBySelection(expandedArtifacts, rawNode.node.selected);
7291
+ const selectedArtifacts = filterArtifactsBySelection(
7292
+ expandedArtifacts,
7293
+ rawNode.node.selected,
7294
+ void 0,
7295
+ { validationArtifacts: staged.artifacts }
7296
+ );
7279
7297
  const claudeRenderedArtifacts = await renderClaudeSubagents(selectedArtifacts, staged.root, targetContext.adapter);
7280
7298
  const codexRenderedArtifacts = await renderCodexSubagents(claudeRenderedArtifacts, staged.root, targetContext.adapter);
7281
7299
  const openClawRenderedArtifacts = await renderOpenClawSubagents(codexRenderedArtifacts, staged.root, targetContext.adapter);
@@ -7389,8 +7407,9 @@ function sha2563(content) {
7389
7407
  return createHash7("sha256").update(content).digest("hex");
7390
7408
  }
7391
7409
  function assignInstallNames(graph, artifacts) {
7392
- const superseded = supersededLogicalSelectors(artifacts);
7393
- const effectiveArtifacts = artifacts.filter((artifact) => !superseded.has(artifact.logicalSelector));
7410
+ const equivalentArtifacts = coalesceEquivalentDirectArtifacts(artifacts);
7411
+ const superseded = supersededLogicalSelectors(equivalentArtifacts);
7412
+ const effectiveArtifacts = equivalentArtifacts.filter((artifact) => !superseded.has(artifact.logicalSelector));
7394
7413
  const aliases = workspaceAliases(graph);
7395
7414
  validateAliasScopes(graph, effectiveArtifacts, aliases);
7396
7415
  const decisions = /* @__PURE__ */ new Map();
@@ -7433,6 +7452,37 @@ function assignInstallNames(graph, artifacts) {
7433
7452
  }));
7434
7453
  return { artifacts: out, namespacing: [...decisions.values()], overrides: finalOverrides };
7435
7454
  }
7455
+ function coalesceEquivalentDirectArtifacts(artifacts) {
7456
+ const equivalent = /* @__PURE__ */ new Map();
7457
+ const passthrough = [];
7458
+ for (const artifact of artifacts) {
7459
+ if (artifact.dependencyRole !== "direct") {
7460
+ passthrough.push(artifact);
7461
+ continue;
7462
+ }
7463
+ const key = `${artifact.type}\0${artifact.name}\0${artifact.hash}`;
7464
+ const group = equivalent.get(key) ?? [];
7465
+ group.push(artifact);
7466
+ equivalent.set(key, group);
7467
+ }
7468
+ for (const group of equivalent.values()) {
7469
+ const sorted = [...group].sort((left, right) => left.logicalSelector.localeCompare(right.logicalSelector));
7470
+ const canonical = sorted[0];
7471
+ passthrough.push({
7472
+ ...canonical,
7473
+ owners: [...new Set(sorted.flatMap((artifact) => artifact.owners))].sort((left, right) => left.localeCompare(right)),
7474
+ composedFrom: mergeComposedFrom(sorted)
7475
+ });
7476
+ }
7477
+ return passthrough;
7478
+ }
7479
+ function mergeComposedFrom(artifacts) {
7480
+ const entries = /* @__PURE__ */ new Map();
7481
+ for (const entry of artifacts.flatMap((artifact) => artifact.composedFrom ?? [])) {
7482
+ entries.set(`${entry.selector}\0${entry.hash}`, entry);
7483
+ }
7484
+ return entries.size > 0 ? [...entries.values()].sort((left, right) => `${left.selector}\0${left.hash}`.localeCompare(`${right.selector}\0${right.hash}`)) : void 0;
7485
+ }
7436
7486
  function supersededLogicalSelectors(artifacts) {
7437
7487
  const removed = /* @__PURE__ */ new Set();
7438
7488
  for (const replacement of artifacts) {
@@ -7595,7 +7645,8 @@ function reachableNodeIds(graph, rootNodeId) {
7595
7645
  return reachable;
7596
7646
  }
7597
7647
  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 ")}`);
7648
+ const details = group.map((artifact) => `${artifact.logicalSelector}=sha256:${artifact.hash}`).sort().join("; ");
7649
+ return new Error(`Install name collision for ${group[0]?.type}/${group[0]?.installName}. Artifact content hashes: ${details}`);
7599
7650
  }
7600
7651
  function namespaceDecision(artifact, reason) {
7601
7652
  return {
@@ -8029,11 +8080,71 @@ function containsPath(parent, candidate) {
8029
8080
  return path === "" || !path.startsWith("..") && !isAbsolute2(path);
8030
8081
  }
8031
8082
 
8083
+ // src/model/target-state.ts
8084
+ import { posix, resolve as resolve15 } from "path";
8085
+ var targetEvolutionFields = /* @__PURE__ */ new Set([
8086
+ "adapterCodeHash",
8087
+ "adapterConfig",
8088
+ "adapterModule",
8089
+ "targetRoot",
8090
+ "transportDescription",
8091
+ "ssh"
8092
+ ]);
8093
+ function resolveTargetStateIdentity(options) {
8094
+ return {
8095
+ targetFingerprint: computeTargetFingerprint(options.targetFingerprintParts),
8096
+ stateFingerprint: computeTargetFingerprint({
8097
+ identityVersion: 1,
8098
+ contributionScope: {
8099
+ workspaceRoot: resolve15(options.workspaceRoot),
8100
+ targetKey: options.targetKey
8101
+ },
8102
+ target: {
8103
+ ...persistentTargetParts(options.targetFingerprintParts),
8104
+ installRoot: normalizeInstallRoot(options.resolvedInstallRoot, options.transportKind),
8105
+ endpoint: targetEndpoint(options.targetFingerprintParts, options.transportKind)
8106
+ }
8107
+ })
8108
+ };
8109
+ }
8110
+ function persistentTargetParts(value) {
8111
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { value };
8112
+ return Object.fromEntries(
8113
+ Object.entries(value).filter(([key]) => !targetEvolutionFields.has(key))
8114
+ );
8115
+ }
8116
+ function normalizeInstallRoot(path, transportKind) {
8117
+ return transportKind === "ssh" ? posix.normalize(path) : resolve15(path);
8118
+ }
8119
+ function targetEndpoint(value, transportKind) {
8120
+ if (transportKind !== "ssh") return { kind: "local" };
8121
+ const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
8122
+ const ssh = record.ssh && typeof record.ssh === "object" && !Array.isArray(record.ssh) ? record.ssh : {};
8123
+ if (typeof ssh.host !== "string" || ssh.host.trim().length === 0) {
8124
+ throw new Error("SSH endpoint host is required for target state identity; transport description is not an identity source.");
8125
+ }
8126
+ if (ssh.user !== void 0 && (typeof ssh.user !== "string" || ssh.user.trim().length === 0)) {
8127
+ throw new Error("SSH target state identity requires a non-empty endpoint user when one is provided.");
8128
+ }
8129
+ if (ssh.port !== void 0 && (!Number.isInteger(ssh.port) || ssh.port <= 0)) {
8130
+ throw new Error("SSH target state identity requires a positive integer endpoint port.");
8131
+ }
8132
+ return {
8133
+ kind: "ssh",
8134
+ host: ssh.host.trim(),
8135
+ user: ssh.user,
8136
+ port: ssh.port ?? 22
8137
+ };
8138
+ }
8139
+
8032
8140
  // src/lifecycle/source-plan.ts
8033
8141
  async function createGraphSourcePlan(options) {
8034
8142
  if (options.roots.length === 0) {
8035
8143
  throw new Error("At least one source is required for a graph plan.");
8036
8144
  }
8145
+ if (options.freshGraphOnly && options.readOnly !== true) {
8146
+ throw new Error("Fresh graph recovery is read-only and cannot be used for install planning.");
8147
+ }
8037
8148
  const workspaceRoot = options.workspaceRoot ?? options.targetRoot;
8038
8149
  const transport = options.transport ?? localTransport;
8039
8150
  const warnings = [];
@@ -8042,22 +8153,62 @@ async function createGraphSourcePlan(options) {
8042
8153
  options.warn?.(message);
8043
8154
  };
8044
8155
  const installationType = options.installationType ?? resolveInstallationTypeForAdapterTarget(options.adapter);
8045
- const targetFingerprint = computeTargetFingerprint(options.targetFingerprintParts ?? {
8156
+ const targetFingerprintParts2 = options.targetFingerprintParts ?? {
8046
8157
  adapter: options.adapter.name,
8047
8158
  fleetId: options.fleetId,
8048
8159
  installationType,
8049
8160
  targetRoot: options.targetRoot,
8050
8161
  transport: transport.kind,
8051
8162
  transportDescription: transport.description
8163
+ };
8164
+ const installRoot = installRootForAdapterInstallationType(options.adapter, options.targetRoot, installationType, transport.kind === "ssh");
8165
+ const targetIdentity = resolveTargetStateIdentity({
8166
+ targetFingerprintParts: targetFingerprintParts2,
8167
+ workspaceRoot,
8168
+ targetKey: options.targetKey ?? "default",
8169
+ resolvedInstallRoot: installRoot,
8170
+ transportKind: transport.kind
8052
8171
  });
8172
+ const targetFingerprint = targetIdentity.targetFingerprint;
8053
8173
  const stateKey = stateKeyFor(options.adapter.name, {
8054
8174
  installationType,
8055
8175
  stateKey: options.stateKey,
8056
- targetFingerprint,
8176
+ targetFingerprint: targetIdentity.stateFingerprint,
8057
8177
  fleetId: options.fleetId
8058
8178
  });
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 });
8179
+ const graphLockPath = pathForGraphLock(
8180
+ workspaceRoot,
8181
+ options.targetKey ?? "default",
8182
+ options.adapter.name,
8183
+ targetIdentity.stateFingerprint
8184
+ );
8185
+ const ownership = await resolveWorkspaceOwnershipScope(workspaceRoot, {
8186
+ fleetId: options.fleetId,
8187
+ globalRoot: options.globalRoot
8188
+ });
8189
+ const workspaceOwner = workspaceOwnerForRoot(ownership.root, ownership.fleetId);
8190
+ let recoveredPendingApply = false;
8191
+ if (options.readOnly !== true) {
8192
+ const pendingJournal = await discoverTargetApplyJournal({
8193
+ adapter: options.adapter.name,
8194
+ installationType,
8195
+ stateKey,
8196
+ explicitStateKey: options.stateKey,
8197
+ fleetId: options.fleetId,
8198
+ installRoot,
8199
+ workspaceOwner,
8200
+ graphLockPath,
8201
+ transport
8202
+ });
8203
+ if (pendingJournal) {
8204
+ recoveredPendingApply = await recoverPendingApplyIfSafe(
8205
+ installRoot,
8206
+ options.adapter.name,
8207
+ transport,
8208
+ { installationType, stateKey: pendingJournal.stateKey }
8209
+ );
8210
+ }
8211
+ }
8061
8212
  const workspaceConfig = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: options.globalRoot });
8062
8213
  const trustPolicy = {
8063
8214
  ...normalizeTrustPolicy(workspaceConfig.trust),
@@ -8065,12 +8216,33 @@ async function createGraphSourcePlan(options) {
8065
8216
  };
8066
8217
  const lockMode = options.frozenLock === true || options.offline === true;
8067
8218
  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);
8070
- const registryClient = new RegistryClient({ workspaceRoot, offline: lockMode, offlineLabel: lockLabel, warn });
8219
+ const readPriorState = async () => {
8220
+ const [stableManifest, stableLock2, stableSourceLock2] = await Promise.all([
8221
+ readInstallManifest(installRoot, options.adapter.name, transport, { installationType, stateKey }),
8222
+ readExistingGraphLock(graphLockPath),
8223
+ readSourceLock(installRoot, options.adapter.name, transport, { installationType, stateKey })
8224
+ ]);
8225
+ const priorState2 = await resolvePriorTargetState({
8226
+ adapter: options.adapter.name,
8227
+ installationType,
8228
+ stateKey,
8229
+ explicitStateKey: options.stateKey,
8230
+ fleetId: options.fleetId,
8231
+ installRoot,
8232
+ workspaceOwner,
8233
+ graphLockPath,
8234
+ stableManifest,
8235
+ stableLock: stableLock2,
8236
+ transport
8237
+ });
8238
+ return { priorState: priorState2, stableLock: stableLock2, stableSourceLock: stableSourceLock2 };
8239
+ };
8240
+ const { priorState, stableLock, stableSourceLock } = options.freshGraphOnly ? { priorState: {}, stableLock: void 0, stableSourceLock: void 0 } : await readPriorState();
8241
+ const previousLock = priorState.graphLock;
8242
+ const registryClient = new RegistryClient({ workspaceRoot, cachePath: options.registryCachePath, offline: lockMode, offlineLabel: lockLabel, warn });
8071
8243
  const graph = await resolveDependencyGraph(options.roots, {
8072
8244
  workspaceRoot,
8073
- cacheRoot: join33(workspaceRoot, ".agentwheel", "cache"),
8245
+ cacheRoot: options.cacheRoot ?? join33(workspaceRoot, ".agentwheel", "cache"),
8074
8246
  registryClient,
8075
8247
  noDeps: options.noDeps,
8076
8248
  includeSuggestions: options.includeSuggestions,
@@ -8107,13 +8279,13 @@ async function createGraphSourcePlan(options) {
8107
8279
  const resolvedInstallRoot = installRootForArtifacts(options.adapter, options.targetRoot, resolvedInstallationType, desiredArtifacts.map((artifact) => artifact.type), transport.kind === "ssh");
8108
8280
  const graphLockDigest = digestGraphLock(bundle.graphLock);
8109
8281
  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(
8282
+ if (normalizeInstallRoot2(resolvedInstallRoot, transport.kind) !== normalizeInstallRoot2(installRoot, transport.kind)) {
8283
+ throw new Error(
8284
+ `Adapter ${options.adapter.name} resolved a different install root after rendering; target state identity cannot be proven.`
8285
+ );
8286
+ }
8287
+ const manifest = priorState.manifest;
8288
+ const basePlan = options.retireExactMcp ? await createExactMcpRetirementPlan(
8117
8289
  desiredArtifacts,
8118
8290
  options.adapter,
8119
8291
  options.targetRoot,
@@ -8135,10 +8307,18 @@ async function createGraphSourcePlan(options) {
8135
8307
  forceDrift: options.forceDrift,
8136
8308
  forceConflict: options.forceConflict,
8137
8309
  replaceConflict: options.replaceConflict,
8310
+ stateMigration: priorState.migration,
8138
8311
  warn
8139
8312
  });
8313
+ const plan = priorState.migration && !basePlan.stateMigration ? { ...basePlan, stateMigration: priorState.migration } : basePlan;
8314
+ plan.targetStateFilePreconditions = {
8315
+ graphLockPath,
8316
+ graphLockRevision: stableLock ? digestGraphLock(stableLock) : null,
8317
+ sourceLockRevision: computeSourceLockRevision(stableSourceLock)
8318
+ };
8319
+ let foreignStateObservations = [];
8140
8320
  if (options.forceForeignState !== true && options.deferForeignStateCheck !== true) {
8141
- await assertNoForeignWorkspaceState({
8321
+ foreignStateObservations = await assertNoForeignWorkspaceState({
8142
8322
  installRoot: resolvedInstallRoot,
8143
8323
  adapter: options.adapter.name,
8144
8324
  transport,
@@ -8146,9 +8326,11 @@ async function createGraphSourcePlan(options) {
8146
8326
  workspaceOwner,
8147
8327
  globalRoot: options.globalRoot,
8148
8328
  stateKey,
8329
+ migratingStateKey: priorState.migration?.fromStateKey,
8149
8330
  plannedPaths: plan.operations.map((operation) => operation.relativeDestPath),
8150
8331
  plannedOperations: plan.operations
8151
8332
  });
8333
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8152
8334
  }
8153
8335
  return {
8154
8336
  plan,
@@ -8161,7 +8343,10 @@ async function createGraphSourcePlan(options) {
8161
8343
  warnings,
8162
8344
  newTransitiveSources: trustEvaluation.promptSources,
8163
8345
  graphDiff,
8164
- recoveredPendingApply
8346
+ recoveredPendingApply,
8347
+ previousManifest: priorState.manifest,
8348
+ previousGraphLock: priorState.graphLock,
8349
+ foreignStateObservations
8165
8350
  };
8166
8351
  }
8167
8352
  async function assertNoForeignWorkspaceStateForPlan(plan, options) {
@@ -8174,12 +8359,15 @@ async function assertNoForeignWorkspaceStateForPlan(plan, options) {
8174
8359
  workspaceOwner: options.workspaceOwner,
8175
8360
  globalRoot: options.globalRoot,
8176
8361
  stateKey: plan.stateKey,
8362
+ migratingStateKey: plan.stateMigration?.fromStateKey,
8177
8363
  plannedPaths: options.plannedPaths ?? plan.operations.map((operation) => operation.relativeDestPath),
8178
8364
  plannedOperations: plan.operations
8179
8365
  });
8366
+ plan.hasBlockingChanges = plan.operations.some((operation) => operation.action === "drift" || operation.action === "conflict");
8180
8367
  }
8181
- function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2) {
8182
- return pathForGraphLock(workspaceRoot, targetKey2, adapter, computeTargetFingerprint(targetFingerprintParts2));
8368
+ function graphLockPathForTarget(workspaceRoot, targetKey2, adapter, targetFingerprintParts2, context) {
8369
+ const identity = resolveTargetStateIdentity({ targetFingerprintParts: targetFingerprintParts2, workspaceRoot, targetKey: targetKey2, ...context });
8370
+ return pathForGraphLock(workspaceRoot, targetKey2, adapter, identity.stateFingerprint);
8183
8371
  }
8184
8372
  function desiredArtifactsFromGraphBundle(bundle) {
8185
8373
  return bundle.artifacts.map((artifact) => desiredArtifactFromResolved(artifact));
@@ -8221,10 +8409,278 @@ async function readExistingGraphLock(path) {
8221
8409
  if (!await pathExists(path)) return void 0;
8222
8410
  return readGraphLock(path);
8223
8411
  }
8412
+ async function discoverTargetApplyJournal(options) {
8413
+ const journals = await listApplyJournals(
8414
+ options.installRoot,
8415
+ options.adapter,
8416
+ options.transport,
8417
+ { installationType: options.installationType }
8418
+ );
8419
+ const correlated = [];
8420
+ for (const item of journals) {
8421
+ const journal = item.journal;
8422
+ const journalStateKey = journal.stateKey ?? stateKeyFor(options.adapter, { installationType: options.installationType });
8423
+ if (normalizeInstallRoot2(journal.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8424
+ continue;
8425
+ }
8426
+ if (journalStateKey === options.stateKey) {
8427
+ validateJournalManifestEnvelope(journal.manifest, options, journalStateKey, item.path);
8428
+ correlated.push({ path: item.path, stateKey: journalStateKey });
8429
+ continue;
8430
+ }
8431
+ if (!journal.graphLockPath || !journal.graphLock || !journal.graphLockDigest) continue;
8432
+ if (resolve16(dirname17(journal.graphLockPath)) !== resolve16(dirname17(options.graphLockPath))) continue;
8433
+ if (options.transport.kind === "ssh") {
8434
+ throw new Error(
8435
+ `Legacy SSH apply journal at ${item.path} does not record its original endpoint; Agentwheel cannot prove endpoint identity, so the journal was preserved.`
8436
+ );
8437
+ }
8438
+ const suffix = ".graph-lock.json";
8439
+ const graphName = basename18(journal.graphLockPath);
8440
+ if (!graphName.endsWith(suffix)) {
8441
+ throw new Error(`Legacy apply journal at ${item.path} has an invalid graph-lock path.`);
8442
+ }
8443
+ const fingerprint = graphName.slice(0, -suffix.length);
8444
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint) || journal.graphLock.canonical.targetFingerprint !== fingerprint) {
8445
+ throw new Error(`Legacy apply journal graph-lock fingerprint does not match its path at ${item.path}.`);
8446
+ }
8447
+ const expectedStateKey = stateKeyFor(options.adapter, {
8448
+ installationType: options.installationType,
8449
+ stateKey: options.explicitStateKey,
8450
+ targetFingerprint: fingerprint,
8451
+ fleetId: options.fleetId
8452
+ });
8453
+ if (journalStateKey !== expectedStateKey) {
8454
+ throw new Error(`Legacy apply journal state key does not match its graph lock at ${item.path}.`);
8455
+ }
8456
+ let actualGraphLock;
8457
+ try {
8458
+ actualGraphLock = await readGraphLock(journal.graphLockPath);
8459
+ } catch (error) {
8460
+ throw new Error(
8461
+ `Legacy apply journal graph lock is missing or invalid at ${journal.graphLockPath}: ` + (error instanceof Error ? error.message : String(error))
8462
+ );
8463
+ }
8464
+ const embeddedDigest = digestGraphLock(journal.graphLock);
8465
+ const actualDigest = digestGraphLock(actualGraphLock);
8466
+ if (embeddedDigest !== journal.graphLockDigest || actualDigest !== journal.graphLockDigest) {
8467
+ throw new Error(`Legacy apply journal graph-lock digest mismatch at ${item.path}.`);
8468
+ }
8469
+ const manifest = validateJournalManifestEnvelope(journal.manifest, options, journalStateKey, item.path);
8470
+ validateLegacyManifestCandidate(manifest, {
8471
+ ...options,
8472
+ candidateStateKey: journalStateKey,
8473
+ graphLockDigest: journal.graphLockDigest,
8474
+ graphLockPath: journal.graphLockPath
8475
+ });
8476
+ correlated.push({ path: item.path, stateKey: journalStateKey });
8477
+ }
8478
+ if (correlated.length > 1) {
8479
+ throw new Error(`Ambiguous apply journals for ${options.adapter}: multiple journals match the requested target state.`);
8480
+ }
8481
+ return correlated[0];
8482
+ }
8483
+ function validateJournalManifestEnvelope(value, options, stateKey, journalPath) {
8484
+ let manifest;
8485
+ try {
8486
+ manifest = installManifestV2Schema.parse(value);
8487
+ } catch (error) {
8488
+ throw new Error(
8489
+ `Apply journal at ${journalPath} has an invalid install manifest: ` + (error instanceof Error ? error.message : String(error))
8490
+ );
8491
+ }
8492
+ if (manifest.adapter !== options.adapter || manifest.installationType !== options.installationType || manifest.stateKey !== stateKey || normalizeInstallRoot2(manifest.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8493
+ throw new Error(`Apply journal target identity does not match its manifest at ${journalPath}.`);
8494
+ }
8495
+ return manifest;
8496
+ }
8497
+ async function resolvePriorTargetState(options) {
8498
+ const candidates = await discoverLegacyStateCandidates(options);
8499
+ const correlated = candidates.filter((candidate) => candidate.manifest);
8500
+ if (options.stableManifest) {
8501
+ for (const candidate of correlated) {
8502
+ if (!sameManifestContributions(options.stableManifest, candidate.manifest)) {
8503
+ throw new Error(
8504
+ `Stable and legacy target states coexist with disagreeing contributions for ${options.adapter}; reconcile them explicitly.`
8505
+ );
8506
+ }
8507
+ }
8508
+ const sameKeyCandidates = correlated.filter((candidate) => candidate.stateKey === options.stateKey);
8509
+ if (!options.stableLock && sameKeyCandidates.length > 1) {
8510
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple correlated graph locks use the explicit state key.`);
8511
+ }
8512
+ if (!options.stableLock && sameKeyCandidates.length === 1) {
8513
+ const candidate = sameKeyCandidates[0];
8514
+ return {
8515
+ graphLock: candidate.graphLock,
8516
+ manifest: options.stableManifest,
8517
+ migration: migrationFromCandidate(candidate)
8518
+ };
8519
+ }
8520
+ return { graphLock: options.stableLock, manifest: options.stableManifest };
8521
+ }
8522
+ if (options.stableLock && correlated.length > 0) {
8523
+ throw new Error(
8524
+ `Stable graph state and legacy manifest state coexist for ${options.adapter} without a stable manifest; reconcile them explicitly.`
8525
+ );
8526
+ }
8527
+ if (correlated.length > 1) {
8528
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple correlated manifests were found.`);
8529
+ }
8530
+ if (correlated.length === 1) {
8531
+ const candidate = correlated[0];
8532
+ return {
8533
+ graphLock: candidate.graphLock,
8534
+ manifest: candidate.manifest,
8535
+ migration: migrationFromCandidate(candidate)
8536
+ };
8537
+ }
8538
+ if (options.stableLock) return { graphLock: options.stableLock };
8539
+ if (candidates.length > 1) {
8540
+ throw new Error(`Ambiguous legacy target state for ${options.adapter}: multiple graph-only candidates were found.`);
8541
+ }
8542
+ if (candidates.length === 1) {
8543
+ throw new Error(
8544
+ `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.`
8545
+ );
8546
+ }
8547
+ return {};
8548
+ }
8549
+ async function discoverLegacyStateCandidates(options) {
8550
+ const directory = dirname17(options.graphLockPath);
8551
+ let names;
8552
+ try {
8553
+ names = await readdir9(directory);
8554
+ } catch (error) {
8555
+ if (error.code === "ENOENT") return [];
8556
+ throw error;
8557
+ }
8558
+ const stableName = basename18(options.graphLockPath);
8559
+ const suffix = ".graph-lock.json";
8560
+ const candidates = [];
8561
+ for (const name of names.sort((left, right) => left.localeCompare(right))) {
8562
+ if (name === stableName || !name.endsWith(suffix)) continue;
8563
+ const fingerprint = name.slice(0, -suffix.length);
8564
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint)) continue;
8565
+ const graphLockPath = join33(directory, name);
8566
+ let graphLock;
8567
+ try {
8568
+ graphLock = await readGraphLock(graphLockPath);
8569
+ } catch (error) {
8570
+ throw new Error(
8571
+ `Legacy graph lock at ${graphLockPath} is invalid or unsupported: ` + (error instanceof Error ? error.message : String(error))
8572
+ );
8573
+ }
8574
+ if (graphLock.canonical.targetFingerprint !== fingerprint) {
8575
+ throw new Error(
8576
+ `Legacy graph lock fingerprint does not match its filename at ${graphLockPath}.`
8577
+ );
8578
+ }
8579
+ if (options.transport.kind === "ssh") {
8580
+ throw new Error(
8581
+ `Legacy SSH target state at ${graphLockPath} does not record its original endpoint; Agentwheel cannot prove endpoint identity, so the graph lock was preserved.`
8582
+ );
8583
+ }
8584
+ const graphLockDigest = digestGraphLock(graphLock);
8585
+ const candidateStateKey = stateKeyFor(options.adapter, {
8586
+ installationType: options.installationType,
8587
+ stateKey: options.explicitStateKey,
8588
+ targetFingerprint: fingerprint,
8589
+ fleetId: options.fleetId
8590
+ });
8591
+ let manifest;
8592
+ let sourceLock;
8593
+ try {
8594
+ manifest = await readInstallManifest(options.installRoot, options.adapter, options.transport, {
8595
+ installationType: options.installationType,
8596
+ stateKey: candidateStateKey
8597
+ });
8598
+ sourceLock = await readSourceLock(options.installRoot, options.adapter, options.transport, {
8599
+ installationType: options.installationType,
8600
+ stateKey: candidateStateKey
8601
+ });
8602
+ } catch (error) {
8603
+ throw new Error(
8604
+ `Legacy target state for ${graphLockPath} is invalid: ` + (error instanceof Error ? error.message : String(error))
8605
+ );
8606
+ }
8607
+ if (!manifest) {
8608
+ if (sourceLock) {
8609
+ throw new Error(`Legacy source lock for ${graphLockPath} has no correlated install manifest.`);
8610
+ }
8611
+ candidates.push({ stateKey: candidateStateKey, graphLockPath, graphLockDigest, graphLock });
8612
+ continue;
8613
+ }
8614
+ const validated = validateLegacyManifestCandidate(manifest, {
8615
+ ...options,
8616
+ candidateStateKey,
8617
+ graphLockDigest,
8618
+ graphLockPath
8619
+ });
8620
+ candidates.push({
8621
+ stateKey: candidateStateKey,
8622
+ graphLockPath,
8623
+ graphLockDigest,
8624
+ graphLock,
8625
+ manifest: validated,
8626
+ sourceLock
8627
+ });
8628
+ }
8629
+ return candidates;
8630
+ }
8631
+ function validateLegacyManifestCandidate(manifest, options) {
8632
+ if (manifest.version !== 2) {
8633
+ throw new Error(`Legacy target state for ${options.graphLockPath} requires an install manifest v2.`);
8634
+ }
8635
+ if (manifest.adapter !== options.adapter) {
8636
+ throw new Error(`Legacy target state adapter mismatch at ${options.graphLockPath}.`);
8637
+ }
8638
+ if (manifest.installationType !== options.installationType) {
8639
+ throw new Error(`Legacy target state installation type mismatch at ${options.graphLockPath}.`);
8640
+ }
8641
+ if (manifest.stateKey !== options.candidateStateKey) {
8642
+ throw new Error(`Legacy target state key mismatch at ${options.graphLockPath}.`);
8643
+ }
8644
+ if (normalizeInstallRoot2(manifest.targetRoot, options.transport.kind) !== normalizeInstallRoot2(options.installRoot, options.transport.kind)) {
8645
+ throw new Error(`Legacy target state target root mismatch at ${options.graphLockPath}.`);
8646
+ }
8647
+ const correlated = manifest.entries.filter((entry) => entry.graphLockDigest === options.graphLockDigest);
8648
+ if (correlated.length === 0) {
8649
+ throw new Error(`Legacy install manifest graph-lock digest does not match ${options.graphLockPath}.`);
8650
+ }
8651
+ if (correlated.some((entry) => entry.workspaceOwner !== options.workspaceOwner)) {
8652
+ throw new Error(`Legacy target state contribution scope does not match ${options.graphLockPath}.`);
8653
+ }
8654
+ return manifest;
8655
+ }
8656
+ function migrationFromCandidate(candidate) {
8657
+ return {
8658
+ fromStateKey: candidate.manifest ? candidate.stateKey : void 0,
8659
+ fromGraphLockPath: candidate.graphLockPath,
8660
+ fromGraphLockDigest: candidate.graphLockDigest,
8661
+ sourceLock: candidate.manifest ? candidate.sourceLock ?? null : void 0
8662
+ };
8663
+ }
8664
+ function sameManifestContributions(left, right) {
8665
+ if (left.version !== 2) return false;
8666
+ return stableJson2(manifestContributionState(left)) === stableJson2(manifestContributionState(right));
8667
+ }
8668
+ function manifestContributionState(manifest) {
8669
+ return manifest.entries.map(({ updatedAt: _updatedAt, graphLockDigest: _graphLockDigest, ...entry }) => entry).sort((left, right) => left.path.localeCompare(right.path));
8670
+ }
8671
+ function stableJson2(value) {
8672
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
8673
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
8674
+ const record = value;
8675
+ 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(",")}}`;
8676
+ }
8677
+ function normalizeInstallRoot2(path, transportKind) {
8678
+ return transportKind === "ssh" ? posix2.normalize(path) : resolve16(path);
8679
+ }
8224
8680
  var workspaceOwnerPrefix2 = "workspace-root:";
8225
8681
  async function assertNoForeignWorkspaceState(check) {
8226
8682
  const planned = new Set(check.plannedPaths);
8227
- if (planned.size === 0) return;
8683
+ if (planned.size === 0) return [];
8228
8684
  const plannedOperations = /* @__PURE__ */ new Map();
8229
8685
  for (const operation of check.plannedOperations) {
8230
8686
  if (!planned.has(operation.relativeDestPath)) continue;
@@ -8233,18 +8689,22 @@ async function assertNoForeignWorkspaceState(check) {
8233
8689
  plannedOperations.set(operation.relativeDestPath, operations);
8234
8690
  }
8235
8691
  const manifests = await listInstallManifests(check.installRoot, check.adapter, check.transport);
8236
- const root = resolve15(check.workspaceRoot);
8692
+ const root = resolve16(check.workspaceRoot);
8237
8693
  const ownsSubWorkspaces = root !== globalConfigRoot(check.globalRoot);
8238
- const foreign = [];
8694
+ const collisions = /* @__PURE__ */ new Map();
8239
8695
  for (const entry of manifests) {
8240
- if (entry.stateKey === check.stateKey) continue;
8241
- const byOwner = /* @__PURE__ */ new Map();
8696
+ if (entry.stateKey === check.stateKey || entry.stateKey === check.migratingStateKey) continue;
8697
+ const entryCounts = /* @__PURE__ */ new Map();
8698
+ for (const manifestEntry of entry.manifest.entries) {
8699
+ const owner = "workspaceOwner" in manifestEntry ? manifestEntry.workspaceOwner : void 0;
8700
+ if (typeof owner === "string" && owner.startsWith(workspaceOwnerPrefix2)) {
8701
+ entryCounts.set(owner, (entryCounts.get(owner) ?? 0) + 1);
8702
+ }
8703
+ }
8242
8704
  for (const manifestEntry of entry.manifest.entries) {
8243
8705
  const owner = "workspaceOwner" in manifestEntry ? manifestEntry.workspaceOwner : void 0;
8244
8706
  if (typeof owner !== "string" || !owner.startsWith(workspaceOwnerPrefix2)) continue;
8245
8707
  if (ownedByWorkspace(owner, check.workspaceOwner, root, ownsSubWorkspaces)) continue;
8246
- const bucket = byOwner.get(owner) ?? { entryCount: 0, collidingPaths: [] };
8247
- bucket.entryCount += 1;
8248
8708
  if (planned.has(manifestEntry.path) && !await isDisjointVerifiedMergeContribution(
8249
8709
  check,
8250
8710
  manifestEntry.path,
@@ -8252,16 +8712,46 @@ async function assertNoForeignWorkspaceState(check) {
8252
8712
  manifestEntry.mergeRemoval,
8253
8713
  plannedOperations.get(manifestEntry.path) ?? []
8254
8714
  )) {
8255
- bucket.collidingPaths.push(manifestEntry.path);
8715
+ const ownedEntry = manifestEntry;
8716
+ const pathCollisions = collisions.get(manifestEntry.path) ?? [];
8717
+ pathCollisions.push({
8718
+ owner,
8719
+ stateKey: entry.stateKey,
8720
+ fileName: entry.fileName,
8721
+ manifestRevision: entry.manifest.revision,
8722
+ entryCount: entryCounts.get(owner) ?? 1,
8723
+ entry: ownedEntry
8724
+ });
8725
+ collisions.set(manifestEntry.path, pathCollisions);
8256
8726
  }
8257
- byOwner.set(owner, bucket);
8258
8727
  }
8259
- for (const [owner, bucket] of byOwner) {
8260
- if (bucket.collidingPaths.length === 0) continue;
8261
- foreign.push({ owner, fileName: entry.fileName, ...bucket });
8728
+ }
8729
+ const foreignByOwner = /* @__PURE__ */ new Map();
8730
+ const observations = [];
8731
+ for (const [path, pathCollisions] of collisions) {
8732
+ const operations = plannedOperations.get(path) ?? [];
8733
+ const exact = pathCollisions.length === 1 ? await exactForeignKeepOperation(check, pathCollisions[0], operations) : void 0;
8734
+ if (exact) {
8735
+ const index = check.plannedOperations.indexOf(operations[0]);
8736
+ if (index < 0) throw new Error(`Foreign-state reconciliation lost planned operation for ${path}.`);
8737
+ check.plannedOperations[index] = exact.operation;
8738
+ observations.push(exact.observation);
8739
+ continue;
8740
+ }
8741
+ for (const collision of pathCollisions) {
8742
+ const key = `${collision.owner}\0${collision.fileName}`;
8743
+ const bucket = foreignByOwner.get(key) ?? {
8744
+ owner: collision.owner,
8745
+ fileName: collision.fileName,
8746
+ entryCount: collision.entryCount,
8747
+ collidingPaths: []
8748
+ };
8749
+ if (!bucket.collidingPaths.includes(path)) bucket.collidingPaths.push(path);
8750
+ foreignByOwner.set(key, bucket);
8262
8751
  }
8263
8752
  }
8264
- if (foreign.length === 0) return;
8753
+ const foreign = [...foreignByOwner.values()];
8754
+ if (foreign.length === 0) return observations.sort((left, right) => left.path.localeCompare(right.path));
8265
8755
  throw new Error([
8266
8756
  `Refusing to plan ${check.adapter} at ${check.installRoot}.`,
8267
8757
  "This runtime root already carries Agentwheel state owned by another workspace, at paths this run would install:",
@@ -8275,6 +8765,36 @@ async function assertNoForeignWorkspaceState(check) {
8275
8765
  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
8766
  ].join("\n"));
8277
8767
  }
8768
+ async function exactForeignKeepOperation(check, collision, operations) {
8769
+ if (operations.length !== 1) return void 0;
8770
+ const operation = operations[0];
8771
+ const entry = collision.entry;
8772
+ if (operation.semanticPlugin || operation.programmaticOperation || operation.mergeStrategy || operation.mode || entry.semanticPlugin || entry.mergeStrategy || entry.mode) return void 0;
8773
+ if (!operation.desiredHash || operation.artifactType !== entry.artifactType || operation.artifactName !== entry.artifactName || operation.kind !== entry.kind || operation.desiredHash !== entry.sourceHash) return void 0;
8774
+ const path = join33(check.installRoot, entry.path);
8775
+ if (!await check.transport.pathExists(path)) return void 0;
8776
+ const currentHash = await check.transport.hashPath(path);
8777
+ if (currentHash !== entry.hash || currentHash !== operation.desiredHash || operation.currentHash !== void 0 && operation.currentHash !== currentHash) return void 0;
8778
+ return {
8779
+ operation: {
8780
+ ...operation,
8781
+ action: "keep",
8782
+ currentHash,
8783
+ manifestHash: entry.hash,
8784
+ reason: `exact foreign artifact owned by ${collision.owner}; kept outside workspace ${check.workspaceOwner}`,
8785
+ preserveInManifest: false
8786
+ },
8787
+ observation: {
8788
+ stateKey: collision.stateKey,
8789
+ fileName: collision.fileName,
8790
+ manifestRevision: collision.manifestRevision,
8791
+ workspaceOwner: collision.owner,
8792
+ path: entry.path,
8793
+ graphNodeId: entry.graphNodeId,
8794
+ logicalSelector: entry.logicalSelector
8795
+ }
8796
+ };
8797
+ }
8278
8798
  async function isDisjointVerifiedMergeContribution(check, relativePath, foreignStrategy, foreignRemoval, operations) {
8279
8799
  if (!foreignStrategy || !hasMergeRemovalContent(foreignRemoval) || operations.length !== 1) return false;
8280
8800
  const operation = operations[0];
@@ -8446,7 +8966,7 @@ function targetLabel(target) {
8446
8966
  }
8447
8967
 
8448
8968
  // src/runtime/target.ts
8449
- import { basename as basename18, dirname as dirname18, join as join34, resolve as resolve16 } from "path";
8969
+ import { basename as basename19, dirname as dirname18, join as join34, resolve as resolve17 } from "path";
8450
8970
  var runtimeMarkers = [
8451
8971
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
8452
8972
  { adapter: "claude", dirs: [".claude"] },
@@ -8455,9 +8975,9 @@ var runtimeMarkers = [
8455
8975
  { adapter: "copilot", dirs: [".github"] }
8456
8976
  ];
8457
8977
  async function resolveRuntimeTarget(request = {}) {
8458
- const cwd = resolve16(request.cwd ?? process.cwd());
8978
+ const cwd = resolve17(request.cwd ?? process.cwd());
8459
8979
  if (request.targetRoot) {
8460
- const targetRoot = resolve16(request.targetRoot);
8980
+ const targetRoot = resolve17(request.targetRoot);
8461
8981
  return {
8462
8982
  adapter: request.adapter ?? "openclaw",
8463
8983
  installationType: request.installationType,
@@ -8489,7 +9009,7 @@ async function resolveRuntimeTarget(request = {}) {
8489
9009
  async function resolveAllRuntimeTargets(request = {}) {
8490
9010
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
8491
9011
  if (request.agent) return [await resolveRuntimeTarget(request)];
8492
- const cwd = resolve16(request.cwd ?? process.cwd());
9012
+ const cwd = resolve17(request.cwd ?? process.cwd());
8493
9013
  const workspaceRoot = await findWorkspaceRoot(cwd);
8494
9014
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8495
9015
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot, request.installationType, request.fleetId));
@@ -8499,8 +9019,8 @@ async function resolveAllRuntimeTargets(request = {}) {
8499
9019
  return targets;
8500
9020
  }
8501
9021
  async function resolveProfileRuntimeTargets(request) {
8502
- const cwd = resolve16(request.cwd ?? process.cwd());
8503
- const workspaceRoot = request.targetRoot ? resolve16(request.targetRoot) : await findWorkspaceRoot(cwd);
9022
+ const cwd = resolve17(request.cwd ?? process.cwd());
9023
+ const workspaceRoot = request.targetRoot ? resolve17(request.targetRoot) : await findWorkspaceRoot(cwd);
8504
9024
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
8505
9025
  const profile = config.profiles[request.profile];
8506
9026
  if (!profile) {
@@ -8566,12 +9086,12 @@ async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
8566
9086
  return unique[0];
8567
9087
  }
8568
9088
  async function detectRuntimeTargets(cwd = process.cwd(), adapterFilter) {
8569
- const root = resolve16(cwd);
9089
+ const root = resolve17(cwd);
8570
9090
  const matches = [];
8571
9091
  for (const marker of runtimeMarkers) {
8572
9092
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
8573
9093
  for (const dir of marker.dirs) {
8574
- if (basename18(root) === dir) {
9094
+ if (basename19(root) === dir) {
8575
9095
  matches.push({ adapter: marker.adapter, targetRoot: dirname18(root) });
8576
9096
  } else if (await pathExists(join34(root, dir))) {
8577
9097
  matches.push({ adapter: marker.adapter, targetRoot: root });
@@ -8615,9 +9135,9 @@ function dedupeTargets(matches) {
8615
9135
  return [...byKey.values()];
8616
9136
  }
8617
9137
  function runtimeScanRoot(request) {
8618
- const root = resolve16(request.targetRoot ?? request.cwd ?? process.cwd());
9138
+ const root = resolve17(request.targetRoot ?? request.cwd ?? process.cwd());
8619
9139
  if (request.targetRoot) return root;
8620
- return runtimeMarkers.some((marker) => marker.dirs.includes(basename18(root))) ? dirname18(root) : root;
9140
+ return runtimeMarkers.some((marker) => marker.dirs.includes(basename19(root))) ? dirname18(root) : root;
8621
9141
  }
8622
9142
 
8623
9143
  // src/lifecycle/profile.ts
@@ -9004,9 +9524,9 @@ function normalizeVersion(version) {
9004
9524
 
9005
9525
  // src/model/package-validate.ts
9006
9526
  import { stat as stat12 } from "fs/promises";
9007
- import { resolve as resolve17 } from "path";
9527
+ import { resolve as resolve18 } from "path";
9008
9528
  async function validatePackage(root) {
9009
- const packageRoot = resolve17(root);
9529
+ const packageRoot = resolve18(root);
9010
9530
  const findings = [];
9011
9531
  const manifestPath = await findPackageManifestPath(packageRoot);
9012
9532
  if (!manifestPath) {
@@ -9107,7 +9627,7 @@ async function validateManifestComposeInclude(packageRoot, selector, optional, f
9107
9627
  try {
9108
9628
  validateSelector(selector, "compose.include", findings, manifestPath, { fragmentsOnly: true, aliases });
9109
9629
  if (isCrossPackageSelector(selector)) return;
9110
- const full = resolve17(packageRoot, selector);
9630
+ const full = resolve18(packageRoot, selector);
9111
9631
  if (full !== packageRoot && !full.startsWith(`${packageRoot}/`)) {
9112
9632
  findings.push({ level: "error", message: `Compose include escapes package root: ${selector}`, path: manifestPath });
9113
9633
  return;
@@ -9161,10 +9681,10 @@ function isCrossPackageSelector(value) {
9161
9681
 
9162
9682
  // src/model/package-migrate.ts
9163
9683
  import { readFile as readFile22, rename as rename3, writeFile as writeFile12 } from "fs/promises";
9164
- import { join as join37, resolve as resolve18 } from "path";
9684
+ import { join as join37, resolve as resolve19 } from "path";
9165
9685
  import { applyEdits, modify, parse as parse2 } from "jsonc-parser";
9166
9686
  async function migratePackageManifest(root) {
9167
- const packageRoot = resolve18(root);
9687
+ const packageRoot = resolve19(root);
9168
9688
  for (const name of openPackManifestNames) {
9169
9689
  const path = join37(packageRoot, name);
9170
9690
  if (await pathExists(path)) {
@@ -9226,7 +9746,7 @@ function resolveCliVersion() {
9226
9746
  }
9227
9747
 
9228
9748
  // src/lifecycle/ownership.ts
9229
- import { resolve as resolve19 } from "path";
9749
+ import { resolve as resolve20 } from "path";
9230
9750
  async function planArtifactOwnershipHandoff(request) {
9231
9751
  return validateOwnershipHandoff(request, request.transport ?? localTransport);
9232
9752
  }
@@ -9352,8 +9872,8 @@ function containedArtifactPath(targetRoot, relativePath) {
9352
9872
  if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\0")) {
9353
9873
  throw new Error(`Unsafe managed artifact path: ${relativePath}`);
9354
9874
  }
9355
- const root = resolve19(targetRoot);
9356
- const candidate = resolve19(root, relativePath);
9875
+ const root = resolve20(targetRoot);
9876
+ const candidate = resolve20(root, relativePath);
9357
9877
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
9358
9878
  throw new Error(`Managed artifact path escapes target root: ${relativePath}`);
9359
9879
  }
@@ -9386,7 +9906,7 @@ function assertManifestIdentity(manifest, request) {
9386
9906
 
9387
9907
  // src/lifecycle/ownership-retire-stale.ts
9388
9908
  import { createHash as createHash9 } from "crypto";
9389
- import { resolve as resolve20 } from "path";
9909
+ import { resolve as resolve21 } from "path";
9390
9910
  async function planRetireStaleOwnership(request) {
9391
9911
  return observe(request, request.transport ?? localTransport);
9392
9912
  }
@@ -9519,7 +10039,7 @@ async function observe(request, transport) {
9519
10039
  return { ...withoutDigest, planDigest: digest(withoutDigest) };
9520
10040
  }
9521
10041
  function normalizeRequest(request) {
9522
- const targetRoot = resolve20(request.targetRoot);
10042
+ const targetRoot = resolve21(request.targetRoot);
9523
10043
  const sourceStateKey = exactStateKey(request.adapter, request.sourceStateKey, request.installationType, "source");
9524
10044
  const destinationStateKey = exactStateKey(request.adapter, request.destinationStateKey, request.installationType, "destination");
9525
10045
  if (sourceStateKey === destinationStateKey) throw new Error("Source and destination state keys must differ.");
@@ -9530,8 +10050,8 @@ function normalizeRequest(request) {
9530
10050
  installationType: request.installationType.trim(),
9531
10051
  sourceStateKey,
9532
10052
  destinationStateKey,
9533
- fromWorkspaceRoot: resolve20(request.fromWorkspaceRoot),
9534
- toWorkspaceRoot: resolve20(request.toWorkspaceRoot),
10053
+ fromWorkspaceRoot: resolve21(request.fromWorkspaceRoot),
10054
+ toWorkspaceRoot: resolve21(request.toWorkspaceRoot),
9535
10055
  toFleetId: request.toFleetId.trim()
9536
10056
  };
9537
10057
  }
@@ -9552,7 +10072,7 @@ async function requireManifest(request, stateKey, transport, label) {
9552
10072
  installationType: manifest.installationType,
9553
10073
  stateKey: manifest.stateKey
9554
10074
  });
9555
- if (manifest.adapter !== request.adapter || manifest.installationType !== request.installationType || resolve20(manifest.targetRoot) !== request.targetRoot || actualStateKey !== stateKey) {
10075
+ if (manifest.adapter !== request.adapter || manifest.installationType !== request.installationType || resolve21(manifest.targetRoot) !== request.targetRoot || actualStateKey !== stateKey) {
9556
10076
  throw new Error(`${label} install manifest identity does not match the requested adapter, target, installation type, and state key.`);
9557
10077
  }
9558
10078
  return manifest;
@@ -9633,10 +10153,262 @@ function canonicalJson(value) {
9633
10153
  return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
9634
10154
  }
9635
10155
 
10156
+ // src/lifecycle/legacy-recovery-plan.ts
10157
+ import { createHash as createHash10 } from "crypto";
10158
+ import { readdir as readdir10 } from "fs/promises";
10159
+ import { basename as basename20, dirname as dirname21, join as join39, posix as posix3, relative as relative8, resolve as resolve22 } from "path";
10160
+ async function inventoryHistoricalLocks(fleetRoot) {
10161
+ const root = join39(fleetRoot, ".agentwheel", "locks");
10162
+ const found = [];
10163
+ async function visit(dir) {
10164
+ for (const entry of await readdir10(dir, { withFileTypes: true }).catch((error) => {
10165
+ if (error.code === "ENOENT") return [];
10166
+ throw error;
10167
+ })) {
10168
+ const path = join39(dir, entry.name);
10169
+ if (entry.isDirectory()) await visit(path);
10170
+ else if (entry.isFile() && entry.name.endsWith(".graph-lock.json")) {
10171
+ const lock = await readGraphLock(path);
10172
+ found.push({ path, digest: sha2564(canonicalGraphLockJson(lock)), lock });
10173
+ }
10174
+ }
10175
+ }
10176
+ await visit(root);
10177
+ return found.sort((a, b) => a.path.localeCompare(b.path));
10178
+ }
10179
+ async function planLegacyRecovery(input) {
10180
+ const { graphPlan, installRoot, targetKey: targetKey2, adapter, installationType, historicalLocks, transport } = input;
10181
+ assertCompleteGraphPlan(graphPlan);
10182
+ const binding = {
10183
+ targetLockDir: dirname21(graphPlan.graphLockPath),
10184
+ stableLockPath: graphPlan.graphLockPath,
10185
+ stableStateKey: graphPlan.plan.stateKey,
10186
+ currentFingerprint: graphPlan.targetFingerprint,
10187
+ expectedOwner: workspaceOwnerForRoot(input.workspaceRoot, input.fleetId),
10188
+ installRoot,
10189
+ adapter,
10190
+ installationType,
10191
+ fleetId: input.fleetId,
10192
+ explicitStateKey: input.explicitStateKey,
10193
+ transportKind: transport.kind
10194
+ };
10195
+ const manifests = await listInstallManifests(installRoot, adapter, transport);
10196
+ const paths = /* @__PURE__ */ new Set();
10197
+ const desired = /* @__PURE__ */ new Map();
10198
+ const nodeHashes = new Map(graphPlan.bundle.graphLock.canonical.nodes.map((node) => [node.id, node.sourceHash]));
10199
+ for (const op of graphPlan.plan.operations) {
10200
+ if (!op.desiredHash || op.action === "remove" || op.action === "keep") continue;
10201
+ const path = relative8(installRoot, op.destPath);
10202
+ if (path.startsWith("..") || path === "") throw new Error(`Planner destination escapes target: ${op.destPath}`);
10203
+ paths.add(path);
10204
+ const items = desired.get(path) ?? [];
10205
+ items.push(op);
10206
+ desired.set(path, items);
10207
+ }
10208
+ for (const item of manifests) for (const entry of item.manifest.entries) paths.add(entry.path);
10209
+ const resolutionWarnings = graphPlan.warnings.filter((warning) => !/^skip (?:artifact|dependency) .+ \((?:selected but )?not targeted: runtimes=\[[^\]]+\]\)$/.test(warning));
10210
+ const complete = resolutionWarnings.length === 0;
10211
+ const output = [];
10212
+ for (const path of [...paths].sort()) {
10213
+ const allClaims = manifests.flatMap((item) => item.manifest.entries.filter((entry) => entry.path === path).map((entry) => claimFor(item, entry, historicalLocks, binding)));
10214
+ const claims = allClaims.filter((claim) => claim.scope !== "foreign");
10215
+ const foreignClaims = allClaims.filter((claim) => claim.scope === "foreign");
10216
+ const current = desired.get(path) ?? [];
10217
+ const currentHashes = [...new Set(current.map((item) => item.desiredHash))];
10218
+ const currentSourceHashes = [...new Set(current.map((item) => item.graphNodeId ? nodeHashes.get(item.graphNodeId) : void 0).filter((value) => Boolean(value)))].sort();
10219
+ const currentNodeIds = [...new Set(current.map((item) => item.graphNodeId).filter((value) => Boolean(value)))].sort();
10220
+ const currentSelectors = [...new Set(current.map((item) => item.logicalSelector).filter((value) => Boolean(value)))].sort();
10221
+ const currentOwners = [...new Set(current.flatMap((item) => item.owners ?? []))].sort();
10222
+ const livePath = resolve22(installRoot, path);
10223
+ if (relative8(installRoot, livePath).startsWith("..")) throw new Error(`Manifest path escapes target: ${path}`);
10224
+ const liveHash = await transport.pathExists(livePath) ? await transport.hashPath(livePath) : null;
10225
+ const reasons = [];
10226
+ const currentResults = await Promise.all(current.map((op) => verifyCurrentOperation(op, liveHash, transport)));
10227
+ const currentGraphStatus = current.length === 0 ? "absent" : currentResults.every((result) => result === "exact-match") ? "exact-match" : currentResults.some((result) => result === "unverifiable") ? "unverifiable" : "mismatch";
10228
+ if (currentHashes.length > 1 && current.some((op) => !op.mergeStrategy && op.mode !== "managed-block")) {
10229
+ reasons.push("current graph has divergent hashes for the destination");
10230
+ }
10231
+ if (currentGraphStatus === "mismatch") reasons.push("live contribution differs from current source");
10232
+ if (currentGraphStatus === "unverifiable") reasons.push("current contribution could not be verified");
10233
+ if (current.some((op) => op.action === "conflict" || op.action === "drift")) reasons.push("install planner has a blocking operation");
10234
+ if (!complete) reasons.push("current source graph has resolution warnings; completeness unproven");
10235
+ if (claims.some((claim) => claim.scope === "unbound" || claim.evidence.length)) reasons.push("historical provenance unresolved");
10236
+ if (foreignClaims.length) reasons.push("foreign manifest claim conflicts with destination");
10237
+ if (new Set(claims.map((claim) => `${claim.owner}:${claim.hash}:${claim.sourceHash}:${claim.graphLockDigest ?? ""}`)).size > 1) {
10238
+ reasons.push("competing same-target claims diverge");
10239
+ }
10240
+ if (current.length === 0 && claims.length === 0) reasons.push("no same-target historical claim");
10241
+ if (current.length === 0) {
10242
+ for (const claim of claims) {
10243
+ if (claim.scope !== "same-target") continue;
10244
+ const item = manifests.find((manifest) => manifest.path === claim.manifestPath);
10245
+ const entry = item.manifest.entries.find((candidate) => candidate.path === path && candidate.sourceHash === claim.sourceHash);
10246
+ if (!await verifyHistoricalEntry(entry, livePath, liveHash, transport)) {
10247
+ reasons.push("live contribution differs from same-target historical claim");
10248
+ break;
10249
+ }
10250
+ }
10251
+ }
10252
+ const category = reasons.length ? "unresolved" : currentGraphStatus === "exact-match" ? "current-graph-exact-match" : "retirement-candidate";
10253
+ output.push({
10254
+ path,
10255
+ category,
10256
+ reasons,
10257
+ currentHash: currentHashes.length === 1 ? currentHashes[0] : null,
10258
+ currentHashes: currentHashes.sort(),
10259
+ currentSourceHashes,
10260
+ currentNodeIds,
10261
+ currentSelectors,
10262
+ currentOwners,
10263
+ currentGraphStatus,
10264
+ liveHash,
10265
+ claims,
10266
+ foreignClaims
10267
+ });
10268
+ }
10269
+ const counts = { "current-graph-exact-match": 0, "retirement-candidate": 0, unresolved: 0 };
10270
+ const currentGraphCounts = { "exact-match": 0, mismatch: 0, absent: 0, unverifiable: 0 };
10271
+ for (const path of output) counts[path.category]++;
10272
+ for (const path of output) currentGraphCounts[path.currentGraphStatus]++;
10273
+ return {
10274
+ targetRoot: installRoot,
10275
+ targetKey: targetKey2,
10276
+ adapter,
10277
+ installationType,
10278
+ currentGraphLockDigest: graphPlan.graphLockDigest,
10279
+ roots: graphPlan.graph.roots.map((root) => ({ rootId: root.rootId, source: root.source, graphNodeId: root.graphNodeId, selected: root.selected })),
10280
+ resolutionWarnings,
10281
+ complete,
10282
+ paths: output,
10283
+ counts,
10284
+ currentGraphCounts
10285
+ };
10286
+ }
10287
+ function claimFor(item, entry, locks, binding) {
10288
+ const digest2 = "graphLockDigest" in entry ? entry.graphLockDigest : void 0;
10289
+ const nodeId = "graphNodeId" in entry ? entry.graphNodeId : void 0;
10290
+ const selector = "logicalSelector" in entry ? entry.logicalSelector : void 0;
10291
+ const matches = locks.filter((candidate) => candidate.digest === digest2);
10292
+ const evidence = [];
10293
+ const owner = "workspaceOwner" in entry ? entry.workspaceOwner : legacyUnownedWorkspaceOwner;
10294
+ const sameRoot = binding.transportKind === "ssh" ? posix3.normalize(item.manifest.targetRoot) === posix3.normalize(binding.installRoot) : resolve22(item.manifest.targetRoot) === resolve22(binding.installRoot);
10295
+ const inTargetDir = matches.filter((candidate) => resolve22(dirname21(candidate.path)) === resolve22(binding.targetLockDir));
10296
+ const boundLocks = inTargetDir.filter((candidate) => lockBindsState(candidate, item, binding));
10297
+ if (owner !== binding.expectedOwner) evidence.push("workspace owner differs from selected Fleet target");
10298
+ if (item.manifest.version !== 2 || item.manifest.installationType !== binding.installationType) evidence.push("installation type differs or manifest is legacy v1");
10299
+ if (!sameRoot) evidence.push("manifest target root differs from selected Fleet target");
10300
+ if (item.manifest.version === 2 && (item.manifest.stateKey !== item.stateKey || item.manifest.adapter !== binding.adapter)) {
10301
+ evidence.push("manifest state key or adapter differs from filename");
10302
+ }
10303
+ if (!digest2) evidence.push("historical graph-lock digest missing");
10304
+ else if (matches.length === 0) evidence.push("historical graph-lock digest has no matching lock");
10305
+ if (matches.length && !inTargetDir.length) evidence.push("claim belongs to another Fleet target graph");
10306
+ if (inTargetDir.length && !boundLocks.length) evidence.push("graph-lock fingerprint, filename, or state key does not bind claim");
10307
+ if (boundLocks.length && !boundLocks.some((candidate) => candidate.lock.canonical.artifacts.some((artifact) => artifact.graphNodeId === nodeId && artifact.logicalSelector === selector))) {
10308
+ evidence.push("historical graph-lock selector/node does not cover claim");
10309
+ } else if (boundLocks.length && !boundLocks.some((candidate) => candidate.lock.canonical.artifacts.some((artifact) => artifact.graphNodeId === nodeId && artifact.logicalSelector === selector && artifact.hash === entry.sourceHash))) {
10310
+ evidence.push("historical graph-lock artifact hash differs from manifest source hash");
10311
+ }
10312
+ const foreign = owner !== binding.expectedOwner || !sameRoot || item.manifest.version !== 2 || item.manifest.installationType !== binding.installationType || item.manifest.version === 2 && (item.manifest.stateKey !== item.stateKey || item.manifest.adapter !== binding.adapter) || matches.length > 0 && inTargetDir.length === 0;
10313
+ const scope = foreign ? "foreign" : boundLocks.length && evidence.length === 0 ? "same-target" : "unbound";
10314
+ return {
10315
+ scope,
10316
+ manifestPath: item.path,
10317
+ stateKey: item.stateKey,
10318
+ manifestStateKey: item.manifest.version === 2 ? item.manifest.stateKey : void 0,
10319
+ installationType: item.manifest.version === 2 ? item.manifest.installationType : void 0,
10320
+ targetRoot: item.manifest.targetRoot,
10321
+ owner,
10322
+ hash: entry.hash,
10323
+ sourceHash: entry.sourceHash,
10324
+ graphLockDigest: digest2,
10325
+ graphNodeId: nodeId,
10326
+ logicalSelector: selector,
10327
+ owners: "owners" in entry ? entry.owners : [entry.packageName ?? "legacy"],
10328
+ lockPaths: matches.map((candidate) => candidate.path),
10329
+ evidence
10330
+ };
10331
+ }
10332
+ function lockBindsState(candidate, manifest, binding) {
10333
+ const name = basename20(candidate.path);
10334
+ if (!name.endsWith(".graph-lock.json")) return false;
10335
+ const fingerprint = name.slice(0, -".graph-lock.json".length);
10336
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint)) return false;
10337
+ const stable = resolve22(candidate.path) === resolve22(binding.stableLockPath);
10338
+ if (stable) {
10339
+ return manifest.stateKey === binding.stableStateKey && candidate.lock.canonical.targetFingerprint === binding.currentFingerprint;
10340
+ }
10341
+ return candidate.lock.canonical.targetFingerprint === fingerprint && manifest.stateKey === stateKeyFor(binding.adapter, {
10342
+ installationType: binding.installationType,
10343
+ stateKey: binding.explicitStateKey,
10344
+ targetFingerprint: fingerprint,
10345
+ fleetId: binding.fleetId
10346
+ });
10347
+ }
10348
+ async function verifyCurrentOperation(op, liveHash, transport) {
10349
+ if (liveHash === null || !op.desiredHash) return "mismatch";
10350
+ if (op.semanticPlugin || op.programmaticOperation) return "unverifiable";
10351
+ if (op.mode === "managed-block") {
10352
+ const selector = managedInstructionSelector(op.logicalSelector, op.artifactType, op.artifactName);
10353
+ const state = await readManagedInstructionBlockState(op.destPath, selector, transport);
10354
+ return state.hasBlock && !state.drifted && state.hash === op.desiredHash ? "exact-match" : "mismatch";
10355
+ }
10356
+ if (op.mergeStrategy) {
10357
+ if (!op.sourcePath) return "unverifiable";
10358
+ try {
10359
+ await assertMergedSourceContribution(op.sourcePath, op.mergeStrategy, await transport.readFile(op.destPath));
10360
+ return "exact-match";
10361
+ } catch {
10362
+ return "mismatch";
10363
+ }
10364
+ }
10365
+ return liveHash === op.desiredHash ? "exact-match" : "mismatch";
10366
+ }
10367
+ async function verifyHistoricalEntry(entry, livePath, liveHash, transport) {
10368
+ if (liveHash === null || entry.semanticPlugin) return false;
10369
+ if (entry.mode === "managed-block") {
10370
+ const selector = managedInstructionSelector(
10371
+ "logicalSelector" in entry ? entry.logicalSelector : void 0,
10372
+ entry.artifactType,
10373
+ entry.artifactName
10374
+ );
10375
+ const state = await readManagedInstructionBlockState(livePath, selector, transport);
10376
+ return state.hasBlock && !state.drifted && state.hash === entry.hash;
10377
+ }
10378
+ if (entry.mergeStrategy) {
10379
+ if (!hasMergeRemovalContent(entry.mergeRemoval)) return false;
10380
+ try {
10381
+ assertExactMergeContribution(entry.mergeRemoval, entry.mergeStrategy, await transport.readFile(livePath));
10382
+ return true;
10383
+ } catch {
10384
+ return false;
10385
+ }
10386
+ }
10387
+ return entry.hash === liveHash;
10388
+ }
10389
+ function assertCompleteGraphPlan(plan) {
10390
+ const sourceNodes = plan.graph.nodes.filter((node) => node.selected.length > 0);
10391
+ const lockedNodes = new Map(plan.bundle.graphLock.canonical.nodes.map((node) => [node.id, node]));
10392
+ for (const node of sourceNodes) {
10393
+ const locked = lockedNodes.get(node.id);
10394
+ if (!locked || locked.sourceHash !== node.sourceHash || node.selected.some((selector) => !locked.selected.includes(selector))) {
10395
+ throw new Error(`Current source graph omitted selected node or dependency: ${node.id}`);
10396
+ }
10397
+ }
10398
+ for (const edge of plan.graph.edges) {
10399
+ if (!plan.bundle.graphLock.canonical.edges.some((locked) => locked.from === edge.from && locked.to === edge.to && locked.alias === edge.alias)) {
10400
+ throw new Error(`Current source graph omitted dependency edge: ${edge.from} -> ${edge.to}`);
10401
+ }
10402
+ }
10403
+ }
10404
+ function sha2564(value) {
10405
+ return createHash10("sha256").update(value).digest("hex");
10406
+ }
10407
+
9636
10408
  // src/version/policy.ts
9637
10409
  import { execFile as execFile3 } from "child_process";
9638
10410
  import { readFile as readFile23 } from "fs/promises";
9639
- import { join as join39, resolve as resolve21 } from "path";
10411
+ import { join as join40, resolve as resolve23 } from "path";
9640
10412
  import { promisify as promisify3 } from "util";
9641
10413
  import { parse as parseJsonc } from "jsonc-parser";
9642
10414
  import { z as z5 } from "zod";
@@ -9730,7 +10502,7 @@ async function discoverVersionsFromSource(pkg, workspaceRoot) {
9730
10502
  if (tagged.length > 0) return tagged;
9731
10503
  }
9732
10504
  if (driverName === "local") {
9733
- const root = resolve21(workspaceRoot, pkg.source);
10505
+ const root = resolve23(workspaceRoot, pkg.source);
9734
10506
  const manifest2 = await readPackageManifest(root);
9735
10507
  const current = manifest2 ? [{ version: manifest2.version, ref: pkg.requestedRef ?? root }] : [];
9736
10508
  try {
@@ -9745,7 +10517,7 @@ async function discoverVersionsFromSource(pkg, workspaceRoot) {
9745
10517
  }
9746
10518
  const driver = getSourceDriver(driverName);
9747
10519
  const resolved = await driver.resolve(pkg.source, {
9748
- cacheRoot: join39(workspaceRoot, ".agentwheel", "cache"),
10520
+ cacheRoot: join40(workspaceRoot, ".agentwheel", "cache"),
9749
10521
  mode: "tracking",
9750
10522
  ref: pkg.requestedRef
9751
10523
  });
@@ -9839,7 +10611,7 @@ function gitUrlFromSource(source) {
9839
10611
  throw new Error(`Version discovery does not support Git source: ${source}`);
9840
10612
  }
9841
10613
  function versionCachePath(workspaceRoot) {
9842
- return join39(workspaceRoot, ".agentwheel", "cache", "version-index.json");
10614
+ return join40(workspaceRoot, ".agentwheel", "cache", "version-index.json");
9843
10615
  }
9844
10616
  async function readVersionCache(path) {
9845
10617
  if (!await pathExists(path)) return { schemaVersion: 1, sources: {} };
@@ -9853,7 +10625,7 @@ async function readVersionCache(path) {
9853
10625
  // src/profile/members.ts
9854
10626
  import { execFile as execFile4 } from "child_process";
9855
10627
  import { readFile as readFile24 } from "fs/promises";
9856
- import { join as join40, resolve as resolve22 } from "path";
10628
+ import { join as join41, resolve as resolve24 } from "path";
9857
10629
  import { promisify as promisify4 } from "util";
9858
10630
  import { z as z7 } from "zod";
9859
10631
 
@@ -10028,7 +10800,7 @@ async function invokeMemberStatus(member, parentWorkspace, chain, options, cliEn
10028
10800
  let stderr = "";
10029
10801
  try {
10030
10802
  if (member.transport === "local") {
10031
- const workspace = resolve22(parentWorkspace, member.workspace);
10803
+ const workspace = resolve24(parentWorkspace, member.workspace);
10032
10804
  const result = await execFileAsync4(process.execPath, [cliEntry, ...args], {
10033
10805
  cwd: workspace,
10034
10806
  env: env2,
@@ -10074,7 +10846,7 @@ async function runMemberAgentwheel(member, parentWorkspace, args, chain) {
10074
10846
  process.execPath,
10075
10847
  [process.argv[1], ...memberCommandArgs(member, args)],
10076
10848
  {
10077
- cwd: resolve22(parentWorkspace, member.workspace),
10849
+ cwd: resolve24(parentWorkspace, member.workspace),
10078
10850
  env: env2,
10079
10851
  maxBuffer: 20 * 1024 * 1024
10080
10852
  }
@@ -10154,7 +10926,7 @@ function memberFailure(member, health, error) {
10154
10926
  };
10155
10927
  }
10156
10928
  function memberCachePath(workspaceRoot, profileName, memberId) {
10157
- return join40(workspaceRoot, ".agentwheel", "cache", "member-status", profileName, `${memberId}.json`);
10929
+ return join41(workspaceRoot, ".agentwheel", "cache", "member-status", profileName, `${memberId}.json`);
10158
10930
  }
10159
10931
  async function readMemberCache(path) {
10160
10932
  if (!await pathExists(path)) return void 0;
@@ -10180,7 +10952,7 @@ function assertNoCompositeCycle(workspaceRoot, profileName, chain) {
10180
10952
  }
10181
10953
  }
10182
10954
  function compositeKey(workspaceRoot, profileName) {
10183
- return `${resolve22(workspaceRoot)}#${profileName}`;
10955
+ return `${resolve24(workspaceRoot)}#${profileName}`;
10184
10956
  }
10185
10957
 
10186
10958
  // src/status/repository.ts
@@ -10227,10 +10999,10 @@ function valueAfter(lines, prefix) {
10227
10999
  }
10228
11000
 
10229
11001
  // src/catalogue/client.ts
10230
- import { createHash as createHash10 } from "crypto";
11002
+ import { createHash as createHash11 } from "crypto";
10231
11003
  import { readFile as readFile25, rm as rm8 } from "fs/promises";
10232
11004
  import { homedir as homedir9 } from "os";
10233
- import { join as join41 } from "path";
11005
+ import { join as join42 } from "path";
10234
11006
 
10235
11007
  // src/model/catalogue.ts
10236
11008
  import { z as z8 } from "zod";
@@ -10502,18 +11274,18 @@ var CatalogueClient = class {
10502
11274
  }
10503
11275
  return {
10504
11276
  value: schema.parse(value),
10505
- digest: createHash10("sha256").update(bytes).digest("hex")
11277
+ digest: createHash11("sha256").update(bytes).digest("hex")
10506
11278
  };
10507
11279
  }
10508
11280
  };
10509
11281
  function defaultCatalogueCachePath() {
10510
- return join41(homedir9(), ".agentwheel", "catalogue-cache.json");
11282
+ return join42(homedir9(), ".agentwheel", "catalogue-cache.json");
10511
11283
  }
10512
11284
  function sameSources2(a, b) {
10513
11285
  return a.length === b.length && a.every((source, index) => source === b[index]);
10514
11286
  }
10515
11287
  function catalogueContentHash(enriched, vercel) {
10516
- return createHash10("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
11288
+ return createHash11("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
10517
11289
  }
10518
11290
 
10519
11291
  // src/search/index.ts
@@ -10875,9 +11647,9 @@ function includesToken(normalizedText, token) {
10875
11647
  }
10876
11648
 
10877
11649
  // src/semantic/index.ts
10878
- import { createHash as createHash11 } from "crypto";
11650
+ import { createHash as createHash12 } from "crypto";
10879
11651
  import { homedir as homedir10 } from "os";
10880
- import { join as join42 } from "path";
11652
+ import { join as join43 } from "path";
10881
11653
  import { env, pipeline } from "@huggingface/transformers";
10882
11654
  var DEFAULT_SEMANTIC_INDEX_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-semantic-index/gte-v1/";
10883
11655
  var CONTRACT = {
@@ -10965,13 +11737,13 @@ var SemanticSearchClient = class {
10965
11737
  if (!response.ok) throw new Error(`Semantic index file failed (${response.status}): ${descriptor.path}`);
10966
11738
  const bytes = new Uint8Array(await response.arrayBuffer());
10967
11739
  if (bytes.byteLength !== descriptor.bytes) throw new Error(`Semantic index file size does not match: ${descriptor.path}`);
10968
- const digest2 = createHash11("sha256").update(bytes).digest("hex");
11740
+ const digest2 = createHash12("sha256").update(bytes).digest("hex");
10969
11741
  if (digest2 !== descriptor.sha256) throw new Error(`Semantic index checksum does not match: ${descriptor.path}`);
10970
11742
  return bytes;
10971
11743
  }
10972
11744
  };
10973
11745
  async function embedQuery(query) {
10974
- env.cacheDir = join42(homedir10(), ".agentwheel", "semantic-models");
11746
+ env.cacheDir = join43(homedir10(), ".agentwheel", "semantic-models");
10975
11747
  const extractor = await pipeline("feature-extraction", CONTRACT.model.id, {
10976
11748
  revision: CONTRACT.model.revision,
10977
11749
  dtype: CONTRACT.model.dtype,
@@ -11055,9 +11827,9 @@ function ensureTrailingSlash(value) {
11055
11827
  }
11056
11828
 
11057
11829
  // src/trial/skill.ts
11058
- import { createHash as createHash12 } from "crypto";
11830
+ import { createHash as createHash13 } from "crypto";
11059
11831
  import { readFile as readFile26, stat as stat13 } from "fs/promises";
11060
- import { join as join43 } from "path";
11832
+ import { join as join44 } from "path";
11061
11833
  import { parse as parseYaml2 } from "yaml";
11062
11834
  var MAX_TRIAL_SKILL_BYTES = 512 * 1024;
11063
11835
  async function createSkillTrial(driver, resolved, selectors) {
@@ -11069,7 +11841,7 @@ async function createSkillTrial(driver, resolved, selectors) {
11069
11841
  throw new Error("Skill trial requires exactly one selected skill. Use --skill <name> or --select skills/<name>.");
11070
11842
  }
11071
11843
  const artifact = skills[0];
11072
- const path = artifact.kind === "dir" ? join43(artifact.sourcePath, "SKILL.md") : artifact.sourcePath;
11844
+ const path = artifact.kind === "dir" ? join44(artifact.sourcePath, "SKILL.md") : artifact.sourcePath;
11073
11845
  const info = await stat13(path);
11074
11846
  if (info.size > MAX_TRIAL_SKILL_BYTES) {
11075
11847
  throw new Error(`Skill trial exceeds the ${MAX_TRIAL_SKILL_BYTES / 1024} KiB content limit.`);
@@ -11085,7 +11857,7 @@ async function createSkillTrial(driver, resolved, selectors) {
11085
11857
  skill: {
11086
11858
  name: artifact.name,
11087
11859
  relativePath: artifact.relativePath,
11088
- sha256: createHash12("sha256").update(content).digest("hex"),
11860
+ sha256: createHash13("sha256").update(content).digest("hex"),
11089
11861
  frontmatter,
11090
11862
  content
11091
11863
  }
@@ -11104,10 +11876,10 @@ function readSkillFrontmatter(content, artifact) {
11104
11876
  }
11105
11877
 
11106
11878
  // src/lifecycle/fleet-normalize.ts
11107
- import { createHash as createHash13 } from "crypto";
11108
- import { readFile as readFile27, readdir as readdir9, rm as rm9 } from "fs/promises";
11879
+ import { createHash as createHash14 } from "crypto";
11880
+ import { readFile as readFile27, readdir as readdir11, rm as rm9 } from "fs/promises";
11109
11881
  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";
11882
+ import { dirname as dirname22, isAbsolute as isAbsolute3, join as join45, relative as relative9, resolve as resolve25 } from "path";
11111
11883
  async function planFleetNormalization(request) {
11112
11884
  const normalizedRequest = normalizeRequest2(request);
11113
11885
  const destinationScope = await resolveWorkspaceScope({
@@ -11137,7 +11909,7 @@ async function planFleetNormalization(request) {
11137
11909
  `Package '${name}' has divergent declarations. Source, driver, adapter, adapter configuration, installation type, version policy, selections, aliases, overrides, and suggestions must match before normalization.`
11138
11910
  );
11139
11911
  }
11140
- packages.push({ name, declarationDigest: sha2564(sourceDeclaration) });
11912
+ packages.push({ name, declarationDigest: sha2565(sourceDeclaration) });
11141
11913
  }
11142
11914
  if (!legacySelfNormalization) await assertSourceFleetPostcondition(normalizedRequest, sourceScope, candidates);
11143
11915
  if ((normalizedRequest.profile || normalizedRequest.agent || normalizedRequest.artifacts || normalizedRequest.orphanedOwnerRoots) && !legacySelfNormalization) {
@@ -11168,7 +11940,7 @@ async function planFleetNormalization(request) {
11168
11940
  packages,
11169
11941
  installedState
11170
11942
  };
11171
- return { ...planWithoutDigest, planDigest: sha2564(canonicalJson2(planWithoutDigest)) };
11943
+ return { ...planWithoutDigest, planDigest: sha2565(canonicalJson2(planWithoutDigest)) };
11172
11944
  }
11173
11945
  async function applyFleetNormalization(request, hooks = {}) {
11174
11946
  if (!request.apply) throw new Error("Fleet normalization is dry-run by default; pass --apply with --plan-digest <digest>.");
@@ -11306,7 +12078,7 @@ async function assertNormalizationPostconditions(journal) {
11306
12078
  }
11307
12079
  for (const graphLock of journal.graphLocks) {
11308
12080
  const raw = await readOptionalRecord(graphLock.path);
11309
- const digest2 = raw ? sha2564(canonicalJson2(await readGraphLock(graphLock.path))) : null;
12081
+ const digest2 = raw ? sha2565(canonicalJson2(await readGraphLock(graphLock.path))) : null;
11310
12082
  if (digest2 !== graphLock.afterDigest) {
11311
12083
  throw new Error(`Fleet normalization graph-lock postcheck failed: ${graphLock.path}`);
11312
12084
  }
@@ -11378,17 +12150,17 @@ function normalizeRequest2(request) {
11378
12150
  ...profile ? { profile } : {},
11379
12151
  ...agent ? { agent } : {},
11380
12152
  ...orphanedOwnerRoots ? { orphanedOwnerRoots } : {},
11381
- ...request.globalRoot ? { globalRoot: resolve23(request.globalRoot) } : {}
12153
+ ...request.globalRoot ? { globalRoot: resolve25(request.globalRoot) } : {}
11382
12154
  };
11383
12155
  }
11384
12156
  function normalizeOrphanedOwnerRoot(value) {
11385
12157
  const root = value.trim();
11386
12158
  if (!root || !isAbsolute3(root)) throw new Error(`Invalid orphaned owner root '${value}'; expected an absolute workspace path.`);
11387
- return resolve23(root);
12159
+ return resolve25(root);
11388
12160
  }
11389
12161
  async function normalizationRoots(request) {
11390
12162
  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;
12163
+ const source = request.from === "user" ? resolve25(request.globalRoot ?? homedir11()) : (await showRegisteredFleet(request.from.slice("fleet:".length), { globalRoot: request.globalRoot })).root;
11392
12164
  return { source, destination: destination.root };
11393
12165
  }
11394
12166
  async function withOrderedLocks(roots, fn) {
@@ -11396,7 +12168,7 @@ async function withOrderedLocks(roots, fn) {
11396
12168
  if (index === roots.length) return fn();
11397
12169
  const root = roots[index];
11398
12170
  return withFilesystemLock(
11399
- join44(root, ".agentwheel", "fleet-normalize.lock"),
12171
+ join45(root, ".agentwheel", "fleet-normalize.lock"),
11400
12172
  5e3,
11401
12173
  () => run(index + 1),
11402
12174
  "fleet normalization"
@@ -11454,11 +12226,28 @@ async function inspectInstalledState(source, destination, packageNames) {
11454
12226
  const coveredDestinationGraphs = /* @__PURE__ */ new Set();
11455
12227
  const plannedDestinationGraphs = /* @__PURE__ */ new Set();
11456
12228
  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}`);
12229
+ const sourceState = await installedTargetStateForGraph(
12230
+ source,
12231
+ sourceGraph,
12232
+ manifests,
12233
+ sourceOwners,
12234
+ selected
12235
+ );
12236
+ const destinationStableState = await targetStateForGraph(destination, sourceGraph);
12237
+ const matchingDestinationGraphs = destinationGraphs.filter((candidate) => candidate.targetKey === sourceGraph.targetKey && candidate.adapter === sourceGraph.adapter);
12238
+ if (matchingDestinationGraphs.length > 1) {
12239
+ throw new Error(
12240
+ `Ambiguous destination graph state for target '${sourceGraph.targetKey}': ` + matchingDestinationGraphs.map((candidate) => candidate.path).join(", ")
12241
+ );
11461
12242
  }
12243
+ const destinationGraph = matchingDestinationGraphs[0];
12244
+ const destinationState = destinationGraph ? await installedTargetStateForGraph(
12245
+ destination,
12246
+ destinationGraph,
12247
+ manifests,
12248
+ destinationOwners,
12249
+ selected
12250
+ ) : destinationStableState;
11462
12251
  if (sourceState.adapter !== destinationState.adapter) {
11463
12252
  throw new Error(`Source and destination adapters diverge for target '${sourceGraph.targetKey}'.`);
11464
12253
  }
@@ -11467,7 +12256,6 @@ async function inspectInstalledState(source, destination, packageNames) {
11467
12256
  throw new Error(`Source graph lock is not covered by its canonical install manifest: ${sourceState.manifestPath}`);
11468
12257
  }
11469
12258
  const sourceGraphArtifacts = new Set(sourceGraph.artifactIdentities);
11470
- const destinationGraph = destinationGraphs.find((candidate) => candidate.path === destinationState.graphLockPath);
11471
12259
  if (destinationGraph && destinationGraph.digest !== sourceGraph.digest) {
11472
12260
  throw new Error("Source and destination graph declarations, selections, versions, identities, or rendered paths diverge.");
11473
12261
  }
@@ -11479,10 +12267,10 @@ async function inspectInstalledState(source, destination, packageNames) {
11479
12267
  const destinationGraphAfter = destinationGraph?.lock ?? destinationGraphFromSource(sourceGraph.lock, destinationState.targetFingerprint);
11480
12268
  graphTransfers.push({
11481
12269
  sourceGraphLockPath: sourceGraph.path,
11482
- sourceGraphLockDigest: sha2564(canonicalJson2(sourceGraph.lock)),
12270
+ sourceGraphLockDigest: sha2565(canonicalJson2(sourceGraph.lock)),
11483
12271
  destinationGraphLockPath: destinationState.graphLockPath,
11484
- destinationGraphLockDigest: destinationGraph ? sha2564(canonicalJson2(destinationGraph.lock)) : null,
11485
- destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraphAfter)),
12272
+ destinationGraphLockDigest: destinationGraph ? sha2565(canonicalJson2(destinationGraph.lock)) : null,
12273
+ destinationGraphLockAfterDigest: sha2565(canonicalJson2(destinationGraphAfter)),
11486
12274
  targetKey: sourceGraph.targetKey,
11487
12275
  adapter: destinationState.adapter,
11488
12276
  targetFingerprint: destinationState.targetFingerprint
@@ -11585,16 +12373,30 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11585
12373
  const transferableOwners = orphanedOwners.size > 0 ? orphanedOwners : /* @__PURE__ */ new Set([legacyOwner]);
11586
12374
  let normalizedGraphCount = 0;
11587
12375
  for (const graph of graphCandidates) {
12376
+ const destinationState = await targetStateForGraph(fleet, graph, fleet.fleetId);
12377
+ if (resolve25(destinationState.graphLockPath) === resolve25(graph.path)) {
12378
+ const destinationManifest = (await collectManifestPaths([destinationState.manifestPath]))[0];
12379
+ if (destinationManifest?.manifest.entries.some((entry) => entry.workspaceOwner === fleetOwner && entryMatchesPackages(entry, selected))) {
12380
+ assertCorrelatedGraphManifest(
12381
+ graph,
12382
+ destinationState,
12383
+ destinationManifest,
12384
+ /* @__PURE__ */ new Set([fleetOwner]),
12385
+ selected
12386
+ );
12387
+ normalizedGraphCount += 1;
12388
+ continue;
12389
+ }
12390
+ }
11588
12391
  const legacyState = await legacyTargetStateForGraph(fleet, graph);
12392
+ if (resolve25(legacyState.graphLockPath) !== resolve25(graph.path)) {
12393
+ throw new Error(`Legacy graph lock target identity is stale or noncanonical: ${graph.path}`);
12394
+ }
11589
12395
  const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
11590
12396
  const relevantManifestEntries2 = legacyManifest?.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected) && entryMatchesArtifacts(entry, selectedArtifacts)) ?? [];
11591
12397
  if (relevantManifestEntries2.length > 0) {
11592
12398
  graphs.push(graph);
11593
12399
  }
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
12400
  }
11599
12401
  if (graphs.length === 0 && normalizedGraphCount > 0) {
11600
12402
  throw new Error(`Fleet '${fleet.fleetId}' installed state is already normalized to fleet-qualified ownership.`);
@@ -11695,7 +12497,7 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11695
12497
  )
11696
12498
  });
11697
12499
  if (!selectedArtifacts) {
11698
- const fullDigest = sha2564(canonicalJson2(graph.lock));
12500
+ const fullDigest = sha2565(canonicalJson2(graph.lock));
11699
12501
  const existingDestinationGraph = graphCandidates.find((candidate) => candidate.path === destinationState.graphLockPath);
11700
12502
  if (existingDestinationGraph) assertGraphIsSubset(graph, existingDestinationGraph);
11701
12503
  const destinationGraph = destinationGraphFromSource(graph.lock, destinationState.targetFingerprint);
@@ -11703,8 +12505,8 @@ async function inspectLegacySelfInstalledState(fleet, packageNames, profileName,
11703
12505
  sourceGraphLockPath: graph.path,
11704
12506
  sourceGraphLockDigest: fullDigest,
11705
12507
  destinationGraphLockPath: destinationState.graphLockPath,
11706
- destinationGraphLockDigest: existingDestinationGraph ? sha2564(canonicalJson2(existingDestinationGraph.lock)) : null,
11707
- destinationGraphLockAfterDigest: sha2564(canonicalJson2(destinationGraph)),
12508
+ destinationGraphLockDigest: existingDestinationGraph ? sha2565(canonicalJson2(existingDestinationGraph.lock)) : null,
12509
+ destinationGraphLockAfterDigest: sha2565(canonicalJson2(destinationGraph)),
11708
12510
  targetKey: graph.targetKey,
11709
12511
  adapter: graph.adapter,
11710
12512
  targetFingerprint: destinationState.targetFingerprint
@@ -11762,7 +12564,7 @@ function targetKeysForAgent(scope, name) {
11762
12564
  async function runtimeRoots(scope) {
11763
12565
  const roots = /* @__PURE__ */ new Set([scope.root]);
11764
12566
  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()));
12567
+ roots.add(resolve25(process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir11()));
11766
12568
  }
11767
12569
  for (const [name, agent] of Object.entries(scope.config.agents)) {
11768
12570
  if (agent.transport === "ssh") throw new Error(`Installed-state normalization cannot inspect SSH agent '${name}' locally.`);
@@ -11780,17 +12582,17 @@ async function runtimeRoots(scope) {
11780
12582
  async function collectManifests(roots) {
11781
12583
  const found = /* @__PURE__ */ new Map();
11782
12584
  for (const root of roots) {
11783
- const dir = join44(root, ".agentwheel");
12585
+ const dir = join45(root, ".agentwheel");
11784
12586
  let names;
11785
12587
  try {
11786
- names = await readdir9(dir);
12588
+ names = await readdir11(dir);
11787
12589
  } catch (error) {
11788
12590
  if (isMissing(error)) continue;
11789
12591
  throw error;
11790
12592
  }
11791
12593
  for (const name of names.sort((a, b) => a.localeCompare(b))) {
11792
12594
  if (!name.endsWith(".install-manifest.json")) continue;
11793
- const path = join44(dir, name);
12595
+ const path = join45(dir, name);
11794
12596
  const raw = JSON.parse(await readFile27(path, "utf8"));
11795
12597
  const parsed = installManifestSchema.parse(raw);
11796
12598
  if (parsed.version !== 2) {
@@ -11799,14 +12601,14 @@ async function collectManifests(roots) {
11799
12601
  }
11800
12602
  continue;
11801
12603
  }
11802
- if (resolve23(parsed.targetRoot) !== resolve23(root)) {
12604
+ if (resolve25(parsed.targetRoot) !== resolve25(root)) {
11803
12605
  throw new Error(`Install manifest target root does not match its enumerated runtime root: ${path}`);
11804
12606
  }
11805
12607
  const expectedPath = installManifestPath(parsed.targetRoot, parsed.adapter, {
11806
12608
  installationType: parsed.installationType,
11807
12609
  stateKey: parsed.stateKey
11808
12610
  });
11809
- if (resolve23(expectedPath) !== resolve23(path)) {
12611
+ if (resolve25(expectedPath) !== resolve25(path)) {
11810
12612
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11811
12613
  }
11812
12614
  found.set(path, { path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11827,7 +12629,7 @@ async function collectManifestPaths(paths) {
11827
12629
  installationType: parsed.installationType,
11828
12630
  stateKey: parsed.stateKey
11829
12631
  });
11830
- if (resolve23(expectedPath) !== resolve23(path)) {
12632
+ if (resolve25(expectedPath) !== resolve25(path)) {
11831
12633
  throw new Error(`Install manifest state identity does not match its canonical path: ${path}`);
11832
12634
  }
11833
12635
  found.push({ path, raw, manifest: { ...parsed, revision: computeManifestRevision(raw), legacy: false } });
@@ -11846,21 +12648,17 @@ function assertNoForeignSamePathOwnership(manifests, renderedPaths, allowedOwner
11846
12648
  }
11847
12649
  }
11848
12650
  async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include) {
11849
- const root = join44(workspaceRoot, ".agentwheel", "locks");
12651
+ const root = join45(workspaceRoot, ".agentwheel", "locks");
11850
12652
  if (!await pathExists(root)) return [];
11851
12653
  const results = [];
11852
12654
  for (const path of await listFiles(root)) {
11853
12655
  if (!path.endsWith(".graph-lock.json")) continue;
11854
12656
  const lock = await readGraphLock(path);
11855
12657
  if (!lock.canonical.roots.some((candidate2) => selected.has(candidate2.rootId))) continue;
11856
- const parts = relative8(root, path).split(/[\\/]/);
12658
+ const parts = relative9(root, path).split(/[\\/]/);
11857
12659
  if (parts.length !== 3) throw new Error(`Graph lock path is not canonical: ${path}`);
11858
- const [targetKey2, adapter, fileName] = parts;
12660
+ const [targetKey2, adapter] = parts;
11859
12661
  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
12662
  const candidate = {
11865
12663
  path,
11866
12664
  lock,
@@ -11874,7 +12672,7 @@ async function relevantGraphLocks(workspaceRoot, selected, targetKeys, include)
11874
12672
  const projection = relevantGraphProjection(lock, selected);
11875
12673
  results.push({
11876
12674
  ...candidate,
11877
- digest: sha2564(canonicalJson2(projection)),
12675
+ digest: sha2565(canonicalJson2(projection)),
11878
12676
  artifactIdentities: lock.canonical.artifacts.filter((artifact) => artifact.owners.some((owner) => ownerMatchesGraphPackage(lock, owner, selected))).map(graphArtifactIdentity).sort()
11879
12677
  });
11880
12678
  }
@@ -11889,9 +12687,19 @@ function isCurrentLocalTarget(scope, graph) {
11889
12687
  }
11890
12688
  async function hasRelevantLegacyManifestCandidate(scope, graph, selected) {
11891
12689
  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;
12690
+ const configuredLegacyPath = await legacyManifestPathForConfiguredTarget(scope, graph);
12691
+ if (configuredLegacyPath) {
12692
+ const manifest = (await collectManifestPaths([configuredLegacyPath]))[0];
12693
+ if (manifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected))) return true;
12694
+ }
12695
+ if (graph.lock.canonical.roots.some((root) => !scope.config.packages.some((pkg) => pkg.name === root.rootId))) return false;
12696
+ const legacyState = await legacyTargetStateForGraph(scope, graph);
12697
+ const legacyManifest = (await collectManifestPaths([legacyState.manifestPath]))[0];
12698
+ if (legacyManifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected))) return true;
12699
+ const stableState = await targetStateForGraph(scope, graph, scope.fleetId ?? null);
12700
+ if (resolve25(stableState.graphLockPath) !== resolve25(graph.path)) return false;
12701
+ const stableManifest = (await collectManifestPaths([stableState.manifestPath]))[0];
12702
+ return stableManifest?.manifest.entries.some((entry) => entryMatchesGraphPackages(entry, graph.lock, selected)) ?? false;
11895
12703
  }
11896
12704
  async function legacyManifestPathForConfiguredTarget(scope, graph) {
11897
12705
  const agent = scope.config.agents[graph.targetKey];
@@ -11910,9 +12718,10 @@ async function legacyManifestPathForConfiguredTarget(scope, graph) {
11910
12718
  baseDir: scope.root
11911
12719
  });
11912
12720
  const targetRoot = resolveConfigPath(agent.root, scope.root);
11913
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
12721
+ const installRoot = resolve25(installRootForAdapterInstallationType(adapter, targetRoot, agent.installationType, false));
11914
12722
  const stateKey = stateKeyFor(adapter.name, {
11915
12723
  installationType: agent.installationType,
12724
+ stateKey: agent.stateKey,
11916
12725
  targetFingerprint: graph.lock.canonical.targetFingerprint
11917
12726
  });
11918
12727
  return installManifestPath(installRoot, adapter.name, { installationType: agent.installationType, stateKey });
@@ -11977,14 +12786,21 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11977
12786
  ssh: void 0,
11978
12787
  ...agent?.stateKey ? { stateKey: agent.stateKey } : {}
11979
12788
  };
11980
- const targetFingerprint = computeTargetFingerprint(fingerprintParts);
12789
+ const installRoot = resolve25(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
12790
+ const targetIdentity = resolveTargetStateIdentity({
12791
+ targetFingerprintParts: fingerprintParts,
12792
+ workspaceRoot: scope.root,
12793
+ targetKey: graph.targetKey,
12794
+ resolvedInstallRoot: installRoot,
12795
+ transportKind: "local"
12796
+ });
12797
+ const targetFingerprint = targetIdentity.targetFingerprint;
11981
12798
  const stateKey = stateKeyFor(adapter.name, {
11982
12799
  installationType,
11983
12800
  stateKey: agent?.stateKey,
11984
- targetFingerprint,
12801
+ targetFingerprint: targetIdentity.stateFingerprint,
11985
12802
  ...identityFleetId ? { fleetId: identityFleetId } : {}
11986
12803
  });
11987
- const installRoot = resolve23(installRootForAdapterInstallationType(adapter, targetRoot, installationType, false));
11988
12804
  return {
11989
12805
  adapter: adapter.name,
11990
12806
  installationType,
@@ -11992,28 +12808,81 @@ async function targetStateForGraph(scope, graph, identityFleetId = scope.fleetId
11992
12808
  installRoot,
11993
12809
  stateKey,
11994
12810
  targetFingerprint,
11995
- graphLockPath: graphLockPathForTarget(scope.root, graph.targetKey, adapter.name, fingerprintParts),
12811
+ graphLockPath: graphLockPathForTarget(scope.root, graph.targetKey, adapter.name, fingerprintParts, {
12812
+ resolvedInstallRoot: installRoot,
12813
+ transportKind: "local"
12814
+ }),
11996
12815
  manifestPath: installManifestPath(installRoot, adapter.name, { installationType, stateKey })
11997
12816
  };
11998
12817
  }
11999
- async function legacyTargetStateForGraph(scope, graph) {
12000
- const current = await targetStateForGraph(scope, graph, null);
12818
+ async function installedTargetStateForGraph(scope, graph, manifests, owners, selected, identityFleetId = scope.fleetId ?? null) {
12819
+ const stable = await targetStateForGraph(scope, graph, identityFleetId);
12820
+ let state;
12821
+ if (resolve25(graph.path) === resolve25(stable.graphLockPath)) {
12822
+ state = stable;
12823
+ } else {
12824
+ const released = await releasedTargetStateForGraph(scope, graph, identityFleetId);
12825
+ if (resolve25(graph.path) !== resolve25(released.graphLockPath)) {
12826
+ throw new Error(`Graph lock target identity is stale or noncanonical: ${graph.path}`);
12827
+ }
12828
+ state = released;
12829
+ }
12830
+ const manifest = manifests.find((candidate) => resolve25(candidate.path) === resolve25(state.manifestPath));
12831
+ if (!manifest) {
12832
+ throw new Error(`Graph lock is not covered by its canonical install manifest: ${state.manifestPath}`);
12833
+ }
12834
+ assertCorrelatedGraphManifest(graph, state, manifest, owners, selected);
12835
+ return state;
12836
+ }
12837
+ async function releasedTargetStateForGraph(scope, graph, identityFleetId = scope.fleetId ?? null) {
12838
+ const current = await targetStateForGraph(scope, graph, identityFleetId);
12001
12839
  const targetFingerprint = graph.lock.canonical.targetFingerprint;
12840
+ if (!targetFingerprint || !/^[a-f0-9]{64}$/u.test(targetFingerprint)) {
12841
+ throw new Error(`Released graph lock has an invalid target fingerprint: ${graph.path}`);
12842
+ }
12843
+ const agent = scope.config.agents[graph.targetKey];
12002
12844
  const stateKey = stateKeyFor(current.adapter, {
12003
12845
  installationType: current.installationType,
12004
- targetFingerprint
12846
+ stateKey: agent?.stateKey,
12847
+ targetFingerprint,
12848
+ ...identityFleetId ? { fleetId: identityFleetId } : {}
12005
12849
  });
12006
12850
  return {
12007
12851
  ...current,
12008
12852
  stateKey,
12009
12853
  targetFingerprint,
12010
- graphLockPath: graph.path,
12854
+ graphLockPath: join45(dirname22(graph.path), `${targetFingerprint}.graph-lock.json`),
12011
12855
  manifestPath: installManifestPath(current.installRoot, current.adapter, {
12012
12856
  installationType: current.installationType,
12013
12857
  stateKey
12014
12858
  })
12015
12859
  };
12016
12860
  }
12861
+ function assertCorrelatedGraphManifest(graph, state, manifest, owners, selected) {
12862
+ if (manifest.manifest.adapter !== state.adapter || manifest.manifest.installationType !== state.installationType || manifest.manifest.stateKey !== state.stateKey || resolve25(manifest.manifest.targetRoot) !== resolve25(state.installRoot)) {
12863
+ throw new Error(`Install manifest identity does not match graph target state: ${manifest.path}`);
12864
+ }
12865
+ const entries = manifest.manifest.entries.filter((entry) => entryMatchesGraphPackages(entry, graph.lock, selected));
12866
+ if (entries.length === 0) {
12867
+ throw new Error(`Graph lock is not covered by selected install-manifest contributions: ${graph.path}`);
12868
+ }
12869
+ const graphArtifacts = new Set(graph.artifactIdentities);
12870
+ const graphLockDigest = sha2565(canonicalJson2(graph.lock));
12871
+ for (const entry of entries) {
12872
+ if (!owners.has(entry.workspaceOwner)) {
12873
+ throw new Error(`Graph lock manifest owner mismatch at ${manifest.path}: ${entry.workspaceOwner}.`);
12874
+ }
12875
+ if (!graphArtifacts.has(graphEntryIdentity(entry))) {
12876
+ throw new Error(`Install manifest entry is not covered by its graph lock: ${renderedEntryPath(manifest.manifest, entry)}`);
12877
+ }
12878
+ if (entry.graphLockDigest !== void 0 && entry.graphLockDigest !== graphLockDigest) {
12879
+ throw new Error(`Install manifest graph-lock digest does not match ${graph.path}.`);
12880
+ }
12881
+ }
12882
+ }
12883
+ async function legacyTargetStateForGraph(scope, graph) {
12884
+ return releasedTargetStateForGraph(scope, graph, null);
12885
+ }
12017
12886
  function assertGraphIsSubset(source, destination) {
12018
12887
  const sourceRoots = new Map(source.lock.canonical.roots.map((root) => [root.rootId, canonicalJson2(root)]));
12019
12888
  for (const root of destination.lock.canonical.roots) {
@@ -12047,7 +12916,7 @@ function destinationGraphFromSource(source, targetFingerprint) {
12047
12916
  function mergeDestinationManifest(existing, state, generatedAt, desiredEntries, replaceOwners = /* @__PURE__ */ new Set()) {
12048
12917
  if (existing) {
12049
12918
  const manifest2 = existing.manifest;
12050
- if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve23(manifest2.targetRoot) !== state.installRoot) {
12919
+ if (manifest2.adapter !== state.adapter || manifest2.installationType !== state.installationType || manifest2.stateKey !== state.stateKey || resolve25(manifest2.targetRoot) !== state.installRoot) {
12051
12920
  throw new Error(`Destination manifest identity diverges from the derived target: ${existing.path}`);
12052
12921
  }
12053
12922
  }
@@ -12107,7 +12976,7 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12107
12976
  const content = await readFile27(path, "utf8");
12108
12977
  if (hasMergeRemovalContent(entry.mergeRemoval)) {
12109
12978
  assertExactMergeContribution(entry.mergeRemoval, entry.mergeStrategy, content);
12110
- } else if (resolve23(sourcePath) === resolve23(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12979
+ } else if (resolve25(sourcePath) === resolve25(destinationPath) && entry.mergeStrategy !== "codex-toml-mcp") {
12111
12980
  assertExactMergeContribution({}, entry.mergeStrategy, content);
12112
12981
  } else {
12113
12982
  throw new Error(`Installed-state normalization cannot prove incomplete merge ownership at ${path}.`);
@@ -12118,8 +12987,8 @@ async function assertEquivalentRuntimeState(entry, sourcePath, destinationPath)
12118
12987
  await assertEquivalentRuntimeBytes(sourcePath, destinationPath, entry.hash);
12119
12988
  }
12120
12989
  function renderedEntryPathForRoot(root, entryPath) {
12121
- const normalizedRoot = resolve23(root);
12122
- const candidate = resolve23(normalizedRoot, entryPath);
12990
+ const normalizedRoot = resolve25(root);
12991
+ const candidate = resolve25(normalizedRoot, entryPath);
12123
12992
  if (candidate !== normalizedRoot && !candidate.startsWith(`${normalizedRoot}/`)) {
12124
12993
  throw new Error(`Install manifest entry escapes its target root: ${entryPath}`);
12125
12994
  }
@@ -12210,8 +13079,8 @@ function ownerMatchesGraphPackage(lock, owner, selected) {
12210
13079
  return ownerMatchesPackage(owner, selected) || lock.canonical.roots.some((root) => selected.has(root.rootId) && root.graphNodeId === owner);
12211
13080
  }
12212
13081
  function renderedEntryPath(manifest, entry) {
12213
- const root = resolve23(manifest.targetRoot);
12214
- const candidate = resolve23(root, entry.path);
13082
+ const root = resolve25(manifest.targetRoot);
13083
+ const candidate = resolve25(root, entry.path);
12215
13084
  if (candidate !== root && !candidate.startsWith(`${root}/`)) {
12216
13085
  throw new Error(`Install manifest entry escapes its target root: ${entry.path}`);
12217
13086
  }
@@ -12366,7 +13235,7 @@ async function buildJournalGraphLockStates(plan) {
12366
13235
  for (const transfer of plan.installedState.graphTransfers) {
12367
13236
  const sourceBefore = JSON.parse(await readFile27(transfer.sourceGraphLockPath, "utf8"));
12368
13237
  const sourceLock = await readGraphLock(transfer.sourceGraphLockPath);
12369
- const sourceDigest = sha2564(canonicalJson2(sourceLock));
13238
+ const sourceDigest = sha2565(canonicalJson2(sourceLock));
12370
13239
  if (sourceDigest !== transfer.sourceGraphLockDigest) {
12371
13240
  throw new Error(`Source graph lock changed after planning: ${transfer.sourceGraphLockPath}`);
12372
13241
  }
@@ -12384,9 +13253,9 @@ async function buildJournalGraphLockStates(plan) {
12384
13253
  continue;
12385
13254
  }
12386
13255
  const destinationBefore = await readOptionalRecord(transfer.destinationGraphLockPath);
12387
- const destinationBeforeDigest = destinationBefore ? sha2564(canonicalJson2(await readGraphLock(transfer.destinationGraphLockPath))) : null;
13256
+ const destinationBeforeDigest = destinationBefore ? sha2565(canonicalJson2(await readGraphLock(transfer.destinationGraphLockPath))) : null;
12388
13257
  const destinationAfter = destinationBefore ? await readGraphLock(transfer.destinationGraphLockPath) : destinationGraphFromSource(sourceLock, transfer.targetFingerprint);
12389
- const destinationAfterDigest = sha2564(canonicalJson2(destinationAfter));
13258
+ const destinationAfterDigest = sha2565(canonicalJson2(destinationAfter));
12390
13259
  if (destinationBeforeDigest !== transfer.destinationGraphLockDigest || destinationAfterDigest !== transfer.destinationGraphLockAfterDigest) {
12391
13260
  throw new Error(`Destination graph lock changed after planning: ${transfer.destinationGraphLockPath}`);
12392
13261
  }
@@ -12496,7 +13365,7 @@ async function assertJournalRecoverable(journal) {
12496
13365
  }
12497
13366
  for (const graphLock of journal.graphLocks) {
12498
13367
  const raw = await readOptionalRecord(graphLock.path);
12499
- const digest2 = raw ? sha2564(canonicalJson2(await readGraphLock(graphLock.path))) : null;
13368
+ const digest2 = raw ? sha2565(canonicalJson2(await readGraphLock(graphLock.path))) : null;
12500
13369
  if (digest2 !== graphLock.beforeDigest && digest2 !== graphLock.afterDigest) {
12501
13370
  throw new Error(`Cannot recover fleet normalization because graph lock changed outside the transaction: ${graphLock.path}`);
12502
13371
  }
@@ -12549,10 +13418,10 @@ function packageMap(config) {
12549
13418
  return result;
12550
13419
  }
12551
13420
  function configRevision(config) {
12552
- return sha2564(canonicalJson2(config));
13421
+ return sha2565(canonicalJson2(config));
12553
13422
  }
12554
13423
  function normalizationJournalPath(sourceRoot, destinationFleet) {
12555
- return join44(sourceRoot, ".agentwheel", `fleet-normalize-${destinationFleet}.journal.json`);
13424
+ return join45(sourceRoot, ".agentwheel", `fleet-normalize-${destinationFleet}.journal.json`);
12556
13425
  }
12557
13426
  function canonicalJson2(value) {
12558
13427
  return JSON.stringify(sortValue(value));
@@ -12564,8 +13433,8 @@ function sortValue(value) {
12564
13433
  }
12565
13434
  return value;
12566
13435
  }
12567
- function sha2564(value) {
12568
- return createHash13("sha256").update(value).digest("hex");
13436
+ function sha2565(value) {
13437
+ return createHash14("sha256").update(value).digest("hex");
12569
13438
  }
12570
13439
  function assertDigest(value) {
12571
13440
  if (!/^[a-f0-9]{64}$/.test(value)) throw new Error(`Invalid plan digest: ${value}`);
@@ -12603,6 +13472,7 @@ var commandGovernance = {
12603
13472
  "package migrate": "governed-always",
12604
13473
  remember: "governed-always",
12605
13474
  "ownership handoff": "governed-unless-dry-run",
13475
+ "ownership recovery-plan": "incidental-cache",
12606
13476
  "ownership retire-stale": "governed-apply",
12607
13477
  "mcp retire": "governed-apply",
12608
13478
  eject: "governed-always",
@@ -12709,7 +13579,7 @@ fleetCommand.command("normalize").description("plan or apply duplicate desired-s
12709
13579
  });
12710
13580
  cacheCommand.command("prune").description("remove old Git source snapshots while preserving locked commits").option("--user", "use the user workspace", false).option("--local", "use the nearest local workspace", false).option("--fleet <id>", "use one registered named fleet").option("-t, --target-root <path>", "explicit workspace root").option("--keep <count>", "newest snapshots to retain per source", parsePositiveInteger, 3).option("--apply", "delete the selected snapshots; without this flag only preview", false).action(async (options) => {
12711
13581
  const targetRoot = await workspaceContextRootFromOptions(normalizeRuntimeScopeOptions(options));
12712
- const cacheRoot = join45(targetRoot, ".agentwheel", "cache");
13582
+ const cacheRoot = join46(targetRoot, ".agentwheel", "cache");
12713
13583
  const result = await pruneGitCache(cacheRoot, { keepSnapshots: options.keep, dryRun: !options.apply });
12714
13584
  const verb = options.apply ? "Removed" : "Would remove";
12715
13585
  for (const path of result.removedPaths) console.log(`${verb} ${path}`);
@@ -12727,7 +13597,7 @@ program.command("list").description("list artifacts exposed by a package source"
12727
13597
  const resolvedInput = await resolvePackageSource(source, targetRoot);
12728
13598
  const selectedArtifacts = selectedArtifactsFromOptionsOrRegistry(options, resolvedInput.registryEntry);
12729
13599
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
12730
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
13600
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join46(targetRoot, ".agentwheel", "cache") }))));
12731
13601
  try {
12732
13602
  const artifacts = filterArtifactsBySelection(await driver.list(resolved), selectedArtifacts);
12733
13603
  for (const artifact of artifacts) {
@@ -12796,7 +13666,7 @@ program.command("try").description("read and validate one skill for the current
12796
13666
  const resolvedInput = await resolvePackageSource(source, targetRoot);
12797
13667
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
12798
13668
  const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, {
12799
- cacheRoot: join45(targetRoot, ".agentwheel", "cache")
13669
+ cacheRoot: join46(targetRoot, ".agentwheel", "cache")
12800
13670
  }))));
12801
13671
  try {
12802
13672
  const trial = await createSkillTrial(driver, resolved, selectedArtifactsFromOptionsOrRegistry(options, resolvedInput.registryEntry));
@@ -12818,7 +13688,7 @@ program.command("scan").description("scan a package source for validation findin
12818
13688
  const targetRoot = await workspaceContextRootFromOptions(normalizeRuntimeScopeOptions(options));
12819
13689
  const resolvedInput = await resolvePackageSource(source, targetRoot);
12820
13690
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
12821
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
13691
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join46(targetRoot, ".agentwheel", "cache") }))));
12822
13692
  try {
12823
13693
  const result = await driver.scan(resolved);
12824
13694
  if (result.findings.length === 0) {
@@ -12918,11 +13788,11 @@ program.command("deps").description("inspect the OpenPack dependency graph").add
12918
13788
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
12919
13789
  const targets = await resolveCliTargets(normalizedOptions);
12920
13790
  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));
13791
+ const state = await resolveCliTargetState(target, normalizedOptions);
13792
+ if (!state.graphLock) {
13793
+ throw new Error(`No graph lock for ${state.adapter.name} at ${target.targetRoot}: ${state.stableGraphLockPath}`);
13794
+ }
13795
+ console.log(formatDepsWhy(state.graphLock, state.manifest, selector));
12926
13796
  }
12927
13797
  })
12928
13798
  );
@@ -13013,19 +13883,23 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
13013
13883
  throw new Error(`Ownership handoff selector must be exactly type/name: ${selector}`);
13014
13884
  }
13015
13885
  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);
13886
+ const state = await resolveCliTargetState(target, normalizedOptions);
13887
+ const { adapter, installationType } = state;
13888
+ if (state.migration) {
13889
+ throw new Error(
13890
+ "Migrate the target state before ownership handoff by running a full agentwheel install for this target."
13891
+ );
13892
+ }
13020
13893
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13021
13894
  if (options.toFleet) {
13022
13895
  const fleet = await showRegisteredFleet(options.toFleet);
13023
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
13896
+ if (resolve26(fleet.root) !== resolve26(toWorkspaceRoot)) {
13024
13897
  throw new Error(`Destination fleet '${options.toFleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13025
13898
  }
13026
13899
  }
13027
13900
  const request = {
13028
- ...state,
13901
+ installationType,
13902
+ stateKey: state.stableStateKey,
13029
13903
  targetRoot: state.installRoot,
13030
13904
  adapter: adapter.name,
13031
13905
  artifactType,
@@ -13045,11 +13919,85 @@ ownershipCommand.command("handoff").description("transfer one managed artifact b
13045
13919
  console.log(`Manifest revision: ${result.manifestRevision}`);
13046
13920
  console.log(`Owner: ${result.fromOwner} -> ${result.toOwner}`);
13047
13921
  });
13922
+ ownershipCommand.command("recovery-plan").description("write a read-only per-path legacy ownership evidence report").requiredOption("--fleet <id>", "registered Fleet containing the current source graph").requiredOption("--report <path>", "new private JSON report path").option("--agent <name>", "one named Fleet target").option("--profile <name>", "Fleet profile resolving to one target").action(async (options) => {
13923
+ const targets = await resolveCliTargets({ fleet: options.fleet, agent: options.agent, profile: options.profile });
13924
+ if (targets.length !== 1) throw new Error(`Recovery planning requires one Fleet target, found ${targets.length}.`);
13925
+ const target = targets[0];
13926
+ if (target.fleetId !== options.fleet) throw new Error("Resolved target does not belong to the selected Fleet.");
13927
+ const adapter = await resolveAdapterForTarget(target, {});
13928
+ const transport = transportForTarget(target);
13929
+ const requestedReport = normalizeCliPath(options.report);
13930
+ const reportPath = join46(await realpath2(dirname23(requestedReport)), basename21(requestedReport));
13931
+ const reportInsideTarget = relative10(resolve26(target.targetRoot), reportPath);
13932
+ if (reportInsideTarget === "" || !reportInsideTarget.startsWith("..") && !isAbsolute4(reportInsideTarget)) {
13933
+ throw new Error("Recovery report must be outside the runtime target root.");
13934
+ }
13935
+ const installationType = target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
13936
+ const config = await readMergedWorkspaceConfig(target.workspaceRoot);
13937
+ if (config.packages.length === 0) throw new Error("Fleet has no configured source packages.");
13938
+ const stage = await mkdtemp4(join46(tmpdir5(), "agentwheel-recovery-"));
13939
+ const priorTmpdir = process.env.TMPDIR;
13940
+ try {
13941
+ process.env.TMPDIR = stage;
13942
+ const graphPlan = await createGraphSourcePlan({
13943
+ roots: config.packages.map((pkg) => ({
13944
+ rootId: pkg.name,
13945
+ source: pkg.source,
13946
+ mode: pkg.mode,
13947
+ version: pkg.version,
13948
+ ref: pkg.requestedRef,
13949
+ select: pkg.selection ? void 0 : normalizeArtifactSelectors(pkg.select, pkg.skills),
13950
+ selection: pkg.selection,
13951
+ aliases: pkg.aliases,
13952
+ overrides: pkg.overrides,
13953
+ includeSuggestions: pkg.withSuggestions,
13954
+ suggestionAliases: pkg.suggestions,
13955
+ useLock: false
13956
+ })),
13957
+ targetRoot: target.targetRoot,
13958
+ workspaceRoot: target.workspaceRoot,
13959
+ fleetId: target.fleetId,
13960
+ adapter,
13961
+ transport,
13962
+ targetKey: targetKeyForTarget(target, adapter.name),
13963
+ targetFingerprintParts: targetFingerprintParts(target, adapter, adapterOptionsForTarget(target, {}), installationType),
13964
+ installationType,
13965
+ stateKey: target.stateKey,
13966
+ readOnly: true,
13967
+ yes: true,
13968
+ freshGraphOnly: true,
13969
+ deferForeignStateCheck: true,
13970
+ cacheRoot: join46(stage, "source-cache"),
13971
+ registryCachePath: join46(stage, "registry.json")
13972
+ });
13973
+ const installRoot = installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, transport.kind === "ssh");
13974
+ const report = await planLegacyRecovery({
13975
+ graphPlan,
13976
+ installRoot,
13977
+ workspaceRoot: target.workspaceRoot,
13978
+ fleetId: target.fleetId,
13979
+ explicitStateKey: target.stateKey,
13980
+ targetKey: targetKeyForTarget(target, adapter.name),
13981
+ adapter: adapter.name,
13982
+ installationType,
13983
+ historicalLocks: await inventoryHistoricalLocks(target.workspaceRoot),
13984
+ transport
13985
+ });
13986
+ await writeFile13(reportPath, `${JSON.stringify(report, null, 2)}
13987
+ `, { mode: 384, flag: "wx" });
13988
+ console.log(`Recovery report: ${reportPath}`);
13989
+ console.log(JSON.stringify(report.counts));
13990
+ } finally {
13991
+ if (priorTmpdir === void 0) delete process.env.TMPDIR;
13992
+ else process.env.TMPDIR = priorTmpdir;
13993
+ await rm10(stage, { recursive: true, force: true });
13994
+ }
13995
+ });
13048
13996
  ownershipCommand.command("retire-stale").description("retire stale source-manifest ownership already covered by one Fleet manifest").requiredOption("--from-workspace-root <path>", "exact stale workspace owner root").requiredOption("--to-workspace-root <path>", "registered destination Fleet root").requiredOption("--source-state-key <key>", "exact source install-manifest state key").requiredOption("--fleet <id>", "registered destination Fleet").option("--adapter <adapter>", "built-in adapter").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named destination Fleet agent").option("--profile <name>", "destination Fleet profile resolving to one target").option("--plan-digest <sha256>", "reviewed plan digest; required with --apply").option("--expected-source-revision <sha256>", "reviewed source manifest revision; required with --apply").option("--expected-destination-revision <sha256>", "reviewed destination manifest revision; required with --apply").option("--expected-inventory-revision <sha256>", "reviewed runtime manifest inventory revision; required with --apply").option("--abandon-incomplete-merge-owner", "explicitly retire incomplete legacy merge metadata without changing runtime bytes", false).option("--apply", "apply the exact reviewed metadata-only retirement", false).option("--json", "print the complete deterministic plan as JSON", false).action(async (options) => {
13049
13997
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
13050
13998
  const fleet = await showRegisteredFleet(options.fleet);
13051
13999
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
13052
- if (resolve24(fleet.root) !== resolve24(toWorkspaceRoot)) {
14000
+ if (resolve26(fleet.root) !== resolve26(toWorkspaceRoot)) {
13053
14001
  throw new Error(`Destination Fleet '${options.fleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
13054
14002
  }
13055
14003
  const targets = await resolveCliTargets(normalizedOptions);
@@ -13057,19 +14005,22 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
13057
14005
  throw new Error(`Stale ownership retirement requires exactly one destination Fleet target, found ${targets.length}.`);
13058
14006
  }
13059
14007
  const target = targets[0];
13060
- if (target.fleetId !== options.fleet || resolve24(target.workspaceRoot) !== resolve24(fleet.root)) {
14008
+ if (target.fleetId !== options.fleet || resolve26(target.workspaceRoot) !== resolve26(fleet.root)) {
13061
14009
  throw new Error(`Resolved target does not belong to destination Fleet '${options.fleet}'.`);
13062
14010
  }
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);
14011
+ const state = await resolveCliTargetState(target, normalizedOptions);
14012
+ const { adapter, installationType } = state;
14013
+ if (state.migration) {
14014
+ throw new Error(
14015
+ "Migrate the target state before ownership retirement by running a full agentwheel install for this target."
14016
+ );
14017
+ }
13067
14018
  const request = {
13068
14019
  targetRoot: state.installRoot,
13069
14020
  adapter: adapter.name,
13070
14021
  installationType,
13071
14022
  sourceStateKey: options.sourceStateKey,
13072
- destinationStateKey: state.stateKey,
14023
+ destinationStateKey: state.stableStateKey,
13073
14024
  fromWorkspaceRoot: normalizeCliPath(options.fromWorkspaceRoot),
13074
14025
  toWorkspaceRoot,
13075
14026
  toFleetId: fleet.id,
@@ -13154,13 +14105,18 @@ program.command("uninstall").description("remove configured packages or managed
13154
14105
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
13155
14106
  const transport = transportForTarget(target);
13156
14107
  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);
14108
+ const state = await resolveCliTargetState(target, normalizedOptions);
14109
+ const manifest = state.manifest;
13159
14110
  if (!manifest) {
13160
14111
  console.log(`No install manifest for ${adapter.name}/${installationType} at ${state.installRoot}`);
13161
14112
  continue;
13162
14113
  }
13163
- const plan = filterUninstallPlanBySelection(await createUninstallPlan(manifest), selectedArtifactsFromOptions(options));
14114
+ const plan = {
14115
+ ...filterUninstallPlanBySelection(await createUninstallPlan(manifest), selectedArtifactsFromOptions(options)),
14116
+ stateKey: state.stableStateKey,
14117
+ stateMigration: state.migration,
14118
+ targetStateFilePreconditions: state.targetStateFilePreconditions
14119
+ };
13164
14120
  console.log(formatPlan(plan));
13165
14121
  const result = await uninstall(plan, { dryRun: normalizedOptions.dryRun, force: normalizedOptions.force, transport });
13166
14122
  if (!normalizedOptions.dryRun) {
@@ -13207,7 +14163,7 @@ journalCommand.command("list").description("show pending apply journals for reso
13207
14163
  if (!journal) continue;
13208
14164
  pending += 1;
13209
14165
  console.log(`PENDING ${state.adapter.name}/${state.installationType} at ${state.installRoot}`);
13210
- console.log(` journal: ${join45(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
14166
+ console.log(` journal: ${join46(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
13211
14167
  console.log(` stateKey: ${state.state.stateKey}`);
13212
14168
  console.log(` createdAt: ${journal.createdAt}`);
13213
14169
  console.log(` updatedAt: ${journal.updatedAt}`);
@@ -13543,14 +14499,14 @@ async function packageEntryFromSource(source, targetRoot, options) {
13543
14499
  );
13544
14500
  }
13545
14501
  const bundle = options.deferGraphRendering ? await stageSourceRaw(driver, resolvedSource, {
13546
- cacheRoot: join45(targetRoot, ".agentwheel", "cache"),
14502
+ cacheRoot: join46(targetRoot, ".agentwheel", "cache"),
13547
14503
  mode: options.mode,
13548
14504
  ref: initialVersion?.ref,
13549
14505
  frozenLock: lockMode
13550
14506
  }) : await stageSource(driver, resolvedSource, {
13551
14507
  workspaceRoot: targetRoot,
13552
14508
  adapter,
13553
- cacheRoot: join45(targetRoot, ".agentwheel", "cache"),
14509
+ cacheRoot: join46(targetRoot, ".agentwheel", "cache"),
13554
14510
  mode: options.mode,
13555
14511
  ref: initialVersion?.ref,
13556
14512
  frozenLock: lockMode,
@@ -13622,15 +14578,12 @@ async function packageSelectsSkillForTarget(target, pkg, selector, options) {
13622
14578
  if (explicitSelection && !explicitSelection.some((candidate) => candidate === selector)) return false;
13623
14579
  const groups = /* @__PURE__ */ new Map();
13624
14580
  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);
14581
+ const state = await resolveCliTargetState(group.target, {
14582
+ ...options,
14583
+ installationType: group.installationType
14584
+ });
14585
+ if (state.graphLock) {
14586
+ const lock = state.graphLock;
13634
14587
  const root = lock.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
13635
14588
  if (root?.selected.includes(selector)) return true;
13636
14589
  }
@@ -13933,13 +14886,30 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
13933
14886
  const allPackages = [...group.packages, ...group.extraPackages];
13934
14887
  const groupHasScope = !scopedRootId || allPackages.some((pkg) => pkg.name === scopedRootId || pkg.source === targetOptions.scope);
13935
14888
  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);
14889
+ if (behavior.readOnly !== true && targetOptions.dryRun !== true) {
14890
+ const pendingState = await journalStateForTarget(group.target, {
14891
+ ...targetOptions,
14892
+ installationType: group.installationType
14893
+ });
14894
+ if (await readApplyJournal(
14895
+ pendingState.installRoot,
14896
+ pendingState.adapter.name,
14897
+ pendingState.transport,
14898
+ pendingState.state
14899
+ )) {
14900
+ await recoverPendingApply(
14901
+ pendingState.installRoot,
14902
+ pendingState.adapter.name,
14903
+ pendingState.transport,
14904
+ pendingState.state
14905
+ );
14906
+ }
14907
+ }
14908
+ const priorGroupState = await resolveCliTargetState(group.target, {
14909
+ ...targetOptions,
14910
+ installationType: group.installationType
14911
+ });
14912
+ const previousGroupLock = priorGroupState.graphLock;
13943
14913
  const dependencyUpdateRootNames = /* @__PURE__ */ new Set();
13944
14914
  if (scopedDependencyUpdate) {
13945
14915
  for (const pkg of group.packages) {
@@ -14049,14 +15019,14 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14049
15019
  expectedFromWorkspaceOwner: targetOptions.expectedFromWorkspaceOwner
14050
15020
  });
14051
15021
  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);
15022
+ const manifest = result.previousManifest;
15023
+ const previousLock = result.previousGraphLock;
15024
+ 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
15025
  if (targetOptions.focusedArtifact && targetOptions.forceForeignState !== true) {
14056
15026
  const focusedPaths = scopedResult.plan.operations.filter((operation) => operationMatchesFocusedArtifact(
14057
15027
  operation,
14058
15028
  targetOptions.focusedArtifact,
14059
- focusedArtifactOwnerKeys(result.bundle.graphLock, previousGroupLock, scopedRootId, targetOptions.focusedArtifact)
15029
+ focusedArtifactOwnerKeys(result.bundle.graphLock, previousLock, scopedRootId, targetOptions.focusedArtifact)
14060
15030
  )).map((operation) => operation.relativeDestPath);
14061
15031
  const ownership = await resolveWorkspaceOwnershipScope(group.target.workspaceRoot, {
14062
15032
  fleetId: group.target.fleetId
@@ -14070,9 +15040,11 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
14070
15040
  }
14071
15041
  results.push(scopedResult);
14072
15042
  } 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);
15043
+ const manifest = result.previousManifest;
15044
+ const previousLock = result.previousGraphLock;
15045
+ if (!previousLock) {
15046
+ throw new Error(`Dependency update requires an existing graph lock for ${adapter.name} at ${group.target.targetRoot}.`);
15047
+ }
14076
15048
  results.push(scopeUpdatePlanToDependencies(result, dependencyUpdateSelectors, previousLock, manifest));
14077
15049
  } else {
14078
15050
  results.push(result);
@@ -14115,7 +15087,7 @@ function scopeUpdatePlanToDependencies(result, selectors, previousLock, manifest
14115
15087
  selectedPreviousNodeIds,
14116
15088
  selectedRootIds
14117
15089
  );
14118
- const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
15090
+ const graphLockDigest = createHash15("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14119
15091
  return {
14120
15092
  ...result,
14121
15093
  bundle: { ...result.bundle, graphLock },
@@ -14241,7 +15213,7 @@ function operationBelongsToDependencyUpdate(operation, allowedNodeIds, allowedNa
14241
15213
  function scopeInstallPlanToArtifact(result, rootId, focused, manifest) {
14242
15214
  const scoped = scopePlanOperationsToArtifact(result, rootId, focused, manifest);
14243
15215
  const graphLock = focusedGraphLockForInstall(scoped.bundle.graphLock, rootId, focused);
14244
- const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
15216
+ const graphLockDigest = createHash15("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14245
15217
  return {
14246
15218
  ...scoped,
14247
15219
  bundle: { ...scoped.bundle, graphLock },
@@ -14252,7 +15224,7 @@ function scopeInstallPlanToArtifact(result, rootId, focused, manifest) {
14252
15224
  function scopeUpdatePlanToArtifact(result, rootId, focused, previousLock, manifest) {
14253
15225
  const scoped = scopePlanOperationsToArtifact(result, rootId, focused, manifest, previousLock);
14254
15226
  const graphLock = mergeFocusedArtifactGraphLock(scoped.bundle.graphLock, previousLock, rootId, focused);
14255
- const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
15227
+ const graphLockDigest = createHash15("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14256
15228
  return {
14257
15229
  ...scoped,
14258
15230
  bundle: { ...scoped.bundle, graphLock },
@@ -14547,7 +15519,7 @@ function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
14547
15519
  selectedPreviousNodeIds,
14548
15520
  /* @__PURE__ */ new Set([rootId])
14549
15521
  );
14550
- const graphLockDigest = createHash14("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
15522
+ const graphLockDigest = createHash15("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
14551
15523
  return {
14552
15524
  ...scoped,
14553
15525
  bundle: { ...scoped.bundle, graphLock },
@@ -14619,7 +15591,7 @@ function keepManifestEntryOperation(entry, targetRoot, scopeDescription, operati
14619
15591
  artifactType: entry.artifactType,
14620
15592
  artifactName: entry.artifactName,
14621
15593
  kind: entry.kind,
14622
- destPath: operation?.destPath ?? join45(targetRoot, entry.path),
15594
+ destPath: operation?.destPath ?? join46(targetRoot, entry.path),
14623
15595
  relativeDestPath: entry.path,
14624
15596
  desiredHash: entry.sourceHash,
14625
15597
  currentHash: operation?.currentHash ?? entry.hash,
@@ -14665,8 +15637,12 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14665
15637
  const removedInstallationType = options.installationType ?? pkg.installationType ?? removedTarget.installationType ?? "local";
14666
15638
  const adapter = await resolveAdapterForTarget(removedTarget, removedAdapterOptions);
14667
15639
  const transport = transportForTarget(removedTarget);
14668
- const removedState = installStateForTarget(removedTarget, adapter, removedAdapterOptions, removedInstallationType);
14669
- const manifest = await readInstallManifest(removedState.installRoot, adapter.name, transport, removedState);
15640
+ const removedState = await resolveCliTargetState(removedTarget, {
15641
+ ...options,
15642
+ ...removedAdapterOptions,
15643
+ installationType: removedInstallationType
15644
+ });
15645
+ const manifest = removedState.manifest;
14670
15646
  if (!manifest) {
14671
15647
  console.log(`No install manifest for ${adapter.name} at ${removedTarget.targetRoot}`);
14672
15648
  continue;
@@ -14714,25 +15690,24 @@ async function uninstallConfiguredPackage(target, packageName, options) {
14714
15690
  remainingDesired = desiredArtifactsFromGraphBundle(result2.bundle);
14715
15691
  renderedRoot = result2.bundle.root;
14716
15692
  }
14717
- const plan = await createOwnershipUninstallPlan(manifest, remainingDesired, adapter, transport, { graphLockDigest: remainingGraphPlan?.graphLockDigest });
15693
+ const uninstallPlan = await createOwnershipUninstallPlan(manifest, remainingDesired, adapter, transport, {
15694
+ graphLockDigest: remainingGraphPlan?.graphLockDigest
15695
+ });
15696
+ const transitionPlan = remainingGraphPlan?.plan;
15697
+ const plan = {
15698
+ ...uninstallPlan,
15699
+ stateKey: transitionPlan?.stateKey ?? removedState.stableStateKey,
15700
+ stateMigration: transitionPlan?.stateMigration ?? removedState.migration,
15701
+ targetStateFilePreconditions: transitionPlan?.targetStateFilePreconditions ?? removedState.targetStateFilePreconditions
15702
+ };
14718
15703
  console.log(`Uninstall ${pkg.name} (${adapter.name} at ${removedTarget.targetRoot}):`);
14719
15704
  console.log(formatPlan(plan));
14720
15705
  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
- )
15706
+ removeGraphLockPath: removedState.graphLockPath ?? removedState.stableGraphLockPath
14727
15707
  };
14728
15708
  if (!options.dryRun) {
14729
15709
  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
- );
15710
+ const declarativeGraphPath = remainingGraphPlan ? remainingGraphPlan.graphLockPath : removedState.graphLockPath ?? removedState.stableGraphLockPath;
14736
15711
  declareMutationPath(declarativeGraphPath);
14737
15712
  }
14738
15713
  const result = await uninstall(plan, {
@@ -14807,36 +15782,92 @@ function targetFingerprintParts(target, adapter, options, installationType) {
14807
15782
  };
14808
15783
  }
14809
15784
  function installStateForTarget(target, adapter, options, installationType) {
14810
- const targetFingerprint = targetFingerprintDigest(target, adapter, options, installationType);
15785
+ const installRoot = installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, target.transport === "ssh");
15786
+ const targetIdentity = resolveTargetStateIdentity({
15787
+ targetFingerprintParts: targetFingerprintParts(target, adapter, options, installationType),
15788
+ workspaceRoot: target.workspaceRoot,
15789
+ targetKey: targetKeyForTarget(target, adapter.name),
15790
+ resolvedInstallRoot: installRoot,
15791
+ transportKind: target.transport
15792
+ });
14811
15793
  return {
14812
15794
  installationType,
14813
15795
  stateKey: stateKeyFor(adapter.name, {
14814
15796
  installationType,
14815
15797
  stateKey: target.stateKey,
14816
- targetFingerprint,
15798
+ targetFingerprint: targetIdentity.stateFingerprint,
14817
15799
  fleetId: target.fleetId
14818
15800
  }),
14819
- installRoot: installRootForAdapterInstallationType(adapter, target.targetRoot, installationType, target.transport === "ssh")
15801
+ installRoot
14820
15802
  };
14821
15803
  }
14822
- function targetFingerprintDigest(target, adapter, options, installationType) {
14823
- const fingerprintInput = targetFingerprintParts(target, adapter, options, installationType);
14824
- return computeTargetFingerprint(fingerprintInput);
15804
+ function targetIdentityContext(target, adapter, installationType) {
15805
+ return {
15806
+ resolvedInstallRoot: installRootForAdapterInstallationType(
15807
+ adapter,
15808
+ target.targetRoot,
15809
+ installationType,
15810
+ target.transport === "ssh"
15811
+ ),
15812
+ transportKind: target.transport
15813
+ };
14825
15814
  }
14826
- async function readTargetGraphLock(target, options) {
15815
+ async function resolveCliTargetState(target, options) {
14827
15816
  const adapterOptions = adapterOptionsForTarget(target, options);
14828
15817
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15818
+ const transport = transportForTarget(target);
14829
15819
  const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
14830
- const path = graphLockPathForTarget(
15820
+ const stable = installStateForTarget(target, adapter, adapterOptions, installationType);
15821
+ const stableGraphLockPath = graphLockPathForTarget(
14831
15822
  target.workspaceRoot,
14832
15823
  targetKeyForTarget(target, adapter.name),
14833
15824
  adapter.name,
14834
- targetFingerprintParts(target, adapter, adapterOptions, installationType)
15825
+ targetFingerprintParts(target, adapter, adapterOptions, installationType),
15826
+ targetIdentityContext(target, adapter, installationType)
14835
15827
  );
14836
- if (!await pathExists(path)) {
14837
- throw new Error(`No graph lock for ${adapter.name} at ${target.targetRoot}: ${path}`);
15828
+ const [stableManifest, stableLock, stableSourceLock, ownership] = await Promise.all([
15829
+ readInstallManifest(stable.installRoot, adapter.name, transport, stable),
15830
+ pathExists(stableGraphLockPath).then((exists) => exists ? readGraphLock(stableGraphLockPath) : void 0),
15831
+ readSourceLock(stable.installRoot, adapter.name, transport, stable),
15832
+ resolveWorkspaceOwnershipScope(target.workspaceRoot, { fleetId: target.fleetId })
15833
+ ]);
15834
+ const prior = await resolvePriorTargetState({
15835
+ adapter: adapter.name,
15836
+ installationType,
15837
+ stateKey: stable.stateKey,
15838
+ explicitStateKey: target.stateKey,
15839
+ fleetId: target.fleetId,
15840
+ installRoot: stable.installRoot,
15841
+ workspaceOwner: workspaceOwnerForRoot(ownership.root, ownership.fleetId),
15842
+ graphLockPath: stableGraphLockPath,
15843
+ stableManifest,
15844
+ stableLock,
15845
+ transport
15846
+ });
15847
+ return {
15848
+ adapter,
15849
+ installationType,
15850
+ installRoot: stable.installRoot,
15851
+ stateKey: prior.migration?.fromStateKey ?? stable.stateKey,
15852
+ stableStateKey: stable.stateKey,
15853
+ stableGraphLockPath,
15854
+ manifest: prior.manifest,
15855
+ graphLock: prior.graphLock,
15856
+ graphLockPath: prior.graphLock ? prior.migration?.fromGraphLockPath ?? stableGraphLockPath : null,
15857
+ migration: prior.migration,
15858
+ targetStateFilePreconditions: {
15859
+ graphLockPath: stableGraphLockPath,
15860
+ graphLockRevision: stableLock ? createHash15("sha256").update(canonicalGraphLockJson(stableLock)).digest("hex") : null,
15861
+ sourceLockRevision: computeSourceLockRevision(stableSourceLock)
15862
+ }
15863
+ };
15864
+ }
15865
+ async function readTargetGraphLock(target, options) {
15866
+ const state = await resolveCliTargetState(target, options);
15867
+ if (!state.graphLock || !state.graphLockPath) {
15868
+ throw new Error(`No graph lock for ${state.adapter.name} at ${target.targetRoot}: ${state.stableGraphLockPath}`);
14838
15869
  }
14839
- return { adapter, path, lock: await readGraphLock(path) };
15870
+ return { adapter: state.adapter, path: state.graphLockPath, lock: state.graphLock };
14840
15871
  }
14841
15872
  async function printStatus(target, options) {
14842
15873
  const report = await collectTargetStatus(target, options);
@@ -14844,30 +15875,24 @@ async function printStatus(target, options) {
14844
15875
  }
14845
15876
  async function collectTargetStatus(target, options) {
14846
15877
  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
- }
15878
+ const state = await resolveCliTargetState(target, { ...options, warn: () => void 0 });
15879
+ const { adapter, installationType, manifest, graphLock, graphLockPath } = state;
15880
+ const pending = await collectPendingInstallWork(target, options);
15881
+ const foreignObservations = pending.foreignStateObservations;
15882
+ const effectiveGraphLock = graphLock ?? pending.observedGraphLock;
15883
+ const observedManifestRevision = foreignObservations.length > 0 ? createHash15("sha256").update(JSON.stringify(foreignObservations.map((observation) => [
15884
+ observation.fileName,
15885
+ observation.manifestRevision
15886
+ ]).sort())).digest("hex") : null;
14862
15887
  const packages = [];
14863
15888
  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;
15889
+ const root = effectiveGraphLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
15890
+ const node = root ? effectiveGraphLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId) : void 0;
14866
15891
  const availability = await discoverPackageVersions(pkg, target.workspaceRoot, {
14867
15892
  forceRefresh: options.refresh,
14868
15893
  offline: options.offline
14869
15894
  });
14870
- const installed = node && manifest?.entries.some((entry) => "graphNodeId" in entry && entry.graphNodeId === node.id) ? node.version : null;
15895
+ 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
15896
  const locked = node?.version ?? null;
14872
15897
  const baseline = installed ?? locked;
14873
15898
  packages.push({
@@ -14890,14 +15915,13 @@ async function collectTargetStatus(target, options) {
14890
15915
  )
14891
15916
  });
14892
15917
  }
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) => {
15918
+ const artifacts = (effectiveGraphLock?.canonical.artifacts ?? []).map((artifact) => {
15919
+ const node = effectiveGraphLock?.canonical.nodes.find((candidate) => candidate.id === artifact.graphNodeId);
15920
+ const installed = (manifest?.entries.some((entry) => {
14897
15921
  if (!("graphNodeId" in entry) || entry.graphNodeId !== artifact.graphNodeId) return false;
14898
15922
  if ("logicalSelector" in entry && entry.logicalSelector) return entry.logicalSelector === artifact.logicalSelector;
14899
15923
  return entry.artifactType === artifact.type && entry.artifactName === artifact.name;
14900
- }) ?? false;
15924
+ }) ?? false) || foreignObservations.some((observation) => observation.graphNodeId === artifact.graphNodeId && (!observation.logicalSelector || observation.logicalSelector === artifact.logicalSelector));
14901
15925
  return {
14902
15926
  selector: artifact.logicalSelector,
14903
15927
  type: artifact.type,
@@ -14910,7 +15934,7 @@ async function collectTargetStatus(target, options) {
14910
15934
  };
14911
15935
  });
14912
15936
  const health = [];
14913
- if (!manifest || !graphLock) health.push("FAIL");
15937
+ if (!manifest && foreignObservations.length === 0 || !effectiveGraphLock) health.push("FAIL");
14914
15938
  if (pending.error) health.push("DEGRADED");
14915
15939
  if (pending.driftCount > 0 || pending.conflictCount > 0) health.push("FAIL");
14916
15940
  else if (pending.pendingCount > 0) health.push("WARN");
@@ -14921,11 +15945,11 @@ async function collectTargetStatus(target, options) {
14921
15945
  installationType,
14922
15946
  targetRoot: state.installRoot,
14923
15947
  health: worstStatusHealth(health),
14924
- manifestRevision: manifest?.revision ?? null,
14925
- manifestEntryCount: manifest?.entries.length ?? 0,
15948
+ manifestRevision: manifest?.revision ?? observedManifestRevision,
15949
+ manifestEntryCount: (manifest?.entries.length ?? 0) + foreignObservations.length,
14926
15950
  graphLockPath,
14927
15951
  packageCount: packages.length,
14928
- artifactCount: graphLock?.canonical.artifacts.length ?? 0,
15952
+ artifactCount: effectiveGraphLock?.canonical.artifacts.length ?? 0,
14929
15953
  pendingCount: pending.pendingCount,
14930
15954
  driftCount: pending.driftCount,
14931
15955
  conflictCount: pending.conflictCount,
@@ -15103,7 +16127,7 @@ async function assertCompositeMutationSupported(workspaceRoot, profile, readOnly
15103
16127
  `Composite-profile mutation is refused before member execution because ${member.id} uses ${member.transport}; durable remote policy and recovery preflight is not implemented.`
15104
16128
  );
15105
16129
  }
15106
- const memberRoot = resolve24(workspaceRoot, member.workspace);
16130
+ const memberRoot = resolve26(workspaceRoot, member.workspace);
15107
16131
  const memberPolicy = await mutationPolicyForWorkspace(memberRoot);
15108
16132
  if (memberPolicy?.revisioning.mode === "commit-after-verify") {
15109
16133
  throw new Error(
@@ -15260,9 +16284,34 @@ async function journalStateForTarget(target, options) {
15260
16284
  const adapterOptions = adapterOptionsForTarget(target, options);
15261
16285
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
15262
16286
  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 };
16287
+ const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter, void 0);
16288
+ const stable = installStateForTarget(target, adapter, adapterOptions, installationType);
16289
+ const graphLockPath = graphLockPathForTarget(
16290
+ target.workspaceRoot,
16291
+ targetKeyForTarget(target, adapter.name),
16292
+ adapter.name,
16293
+ targetFingerprintParts(target, adapter, adapterOptions, installationType),
16294
+ targetIdentityContext(target, adapter, installationType)
16295
+ );
16296
+ const ownership = await resolveWorkspaceOwnershipScope(target.workspaceRoot, { fleetId: target.fleetId });
16297
+ const pending = await discoverTargetApplyJournal({
16298
+ adapter: adapter.name,
16299
+ installationType,
16300
+ stateKey: stable.stateKey,
16301
+ explicitStateKey: target.stateKey,
16302
+ fleetId: target.fleetId,
16303
+ installRoot: stable.installRoot,
16304
+ workspaceOwner: workspaceOwnerForRoot(ownership.root, ownership.fleetId),
16305
+ graphLockPath,
16306
+ transport
16307
+ });
16308
+ return {
16309
+ adapter,
16310
+ transport,
16311
+ installationType,
16312
+ installRoot: stable.installRoot,
16313
+ state: { installationType, stateKey: pending?.stateKey ?? stable.stateKey }
16314
+ };
15266
16315
  }
15267
16316
  async function collectPendingInstallWork(target, options) {
15268
16317
  let results = [];
@@ -15283,11 +16332,20 @@ async function collectPendingInstallWork(target, options) {
15283
16332
  pendingCount: pending.length,
15284
16333
  driftCount: counts.drift ?? 0,
15285
16334
  conflictCount: counts.conflict ?? 0,
15286
- counts
16335
+ counts,
16336
+ foreignStateObservations: results.flatMap((result) => result.foreignStateObservations),
16337
+ observedGraphLock: results.length === 1 ? results[0].bundle.graphLock : void 0
15287
16338
  };
15288
16339
  } catch (error) {
15289
16340
  const message = error instanceof Error ? error.message : String(error);
15290
- return { pendingCount: 0, driftCount: 0, conflictCount: 0, counts: {}, error: message };
16341
+ return {
16342
+ pendingCount: 0,
16343
+ driftCount: 0,
16344
+ conflictCount: 0,
16345
+ counts: {},
16346
+ foreignStateObservations: [],
16347
+ error: message
16348
+ };
15291
16349
  } finally {
15292
16350
  await Promise.all(results.map((result) => rm10(result.bundle.root, { recursive: true, force: true })));
15293
16351
  }
@@ -15300,18 +16358,17 @@ async function printDoctor(target, options) {
15300
16358
  if (!targetMapping?.enabled) {
15301
16359
  throw new Error(`Adapter ${adapter.name} does not support skills for installation type '${installationType}'.`);
15302
16360
  }
15303
- const transport = transportForTarget(target);
15304
- const state = installStateForTarget(target, adapter, adapterOptions, installationType);
15305
- const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
16361
+ const state = await resolveCliTargetState(target, { ...options, installationType });
16362
+ const manifest = state.manifest;
15306
16363
  const requestedSkills = doctorSkillRequests(target, options);
15307
16364
  const skills = [];
15308
16365
  for (const request of requestedSkills) {
15309
- const skillPath = join45(state.installRoot, targetMapping.dest, request.name);
16366
+ const skillPath = join46(state.installRoot, targetMapping.dest, request.name);
15310
16367
  const exists = await pathExists(skillPath);
15311
16368
  const manifestEntry = manifest?.entries.find((entry) => {
15312
16369
  if (entry.artifactType !== "skills") return false;
15313
16370
  const legacyInstallName = "installName" in entry && typeof entry.installName === "string" ? entry.installName : void 0;
15314
- return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join45(targetMapping.dest, request.name);
16371
+ return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join46(targetMapping.dest, request.name);
15315
16372
  });
15316
16373
  const status = manifestEntry ? "managed" : exists ? "present-unmanaged" : "missing";
15317
16374
  skills.push({
@@ -15391,7 +16448,7 @@ function doctorSkillLabel(name) {
15391
16448
  return `${name} skill`;
15392
16449
  }
15393
16450
  function isSyncwheelWorkspace(targetRoot) {
15394
- return existsSync(join45(targetRoot, ".syncwheel", "manifest.json"));
16451
+ return existsSync(join46(targetRoot, ".syncwheel", "manifest.json"));
15395
16452
  }
15396
16453
  function skillInstallCommand(adapter, installationType, options, skill, behavior = {}) {
15397
16454
  const args = [
@@ -15554,14 +16611,14 @@ function looksLikeSourceSpecifier(value) {
15554
16611
  }
15555
16612
  function normalizeCliPath(value) {
15556
16613
  if (value === "~") return homedir12();
15557
- if (value.startsWith("~/")) return resolve24(homedir12(), value.slice(2));
15558
- return resolve24(value);
16614
+ if (value.startsWith("~/")) return resolve26(homedir12(), value.slice(2));
16615
+ return resolve26(value);
15559
16616
  }
15560
16617
  function shellQuoteArgument(value) {
15561
16618
  return /^[a-z0-9_./:@%+=,-]+$/i.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
15562
16619
  }
15563
16620
  function isHomePath(path) {
15564
- return resolve24(path) === resolve24(homedir12());
16621
+ return resolve26(path) === resolve26(homedir12());
15565
16622
  }
15566
16623
  function adapterListFromOption(adapter) {
15567
16624
  if (!adapter) return [];
@@ -15616,11 +16673,11 @@ function filterUninstallPlanBySelection(plan, selected) {
15616
16673
  };
15617
16674
  }
15618
16675
  async function initPackage(root) {
15619
- await mkdir16(join45(root, "instructions"), { recursive: true });
15620
- await mkdir16(join45(root, "rules"), { recursive: true });
15621
- await mkdir16(join45(root, "skills"), { recursive: true });
15622
- const manifestPath = join45(root, "openpack.json");
15623
- const instructionsPath = join45(root, "instructions", "AGENTS.md");
16676
+ await mkdir16(join46(root, "instructions"), { recursive: true });
16677
+ await mkdir16(join46(root, "rules"), { recursive: true });
16678
+ await mkdir16(join46(root, "skills"), { recursive: true });
16679
+ const manifestPath = join46(root, "openpack.json");
16680
+ const instructionsPath = join46(root, "instructions", "AGENTS.md");
15624
16681
  const manifest = {
15625
16682
  schemaVersion: CURRENT_OPENPACK_SCHEMA_VERSION,
15626
16683
  name: "example/agentwheel-package",
@@ -15638,7 +16695,7 @@ async function initPackage(root) {
15638
16695
  await writeFile13(instructionsPath, "# Agent Instructions\n", "utf8");
15639
16696
  }
15640
16697
  async function defaultBootstrapPackage(_root) {
15641
- const packageRoot = await findAgentwheelPackageRoot(dirname22(fileURLToPath3(import.meta.url)));
16698
+ const packageRoot = await findAgentwheelPackageRoot(dirname23(fileURLToPath3(import.meta.url)));
15642
16699
  if (!packageRoot) return void 0;
15643
16700
  return {
15644
16701
  name: "agentwheel",
@@ -15682,10 +16739,10 @@ function withFleetExample(config) {
15682
16739
  };
15683
16740
  }
15684
16741
  async function findAgentwheelPackageRoot(start) {
15685
- let current = resolve24(start);
16742
+ let current = resolve26(start);
15686
16743
  while (true) {
15687
16744
  if (await findPackageManifestPath(current, { warnLegacy: false })) return current;
15688
- const parent = dirname22(current);
16745
+ const parent = dirname23(current);
15689
16746
  if (parent === current) return void 0;
15690
16747
  current = parent;
15691
16748
  }
@@ -15810,14 +16867,14 @@ async function governedCommandContext(command, options) {
15810
16867
  const kind = String(command.args[0] ?? "workspace");
15811
16868
  return {
15812
16869
  workspaceRoot: workspaceRoot2,
15813
- anticipatedPaths: kind === "package" ? [join45(targetRoot, "openpack.json"), join45(targetRoot, "instructions", "AGENTS.md")] : [workspaceConfigPath(targetRoot)]
16870
+ anticipatedPaths: kind === "package" ? [join46(targetRoot, "openpack.json"), join46(targetRoot, "instructions", "AGENTS.md")] : [workspaceConfigPath(targetRoot)]
15814
16871
  };
15815
16872
  }
15816
16873
  if (path === "package migrate") {
15817
16874
  const packageRoot = normalizeTargetRoot(String(command.args[0] ?? "."));
15818
16875
  return {
15819
16876
  workspaceRoot: await findExistingWorkspaceRoot(packageRoot) ?? packageRoot,
15820
- anticipatedPaths: ["agentwheel.json", "agentwheel.jsonc", "openpack.json", "openpack.jsonc"].map((name) => join45(packageRoot, name))
16877
+ anticipatedPaths: ["agentwheel.json", "agentwheel.jsonc", "openpack.json", "openpack.jsonc"].map((name) => join46(packageRoot, name))
15821
16878
  };
15822
16879
  }
15823
16880
  const source = typeof command.args[0] === "string" ? command.args[0] : void 0;
@@ -15829,7 +16886,7 @@ async function governedCommandContext(command, options) {
15829
16886
  const anticipatedPaths = [];
15830
16887
  if (path === "add") anticipatedPaths.push(workspaceConfigPath(workspaceRoot));
15831
16888
  if (path === "remember" && options.runtime) {
15832
- anticipatedPaths.push(join45(workspaceRoot, ".agentwheel", "overlays", options.runtime, "instructions.local.md"));
16889
+ anticipatedPaths.push(join46(workspaceRoot, ".agentwheel", "overlays", options.runtime, "instructions.local.md"));
15833
16890
  }
15834
16891
  if (path === "uninstall" && source) anticipatedPaths.push(workspaceConfigPath(workspaceRoot));
15835
16892
  return {