agentwheel 0.22.1 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -546,6 +546,23 @@ agentwheel ownership retire-stale \
546
546
  --fleet <fleet-id> --agent <agent> --json
547
547
  ```
548
548
 
549
+ To use a target configured in a nested workspace while retiring ownership into its containing
550
+ registered Fleet, run from that workspace and select the destination owner separately:
551
+
552
+ ```bash
553
+ agentwheel ownership retire-stale --local --agent service-agent \
554
+ --to-fleet <fleet-id> --to-workspace-root <fleet-root> \
555
+ --from-workspace-root <stale-owner-root> \
556
+ --source-state-key <stale-state-key> \
557
+ --destination-state-key <fleet-owned-state-key> --json
558
+ ```
559
+
560
+ `--to-fleet` qualifies the destination owner; `--local`, `--user`, or `--fleet` selects the target
561
+ workspace and its configured transport. The selected workspace must be within the registered
562
+ Fleet root, including after symlink resolution, and cannot identify a different Fleet.
563
+ The original `--fleet <id>` command remains supported for targets configured directly at that
564
+ Fleet root. Neither form changes agent configuration or relaxes contribution and revision guards.
565
+
549
566
  The dry-run still proves exact destination coverage and runtime bytes. Apply only its emitted command,
550
567
  which includes the plan, source, destination, and manifest-inventory revisions.
551
568
  For duplicate state owned by the same registered Fleet, set both workspace roots to the Fleet root
package/dist/index.js CHANGED
@@ -797,11 +797,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
797
797
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
798
798
  ensurePropertyComplete(offset + length);
799
799
  },
800
- onSeparator: (sep12, offset, length) => {
800
+ onSeparator: (sep13, offset, length) => {
801
801
  if (currentParent.type === "property") {
802
- if (sep12 === ":") {
802
+ if (sep13 === ":") {
803
803
  currentParent.colonOffset = offset;
804
- } else if (sep12 === ",") {
804
+ } else if (sep13 === ",") {
805
805
  ensurePropertyComplete(offset);
806
806
  }
807
807
  }
@@ -14730,14 +14730,14 @@ var init_artifact = __esm({
14730
14730
  include: external_exports.string().min(1),
14731
14731
  markers: external_exports.boolean().optional(),
14732
14732
  optional: external_exports.boolean().optional()
14733
- });
14733
+ }).strict();
14734
14734
  packageCompositionRuleSchema = external_exports.object({
14735
14735
  target: external_exports.string().min(1),
14736
14736
  include: external_exports.string().min(1),
14737
14737
  exclude: external_exports.array(external_exports.string().min(1)).optional(),
14738
14738
  runtimes: external_exports.array(external_exports.string().min(1)).optional(),
14739
14739
  markers: external_exports.boolean().optional()
14740
- });
14740
+ }).strict();
14741
14741
  packageSupersedesEntrySchema = external_exports.object({
14742
14742
  package: external_exports.string().min(1),
14743
14743
  selector: external_exports.string().min(1),
@@ -20415,6 +20415,7 @@ var init_transaction = __esm({
20415
20415
 
20416
20416
  // src/install/toml-merge.ts
20417
20417
  import { mkdir as mkdir8, readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
20418
+ import { parse as parseToml } from "smol-toml";
20418
20419
  import { dirname as dirname13 } from "path";
20419
20420
  async function mergeCodexTomlMcp(sourcePath, destPath) {
20420
20421
  const source = JSON.parse(await readFile16(sourcePath, "utf8"));
@@ -20429,6 +20430,39 @@ function mismatchedCodexTomlMcpServers(source, currentContent) {
20429
20430
  const servers = extractMcpServers(source);
20430
20431
  return Object.entries(servers).filter(([name, server]) => extractMcpServerBlock(currentContent, name) !== formatMcpServer(name, server)).map(([name]) => name);
20431
20432
  }
20433
+ function mismatchedManagedCodexTomlMcpServers(source, currentContent) {
20434
+ const servers = extractMcpServers(source);
20435
+ let current;
20436
+ try {
20437
+ const parsed = parseToml(currentContent, { integersAsBigInt: "asNeeded" });
20438
+ current = isPlainRecord(parsed.mcp_servers) ? parsed.mcp_servers : void 0;
20439
+ } catch {
20440
+ return Object.keys(servers);
20441
+ }
20442
+ return Object.entries(servers).filter(([name, server]) => {
20443
+ try {
20444
+ const rendered = parseToml(formatMcpServer(name, server), { integersAsBigInt: "asNeeded" });
20445
+ const expected = isPlainRecord(rendered.mcp_servers) ? rendered.mcp_servers[name] : void 0;
20446
+ return !current || !Object.hasOwn(current, name) || !managedValueMatches(current[name], expected);
20447
+ } catch {
20448
+ return true;
20449
+ }
20450
+ }).map(([name]) => name);
20451
+ }
20452
+ function managedValueMatches(actual, expected) {
20453
+ if (Array.isArray(expected)) {
20454
+ return Array.isArray(actual) && actual.length === expected.length && expected.every((value, index) => managedValueMatches(actual[index], value));
20455
+ }
20456
+ if (isPlainRecord(expected)) {
20457
+ return isPlainRecord(actual) && Object.entries(expected).every(([key, value]) => Object.hasOwn(actual, key) && managedValueMatches(actual[key], value));
20458
+ }
20459
+ return actual === expected;
20460
+ }
20461
+ function isPlainRecord(value) {
20462
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
20463
+ const prototype = Object.getPrototypeOf(value);
20464
+ return prototype === null || prototype === Object.prototype;
20465
+ }
20432
20466
  function extractMcpServers(source) {
20433
20467
  const raw = isRecord2(source.mcpServers) ? source.mcpServers : source;
20434
20468
  const servers = {};
@@ -20617,8 +20651,18 @@ function assertExactMergeContribution(contribution, strategy, currentContent) {
20617
20651
  throw new MergeAdoptionMismatchError(`exact merge contribution differs or is missing at ${mismatch}`);
20618
20652
  }
20619
20653
  }
20654
+ function assertManagedMergeContribution(contribution, strategy, currentContent) {
20655
+ if (strategy !== "codex-toml-mcp") {
20656
+ assertExactMergeContribution(contribution, strategy, currentContent);
20657
+ return;
20658
+ }
20659
+ const mismatched = mismatchedManagedCodexTomlMcpServers(contribution, currentContent);
20660
+ if (mismatched.length > 0) {
20661
+ throw new MergeAdoptionMismatchError(`managed MCP fields differ or are missing for ${mismatched.join(", ")}`);
20662
+ }
20663
+ }
20620
20664
  async function assertMergedSourceContribution(sourcePath, strategy, currentContent) {
20621
- assertExactMergeContribution(await readMergeSource(sourcePath, strategy), strategy, currentContent);
20665
+ assertManagedMergeContribution(await readMergeSource(sourcePath, strategy), strategy, currentContent);
20622
20666
  }
20623
20667
  function assertExactMcpMergeContribution(removal, strategy, currentContent) {
20624
20668
  if (strategy === "codex-toml-mcp") {
@@ -21813,7 +21857,9 @@ async function applyOperation(operation, context) {
21813
21857
  if (!await transport.pathExists(operation.destPath)) {
21814
21858
  throw new Error(`Merge skip destination disappeared after planning: ${operation.relativeDestPath}`);
21815
21859
  }
21816
- assertExactMergeContribution(operation.mergeRemoval, operation.mergeStrategy, await transport.readFile(operation.destPath));
21860
+ const exactAdoption = operation.exactMergeAdoption === true || operation.reason === "force adopting exact unmanaged merge contribution" || operation.reason === "repairing exact incomplete merge ownership";
21861
+ const verify = exactAdoption ? assertExactMergeContribution : assertManagedMergeContribution;
21862
+ verify(operation.mergeRemoval, operation.mergeStrategy, await transport.readFile(operation.destPath));
21817
21863
  verifiedCurrentHash = await transport.hashPath(operation.destPath);
21818
21864
  } else if (!operation.semanticPlugin && !operation.programmaticOperation) {
21819
21865
  if (!await transport.pathExists(operation.destPath)) {
@@ -22448,7 +22494,7 @@ import { createHash as createHash32 } from "crypto";
22448
22494
  import { existsSync } from "fs";
22449
22495
  import { mkdir as mkdir28, mkdtemp as mkdtemp6, readFile as readFile45, realpath as realpath4, rm as rm16, writeFile as writeFile24 } from "fs/promises";
22450
22496
  import { homedir as homedir18, tmpdir as tmpdir7 } from "os";
22451
- import { basename as basename24, dirname as dirname41, isAbsolute as isAbsolute17, join as join66, relative as relative22, resolve as resolve48 } from "path";
22497
+ import { basename as basename24, dirname as dirname41, isAbsolute as isAbsolute17, join as join66, relative as relative22, resolve as resolve48, sep as sep12 } from "path";
22452
22498
  import { fileURLToPath as fileURLToPath4 } from "url";
22453
22499
  import { Command } from "commander";
22454
22500
 
@@ -23690,6 +23736,7 @@ async function createPlanFromOperations(desiredOps, adapter, targetRoot, manifes
23690
23736
  mergeCreatedDestination: existing2?.mergeCreatedDestination,
23691
23737
  currentHash: currentHash2,
23692
23738
  manifestHash: existing2?.hash,
23739
+ exactMergeAdoption: true,
23693
23740
  reason: existing2 ? "repairing exact incomplete merge ownership" : "force adopting exact unmanaged merge contribution"
23694
23741
  });
23695
23742
  continue;
@@ -27082,7 +27129,8 @@ async function expandMarkdownIncludes(artifacts, packageRoot, options = {}) {
27082
27129
  entry,
27083
27130
  packageRoot,
27084
27131
  artifactPaths,
27085
- nodeId: options.nodeId
27132
+ nodeId: options.nodeId,
27133
+ packageName: options.packageName
27086
27134
  }));
27087
27135
  const externalEntries = isPrimaryMarkdownFile(artifact, file2) ? options.additionalComposeEntries?.(artifact) ?? [] : [];
27088
27136
  const result = await expandFile(file2, packageRoot, [...localEntries, ...externalEntries], artifactPaths, options);
@@ -27114,12 +27162,13 @@ async function expandFile(file2, packageRoot, appendEntries, artifactPaths, opti
27114
27162
  const expanded = await expandContent(raw, packageRoot, [owner], artifactPaths, options);
27115
27163
  let content = expanded.content;
27116
27164
  const composedFrom = [...expanded.composedFrom];
27117
- const appliedComposeIdentities = /* @__PURE__ */ new Set();
27165
+ const appliedComposeIdentities = /* @__PURE__ */ new Map();
27118
27166
  for (const external of appendEntries) {
27119
27167
  const { entry } = external;
27120
27168
  const included = await expandInclude(entry.include, external.packageRoot, external.artifactPaths, {
27121
27169
  ...options,
27122
27170
  nodeId: external.nodeId,
27171
+ packageName: external.packageName,
27123
27172
  optional: entry.optional === true,
27124
27173
  markers: entry.markers !== false,
27125
27174
  chain: [owner]
@@ -27127,10 +27176,15 @@ async function expandFile(file2, packageRoot, appendEntries, artifactPaths, opti
27127
27176
  if (!included) continue;
27128
27177
  const markerMode = entry.markers === false ? "plain" : "markers";
27129
27178
  const identity = `${markerMode}\0${included.identity}`;
27130
- if (external.deduplicate) {
27131
- if (appliedComposeIdentities.has(identity)) continue;
27179
+ const previous = appliedComposeIdentities.get(identity);
27180
+ if (external.deduplicate && previous) {
27181
+ if (previous.hash !== included.hash) {
27182
+ throw new Error(`Conflicting OpenPack composition revisions for ${included.selector}: ${previous.selector} (${previous.hash}) and ${included.selector} (${included.hash}). Select one revision explicitly.`);
27183
+ }
27184
+ composedFrom.push(...included.composedFrom);
27185
+ continue;
27132
27186
  }
27133
- appliedComposeIdentities.add(identity);
27187
+ appliedComposeIdentities.set(identity, { hash: included.hash, selector: included.selector });
27134
27188
  content = `${content.trimEnd()}
27135
27189
 
27136
27190
  ${included.rendered}
@@ -27150,6 +27204,7 @@ async function expandContent(content, packageRoot, chain, artifactPaths, options
27150
27204
  }
27151
27205
  const composedFrom = [];
27152
27206
  const replacements = [];
27207
+ const semanticReplacements = [];
27153
27208
  for (const match of content.matchAll(includePattern)) {
27154
27209
  const selector = cleanSelector(match[2] ?? "");
27155
27210
  const included = await expandInclude(selector, packageRoot, artifactPaths, {
@@ -27163,13 +27218,14 @@ async function expandContent(content, packageRoot, chain, artifactPaths, options
27163
27218
  end: (match.index ?? 0) + match[0].length,
27164
27219
  value: included?.rendered ?? ""
27165
27220
  });
27221
+ semanticReplacements.push({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length, value: included?.semanticContent ?? "" });
27166
27222
  if (included) composedFrom.push(...included.composedFrom);
27167
27223
  }
27168
27224
  let expanded = applyReplacements(content, replacements);
27169
27225
  expanded = expanded.replace(escapedIncludePattern, (_match, optional2, selector) => {
27170
27226
  return `<!-- openpack:include${optional2 ?? ""} ${cleanSelector(selector)} -->`;
27171
27227
  });
27172
- return { content: expanded, composedFrom: uniqueComposedFrom(composedFrom) };
27228
+ return { content: expanded, semanticContent: applyReplacements(content, semanticReplacements), composedFrom: uniqueComposedFrom(composedFrom) };
27173
27229
  }
27174
27230
  async function expandInclude(selector, packageRoot, artifactPaths, options) {
27175
27231
  const parsed = parseOpenPackIncludeSelector(selector);
@@ -27178,6 +27234,7 @@ async function expandInclude(selector, packageRoot, artifactPaths, options) {
27178
27234
  let includePackageRoot = packageRoot;
27179
27235
  let includeArtifactPaths = artifactPaths;
27180
27236
  let includeNodeId = options.nodeId;
27237
+ let includePackageName = options.packageName;
27181
27238
  let displaySelector = displaySelectorForInclude(parsed.selector, options.nodeId, options.originNodeId);
27182
27239
  if (parsed.alias) {
27183
27240
  if (!options.nodeId || !options.resolveCrossPackageInclude) {
@@ -27196,6 +27253,7 @@ async function expandInclude(selector, packageRoot, artifactPaths, options) {
27196
27253
  includePackageRoot = resolved.packageRoot;
27197
27254
  includeArtifactPaths = resolved.artifactPaths;
27198
27255
  includeNodeId = resolved.toNodeId;
27256
+ includePackageName = resolved.packageName;
27199
27257
  displaySelector = `${resolved.toNodeId}:${parsed.selector}`;
27200
27258
  } else {
27201
27259
  sourcePath = artifactPaths.get(parsed.selector) ?? resolvePackageSelector(packageRoot, parsed.selector);
@@ -27215,13 +27273,14 @@ async function expandInclude(selector, packageRoot, artifactPaths, options) {
27215
27273
  const { optional: _optional2, markers: _markers, chain: _chain, ...childOptions } = options;
27216
27274
  const expanded = await expandContent(raw, includePackageRoot, [...options.chain, displaySelector], includeArtifactPaths, {
27217
27275
  ...childOptions,
27218
- nodeId: includeNodeId
27276
+ nodeId: includeNodeId,
27277
+ packageName: includePackageName
27219
27278
  });
27220
27279
  const contentHash = sha2563(expanded.content);
27221
- const identity = `${includeNodeId ?? resolve28(includePackageRoot)}\0${parsed.selector}\0${contentHash}`;
27280
+ const identity = `${includePackageName ?? includeNodeId ?? resolve28(includePackageRoot)}\0${parsed.selector}`;
27222
27281
  const ownEntry = { selector: displaySelector, hash: contentHash };
27223
27282
  const composedFrom = uniqueComposedFrom([ownEntry, ...expanded.composedFrom]);
27224
- if (!options.markers) return { rendered: expanded.content, composedFrom, identity };
27283
+ if (!options.markers) return { rendered: expanded.content, composedFrom, identity, hash: sha2563(expanded.semanticContent), selector: displaySelector, semanticContent: expanded.semanticContent };
27225
27284
  return {
27226
27285
  rendered: [
27227
27286
  `<!-- BEGIN openpack:include ${displaySelector} sha256:${contentHash.slice(0, markerHashLength)} -->`,
@@ -27229,7 +27288,10 @@ async function expandInclude(selector, packageRoot, artifactPaths, options) {
27229
27288
  `<!-- END openpack:include ${displaySelector} -->`
27230
27289
  ].join("\n"),
27231
27290
  composedFrom,
27232
- identity
27291
+ identity,
27292
+ hash: sha2563(expanded.semanticContent),
27293
+ selector: displaySelector,
27294
+ semanticContent: expanded.semanticContent
27233
27295
  };
27234
27296
  }
27235
27297
  function parseOpenPackIncludeSelector(selector) {
@@ -29497,6 +29559,7 @@ async function renderGraphIntoRoot(root, graph, targetContext) {
29497
29559
  const compositionArtifacts = runtimeArtifacts;
29498
29560
  const expandedArtifacts = await expandMarkdownIncludes(compositionArtifacts, staged.root, {
29499
29561
  nodeId: rawNode.node.id,
29562
+ packageName: rawNode.node.name,
29500
29563
  originNodeId: rawNode.node.id,
29501
29564
  additionalComposeEntries: (artifact) => matchingCompositionEntries(
29502
29565
  artifact,
@@ -29534,6 +29597,7 @@ async function renderGraphIntoRoot(root, graph, targetContext) {
29534
29597
  includeEdges.set(`${includeEdge.fromNodeId}\0${includeEdge.alias}\0${includeEdge.toNodeId}\0${includeEdge.selector}\0${includeEdge.sourceHash}`, includeEdge);
29535
29598
  return {
29536
29599
  toNodeId: edge.to,
29600
+ packageName: target.rawNode.node.name,
29537
29601
  packageRoot: target.root,
29538
29602
  artifactPaths: target.artifactPaths,
29539
29603
  sourcePath,
@@ -29621,6 +29685,7 @@ function matchingCompositionEntries(artifact, targetNodeId, runtime, rules) {
29621
29685
  packageRoot: candidate.packageRoot,
29622
29686
  artifactPaths: candidate.artifactPaths,
29623
29687
  nodeId: candidate.ownerNodeId,
29688
+ packageName: candidate.ownerPackageName,
29624
29689
  deduplicate: true
29625
29690
  });
29626
29691
  }
@@ -37499,20 +37564,43 @@ ownershipCommand.command("recovery-plan").description("write a read-only per-pat
37499
37564
  console.log(JSON.stringify(report.counts));
37500
37565
  });
37501
37566
  });
37502
- 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").option("--from-fleet-id <id>", "exact stale Fleet owner id for same-Fleet duplicate state").requiredOption("--to-workspace-root <path>", "registered destination Fleet root").requiredOption("--source-state-key <key>", "exact source install-manifest state key").option("--destination-state-key <key>", "exact Fleet-owned destination install-manifest state key (defaults to the stable target state)").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) => {
37567
+ 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").option("--from-fleet-id <id>", "exact stale Fleet owner id for same-Fleet duplicate state").requiredOption("--to-workspace-root <path>", "registered destination Fleet root").requiredOption("--source-state-key <key>", "exact source install-manifest state key").option("--destination-state-key <key>", "exact Fleet-owned destination install-manifest state key (defaults to the stable target state)").option("--to-fleet <id>", "registered destination Fleet owner, independent of target workspace scope").option("--user", "use the user workspace", false).option("--local", "use the nearest local workspace", false).option("--fleet <id>", "use one registered named fleet (also the destination owner unless --to-fleet is set)").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) => {
37503
37568
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
37504
- const fleet = await showRegisteredFleet(options.fleet);
37569
+ const destinationFleet = options.toFleet ?? options.fleet;
37570
+ if (!destinationFleet) throw new Error("Stale ownership retirement requires --to-fleet <id> or the legacy --fleet <id> scope.");
37571
+ const fleet = await showRegisteredFleet(destinationFleet);
37505
37572
  const toWorkspaceRoot = normalizeCliPath(options.toWorkspaceRoot);
37506
37573
  if (resolve48(fleet.root) !== resolve48(toWorkspaceRoot)) {
37507
- throw new Error(`Destination Fleet '${options.fleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
37574
+ throw new Error(`Destination Fleet '${destinationFleet}' is registered at ${fleet.root}, not ${toWorkspaceRoot}.`);
37575
+ }
37576
+ const assertDestinationWorkspace = async (workspaceRoot, fleetId) => {
37577
+ const fleetRoot = resolve48(fleet.root);
37578
+ const canonicalFleetRoot = await realpath4(fleetRoot);
37579
+ const within = (root, path) => {
37580
+ const suffix = relative22(root, path);
37581
+ return suffix === "" || suffix !== ".." && !suffix.startsWith(`..${sep12}`) && !isAbsolute17(suffix);
37582
+ };
37583
+ if (canonicalFleetRoot !== fleetRoot || fleetId !== void 0 && fleetId !== fleet.id || !within(fleetRoot, resolve48(workspaceRoot)) || !within(canonicalFleetRoot, await realpath4(workspaceRoot))) {
37584
+ throw new Error(`Resolved target workspace does not belong to destination Fleet '${fleet.id}'.`);
37585
+ }
37586
+ };
37587
+ if (options.toFleet) {
37588
+ await resolveWorkspaceScope({ fleet: fleet.id });
37589
+ if (normalizedOptions.targetRoot && !normalizedOptions.workspaceTargetDerived) {
37590
+ await assertDestinationWorkspace(normalizedOptions.targetRoot);
37591
+ }
37592
+ const scope = await resolveCliWorkspaceScope(normalizedOptions);
37593
+ await assertDestinationWorkspace(scope.root, scope.fleetId);
37508
37594
  }
37509
37595
  const targets = await resolveCliTargets(normalizedOptions);
37510
37596
  if (targets.length !== 1) {
37511
37597
  throw new Error(`Stale ownership retirement requires exactly one destination Fleet target, found ${targets.length}.`);
37512
37598
  }
37513
37599
  const target = targets[0];
37514
- if (target.fleetId !== options.fleet || resolve48(target.workspaceRoot) !== resolve48(fleet.root)) {
37515
- throw new Error(`Resolved target does not belong to destination Fleet '${options.fleet}'.`);
37600
+ if (options.toFleet) {
37601
+ await assertDestinationWorkspace(target.workspaceRoot, target.fleetId);
37602
+ } else if (target.fleetId !== fleet.id || resolve48(target.workspaceRoot) !== resolve48(fleet.root)) {
37603
+ throw new Error(`Resolved target does not belong to destination Fleet '${fleet.id}'.`);
37516
37604
  }
37517
37605
  const adapterOptions = adapterOptionsForTarget(target, normalizedOptions);
37518
37606
  const adapter = await resolveAdapterForTarget(target, adapterOptions);
@@ -37536,7 +37624,8 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
37536
37624
  transport: transportForTarget(target)
37537
37625
  };
37538
37626
  const result = options.apply ? await applyRetireStaleOwnership(request) : await planRetireStaleOwnership(request);
37539
- const targetArgs = options.profile ? ["--profile", options.profile] : target.agentName ? ["--agent", target.agentName] : ["--adapter", adapter.name, "--target-root", target.targetRoot];
37627
+ const workspaceArgs = options.toFleet ? options.fleet ? ["--fleet", options.fleet] : options.user ? ["--user"] : options.targetRoot ? [] : ["--local"] : ["--fleet", fleet.id];
37628
+ const targetArgs = options.profile ? ["--profile", options.profile] : target.agentName ? ["--agent", target.agentName] : ["--adapter", adapter.name, ...workspaceArgs.length ? [] : ["--target-root", target.targetRoot]];
37540
37629
  const applyArgs = [
37541
37630
  "agentwheel",
37542
37631
  "ownership",
@@ -37550,8 +37639,9 @@ ownershipCommand.command("retire-stale").description("retire stale source-manife
37550
37639
  request.sourceStateKey,
37551
37640
  "--destination-state-key",
37552
37641
  request.destinationStateKey,
37553
- "--fleet",
37554
- request.toFleetId,
37642
+ ...options.toFleet ? ["--to-fleet", request.toFleetId] : [],
37643
+ ...workspaceArgs,
37644
+ ...(options.profile || target.agentName) && options.targetRoot ? ["--target-root", normalizedOptions.targetRoot] : [],
37555
37645
  ...targetArgs,
37556
37646
  "--installation-type",
37557
37647
  installationType,
@@ -38791,13 +38881,23 @@ function preserveUnrelatedGraphPackages(current, previous, selectedCurrentNodeId
38791
38881
  for (const root of selectedCurrentRoots) {
38792
38882
  if (!previous.canonical.roots.some((candidate) => candidate.rootId === root.rootId)) roots.push(root);
38793
38883
  }
38884
+ const preservedNodeIds = /* @__PURE__ */ new Set();
38885
+ if (selectedRootIds.size > 0) {
38886
+ for (const root of previous.canonical.roots) {
38887
+ if (!selectedRootIds.has(root.rootId)) {
38888
+ for (const id of graphRootClosure(previous, root.rootId)) preservedNodeIds.add(id);
38889
+ }
38890
+ }
38891
+ }
38892
+ const replacedOwners = new Set([...selectedPreviousNodeIds].filter((id) => !preservedNodeIds.has(id)));
38893
+ for (const id of selectedRootIds) replacedOwners.add(`workspace:${id}`);
38794
38894
  const currentNodes = current.canonical.nodes.filter((node) => selectedCurrentNodeIds.has(node.id));
38795
- const previousNodes = previous.canonical.nodes.filter((node) => !selectedPreviousNodeIds.has(node.id));
38895
+ const previousNodes = previous.canonical.nodes.filter((node) => !selectedPreviousNodeIds.has(node.id) || preservedNodeIds.has(node.id)).map((node) => ({ ...node, requiredBy: node.requiredBy.filter((owner) => !replacedOwners.has(owner)) }));
38796
38896
  const selectedCurrentArtifacts = current.canonical.artifacts.filter((artifact) => selectedCurrentNodeIds.has(artifact.graphNodeId));
38797
- const previousArtifacts = previous.canonical.artifacts.filter((artifact) => !selectedPreviousNodeIds.has(artifact.graphNodeId));
38897
+ const previousArtifacts = previous.canonical.artifacts.filter((artifact) => !selectedPreviousNodeIds.has(artifact.graphNodeId) || preservedNodeIds.has(artifact.graphNodeId)).map((artifact) => ({ ...artifact, owners: artifact.owners.filter((owner) => !replacedOwners.has(owner)) }));
38798
38898
  const edgeTouchesSelected = (from, to) => selectedCurrentNodeIds.has(from) || selectedCurrentNodeIds.has(to);
38799
- const previousEdge = (edge) => !selectedPreviousNodeIds.has(edge.from) && !selectedPreviousNodeIds.has(edge.to);
38800
- const previousIncludeEdge = (edge) => !selectedPreviousNodeIds.has(edge.fromNodeId) && !selectedPreviousNodeIds.has(edge.toNodeId);
38899
+ const previousEdge = (edge) => preservedNodeIds.has(edge.from) || !selectedPreviousNodeIds.has(edge.from) && !selectedPreviousNodeIds.has(edge.to);
38900
+ const previousIncludeEdge = (edge) => preservedNodeIds.has(edge.fromNodeId) || !selectedPreviousNodeIds.has(edge.fromNodeId) && !selectedPreviousNodeIds.has(edge.toNodeId);
38801
38901
  const selectedCurrentRootIds = new Set(selectedCurrentRoots.map((root) => root.graphNodeId));
38802
38902
  const currentRootOrNode = (id) => selectedCurrentNodeIds.has(id) || selectedCurrentRootIds.has(id);
38803
38903
  const previousRootOrNode = (id) => selectedPreviousNodeIds.has(id) || previous.canonical.roots.some((root) => selectedRootIds.has(root.rootId) && root.graphNodeId === id);
@@ -38806,28 +38906,38 @@ function preserveUnrelatedGraphPackages(current, previous, selectedCurrentNodeId
38806
38906
  canonical: {
38807
38907
  ...current.canonical,
38808
38908
  roots,
38809
- nodes: [...currentNodes, ...previousNodes],
38810
- edges: [
38909
+ nodes: uniqueBy([...currentNodes.map((node) => {
38910
+ const old = previousNodes.find((candidate) => candidate.id === node.id);
38911
+ return old ? {
38912
+ ...node,
38913
+ requiredBy: sortedUniqueValues([...node.requiredBy, ...old.requiredBy]),
38914
+ selected: sortedUniqueValues([...node.selected, ...old.selected])
38915
+ } : node;
38916
+ }), ...previousNodes], (node) => node.id),
38917
+ edges: uniqueBy([
38811
38918
  ...current.canonical.edges.filter((edge) => edgeTouchesSelected(edge.from, edge.to)),
38812
38919
  ...previous.canonical.edges.filter(previousEdge)
38813
- ],
38814
- includeEdges: [
38920
+ ], (edge) => `${edge.from}\0${edge.alias}\0${edge.to}`),
38921
+ includeEdges: uniqueBy([
38815
38922
  ...current.canonical.includeEdges.filter((edge) => edgeTouchesSelected(edge.fromNodeId, edge.toNodeId)),
38816
38923
  ...previous.canonical.includeEdges.filter(previousIncludeEdge)
38817
- ],
38818
- artifacts: [...selectedCurrentArtifacts, ...previousArtifacts],
38819
- namespacing: [
38924
+ ], graphIncludeEdgeIdentity),
38925
+ artifacts: uniqueBy([...selectedCurrentArtifacts.map((artifact) => {
38926
+ const old = previousArtifacts.find((candidate) => graphArtifactIdentity2(candidate) === graphArtifactIdentity2(artifact));
38927
+ return old ? { ...artifact, owners: sortedUniqueValues([...artifact.owners, ...old.owners]) } : artifact;
38928
+ }), ...previousArtifacts], graphArtifactIdentity2),
38929
+ namespacing: uniqueBy([
38820
38930
  ...current.canonical.namespacing.filter((entry) => currentRootOrNode(entry.graphNodeId)),
38821
- ...previous.canonical.namespacing.filter((entry) => !previousRootOrNode(entry.graphNodeId))
38822
- ],
38823
- overrides: [
38931
+ ...previous.canonical.namespacing.filter((entry) => !previousRootOrNode(entry.graphNodeId) || preservedNodeIds.has(entry.graphNodeId))
38932
+ ], (entry) => `${entry.graphNodeId}\0${entry.type}\0${entry.name}`),
38933
+ overrides: uniqueBy([
38824
38934
  ...current.canonical.overrides.filter((entry) => currentRootOrNode(entry.graphNodeId) || currentRootOrNode(entry.overriddenGraphNodeId)),
38825
- ...previous.canonical.overrides.filter((entry) => !previousRootOrNode(entry.graphNodeId) && !previousRootOrNode(entry.overriddenGraphNodeId))
38826
- ],
38827
- plainNameIncumbents: [
38935
+ ...previous.canonical.overrides.filter((entry) => !selectedRootIds.has(entry.rootId) && (!previousRootOrNode(entry.graphNodeId) && !previousRootOrNode(entry.overriddenGraphNodeId) || preservedNodeIds.has(entry.graphNodeId)))
38936
+ ], (entry) => `${entry.rootId}\0${entry.selector}`),
38937
+ plainNameIncumbents: uniqueBy([
38828
38938
  ...current.canonical.plainNameIncumbents.filter((entry) => currentRootOrNode(entry.graphNodeId)),
38829
- ...previous.canonical.plainNameIncumbents.filter((entry) => !previousRootOrNode(entry.graphNodeId))
38830
- ]
38939
+ ...previous.canonical.plainNameIncumbents.filter((entry) => !previousRootOrNode(entry.graphNodeId) || preservedNodeIds.has(entry.graphNodeId))
38940
+ ], (entry) => `${entry.adapter}\0${entry.targetFingerprint}\0${entry.type}\0${entry.name}`)
38831
38941
  }
38832
38942
  });
38833
38943
  }
@@ -39157,16 +39267,31 @@ function uniqueBy(values, key) {
39157
39267
  return true;
39158
39268
  });
39159
39269
  }
39160
- function scopeInstallPlanToRoot(result, rootId, manifest) {
39270
+ function scopeInstallPlanToRoot(result, rootId, manifest, previousLock) {
39161
39271
  const scopedOwners = scopedGraphOwnerKeys(result, rootId);
39272
+ const exclusiveOwners = new Set(scopedOwners);
39273
+ if (previousLock) {
39274
+ for (const id of graphRootClosure(previousLock, rootId)) exclusiveOwners.add(id);
39275
+ for (const root of previousLock.canonical.roots) {
39276
+ if (root.rootId === rootId) continue;
39277
+ for (const id of graphRootClosure(previousLock, root.rootId)) exclusiveOwners.delete(id);
39278
+ }
39279
+ }
39162
39280
  const manifestByPath = new Map((manifest?.entries ?? []).map((entry) => [entry.path, entry]));
39163
39281
  const preservedPaths = /* @__PURE__ */ new Set();
39164
39282
  const plannedPaths = /* @__PURE__ */ new Set();
39165
39283
  const operations = [];
39166
39284
  for (const operation of result.plan.operations) {
39167
39285
  plannedPaths.add(operation.relativeDestPath);
39168
- if (operationBelongsToScopedRoot(operation, scopedOwners)) {
39169
- operations.push(operation);
39286
+ const stale = operation.action === "remove" || operation.action === "drift" && !operation.desiredHash;
39287
+ const exclusivelyOwned = operation.owners?.length && operation.owners.every((owner) => exclusiveOwners.has(owner));
39288
+ if (stale ? exclusivelyOwned : operationBelongsToScopedRoot(operation, scopedOwners)) {
39289
+ const entry2 = manifestByPath.get(operation.relativeDestPath);
39290
+ const foreignOwners = entry2 && "owners" in entry2 && Array.isArray(entry2.owners) ? entry2.owners.filter((owner) => typeof owner === "string" && !exclusiveOwners.has(owner)) : [];
39291
+ operations.push(stale || !operation.owners ? operation : {
39292
+ ...operation,
39293
+ owners: sortedUniqueValues([...operation.owners, ...foreignOwners])
39294
+ });
39170
39295
  continue;
39171
39296
  }
39172
39297
  const entry = manifestByPath.get(operation.relativeDestPath);
@@ -39192,7 +39317,7 @@ function scopeInstallPlanToRoot(result, rootId, manifest) {
39192
39317
  };
39193
39318
  }
39194
39319
  function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
39195
- const scoped = scopeInstallPlanToRoot(result, rootId, manifest);
39320
+ const scoped = scopeInstallPlanToRoot(result, rootId, manifest, previousLock);
39196
39321
  const selectedCurrentNodeIds = graphRootClosure(scoped.bundle.graphLock, rootId);
39197
39322
  const selectedPreviousNodeIds = graphRootClosure(previousLock, rootId);
39198
39323
  const graphLock = preserveUnrelatedGraphPackages(
package/openpack.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "name": "NestDevLab/agentwheel",
4
- "version": "0.22.1",
4
+ "version": "0.22.2",
5
5
  "provides": [
6
6
  { "type": "skills", "path": "skills" }
7
7
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,6 +53,7 @@
53
53
  "@skillkit/core": "1.24.0",
54
54
  "commander": "14.0.2",
55
55
  "jsonc-parser": "3.3.1",
56
+ "smol-toml": "1.9.1",
56
57
  "typescript": "5.9.3",
57
58
  "yaml": "2.8.2",
58
59
  "zod": "4.1.13"
@@ -5,7 +5,7 @@ allowed-tools: [Bash]
5
5
  license: MIT
6
6
  metadata:
7
7
  author: NestDevLab
8
- version: "0.22.1"
8
+ version: "0.22.2"
9
9
  ---
10
10
 
11
11
  # agentwheel
@@ -5,7 +5,7 @@ allowed-tools: [Bash]
5
5
  license: MIT
6
6
  metadata:
7
7
  author: NestDevLab
8
- version: "0.22.1"
8
+ version: "0.22.2"
9
9
  ---
10
10
 
11
11
  # Agentwheel Discovery