plugnz 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/plugnz.mjs +937 -81
  2. package/package.json +1 -1
package/dist/plugnz.mjs CHANGED
@@ -9339,10 +9339,23 @@ var dcode = {
9339
9339
  continue;
9340
9340
  const row = entry;
9341
9341
  const path = typeof row.installPath === "string" ? row.installPath : typeof row.install_path === "string" ? row.install_path : undefined;
9342
+ let declaredVersion;
9343
+ if (path && existsSync8(path)) {
9344
+ for (const relative of ["plugin.json", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json"]) {
9345
+ try {
9346
+ const manifest = JSON.parse(readFileSync6(join11(path, relative), "utf8"));
9347
+ if (typeof manifest.version === "string") {
9348
+ declaredVersion = manifest.version;
9349
+ break;
9350
+ }
9351
+ } catch {}
9352
+ }
9353
+ }
9342
9354
  const at = id.indexOf("@");
9343
9355
  const name = at < 0 ? id : id.slice(0, at);
9344
9356
  const marketplace = at < 0 ? undefined : id.slice(at + 1);
9345
- const plugin = { id, name, ...marketplace ? { marketplace } : {}, ...path && existsSync8(path) ? { path } : {}, ...typeof row.version === "string" ? { version: row.version } : {}, ...enabled.enabledPlugins[id] === true ? { enabled: true } : { enabled: false } };
9357
+ const version = typeof row.version === "string" ? row.version : declaredVersion;
9358
+ const plugin = { id, name, ...marketplace ? { marketplace } : {}, ...path && existsSync8(path) ? { path } : {}, ...version === undefined ? {} : { version }, ...enabled.enabledPlugins[id] === true ? { enabled: true } : { enabled: false } };
9346
9359
  result.push(plugin);
9347
9360
  }
9348
9361
  return result;
@@ -12622,12 +12635,18 @@ function assertOutcome(outcome, snapshots, dryRun, command) {
12622
12635
  if (outcome["action"] === "not-attempted" && (outcome["route"] !== "none" || outcome["changed"] !== false || outcome["result"] === "succeeded" || outcome["result"] === "pending")) {
12623
12636
  throw contradiction(`preflight refusal '${outcome["operationId"]}' must be a non-successful, non-mutating no-route action`);
12624
12637
  }
12625
- if (outcome["action"] === "not-attempted" && (outcome["resourceState"] !== "unknown" || outcome["activationState"] !== "unknown")) {
12638
+ if (!dryRun && outcome["action"] === "not-attempted" && (outcome["resourceState"] !== "unknown" || outcome["activationState"] !== "unknown")) {
12626
12639
  throw contradiction(`preflight refusal '${outcome["operationId"]}' cannot claim a terminal resource or activation state`);
12627
12640
  }
12628
12641
  const containment = outcome["action"] === "retain-prior" || outcome["action"] === "disable-nonconforming";
12629
12642
  const appliedDisablement = outcome["action"] === "disable-nonconforming" && outcome["changed"] === true;
12630
- if (containment) {
12643
+ if (outcome["action"] === "disable-nonconforming" && outcome["result"] === "succeeded") {
12644
+ throw contradiction(`disablement '${outcome["operationId"]}' cannot claim Desired convergence`);
12645
+ }
12646
+ if (!dryRun && outcome["action"] === "disable-nonconforming" && outcome["resourceState"] === "retained" && outcome["activationState"] === "inactive" && outcome["changed"] !== true) {
12647
+ throw contradiction(`disablement '${outcome["operationId"]}' cannot claim inactive containment without a verified mutation`);
12648
+ }
12649
+ if (containment && !dryRun && (outcome["action"] === "retain-prior" || isObject2(reason) && reason["category"] === "capability")) {
12631
12650
  const expectedResourceState = "retained";
12632
12651
  const expectedActivationState = outcome["action"] === "retain-prior" ? "retained-prior" : appliedDisablement ? "inactive" : "active-nonconforming";
12633
12652
  if (outcome["result"] !== "failed" || outcome["resourceState"] !== expectedResourceState || outcome["activationState"] !== expectedActivationState) {
@@ -12654,9 +12673,12 @@ function assertOutcome(outcome, snapshots, dryRun, command) {
12654
12673
  throw contradiction(`successful Desired outcome '${outcome["operationId"]}' needs an exact native identity`);
12655
12674
  }
12656
12675
  const terminalStateMatches = outcome["coverage"] === "retirement" ? outcome["resourceState"] === "absent" && outcome["activationState"] === "inactive" : outcome["resourceState"] === "present" && outcome["activationState"] === "active-conforming";
12657
- if (!terminalStateMatches)
12676
+ if (!dryRun && !terminalStateMatches)
12658
12677
  throw contradiction(`successful outcome '${outcome["operationId"]}' contradicts its terminal resource or activation state`);
12659
12678
  }
12679
+ if (dryRun && (outcome["resourceState"] === "potentially-changed" || outcome["resourceState"] === "absent" && outcome["activationState"] !== "inactive" || outcome["resourceState"] === "unknown" && outcome["activationState"] !== "unknown")) {
12680
+ throw contradiction(`dry-run outcome '${outcome["operationId"]}' contradicts its observed resource or activation state`);
12681
+ }
12660
12682
  if (outcome["result"] === "not-attempted" && outcome["resourceState"] === "potentially-changed") {
12661
12683
  throw contradiction(`not-attempted outcome '${outcome["operationId"]}' cannot be potentially changed`);
12662
12684
  }
@@ -13543,7 +13565,8 @@ var dcode0183Managed = createCapabilityEvidenceProfile({
13543
13565
  },
13544
13566
  evidence: [
13545
13567
  "docs/evidence/dcode-native-update-0.1.83-20261009.json",
13546
- "docs/research/dcode-session-findings-2026-10-09.md"
13568
+ "docs/research/dcode-session-findings-2026-10-09.md",
13569
+ "docs/evidence/dcode-managed-lifecycle-20261011.md"
13547
13570
  ]
13548
13571
  });
13549
13572
  var capabilityEvidenceProfiles = Object.freeze([
@@ -14225,6 +14248,12 @@ function createLifecycleHostAdapter(definition) {
14225
14248
  assertReadbackMatch(handle.prior, observation, "rollback");
14226
14249
  return deepFreeze({ phase: "rollback-verified", handle, observation });
14227
14250
  },
14251
+ async releaseRecovery(handle) {
14252
+ assertHandle(definition.id, handle);
14253
+ await invokePhase("cleanup", async () => {
14254
+ await definition.releaseRecovery?.(handle);
14255
+ });
14256
+ },
14228
14257
  async cleanup(target, disposition) {
14229
14258
  const reference = cleanupReference(definition.id, target);
14230
14259
  if (isPreparationProjection(target) && disposition !== "aborted-preparation") {
@@ -20595,10 +20624,431 @@ function assertUnder(root, path) {
20595
20624
  import { cpSync as cpSync8, existsSync as existsSync23, lstatSync as lstatSync16, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync9, readFileSync as readFileSync22, readdirSync as readdirSync17, realpathSync as realpathSync3, renameSync as renameSync8, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "node:fs";
20596
20625
  import { basename as basename5, dirname as dirname11, join as join28, resolve as resolve17 } from "node:path";
20597
20626
  import { tmpdir as tmpdir7 } from "node:os";
20627
+ import { spawn as spawnChild } from "node:child_process";
20598
20628
  var MARKER7 = ".plgnz-install.json";
20599
20629
  var MANIFESTS = ["plugin.json", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json"];
20600
20630
  var READBACK_FAULT = ".plgnz-dcode-readback-fault";
20631
+ var NATIVE_LOCK_SCRIPT = `import sys
20632
+ from importlib.metadata import version
20633
+ from filelock import FileLock
20634
+ if version('deepagents-code') != '0.1.83':
20635
+ raise RuntimeError('unverified dcode lock runtime')
20636
+ with FileLock(sys.argv[1]).acquire(timeout=10):
20637
+ print('DCODE_LOCK_0.1.83', flush=True)
20638
+ sys.stdin.read()
20639
+ `;
20640
+ var nativeLocks = new Map;
20641
+ function nativePython() {
20642
+ const override = process.env["OPEN_PLUGIN_DCODE_PYTHON_BIN"];
20643
+ if (override) {
20644
+ assertIdentity2(basename5(override), "Python binary");
20645
+ return override;
20646
+ }
20647
+ const binary = dcodeBinary();
20648
+ if (binary === undefined)
20649
+ throw new Error("dcode native lock runtime is unavailable");
20650
+ const shebang = readFileSync22(binary, "utf8").split(/\r?\n/, 1)[0] ?? "";
20651
+ const match = /^#!(\/[^\s]+python(?:\d+(?:\.\d+)?)?)$/u.exec(shebang);
20652
+ if (match?.[1] === undefined || !existsSync23(match[1]))
20653
+ throw new Error("dcode native Python interpreter is unverified; set OPEN_PLUGIN_DCODE_PYTHON_BIN to its verified interpreter");
20654
+ return match[1];
20655
+ }
20656
+ function probeLifecycleVersion() {
20657
+ const observation = probeDcodeVersion();
20658
+ if (observation.kind !== "detected" || observation.version !== "0.1.83")
20659
+ return observation;
20660
+ const result = spawnSync([nativePython(), "-c", "from importlib.metadata import version; import filelock; assert version('deepagents-code') == '0.1.83'; print('DCODE_LOCK_0.1.83')"], { stdout: "pipe", stderr: "pipe", timeout: 1e4 });
20661
+ if (result.exitCode !== 0 || new TextDecoder().decode(result.stdout).trim() !== "DCODE_LOCK_0.1.83")
20662
+ throw new Error(`dcode native lock runtime is unverified: ${new TextDecoder().decode(result.stderr).trim()}`);
20663
+ return observation;
20664
+ }
20665
+ async function acquireNativeLock(attemptId, operationId) {
20666
+ const key = `${attemptId}:${operationId}`;
20667
+ const existing = nativeLocks.get(key);
20668
+ if (existing !== undefined) {
20669
+ if (!existing.isAlive())
20670
+ throw new Error("dcode native lock was lost before verification");
20671
+ return;
20672
+ }
20673
+ const file = join28(dcodeRoot(), "plugins", ".mutation.lock");
20674
+ assertManagedPath4(file);
20675
+ mkdirSync9(dirname11(file), { recursive: true });
20676
+ const child = spawnChild(nativePython(), ["-c", NATIVE_LOCK_SCRIPT, file], { env: { ...process.env, HOME: process.env["OPEN_PLUGIN_HOME"] || process.env["HOME"], DEEPAGENTS_HOME: dcodeRoot() }, stdio: ["pipe", "pipe", "pipe"] });
20677
+ let alive = true;
20678
+ const exited = new Promise((resolveExit) => child.on("exit", (code) => {
20679
+ alive = false;
20680
+ resolveExit(code);
20681
+ }));
20682
+ await new Promise((resolveLock, rejectLock) => {
20683
+ let output = "", diagnostic = "", settled = false;
20684
+ const finish = (error) => {
20685
+ if (settled)
20686
+ return;
20687
+ settled = true;
20688
+ clearTimeout(timer);
20689
+ if (error) {
20690
+ child.kill("SIGKILL");
20691
+ rejectLock(error);
20692
+ } else
20693
+ resolveLock();
20694
+ };
20695
+ const timer = setTimeout(() => finish(new Error("dcode native lock acquisition timed out")), 12000);
20696
+ child.stdout.on("data", (data) => {
20697
+ output += new TextDecoder().decode(data);
20698
+ if (output.includes(`DCODE_LOCK_0.1.83
20699
+ `))
20700
+ finish();
20701
+ });
20702
+ child.stderr.on("data", (data) => {
20703
+ diagnostic += new TextDecoder().decode(data);
20704
+ });
20705
+ child.on("error", (error) => finish(new Error(`dcode native lock failed: ${error.message}`)));
20706
+ child.on("exit", (code) => finish(new Error(`dcode native lock failed (${code}): ${diagnostic.trim()}`)));
20707
+ });
20708
+ nativeLocks.set(key, { child, exited, isAlive: () => alive });
20709
+ }
20710
+ async function releaseNativeLock(attemptId, operationId) {
20711
+ const key = `${attemptId}:${operationId}`, held = nativeLocks.get(key);
20712
+ if (held === undefined)
20713
+ return;
20714
+ nativeLocks.delete(key);
20715
+ held.child.stdin.end();
20716
+ let timer;
20717
+ try {
20718
+ const code = await Promise.race([held.exited, new Promise((_, reject) => {
20719
+ timer = setTimeout(() => {
20720
+ held.child.kill("SIGKILL");
20721
+ reject(new Error("dcode native lock release timed out"));
20722
+ }, 2000);
20723
+ })]);
20724
+ if (code !== 0)
20725
+ throw new Error(`dcode native lock process failed during mutation (${code})`);
20726
+ } finally {
20727
+ if (timer !== undefined)
20728
+ clearTimeout(timer);
20729
+ }
20730
+ }
20731
+ function lifecycleDirectory(attemptId, operationId) {
20732
+ const hash = new CryptoHasher("sha256");
20733
+ hash.update(`${attemptId}\x00${operationId}`);
20734
+ const dir = join28(dcodeCacheRoot(), "plgnz-operations", hash.digest("hex"));
20735
+ assertCachePath(dir);
20736
+ return dir;
20737
+ }
20738
+ function lifecycleSlot(nativeId, fingerprint) {
20739
+ return managedSlot(nativeId, fingerprint);
20740
+ }
20741
+ function observeDcodeTarget(target) {
20742
+ if (target.kind !== "dcode" || target.instance !== "default")
20743
+ throw new Error("unsupported dcode target identity");
20744
+ const registry = readRegistry3(dcodeRegistryFile());
20745
+ const enabled = boolObject(readEnablement(dcodeEnablementFile()).enabledPlugins, "enabledPlugins");
20746
+ return Object.entries(object3(registry.plugins, "registry plugins")).map(([nativeId, raw]) => {
20747
+ const rows = rowsFor(raw);
20748
+ if (rows.length !== 1)
20749
+ throw new Error(`dcode install record ${nativeId} has duplicate rows`);
20750
+ const path = rowInstall(rows[0], nativeId);
20751
+ assertCachePath(path);
20752
+ const present = existsSync23(path);
20753
+ const marker = present ? ownership3(path) : null;
20754
+ const body = present ? readDoc2(join28(path, MARKER7), {}, "ownership marker") : {};
20755
+ const owned = marker?.pluginId === nativeId;
20756
+ const fingerprint = present ? fingerprintTree(path) : null;
20757
+ return {
20758
+ nativeId,
20759
+ packageName: nativeId.split("@")[0],
20760
+ ownership: owned ? { kind: "owned", proof: body.proof === "adopted" ? "adopted" : "created", scopeId: typeof body.scopeId === "string" ? body.scopeId : "dcode:default", proofId: `dcode-marker:${nativeId}` } : { kind: "unmanaged" },
20761
+ presence: present ? "present" : "absent",
20762
+ enablement: enabled[nativeId] === true ? "enabled" : "disabled",
20763
+ activation: enabled[nativeId] === true && present ? "active" : "inactive",
20764
+ installedFingerprint: fingerprint,
20765
+ installedVersion: present ? declaredManifest(path, nativeId.split("@")[0]).version : null,
20766
+ source: owned ? { type: body.sourceType === "git" ? "git" : "local", immutableRevision: marker.kind === "current" ? marker.sourceRevision : marker.fingerprint, locator: body.sourceType === "git" ? marker.source : null } : null,
20767
+ contentRoots: fingerprint === null ? [] : [{ label: "managed", path, fingerprint }]
20768
+ };
20769
+ });
20770
+ }
20771
+ function dcodeLifecycleReadback(binding) {
20772
+ if (existsSync23(join28(dcodeStateDir(), READBACK_FAULT)))
20773
+ throw new Error(`dcode readback failed for ${binding.nativeId}`);
20774
+ const installations = observeDcodeTarget(binding.target);
20775
+ const row = installations.find((item) => item.nativeId === binding.nativeId);
20776
+ const present = row?.presence === "present";
20777
+ const enabled = row?.enablement === "enabled" && present;
20778
+ const dataRoot = join28(dcodeRoot(), "plugins", "data");
20779
+ assertManagedPath4(dataRoot);
20780
+ return {
20781
+ adapterId: binding.adapterId,
20782
+ target: binding.target,
20783
+ scopeId: binding.scopeId,
20784
+ packageName: binding.packageName,
20785
+ nativeId: binding.nativeId,
20786
+ route: binding.route ?? (present ? "managed" : "none"),
20787
+ presence: present ? "present" : "absent",
20788
+ enablement: enabled ? "enabled" : "disabled",
20789
+ activation: enabled ? "active" : "inactive",
20790
+ transition: { requirement: present ? "restart" : "none", status: "effective" },
20791
+ installedFingerprint: present ? row.installedFingerprint : null,
20792
+ contentRoots: present ? row.contentRoots : [],
20793
+ retention: { pluginData: existsSync23(dataRoot) ? { state: "present", fingerprint: fingerprintTree(dataRoot) } : { state: "absent", fingerprint: null }, inactiveMetadata: { state: "absent", fingerprint: null } }
20794
+ };
20795
+ }
20796
+ function captureDcodeRollback(binding, attemptId, operationId) {
20797
+ const dir = join28(lifecycleDirectory(attemptId, operationId), "rollback");
20798
+ mkdirSync9(dir, { recursive: true });
20799
+ const registry = readRegistry3(dcodeRegistryFile());
20800
+ const enabled = boolObject(readEnablement(dcodeEnablementFile()).enabledPlugins, "enabledPlugins");
20801
+ const row = object3(registry.plugins, "registry plugins")[binding.nativeId];
20802
+ const state = { row: row ?? null, enabled: enabled[binding.nativeId] ?? null, registryExisted: existsSync23(dcodeRegistryFile()), enablementExisted: existsSync23(dcodeEnablementFile()) };
20803
+ writeFileSync9(join28(dir, "state.json"), JSON.stringify(state));
20804
+ for (const [index, item] of rowsFor(row).entries()) {
20805
+ const path = rowInstall(item, binding.nativeId);
20806
+ assertCachePath(path);
20807
+ assertNoSymlinks6(path);
20808
+ if (existsSync23(path))
20809
+ cpSync8(path, join28(dir, `content-${index}`), { recursive: true });
20810
+ }
20811
+ return dir;
20812
+ }
20813
+ async function lockPreparation(binding, attemptId, operationId, expectedObservationId) {
20814
+ await acquireNativeLock(attemptId, operationId);
20815
+ const observed = createTargetInventoryObservation("dcode", { target: binding.target, installations: observeDcodeTarget(binding.target) });
20816
+ if (observed.observationId !== expectedObservationId)
20817
+ throw new Error("dcode native inventory changed before locked preparation; refusing stale rollback authority");
20818
+ }
20819
+ async function captureOwnedPreparation(binding, attemptId, operationId, expectedObservationId, capture) {
20820
+ try {
20821
+ await lockPreparation(binding, attemptId, operationId, expectedObservationId);
20822
+ return capture();
20823
+ } catch (error) {
20824
+ try {
20825
+ await releaseNativeLock(attemptId, operationId);
20826
+ } catch (releaseError) {
20827
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; dcode preparation lock release failed: ${releaseError instanceof Error ? releaseError.message : String(releaseError)}`);
20828
+ }
20829
+ throw error;
20830
+ }
20831
+ }
20832
+ function preflightAdoption(request) {
20833
+ if (request.operation !== "install" && request.operation !== "update")
20834
+ return;
20835
+ const snapshot = request.snapshot;
20836
+ const installed = request.targetObservation.installations.find((item) => item.nativeId === snapshot.nativeId);
20837
+ if (installed?.ownership.kind !== "unmanaged" || installed.presence !== "present")
20838
+ return;
20839
+ const path = installed.contentRoots[0]?.path;
20840
+ if (path === undefined)
20841
+ throw new Error("dcode unmanaged adoption has no native root");
20842
+ const manifest = declaredManifest(snapshot.packageRoot, snapshot.packageName);
20843
+ assessPrior(object3(readRegistry3(dcodeRegistryFile()).plugins, "registry plugins")[snapshot.nativeId], snapshot.nativeId, snapshot.nativeGit?.locator ?? `local:${snapshot.scopeId}`, true, snapshot.packageName, manifest.version, snapshot.packageRoot);
20844
+ if (fingerprintTree(path) !== installed.installedFingerprint)
20845
+ throw new Error("dcode unmanaged adoption changed during preflight");
20846
+ }
20847
+ function setDcodeRow(nativeId, row, enabled) {
20848
+ assertManagedPath4(dcodeRegistryFile());
20849
+ assertManagedPath4(dcodeEnablementFile());
20850
+ const registry = readRegistry3(dcodeRegistryFile()), enablement = readEnablement(dcodeEnablementFile());
20851
+ const plugins = { ...object3(registry.plugins, "registry plugins") }, flags = { ...boolObject(enablement.enabledPlugins, "enabledPlugins") };
20852
+ if (row === null)
20853
+ delete plugins[nativeId];
20854
+ else
20855
+ plugins[nativeId] = row;
20856
+ if (enabled === null)
20857
+ delete flags[nativeId];
20858
+ else
20859
+ flags[nativeId] = enabled;
20860
+ writeDoc2(dcodeRegistryFile(), { ...registry, plugins });
20861
+ writeDoc2(dcodeEnablementFile(), { ...enablement, enabledPlugins: flags });
20862
+ }
20863
+ var dcodeDefinition = {
20864
+ id: "dcode",
20865
+ evidenceProfiles: capabilityEvidenceProfiles.filter((profile) => profile.host === "dcode").flatMap((profile) => [profile, createCapabilityEvidenceProfile({ ...profile, operations: ["disable"], semantics: { ...profile.semantics, "reversible-disable": "supported" }, evidence: [...profile.evidence, "docs/evidence/dcode-managed-lifecycle-20261011.md"] })]),
20866
+ probeVersion: async () => probeLifecycleVersion(),
20867
+ observeTarget: async (target) => ({ target, installations: observeDcodeTarget(target) }),
20868
+ observeNativeMutationScope: async () => ({ kind: "unavailable" }),
20869
+ observeNativeProjection: async (request) => {
20870
+ preflightAdoption(request);
20871
+ return { kind: "requires-managed", reasonId: "dcode-frozen-projection-and-updater-isolation" };
20872
+ },
20873
+ revalidateTargetPrecondition: async (handle) => {
20874
+ await acquireNativeLock(handle.attemptId, handle.operationId);
20875
+ return { version: probeDcodeVersion(), targetObservationId: createTargetInventoryObservation("dcode", { target: handle.target, installations: observeDcodeTarget(handle.target) }).observationId };
20876
+ },
20877
+ stageActivation: async (request) => {
20878
+ const dir = lifecycleDirectory(request.snapshot.attemptId, request.snapshot.operationId);
20879
+ const stagingRoot = join28(dir, "stage");
20880
+ if (existsSync23(stagingRoot))
20881
+ throw new Error("dcode preparation stage already exists");
20882
+ mkdirSync9(stagingRoot, { recursive: true });
20883
+ const manifest = declaredManifest(request.snapshot.packageRoot, request.snapshot.packageName);
20884
+ try {
20885
+ stagePlugin5(request.snapshot.packageRoot, stagingRoot, request.snapshot.packageName, manifest.version);
20886
+ } catch (error) {
20887
+ rmSync9(dir, { recursive: true, force: true });
20888
+ throw error;
20889
+ }
20890
+ return { stagingRoot, stagingId: `${request.snapshot.attemptId}:${request.snapshot.operationId}` };
20891
+ },
20892
+ applyLifecycleDirectives: async (projection) => {
20893
+ forceAutoUpdateOff(projection.stagingRoot);
20894
+ const prior = observeDcodeTarget(projection.target).find((item) => item.nativeId === projection.nativeId);
20895
+ writeFileSync9(join28(projection.stagingRoot, MARKER7), JSON.stringify({ source: projection.sourceLocator ?? `local:${projection.scopeId}`, pluginId: projection.nativeId, fingerprint: projection.sourceRevision, sourceRevision: projection.sourceRevision, projectedFingerprint: projectionDigest(projection.stagingRoot), scopeId: projection.scopeId, sourceType: projection.sourceType, proof: prior?.ownership.kind === "unmanaged" ? "adopted" : "created" }));
20896
+ return ["dcode.managed-unversioned-registry"];
20897
+ },
20898
+ applyPins: async (projection) => {
20899
+ const found = new Set;
20900
+ for (const candidate of dcodeMcpCandidates()) {
20901
+ const file = join28(projection.stagingRoot, candidate.kind === "spec" ? candidate.file : candidate.manifest);
20902
+ if (!existsSync23(file))
20903
+ continue;
20904
+ const body = readDoc2(file, {}, "MCP document");
20905
+ if (typeof body.mcpServers !== "object" || body.mcpServers === null || Array.isArray(body.mcpServers))
20906
+ continue;
20907
+ let changed = false;
20908
+ for (const pin of projection.pins) {
20909
+ const server = body.mcpServers[pin.server];
20910
+ if (typeof server !== "object" || server === null || Array.isArray(server))
20911
+ continue;
20912
+ server.command = pin.executable;
20913
+ found.add(pin.server);
20914
+ changed = true;
20915
+ }
20916
+ if (changed)
20917
+ writeFileSync9(file, JSON.stringify(body, null, 2));
20918
+ }
20919
+ if (projection.pins.some((pin) => !found.has(pin.server)))
20920
+ throw new Error("dcode resolved pin is absent from staged MCP configuration");
20921
+ const marker = readDoc2(join28(projection.stagingRoot, MARKER7), {}, "ownership marker");
20922
+ marker.projectedFingerprint = projectionDigest(projection.stagingRoot);
20923
+ writeFileSync9(join28(projection.stagingRoot, MARKER7), JSON.stringify(marker));
20924
+ return [...found];
20925
+ },
20926
+ captureActivationPreparation: async (projection, fingerprint) => {
20927
+ await lockPreparation(projection, projection.attemptId, projection.operationId, projection.targetObservationId);
20928
+ const prior = dcodeLifecycleReadback(projection);
20929
+ const target = lifecycleSlot(projection.nativeId, fingerprint);
20930
+ assertCachePath(target);
20931
+ const installed = observeDcodeTarget(projection.target).find((item) => item.nativeId === projection.nativeId);
20932
+ if (installed?.ownership.kind === "owned" && installed.ownership.scopeId !== projection.scopeId)
20933
+ throw new Error("dcode scope ownership conflict");
20934
+ if (installed?.ownership.kind === "unmanaged") {
20935
+ const path = installed.contentRoots[0]?.path;
20936
+ if (path === undefined || !sameTree7(projection.snapshot.packageRoot, path))
20937
+ throw new Error("dcode legacy content differs; refusing adoption");
20938
+ const manifest = declaredManifest(path, projection.packageName);
20939
+ if (manifest.version !== projection.packageVersion)
20940
+ throw new Error("dcode legacy version differs; refusing adoption");
20941
+ }
20942
+ if (existsSync23(target) && ownership3(target)?.pluginId !== projection.nativeId)
20943
+ throw new Error("dcode lifecycle target is not owned");
20944
+ const rollbackReference = captureDcodeRollback(projection, projection.attemptId, projection.operationId);
20945
+ writeFileSync9(join28(rollbackReference, "next-path.json"), JSON.stringify({ target }));
20946
+ return { prior, expected: { ...prior, route: "managed", presence: "present", enablement: "enabled", activation: "active", transition: { requirement: "restart", status: "effective" }, installedFingerprint: fingerprint, contentRoots: [{ label: "managed", path: target, fingerprint }] }, rollbackReference, rollbackCoverageOperationIds: projection.affectedOperationIds };
20947
+ },
20948
+ captureDisablePreparation: async (request) => {
20949
+ const binding = { adapterId: "dcode", ...request.activation };
20950
+ return captureOwnedPreparation(binding, request.attemptId, request.operationId, request.selection.targetObservationId, () => ({ prior: dcodeLifecycleReadback(binding), rollbackReference: captureDcodeRollback(binding, request.attemptId, request.operationId), rollbackCoverageOperationIds: request.selection.affectedOperationIds, transition: { requirement: "restart", status: "effective" } }));
20951
+ },
20952
+ captureRetirementPreparation: async (request) => {
20953
+ const binding = { adapterId: "dcode", ...request.activation };
20954
+ return captureOwnedPreparation(binding, request.attemptId, request.operationId, request.selection.targetObservationId, () => ({ prior: dcodeLifecycleReadback(binding), rollbackReference: captureDcodeRollback(binding, request.attemptId, request.operationId), rollbackCoverageOperationIds: request.selection.affectedOperationIds, transition: { requirement: "none", status: "effective" } }));
20955
+ },
20956
+ apply: async (prepared) => {
20957
+ await acquireNativeLock(prepared.handle.attemptId, prepared.handle.operationId);
20958
+ const target = lifecycleSlot(prepared.handle.nativeId, prepared.handle.projectedFingerprint);
20959
+ assertCachePath(target);
20960
+ mkdirSync9(dirname11(target), { recursive: true });
20961
+ if (existsSync23(target)) {
20962
+ const marker = ownership3(target);
20963
+ if (marker?.pluginId !== prepared.handle.nativeId)
20964
+ throw new Error("dcode lifecycle activation would overwrite foreign content");
20965
+ if (fingerprintTree(target) !== prepared.handle.projectedFingerprint)
20966
+ throw new Error("dcode immutable projection slot has diverged");
20967
+ } else {
20968
+ renameSync8(prepared.stagingRoot, target);
20969
+ }
20970
+ setDcodeRow(prepared.handle.nativeId, [{ installPath: target }], true);
20971
+ return { receiptId: `dcode-apply:${prepared.handle.operationId}`, changed: true };
20972
+ },
20973
+ disable: async (prepared) => {
20974
+ await acquireNativeLock(prepared.handle.attemptId, prepared.handle.operationId);
20975
+ const row = object3(readRegistry3(dcodeRegistryFile()).plugins, "registry plugins")[prepared.handle.nativeId];
20976
+ setDcodeRow(prepared.handle.nativeId, row ?? null, false);
20977
+ return { receiptId: `dcode-disable:${prepared.handle.operationId}`, changed: true };
20978
+ },
20979
+ retire: async (prepared) => {
20980
+ await acquireNativeLock(prepared.handle.attemptId, prepared.handle.operationId);
20981
+ const rows = rowsFor(object3(readRegistry3(dcodeRegistryFile()).plugins, "registry plugins")[prepared.handle.nativeId]);
20982
+ for (const row of rows) {
20983
+ const path = rowInstall(row, prepared.handle.nativeId);
20984
+ assertCachePath(path);
20985
+ if (ownership3(path)?.pluginId !== prepared.handle.nativeId)
20986
+ throw new Error("dcode retirement has no ownership proof");
20987
+ }
20988
+ setDcodeRow(prepared.handle.nativeId, null, null);
20989
+ for (const row of rows)
20990
+ rmSync9(rowInstall(row, prepared.handle.nativeId), { recursive: true, force: true });
20991
+ return { receiptId: `dcode-retire:${prepared.handle.operationId}`, changed: true };
20992
+ },
20993
+ readback: async (handle) => {
20994
+ const held = nativeLocks.get(`${handle.attemptId}:${handle.operationId}`);
20995
+ if (held !== undefined && !held.isAlive())
20996
+ throw new Error("dcode native lock was lost before readback");
20997
+ return dcodeLifecycleReadback(handle);
20998
+ },
20999
+ rollback: async (handle) => {
21000
+ await acquireNativeLock(handle.attemptId, handle.operationId);
21001
+ const target = handle.projectedFingerprint === null ? undefined : lifecycleSlot(handle.nativeId, handle.projectedFingerprint);
21002
+ if (target !== undefined)
21003
+ assertCachePath(target);
21004
+ if (target !== undefined && existsSync23(target) && !handle.prior.contentRoots.some((root) => resolve17(root.path) === resolve17(target))) {
21005
+ if (ownership3(target)?.pluginId !== handle.nativeId)
21006
+ throw new Error("dcode rollback target is not owned");
21007
+ rmSync9(target, { recursive: true });
21008
+ }
21009
+ const state = readDoc2(join28(handle.rollbackReference, "state.json"), {}, "rollback state");
21010
+ for (const [index, row] of rowsFor(state.row).entries()) {
21011
+ const path = rowInstall(row, handle.nativeId);
21012
+ assertCachePath(path);
21013
+ const backup = join28(handle.rollbackReference, `content-${index}`);
21014
+ if (existsSync23(backup)) {
21015
+ mkdirSync9(dirname11(path), { recursive: true });
21016
+ cpSync8(backup, path, { recursive: true });
21017
+ }
21018
+ }
21019
+ setDcodeRow(handle.nativeId, state.row ?? null, typeof state.enabled === "boolean" ? state.enabled : null);
21020
+ if (state.registryExisted === false && Object.keys(object3(readRegistry3(dcodeRegistryFile()).plugins, "registry plugins")).length === 0)
21021
+ rmSync9(dcodeRegistryFile());
21022
+ if (state.enablementExisted === false && Object.keys(boolObject(readEnablement(dcodeEnablementFile()).enabledPlugins, "enabledPlugins")).length === 0)
21023
+ rmSync9(dcodeEnablementFile());
21024
+ return { receiptId: `dcode-rollback:${handle.operationId}`, changed: true };
21025
+ },
21026
+ releaseRecovery: async (handle) => {
21027
+ await releaseNativeLock(handle.attemptId, handle.operationId);
21028
+ },
21029
+ cleanup: async (reference, disposition) => {
21030
+ try {
21031
+ const dir = lifecycleDirectory(reference.attemptId, reference.operationId);
21032
+ const next = readDoc2(join28(dir, "rollback", "next-path.json"), {}, "cleanup target");
21033
+ if (disposition === "verified-commit" && typeof next.target === "string") {
21034
+ const prior = readDoc2(join28(dir, "rollback", "state.json"), {}, "rollback state");
21035
+ for (const row of rowsFor(prior.row)) {
21036
+ const path = rowInstall(row, reference.operationId);
21037
+ assertCachePath(path);
21038
+ if (resolve17(path) !== resolve17(next.target))
21039
+ rmSync9(path, { recursive: true, force: true });
21040
+ }
21041
+ }
21042
+ rmSync9(dir, { recursive: true, force: true });
21043
+ } finally {
21044
+ await releaseNativeLock(reference.attemptId, reference.operationId);
21045
+ }
21046
+ return { cleanupId: `dcode-cleanup:${reference.operationId}`, completed: true };
21047
+ }
21048
+ };
21049
+ var dcodeLifecycle = createLifecycleHostAdapter(dcodeDefinition);
20601
21050
  var dcodeWriter = {
21051
+ lifecyclePinAdapter: dcodeLifecycle,
20602
21052
  ...dcode,
20603
21053
  supportsAdoption: true,
20604
21054
  plannedNativeId: (plugin) => `${plugin.name}@${plugin.marketplace || "local"}`,
@@ -20661,7 +21111,7 @@ var dcodeWriter = {
20661
21111
  throw new Error(`dcode cache target is unowned; refusing to replace it: ${target}`);
20662
21112
  const former = prior.kind === "none" ? undefined : prior.path;
20663
21113
  const unchanged = (former === undefined || resolve17(former) === resolve17(target)) && slot?.kind === "current" && slot.fingerprint === sourceFingerprint && slot.projectedFingerprint === projected && slot.sourceRevision === resolved.sha && sameTree7(stage, target);
20664
- const nextRegistry = { ...registry, plugins: { ...plugins, [id]: [{ installPath: target, version: manifest.version }] } };
21114
+ const nextRegistry = { ...registry, plugins: { ...plugins, [id]: [{ installPath: target }] } };
20665
21115
  const nextEnablement = { ...enablement, enabledPlugins: { ...enabled, [id]: true } };
20666
21116
  if (opts?.dryRun) {
20667
21117
  console.log(`[dcode] would activate directory: ${target}`);
@@ -20712,6 +21162,8 @@ var dcodeWriter = {
20712
21162
  assertCachePath(plugin.path);
20713
21163
  if (ownership3(plugin.path)?.pluginId !== plugin.id)
20714
21164
  return { changes: [], refusals: [] };
21165
+ if (opts?.dryRun !== true)
21166
+ throw new Error("dcode pin requires the durable lifecycle pin workflow; direct in-place pinning is refused");
20715
21167
  return pinPluginMcpFiles(plugin.path, dcodeMcpCandidates(), opts);
20716
21168
  },
20717
21169
  async remove(id) {
@@ -20861,7 +21313,9 @@ function assertReadback(id, registryFile, enablementFile, target, version, proje
20861
21313
  const install = rowInstall(rows[0], id);
20862
21314
  if (resolve17(install) !== resolve17(target))
20863
21315
  throw new Error(`dcode readback path mismatch for ${id}`);
20864
- if (rows[0].version !== version)
21316
+ if (rows[0].version !== undefined && rows[0].version !== null)
21317
+ throw new Error(`dcode managed install is eligible for native auto-update: ${id}`);
21318
+ if (declaredManifest(target, id.split("@")[0]).version !== version)
20865
21319
  throw new Error(`dcode readback version mismatch for ${id}`);
20866
21320
  if (boolObject(object3(enablement.enabledPlugins, "enabledPlugins"), "enabledPlugins")[id] !== true)
20867
21321
  throw new Error(`dcode readback enablement mismatch for ${id}`);
@@ -25031,7 +25485,22 @@ async function planLifecycle(input) {
25031
25485
  const result = await prepareEntry(entry, input.hosts, loaded.state, command, input.transportSource, transportCache);
25032
25486
  if (result.kind === "defect")
25033
25487
  return zeroWrite(command, input.dryRun, result.reason, result.terminalPhase);
25034
- prepared.push(result);
25488
+ if (input.removalSelection !== undefined) {
25489
+ if (command !== "remove" || result.kind !== "retire-source") {
25490
+ return zeroWrite(command, input.dryRun, createLifecycleReason("internal", "internal.invariant", "package retirement selection is valid only for manual removal"));
25491
+ }
25492
+ const selected = input.removalSelection.filter((row) => row.scopeId === result.scope.id);
25493
+ if (selected.length === 0 || selected.some((row) => !loaded.state.activations.some((activation) => activation.scopeId === row.scopeId && activation.packageId === row.packageId && activation.nativeId === row.nativeId))) {
25494
+ return zeroWrite(command, input.dryRun, createLifecycleReason("internal", "internal.invariant", "manual removal selection no longer matches recorded ownership"));
25495
+ }
25496
+ const retirementPackages = selected.map(({ packageId, nativeId }) => ({ packageId, nativeId })).sort((left, right) => left.packageId.localeCompare(right.packageId) || left.nativeId.localeCompare(right.nativeId));
25497
+ prepared.push({
25498
+ ...result,
25499
+ retirementPackages,
25500
+ identity: ["remove-packages", result.scope.id, ...retirementPackages.flatMap((row) => [row.packageId, row.nativeId])]
25501
+ });
25502
+ } else
25503
+ prepared.push(result);
25035
25504
  }
25036
25505
  const attemptId = lifecycleAttemptId(loaded.state.stateGeneration, prepared.map((item) => item.identity));
25037
25506
  const scopes = [];
@@ -25043,7 +25512,7 @@ async function planLifecycle(input) {
25043
25512
  scopes.push(planned.scope);
25044
25513
  operations.push(...planned.operations);
25045
25514
  }
25046
- return frozenPlan(input, command, attemptId, scopes, operations, prepared);
25515
+ return frozenPlan(input, command, attemptId, scopes, operations, prepared, loaded.state);
25047
25516
  }
25048
25517
  async function prepareEntry(entry, hosts, state, command, transportSource, transportCache) {
25049
25518
  switch (entry.operation) {
@@ -25077,6 +25546,10 @@ async function prepareSync(entry, hosts, state, command, transportSource, transp
25077
25546
  throw error;
25078
25547
  }
25079
25548
  const scope = deploymentScopeForSyncManifestEntry(entry);
25549
+ const recordedScope = state.scopes.find((row) => row.id === scope.id);
25550
+ if (recordedScope?.target.context !== undefined && JSON.stringify(canonicalTargetContext(recordedScope.target.context)) !== JSON.stringify(entry.target.context === undefined ? undefined : canonicalTargetContext(entry.target.context))) {
25551
+ return defect(createLifecycleReason("internal", "internal.invariant", `target context changed for recorded scope '${scope.id}'; use a distinct target instance or retire the recorded scope explicitly`));
25552
+ }
25080
25553
  const pending = pendingDefect(state, scope.id, new Set(selected.map((item) => item.plugin.name)), command);
25081
25554
  if (pending !== null)
25082
25555
  return defect(pending);
@@ -25202,7 +25675,7 @@ async function planSync(item, attemptId, validatedAt, state, command) {
25202
25675
  requiredCapabilities: [],
25203
25676
  adoptionRequested: selected.adoptionRequested
25204
25677
  });
25205
- classified.push(uninventoriedUpdate(item, selected.plugin.name, nativeId));
25678
+ classified.push(uninventoriedUpdate(item, selected.plugin.name, nativeId, attemptId));
25206
25679
  continue;
25207
25680
  }
25208
25681
  const nativeId = item.host === null ? selected.plugin.name : item.host.plannedNativeId(selected.plugin);
@@ -25213,7 +25686,7 @@ async function planSync(item, attemptId, validatedAt, state, command) {
25213
25686
  requiredCapabilities: [...requiredSemanticsForOperation(inventory, "install")].sort(),
25214
25687
  adoptionRequested: selected.adoptionRequested
25215
25688
  });
25216
- const row = await classifyDesired(item, selected, inventory, nativeId, attemptId, state, command === "add" || command === "update", command === "update");
25689
+ const row = await classifyDesired(item, selected, inventory, nativeId, attemptId, state, command === "update");
25217
25690
  if (row.kind === "defect")
25218
25691
  return row;
25219
25692
  classified.push(row);
@@ -25226,6 +25699,7 @@ async function planSync(item, attemptId, validatedAt, state, command) {
25226
25699
  kind: "planned",
25227
25700
  scope: {
25228
25701
  scope: item.scope,
25702
+ target: item.observation?.target ?? item.entry.target,
25229
25703
  selectorMode: item.entry.selectors === undefined ? "all" : "explicit",
25230
25704
  desired: desiredGeneration(existing, item.frozen.snapshot.revision, item.frozen.snapshot.fingerprint, packages, validatedAt),
25231
25705
  prune: blocks || retirements.operations.some((row) => row.operation.action !== "retire-orphan") ? "blocked" : "planned"
@@ -25233,8 +25707,8 @@ async function planSync(item, attemptId, validatedAt, state, command) {
25233
25707
  operations: [...classified.map((row) => row.operation), ...retirements.operations]
25234
25708
  };
25235
25709
  }
25236
- async function classifyDesired(item, selected, inventory, nativeId, attemptId, state, applyUpdates, reapplyMatchingUpdate) {
25237
- const operationId = operationIdentity("desired-pair", item.scope.id, selected.plugin.name, nativeId, item.frozen.snapshot.fingerprint);
25710
+ async function classifyDesired(item, selected, inventory, nativeId, attemptId, state, reapplyMatchingUpdate) {
25711
+ const operationId = operationIdentity("desired-pair", item.scope.id, selected.plugin.name, nativeId, `${item.frozen.snapshot.fingerprint}\x00${attemptId}`);
25238
25712
  const snapshotId = sourceSnapshotId(item.frozen);
25239
25713
  const activation = findActivation(state, item.scope.id, selected.plugin.name, nativeId);
25240
25714
  if (item.host === null || item.observation === null) {
@@ -25274,13 +25748,12 @@ async function classifyDesired(item, selected, inventory, nativeId, attemptId, s
25274
25748
  return classified(operationId, "desired-pair", item.scope, snapshotId, selected.plugin.name, nativeId, "not-attempted", "none", decision.reason);
25275
25749
  }
25276
25750
  const recordedRoute = activation?.route.kind === "managed" || activation?.route.kind === "native" ? activation.route.kind : null;
25277
- const matchingBytes = sameBytes(activation, installation, selected.plugin);
25751
+ const matchingBytes = sameBytes(activation, installation, selected.plugin) && installation?.activation === "active" && installation.enablement === "enabled";
25278
25752
  const action = chosenAction(replacing, recordedRoute, decision.route, matchingBytes, reapplyMatchingUpdate);
25279
- const plannedOperationId = action === "update" && matchingBytes && reapplyMatchingUpdate ? operationIdentity("desired-pair", item.scope.id, selected.plugin.name, nativeId, `${item.frozen.snapshot.fingerprint}\x00${state.stateGeneration}`) : operationId;
25280
- if (action === "route-migrate" || action === "update" && !applyUpdates) {
25753
+ if (action === "route-migrate") {
25281
25754
  return classified(operationId, "desired-pair", item.scope, snapshotId, selected.plugin.name, nativeId, "retain-prior", recordedRoute ?? decision.route, createLifecycleReason("capability", "capability.unsupported", `${action} of '${selected.plugin.name}' is not applied by this execution slice`, action));
25282
25755
  }
25283
- return classified(plannedOperationId, "desired-pair", item.scope, snapshotId, selected.plugin.name, nativeId, action, decision.route, null);
25756
+ return classified(operationId, "desired-pair", item.scope, snapshotId, selected.plugin.name, nativeId, action, decision.route, null);
25284
25757
  }
25285
25758
  async function planOmissions(item, attemptId, state, desired) {
25286
25759
  const packageIds = new Set(desired.map((row) => row.packageId));
@@ -25306,7 +25779,7 @@ async function planOmissions(item, attemptId, state, desired) {
25306
25779
  return { kind: "omissions", operations };
25307
25780
  }
25308
25781
  async function planRetire(item, attemptId, state) {
25309
- const activations = state.activations.filter((activation) => activation.scopeId === item.recorded.id).sort((left, right) => left.packageId < right.packageId ? -1 : left.packageId > right.packageId ? 1 : 0);
25782
+ const activations = state.activations.filter((activation) => activation.scopeId === item.recorded.id && (item.retirementPackages === undefined || item.retirementPackages.some((row) => row.packageId === activation.packageId && row.nativeId === activation.nativeId))).sort((left, right) => left.packageId < right.packageId ? -1 : left.packageId > right.packageId ? 1 : 0);
25310
25783
  const operations = [];
25311
25784
  for (const activation of activations) {
25312
25785
  const installation = item.observation?.installations.find((row) => row.nativeId === activation.nativeId);
@@ -25326,8 +25799,10 @@ async function planRetire(item, attemptId, state) {
25326
25799
  kind: "planned",
25327
25800
  scope: {
25328
25801
  scope: item.scope,
25802
+ target: item.observation?.target ?? item.recorded.target,
25329
25803
  selectorMode: "retired",
25330
25804
  desired: null,
25805
+ ...item.retirementPackages === undefined ? {} : { retirementPackages: item.retirementPackages },
25331
25806
  prune: operations.length > 0 && operations.every((row) => row.operation.action === "retire-orphan") ? "planned" : "blocked"
25332
25807
  },
25333
25808
  operations
@@ -25417,7 +25892,7 @@ async function selectDisable(item, nativeId, operationId, attemptId, version, ac
25417
25892
  return decision;
25418
25893
  }
25419
25894
  async function selectRetirement(host, observation, scope, activation, attemptId, source) {
25420
- const operationId = operationIdentity("retirement", scope.id, activation.packageId, activation.nativeId, activation.sourceRevision ?? "unrecorded");
25895
+ const operationId = operationIdentity("retirement", scope.id, activation.packageId, activation.nativeId, `${activation.sourceRevision ?? "unrecorded"}\x00${attemptId}`);
25421
25896
  const installation = observation.installations.find((row) => row.nativeId === activation.nativeId);
25422
25897
  if (installation === undefined) {
25423
25898
  return { kind: "planned", operation: notAttemptedRetirement(operationId, scope, activation, "ownership was not revalidated") };
@@ -25500,8 +25975,9 @@ async function decideRecordedRoute(host, observation, recorded, operationId, att
25500
25975
  function recordedActivation(scope, source, activation, installation, target) {
25501
25976
  return ownedActivation(scope, source, activation, installation, target);
25502
25977
  }
25503
- function frozenPlan(input, command, attemptId, scopes, operations, prepared) {
25978
+ function frozenPlan(input, command, attemptId, scopes, operations, prepared, state) {
25504
25979
  const snapshots = [];
25980
+ const frozenSources = new Map;
25505
25981
  const seen = new Set;
25506
25982
  for (const item of prepared) {
25507
25983
  if (item.kind !== "sync")
@@ -25511,8 +25987,12 @@ function frozenPlan(input, command, attemptId, scopes, operations, prepared) {
25511
25987
  continue;
25512
25988
  seen.add(id);
25513
25989
  snapshots.push({ id, reference: item.frozen.snapshot });
25990
+ frozenSources.set(id, item.frozen);
25514
25991
  }
25515
- const outcomes = operations.map((row) => outcomeFor(row));
25992
+ const outcomes = operations.map((row) => {
25993
+ const outcome = outcomeFor(row);
25994
+ return input.dryRun ? { ...outcome, ...observedStates(row.operation, prepared, state) } : outcome;
25995
+ });
25516
25996
  const failed = outcomes.filter((outcome) => outcome.result !== "succeeded");
25517
25997
  const converged = failed.length === 0;
25518
25998
  const failureCategory = converged ? null : failed[0]?.reason?.category ?? "internal";
@@ -25536,12 +26016,31 @@ function frozenPlan(input, command, attemptId, scopes, operations, prepared) {
25536
26016
  kind: "frozen",
25537
26017
  requestedDryRun: input.dryRun,
25538
26018
  attemptId,
26019
+ frozenSources,
25539
26020
  report,
25540
26021
  scopes,
25541
26022
  operations,
25542
26023
  ...input.transportSource === undefined ? {} : { transportSource: input.transportSource }
25543
26024
  };
25544
26025
  }
26026
+ function observedStates(operation, prepared, state) {
26027
+ const item = prepared.find((candidate) => candidate.scope.id === operation.scope.id);
26028
+ if (item?.observation === null || item === undefined)
26029
+ return { resourceState: "unknown", activationState: "unknown" };
26030
+ const installation = item.observation.installations.find((row) => row.nativeId === operation.nativeId);
26031
+ if (installation === undefined || installation.presence === "absent")
26032
+ return { resourceState: "absent", activationState: "inactive" };
26033
+ if (installation.enablement === "disabled" || installation.activation === "inactive")
26034
+ return { resourceState: "present", activationState: "inactive" };
26035
+ if (installation.enablement === "unknown" || installation.activation === "unknown")
26036
+ return { resourceState: "present", activationState: "unknown" };
26037
+ if (installation.activation === "nonconforming")
26038
+ return { resourceState: "present", activationState: "active-nonconforming" };
26039
+ const activation = findActivation(state, operation.scope.id, operation.package, operation.nativeId ?? "");
26040
+ const plugin = item.kind === "sync" ? item.selected.find((selected) => selected.plugin.name === operation.package)?.plugin : undefined;
26041
+ const conforming = operation.coverage === "retirement" ? activation?.fingerprints.installed === installation.installedFingerprint : plugin !== undefined && sameBytes(activation, installation, plugin);
26042
+ return { resourceState: "present", activationState: conforming ? "active-conforming" : "active-nonconforming" };
26043
+ }
25545
26044
  function outcomeFor(row) {
25546
26045
  const operation = row.operation;
25547
26046
  switch (operation.action) {
@@ -25580,8 +26079,8 @@ function plannedNativeIdFor(item, plugin) {
25580
26079
  return error instanceof Error ? error : new Error(unknownErrorDiagnostic(error));
25581
26080
  }
25582
26081
  }
25583
- function uninventoriedUpdate(item, packageName, nativeId) {
25584
- return classified(operationIdentity("desired-pair", item.scope.id, packageName, nativeId, item.frozen.snapshot.fingerprint), "desired-pair", item.scope, sourceSnapshotId(item.frozen), packageName, nativeId, "update", "managed", null);
26082
+ function uninventoriedUpdate(item, packageName, nativeId, attemptId) {
26083
+ return classified(operationIdentity("desired-pair", item.scope.id, packageName, nativeId, `${item.frozen.snapshot.fingerprint}\x00${attemptId}`), "desired-pair", item.scope, sourceSnapshotId(item.frozen), packageName, nativeId, "update", "managed", null);
25585
26084
  }
25586
26085
  function classified(operationId, coverage, scope, sourceSnapshotId, packageName, nativeId, action, route, reason) {
25587
26086
  return {
@@ -25748,9 +26247,7 @@ function commandName(manifest) {
25748
26247
  return manifest.entries.every((entry) => entry.operation === "retire-source") ? "retire-source" : "sync";
25749
26248
  }
25750
26249
  function resolvePlannedSource(binding, transportSource, cache) {
25751
- const argument = transportArgument(binding, transportSource);
25752
- if (argument === undefined)
25753
- return resolveSource(sourceArgument(binding));
26250
+ const argument = transportArgument(binding, transportSource) ?? sourceArgument(binding);
25754
26251
  const cached = cache.get(argument);
25755
26252
  if (cached !== undefined)
25756
26253
  return cached;
@@ -25806,7 +26303,7 @@ function lifecycleAttemptId(stateGeneration, identities) {
25806
26303
  return `attempt-v1-${digest3([String(stateGeneration), ...identities.flat()])}`;
25807
26304
  }
25808
26305
  function lifecycleAttemptIdForPlannedScopes(stateGeneration, scopes) {
25809
- return lifecycleAttemptId(stateGeneration, scopes.map((planned) => plannedAttemptIdentity(planned.scope.id, planned.desired?.sourceFingerprint, planned.desired?.packages.map((pkg) => pkg.packageId), planned.selectorMode === "retired" || planned.desired === null)));
26306
+ return lifecycleAttemptId(stateGeneration, scopes.map((planned) => planned.retirementPackages !== undefined ? ["remove-packages", planned.scope.id, ...planned.retirementPackages.flatMap((row) => [row.packageId, row.nativeId])] : plannedAttemptIdentity(planned.scope.id, planned.desired?.sourceFingerprint, planned.desired?.packages.map((pkg) => pkg.packageId), planned.selectorMode === "retired" || planned.desired === null)));
25810
26307
  }
25811
26308
  function plannedAttemptIdentity(scopeId, sourceFingerprint, packageIds, retired = false) {
25812
26309
  if (retired || sourceFingerprint === undefined || packageIds === undefined)
@@ -26022,6 +26519,15 @@ async function executeFrozen(plan, hosts, now) {
26022
26519
  const refusal = ledger.generationRefusal(plan);
26023
26520
  if (refusal !== null)
26024
26521
  return refusalReport(plan, refusal);
26522
+ try {
26523
+ for (const frozen of plan.frozenSources.values()) {
26524
+ if (fingerprintTree(frozen.snapshotDir) !== frozen.snapshot.fingerprint) {
26525
+ return refusalReport(plan, createLifecycleReason("internal", "internal.invariant", "retained frozen source bytes drifted before apply"));
26526
+ }
26527
+ }
26528
+ } catch (error) {
26529
+ return refusalReport(plan, thrownReason(error));
26530
+ }
26025
26531
  const outcomes = [];
26026
26532
  let stopped = null;
26027
26533
  for (const row of plan.operations) {
@@ -26060,8 +26566,9 @@ async function runOperation(plan, operation, hosts, ledger) {
26060
26566
  return succeeded(operation, false);
26061
26567
  case "retire-orphan":
26062
26568
  return runRetire(plan, operation, hosts, ledger);
26063
- case "route-migrate":
26064
26569
  case "disable-nonconforming":
26570
+ return runDisable(plan, operation, hosts, ledger);
26571
+ case "route-migrate":
26065
26572
  case "retain-prior":
26066
26573
  case "not-attempted":
26067
26574
  return stop(operation, createLifecycleReason("internal", "internal.invariant", `operation '${operation.operationId}' is not applied by this execution slice`));
@@ -26124,6 +26631,12 @@ async function runActivation(plan, operation, hosts, ledger, action) {
26124
26631
  verified = host.adapter.verify(receipt.handle, observation);
26125
26632
  } catch (error) {
26126
26633
  const reason = thrownReason(error);
26634
+ if (receipt === undefined && ledger.journalState(plan.attemptId, operation.operationId) === "applying" && error instanceof LifecycleHostPhaseError && !error.mutationStarted) {
26635
+ ledger.markNotApplied(plan, operation);
26636
+ if (preparation !== undefined)
26637
+ await abortPreparation(host, preparation);
26638
+ return stop(operation, reason);
26639
+ }
26127
26640
  if (handle !== undefined && ledger.journalState(plan.attemptId, operation.operationId) === "applying") {
26128
26641
  return recoverApplied(plan, operation, host, ledger, handle, reason);
26129
26642
  }
@@ -26186,8 +26699,10 @@ async function prepareActivation(plan, operation, host, nativeId, action, record
26186
26699
  if (context === undefined) {
26187
26700
  return refused(createLifecycleReason("internal", "internal.invariant", `${action} '${operation.package}' has no frozen source snapshot`));
26188
26701
  }
26189
- const frozen = resolvePlannedSource(operation.scope.source, plan.transportSource, new Map);
26190
- if (frozen.snapshot.fingerprint !== context.reference.fingerprint || frozen.snapshot.revision !== context.reference.revision) {
26702
+ const frozen = operation.sourceSnapshotId === null ? undefined : plan.frozenSources.get(operation.sourceSnapshotId);
26703
+ if (frozen === undefined)
26704
+ return refused(createLifecycleReason("internal", "internal.invariant", `${action} '${operation.package}' lost its retained frozen source`));
26705
+ if (frozen.snapshot.fingerprint !== context.reference.fingerprint || frozen.snapshot.revision !== context.reference.revision || fingerprintTree(frozen.snapshotDir) !== context.reference.fingerprint) {
26191
26706
  return refused(createLifecycleReason("internal", "internal.invariant", `${action} '${operation.package}' source bytes drifted from the frozen snapshot`));
26192
26707
  }
26193
26708
  const plugin = frozen.plugins.find((candidate) => candidate.name === operation.package);
@@ -26195,12 +26710,15 @@ async function prepareActivation(plan, operation, host, nativeId, action, record
26195
26710
  if (plugin === undefined || packageFingerprint === undefined) {
26196
26711
  return refused(createLifecycleReason("internal", "internal.invariant", `${action} '${operation.package}' is missing from the frozen source`));
26197
26712
  }
26198
- const target = lifecycleTarget2(operation);
26713
+ const target = lifecycleTarget2(plan, operation);
26199
26714
  const observation = await host.adapter.observeTarget(target);
26200
26715
  const version = await host.adapter.probeVersion(observation.target);
26201
26716
  if (version.kind !== "detected") {
26202
26717
  return refused(createLifecycleReason("runtime", "runtime.operation-failed", `${action} '${operation.package}' lost its detected target version`));
26203
26718
  }
26719
+ if (fingerprintTree(frozen.snapshotDir) !== context.reference.fingerprint) {
26720
+ return refused(createLifecycleReason("internal", "internal.invariant", `${action} '${operation.package}' retained snapshot changed during target observation`));
26721
+ }
26204
26722
  const inventory = inventoryPackageSemantics(plugin);
26205
26723
  const resolvedPins = resolveRecordedPinExecutables(plugin.dir, recordedPins);
26206
26724
  if (resolvedPins.kind === "refused")
@@ -26276,6 +26794,50 @@ function sealActivationSnapshot(operation, attemptId, nativeId, frozen, plugin,
26276
26794
  throw new Error(`frozen snapshot action '${snapshot.action}' is not ${action}`);
26277
26795
  return snapshot;
26278
26796
  }
26797
+ async function runDisable(plan, operation, hosts, ledger) {
26798
+ const host = hosts.find((candidate) => candidate.kinds.includes(operation.scope.target.kind));
26799
+ const reason = plan.report.outcomes.find((row) => row.operationId === operation.operationId)?.reason;
26800
+ if (host === undefined || operation.nativeId === null || reason === undefined || reason === null) {
26801
+ return stop(operation, createLifecycleReason("internal", "internal.invariant", "disable containment lost its adapter, identity or desired failure reason"));
26802
+ }
26803
+ let preparation;
26804
+ let receipt;
26805
+ try {
26806
+ const prepared = await prepareOwnedMutation(plan, operation, host, operation.nativeId, ledger, "disable");
26807
+ if (prepared.kind === "refused")
26808
+ return stop(operation, prepared.reason);
26809
+ if (prepared.selection.operation !== "disable")
26810
+ throw new Error("disable containment selected the wrong operation");
26811
+ ledger.acceptJournal(plan, operation, "disable-nonconforming");
26812
+ preparation = await host.adapter.prepareDisable({ operationId: operation.operationId, attemptId: plan.attemptId, selection: { ...prepared.selection, operation: "disable" }, activation: prepared.activation });
26813
+ ledger.markApplying(plan, operation);
26814
+ receipt = await host.adapter.disable(preparation);
26815
+ const observation = await host.adapter.readback(receipt.handle);
26816
+ const verified = host.adapter.verify(receipt.handle, observation);
26817
+ ledger.confirmDisable(plan, operation);
26818
+ try {
26819
+ await host.adapter.cleanup(verified.handle, "verified-commit");
26820
+ } catch {
26821
+ ledger.markCleanupPending(plan, operation, "disable-nonconforming");
26822
+ return pendingCleanup(operation);
26823
+ }
26824
+ ledger.completeContainment(plan, operation);
26825
+ return { stop: true, reason, outcome: { ...operation, result: "failed", resourceState: "retained", activationState: "inactive", changed: receipt.changed, reason } };
26826
+ } catch (error) {
26827
+ const failure = thrownReason(error);
26828
+ if (receipt === undefined && error instanceof LifecycleHostPhaseError && !error.mutationStarted) {
26829
+ if (ledger.journalState(plan.attemptId, operation.operationId) !== undefined)
26830
+ ledger.markNotApplied(plan, operation);
26831
+ if (preparation !== undefined)
26832
+ await abortPreparation(host, preparation);
26833
+ return stop(operation, failure);
26834
+ }
26835
+ if (preparation !== undefined && (receipt !== undefined || ledger.journalState(plan.attemptId, operation.operationId) === "applying")) {
26836
+ return recoverApplied(plan, operation, host, ledger, preparation.handle, failure);
26837
+ }
26838
+ return stop(operation, failure);
26839
+ }
26840
+ }
26279
26841
  async function runRetire(plan, operation, hosts, ledger) {
26280
26842
  if (ledger.journalState(plan.attemptId, operation.operationId) === "completed")
26281
26843
  return retired(operation, false);
@@ -26293,17 +26855,20 @@ async function runRetire(plan, operation, hosts, ledger) {
26293
26855
  }
26294
26856
  let prepared;
26295
26857
  try {
26296
- prepared = await prepareRetirement(plan, operation, host, nativeId, ledger);
26858
+ prepared = await prepareOwnedMutation(plan, operation, host, nativeId, ledger);
26297
26859
  } catch (error) {
26298
26860
  return stop(operation, thrownReason(error));
26299
26861
  }
26300
26862
  if (prepared.kind === "refused")
26301
26863
  return stop(operation, prepared.reason);
26864
+ if (prepared.selection.operation !== "retire")
26865
+ return stop(operation, createLifecycleReason("internal", "internal.invariant", "retirement selected the wrong operation"));
26302
26866
  try {
26303
26867
  ledger.acceptJournal(plan, operation, "retire-orphan");
26304
26868
  } catch (error) {
26305
26869
  return stop(operation, thrownReason(error));
26306
26870
  }
26871
+ let preparation;
26307
26872
  let receipt;
26308
26873
  let verified;
26309
26874
  try {
@@ -26311,17 +26876,27 @@ async function runRetire(plan, operation, hosts, ledger) {
26311
26876
  operationId: operation.operationId,
26312
26877
  attemptId: plan.attemptId,
26313
26878
  action: "retire-orphan",
26314
- selection: prepared.selection,
26879
+ selection: { ...prepared.selection, operation: "retire" },
26315
26880
  activation: prepared.activation
26316
26881
  });
26882
+ preparation = sealed;
26317
26883
  ledger.markPruning(plan, operation);
26318
26884
  receipt = await host.adapter.retire(sealed);
26319
26885
  const observation = await host.adapter.readback(receipt.handle);
26320
26886
  verified = host.adapter.verify(receipt.handle, observation);
26321
26887
  } catch (error) {
26322
26888
  const reason = thrownReason(error);
26889
+ if (receipt === undefined && ledger.journalState(plan.attemptId, operation.operationId) === "applying" && error instanceof LifecycleHostPhaseError && !error.mutationStarted) {
26890
+ ledger.markNotApplied(plan, operation);
26891
+ if (preparation !== undefined)
26892
+ await abortPreparation(host, preparation);
26893
+ return stop(operation, reason);
26894
+ }
26323
26895
  if (receipt !== undefined)
26324
26896
  return recoverApplied(plan, operation, host, ledger, receipt.handle, reason);
26897
+ if (preparation !== undefined && ledger.journalState(plan.attemptId, operation.operationId) === "applying") {
26898
+ return recoverApplied(plan, operation, host, ledger, preparation.handle, reason);
26899
+ }
26325
26900
  return stop(operation, reason);
26326
26901
  }
26327
26902
  if (receipt === undefined || verified === undefined) {
@@ -26341,12 +26916,13 @@ async function runRetire(plan, operation, hosts, ledger) {
26341
26916
  ledger.markCompleted(plan, operation);
26342
26917
  return retired(operation, receipt.changed);
26343
26918
  }
26344
- async function prepareRetirement(plan, operation, host, nativeId, ledger) {
26919
+ async function prepareOwnedMutation(plan, operation, host, nativeId, ledger, capabilityOperation = "retire") {
26920
+ const operationLabel = capabilityOperation === "disable" ? "disablement" : "retirement";
26345
26921
  const recorded = ledger.activation(operation.scope.id, operation.package, nativeId);
26346
26922
  if (recorded === undefined || recorded.route.kind === "legacy-unverified" || recorded.ownership.kind === "legacy-claim" || recorded.sourceRevision === undefined) {
26347
26923
  return refused(createLifecycleReason("internal", "internal.invariant", `ownership of '${operation.package}' was not revalidated`));
26348
26924
  }
26349
- const target = lifecycleTarget2(operation);
26925
+ const target = lifecycleTarget2(plan, operation);
26350
26926
  const observation = await host.adapter.observeTarget(target);
26351
26927
  const installation = observation.installations.find((row) => row.nativeId === nativeId);
26352
26928
  if (installation === undefined || installation.presence !== "present" || installation.ownership.kind !== "owned" || installation.ownership.scopeId !== operation.scope.id) {
@@ -26354,10 +26930,10 @@ async function prepareRetirement(plan, operation, host, nativeId, ledger) {
26354
26930
  }
26355
26931
  const version = await host.adapter.probeVersion(observation.target);
26356
26932
  if (version.kind !== "detected") {
26357
- return refused(createLifecycleReason("runtime", "runtime.operation-failed", `retirement '${operation.package}' lost its detected target version`));
26933
+ return refused(createLifecycleReason("runtime", "runtime.operation-failed", `${operationLabel} '${operation.package}' lost its detected target version`));
26358
26934
  }
26359
26935
  if (recorded.fingerprints.installed !== installation.installedFingerprint) {
26360
- return refused(createLifecycleReason("internal", "internal.invariant", `retirement '${operation.package}' fingerprint does not match the owned install`));
26936
+ return refused(createLifecycleReason("internal", "internal.invariant", `${operationLabel} '${operation.package}' fingerprint does not match the owned install`));
26361
26937
  }
26362
26938
  const owned = ownedActivation(operation.scope, operation.scope.source, recorded, installation, observation.target);
26363
26939
  if (owned === null) {
@@ -26366,27 +26942,27 @@ async function prepareRetirement(plan, operation, host, nativeId, ledger) {
26366
26942
  const planCoverage = createLifecyclePlanCoverage(observation, [{
26367
26943
  nativeId,
26368
26944
  operationId: operation.operationId,
26369
- operation: "retire",
26945
+ operation: capabilityOperation,
26370
26946
  mutationGroupId: operation.operationId,
26371
26947
  authorization: "observed-owned"
26372
26948
  }]);
26373
26949
  const nativeScope = await host.adapter.observeNativeMutationScope({
26374
26950
  targetObservation: observation,
26375
- operation: "retire",
26951
+ operation: capabilityOperation,
26376
26952
  packageName: owned.packageName,
26377
26953
  nativeId,
26378
26954
  sourceType: owned.sourceType
26379
26955
  });
26380
26956
  const nativeProjection = await host.adapter.observeNativeProjection({
26381
26957
  targetObservation: observation,
26382
- operation: "retire",
26958
+ operation: capabilityOperation,
26383
26959
  operationId: operation.operationId,
26384
26960
  attemptId: plan.attemptId,
26385
26961
  activation: owned
26386
26962
  });
26387
26963
  const decision = host.adapter.decideRoute({
26388
26964
  target: observation.target,
26389
- operation: "retire",
26965
+ operation: capabilityOperation,
26390
26966
  operationId: operation.operationId,
26391
26967
  attemptId: plan.attemptId,
26392
26968
  scopeId: operation.scope.id,
@@ -26400,13 +26976,13 @@ async function prepareRetirement(plan, operation, host, nativeId, ledger) {
26400
26976
  planCoverage,
26401
26977
  activation: owned
26402
26978
  });
26403
- if (decision.kind !== "selected" || decision.operation !== "retire" || decision.route !== operation.route) {
26404
- return refused(createLifecycleReason("runtime", "runtime.operation-failed", `retirement '${operation.package}' kept frozen route '${operation.route}'`));
26979
+ if (decision.kind !== "selected" || decision.operation !== capabilityOperation || decision.route !== operation.route) {
26980
+ return refused(createLifecycleReason("runtime", "runtime.operation-failed", `${operationLabel} '${operation.package}' kept frozen route '${operation.route}'`));
26405
26981
  }
26406
26982
  return { kind: "ready", selection: decision, activation: owned, recorded };
26407
26983
  }
26408
- function lifecycleTarget2(operation) {
26409
- return { kind: operation.scope.target.kind, instance: operation.scope.target.instance };
26984
+ function lifecycleTarget2(plan, operation) {
26985
+ return plan.scopes.find((row) => row.scope.id === operation.scope.id)?.target ?? { kind: operation.scope.target.kind, instance: operation.scope.target.instance };
26410
26986
  }
26411
26987
 
26412
26988
  class Ledger {
@@ -26467,8 +27043,18 @@ class Ledger {
26467
27043
  return this.state.activations.find((row) => row.scopeId === scopeId && row.packageId === packageId && row.nativeId === nativeId);
26468
27044
  }
26469
27045
  markApplying(plan, operation) {
27046
+ if (operation.action === "disable-nonconforming")
27047
+ this.disablePending(plan, operation, "applying");
26470
27048
  this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "applying", "applying", true));
26471
27049
  }
27050
+ markNotApplied(plan, operation) {
27051
+ if (operation.action === "disable-nonconforming")
27052
+ this.clearDisablePending(operation);
27053
+ this.replaceAttempt(plan, (attempt) => {
27054
+ const next = this.journaled(attempt, operation.operationId, "not-attempted", "failed", false);
27055
+ return { ...next, mutationStarted: next.journal.some((row) => mutationBegan(row.state)) };
27056
+ });
27057
+ }
26472
27058
  markPruning(plan, operation) {
26473
27059
  this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "applying", "pruning", true));
26474
27060
  }
@@ -26532,13 +27118,36 @@ class Ledger {
26532
27118
  }
26533
27119
  markRolledBack(plan, operation) {
26534
27120
  this.adoptMovedGeneration();
27121
+ if (operation.action === "disable-nonconforming")
27122
+ this.clearDisablePending(operation, true);
26535
27123
  this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "rolled-back", "failed", true));
26536
27124
  }
27125
+ confirmDisable(plan, operation) {
27126
+ this.clearDisablePending(operation);
27127
+ this.state = { ...this.state, activations: this.state.activations.map((row) => row.scopeId === operation.scope.id && row.packageId === operation.package ? { ...row, activationState: "inactive", readbackState: "verified", readbackAt: this.now, updatedAt: this.now } : row) };
27128
+ this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "readback-verified", "readback", true));
27129
+ }
27130
+ completeContainment(plan, operation) {
27131
+ this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "completed", "failed", true));
27132
+ }
26537
27133
  markRollbackRequired(plan, operation) {
26538
27134
  this.adoptMovedGeneration();
26539
27135
  this.retargetBareAlias(plan, operation);
27136
+ if (operation.action === "disable-nonconforming")
27137
+ this.disablePending(plan, operation, "rollback");
26540
27138
  this.replaceAttempt(plan, (attempt) => this.journaled(attempt, operation.operationId, "rollback", "recovery-required", true));
26541
27139
  }
27140
+ disablePending(plan, operation, phase) {
27141
+ this.state = { ...this.state, activations: this.state.activations.map((row) => row.scopeId === operation.scope.id && row.packageId === operation.package ? { ...row, pending: { operation: "disable-nonconforming", phase, attemptId: plan.attemptId, startedAt: this.now } } : row) };
27142
+ }
27143
+ clearDisablePending(operation, rolledBack = false) {
27144
+ this.state = { ...this.state, activations: this.state.activations.map((row) => {
27145
+ if (row.scopeId !== operation.scope.id || row.packageId !== operation.package)
27146
+ return row;
27147
+ const { pending: _pending, ...prior } = row;
27148
+ return rolledBack ? { ...prior, activationState: "nonconforming", readbackState: "unverified" } : prior;
27149
+ }) };
27150
+ }
26542
27151
  markCompleted(plan, operation) {
26543
27152
  const desiredPairs = plan.operations.filter((row) => row.operation.scope.id === operation.scope.id && row.operation.coverage === "desired-pair");
26544
27153
  const converged = desiredPairs.every((row) => row.operation.operationId === operation.operationId || row.operation.action === "unchanged" || this.journalState(plan.attemptId, row.operation.operationId) === "completed");
@@ -26557,7 +27166,13 @@ class Ledger {
26557
27166
  if (scope.authority !== "authoritative")
26558
27167
  return scope;
26559
27168
  const planned = plan.scopes.find((row) => row.scope.id === scope.id);
27169
+ if (planned?.retirementPackages !== undefined)
27170
+ return scope;
26560
27171
  if (planned?.selectorMode === "retired") {
27172
+ if (this.state.activations.some((row) => row.scopeId === scope.id))
27173
+ return scope;
27174
+ if (plan.operations.some((row) => row.operation.scope.id === scope.id && row.operation.operationId !== operation.operationId && this.journalState(plan.attemptId, row.operation.operationId) !== "completed"))
27175
+ return scope;
26561
27176
  const { desired: _desired, lastConverged: _converged, ...kept } = scope;
26562
27177
  return {
26563
27178
  ...kept,
@@ -26625,7 +27240,7 @@ class Ledger {
26625
27240
  if (existing !== undefined && existing.authority !== "authoritative")
26626
27241
  return existing;
26627
27242
  const selectorMode = planned.selectorMode;
26628
- const target = { kind: planned.scope.target.kind, instance: planned.scope.target.instance };
27243
+ const target = planned.target ?? { kind: planned.scope.target.kind, instance: planned.scope.target.instance };
26629
27244
  if (existing === undefined) {
26630
27245
  return {
26631
27246
  id: planned.scope.id,
@@ -26791,19 +27406,40 @@ function succeeded(operation, changed, notices = []) {
26791
27406
  };
26792
27407
  }
26793
27408
  async function recoverApplied(plan, operation, host, ledger, handle, reason) {
27409
+ let verified = false;
27410
+ let diagnostic = "";
26794
27411
  try {
26795
- await host.adapter.rollback(handle);
26796
- const observation = await host.adapter.readback(handle);
26797
- host.adapter.verifyRollback(handle, observation);
26798
- } catch {
26799
- ledger.markRollbackRequired(plan, operation);
26800
- return pendingRecovery(operation);
27412
+ try {
27413
+ await host.adapter.rollback(handle);
27414
+ const observation = await host.adapter.readback(handle);
27415
+ host.adapter.verifyRollback(handle, observation);
27416
+ verified = true;
27417
+ } catch (error) {
27418
+ diagnostic = `; rollback verification failed: ${unknownErrorDiagnostic(error)}`;
27419
+ }
27420
+ try {
27421
+ if (verified)
27422
+ ledger.markRolledBack(plan, operation);
27423
+ else
27424
+ ledger.markRollbackRequired(plan, operation);
27425
+ } catch (error) {
27426
+ verified = false;
27427
+ diagnostic += `; recovery ledger could not be persisted: ${unknownErrorDiagnostic(error)}`;
27428
+ }
27429
+ if (verified) {
27430
+ try {
27431
+ await host.adapter.cleanup(handle, "verified-rollback");
27432
+ } catch {}
27433
+ }
27434
+ } finally {
27435
+ try {
27436
+ await host.adapter.releaseRecovery(handle);
27437
+ } catch (error) {
27438
+ verified = false;
27439
+ diagnostic += `; recovery resources could not be released: ${unknownErrorDiagnostic(error)}`;
27440
+ }
26801
27441
  }
26802
- ledger.markRolledBack(plan, operation);
26803
- try {
26804
- await host.adapter.cleanup(handle, "verified-rollback");
26805
- } catch {}
26806
- return reportedRollback(operation, reason);
27442
+ return verified ? reportedRollback(operation, reason) : pendingRecovery(operation, `; original failure: ${reason.diagnostic}${diagnostic}`);
26807
27443
  }
26808
27444
  async function abortPreparation(host, preparation) {
26809
27445
  try {
@@ -26831,8 +27467,8 @@ function pendingReadback(operation, reason) {
26831
27467
  }
26832
27468
  };
26833
27469
  }
26834
- function pendingRecovery(operation) {
26835
- const reason = createLifecycleReason("recovery", "recovery.required", `rollback for '${operation.operationId}' did not verify`);
27470
+ function pendingRecovery(operation, diagnostic = "") {
27471
+ const reason = createLifecycleReason("recovery", "recovery.required", `rollback for '${operation.operationId}' did not verify${diagnostic}`);
26836
27472
  return {
26837
27473
  stop: true,
26838
27474
  reason,
@@ -26868,8 +27504,8 @@ function pendingCleanup(operation) {
26868
27504
  outcome: {
26869
27505
  ...operation,
26870
27506
  result: "pending",
26871
- resourceState: "present",
26872
- activationState: "active-conforming",
27507
+ resourceState: operation.action === "disable-nonconforming" ? "retained" : "present",
27508
+ activationState: operation.action === "disable-nonconforming" ? "inactive" : "active-conforming",
26873
27509
  changed: true,
26874
27510
  reason
26875
27511
  }
@@ -26948,7 +27584,7 @@ function summaryFor(outcomes) {
26948
27584
  const failed = outcomes.find((outcome) => outcome.result !== "succeeded");
26949
27585
  const changed = outcomes.some((outcome) => outcome.changed);
26950
27586
  const pending = outcomes.find((outcome) => outcome.result === "pending");
26951
- const readback = outcomes.find((outcome) => outcome.reason?.category === "readback");
27587
+ const readback = outcomes.find((outcome) => outcome.reason?.category === "readback" || outcome.action === "disable-nonconforming" && outcome.changed);
26952
27588
  const reapplied = outcomes.some((outcome) => outcome.result === "succeeded" && outcome.action === "update");
26953
27589
  const mutationStarted = changed || pending !== undefined || reapplied;
26954
27590
  if (failed === undefined) {
@@ -27042,7 +27678,8 @@ async function runFrozenLifecycle(command) {
27042
27678
  validatedAt: command.now,
27043
27679
  hosts: command.hosts,
27044
27680
  ...command.command === undefined ? {} : { command: command.command },
27045
- ...command.transportSource === undefined ? {} : { transportSource: command.transportSource }
27681
+ ...command.transportSource === undefined ? {} : { transportSource: command.transportSource },
27682
+ ...command.removalSelection === undefined ? {} : { removalSelection: command.removalSelection }
27046
27683
  });
27047
27684
  const executed = await executeLifecycle({
27048
27685
  plan,
@@ -27065,6 +27702,7 @@ function plannerHost(kind, adapter, plannedNativeId, announceDryRun) {
27065
27702
  var lifecyclePlannerHostKinds = [
27066
27703
  "claude-code",
27067
27704
  "codex",
27705
+ "dcode",
27068
27706
  "kimi",
27069
27707
  "cursor",
27070
27708
  "omp",
@@ -27076,6 +27714,7 @@ function createLifecyclePlannerHosts() {
27076
27714
  return [
27077
27715
  plannerHost("claude-code", createClaudeCodeLifecycleHost(), claudeCodeWriter.plannedNativeId),
27078
27716
  plannerHost("codex", codexLifecycle, codexWriter.plannedNativeId),
27717
+ plannerHost("dcode", dcodeLifecycle, dcodeWriter.plannedNativeId),
27079
27718
  plannerHost("kimi", createLifecycleHostAdapter(kimiLifecycle), kimiWriter.plannedNativeId),
27080
27719
  plannerHost("cursor", cursorManagedLifecycle, cursorWriter.plannedNativeId, announceCursorDryRun),
27081
27720
  plannerHost("omp", ompLifecycle, ompWriter.plannedNativeId),
@@ -27085,6 +27724,161 @@ function createLifecyclePlannerHosts() {
27085
27724
  ];
27086
27725
  }
27087
27726
 
27727
+ // src/pin-lifecycle.ts
27728
+ import { cpSync as cpSync13, mkdtempSync as mkdtempSync14, rmSync as rmSync15 } from "node:fs";
27729
+ import { tmpdir as tmpdir12 } from "node:os";
27730
+ import { basename as basename9, join as join35, relative as relative8 } from "node:path";
27731
+ async function runOfflineLifecyclePin(host, plugin, dryRun) {
27732
+ const adapter = host.lifecyclePinAdapter;
27733
+ if (adapter === undefined || plugin.path === undefined)
27734
+ throw new Error("offline lifecycle pin has no adapter or active root");
27735
+ let state = readLifecycleState().state;
27736
+ const matches = state.activations.filter((row) => row.nativeId === plugin.id && state.scopes.some((scope) => scope.id === row.scopeId && scope.target.kind === host.id));
27737
+ if (matches.length !== 1)
27738
+ throw new Error("offline lifecycle pin requires one recorded owned activation");
27739
+ const activation = matches[0];
27740
+ const scope = state.scopes.find((row) => row.id === activation.scopeId);
27741
+ if (activation.ownership.kind === "legacy-claim" || activation.route.kind === "legacy-unverified" || activation.sourceRevision === undefined || activation.pending !== undefined)
27742
+ throw new Error("offline lifecycle pin requires verified ownership with no pending recovery");
27743
+ if (activation.activationState !== "active" || activation.readbackState !== "verified")
27744
+ throw new Error("offline lifecycle pin requires a verified active installation");
27745
+ const preview = await host.pin(plugin, { dryRun: true });
27746
+ if (preview.refusals.length !== 0)
27747
+ return { changes: [], refusals: preview.refusals };
27748
+ if (preview.changes.length === 0)
27749
+ return preview;
27750
+ const now = new Date().toISOString();
27751
+ const frozenRoot = mkdtempSync14(join35(tmpdir12(), "plgnz-offline-pin-"));
27752
+ const attemptId = `pin-${basename9(frozenRoot)}`;
27753
+ const operationId = `${attemptId}:${plugin.id}`;
27754
+ let handle;
27755
+ let preparation;
27756
+ let accepted = false, applying = false, committed = false;
27757
+ const attempt = { id: attemptId, command: "update", phase: "accepted", mutationStarted: false, scopeIds: [scope.id], journal: [{ operationId, scopeId: scope.id, packageId: activation.packageId, nativeId: activation.nativeId, action: "update", state: "pending", route: activation.route, startedAt: now, updatedAt: now }], startedAt: now, updatedAt: now };
27758
+ const persist = (nextActivation) => {
27759
+ if (readLifecycleState().state.stateGeneration !== state.stateGeneration)
27760
+ throw new Error("pin ledger changed concurrently; refusing stale commit");
27761
+ const next = { ...state, stateGeneration: state.stateGeneration + 1, activations: state.activations.map((row) => row.scopeId === activation.scopeId && row.nativeId === activation.nativeId ? nextActivation : row), attempts: [...state.attempts.filter((row) => row.id !== attemptId), { ...attempt, journal: attempt.journal.map((row) => ({ ...row })) }] };
27762
+ writeLifecycleState(next, { globalPreflight: "succeeded" });
27763
+ state = next;
27764
+ };
27765
+ const pending = (phase) => persist({ ...activation, pending: { operation: "update", phase, attemptId, startedAt: now }, readbackState: "pending" });
27766
+ try {
27767
+ cpSync13(plugin.path, frozenRoot, { recursive: true });
27768
+ const packageFingerprint = fingerprintTree(frozenRoot);
27769
+ if (packageFingerprint !== activation.fingerprints.installed)
27770
+ throw new Error("pin active bytes differ from recorded readback");
27771
+ const target = { kind: scope.target.kind, instance: scope.target.instance };
27772
+ const observed = await adapter.observeTarget(target);
27773
+ const installed = observed.installations.find((row) => row.nativeId === plugin.id);
27774
+ if (installed?.ownership.kind !== "owned" || installed.ownership.scopeId !== scope.id || installed.installedFingerprint !== packageFingerprint || installed.enablement !== "enabled")
27775
+ throw new Error("pin native identity or bytes differ from the recorded owned activation");
27776
+ const version = await adapter.probeVersion(target);
27777
+ const sourceType = scope.source.kind;
27778
+ const inventory = inventoryPackageSemantics({ dir: frozenRoot, name: activation.packageId, ...plugin.version === undefined ? {} : { version: plugin.version }, contentFingerprint: packageFingerprint });
27779
+ const snapshot = createFrozenPackageSnapshot({ operationId, attemptId, scopeId: scope.id, target, action: "update", packageName: activation.packageId, nativeId: plugin.id, sourceType, immutableRevision: activation.sourceRevision, snapshotRoot: frozenRoot, packageRoot: frozenRoot, relativePackagePath: ".", snapshotFingerprint: packageFingerprint, packageFingerprint, ...sourceType === "git" ? { nativeGit: { locator: scope.source.locator, resolvedRevision: activation.sourceRevision } } : {}, inventory });
27780
+ const pins = createResolvedLifecyclePins(preview.changes.map((change) => ({ server: change.server, executable: change.to })).sort((left, right) => left.server < right.server ? -1 : left.server > right.server ? 1 : 0));
27781
+ const nativeScope = await adapter.observeNativeMutationScope({ targetObservation: observed, operation: "update", packageName: activation.packageId, nativeId: plugin.id, sourceType });
27782
+ const nativeProjection = await adapter.observeNativeProjection({ targetObservation: observed, operation: "update", snapshot, pins });
27783
+ const selection = adapter.decideRoute({ target, operationId, attemptId, scopeId: scope.id, packageName: activation.packageId, nativeId: plugin.id, version, sourceType, targetObservation: observed, nativeScope, nativeProjection, planCoverage: createLifecyclePlanCoverage(observed, [{ nativeId: plugin.id, operationId, operation: "update", mutationGroupId: operationId, authorization: "observed-owned" }]), operation: "update", snapshot, pins });
27784
+ if (selection.kind !== "selected")
27785
+ throw new Error(`pin capability ${selection.status}: ${selection.gaps.map((gap) => gap.capabilityId).join(", ")}`);
27786
+ if (selection.route !== "managed")
27787
+ throw new Error("offline lifecycle pin requires a frozen Managed projection");
27788
+ if (dryRun)
27789
+ return preview;
27790
+ pending("accepted");
27791
+ accepted = true;
27792
+ preparation = await adapter.stageActivation({ selection, snapshot, pins });
27793
+ preparation = await adapter.applyLifecycleDirectives(preparation);
27794
+ preparation = await adapter.applyPins(preparation);
27795
+ const sealed = await adapter.sealActivation(preparation);
27796
+ preparation = sealed;
27797
+ handle = sealed.handle;
27798
+ attempt.phase = "applying";
27799
+ attempt.mutationStarted = true;
27800
+ attempt.journal[0].state = "applying";
27801
+ pending("applying");
27802
+ applying = true;
27803
+ await adapter.apply(sealed);
27804
+ const observation = await adapter.readback(handle);
27805
+ adapter.verify(handle, observation);
27806
+ if (observation.installedFingerprint === null || handle.projectedFingerprint === null)
27807
+ throw new Error("pin readback lacks exact installed/projected fingerprint");
27808
+ attempt.phase = "finalizing";
27809
+ attempt.journal[0].state = "readback-verified";
27810
+ const pinnedActivation = { ...activation, route: { kind: handle.route, evidenceKey: { kind: "capability-profile", key: handle.evidenceId } }, fingerprints: { ...activation.fingerprints, projected: handle.projectedFingerprint, installed: observation.installedFingerprint }, pins: [...new Set([...activation.pins, ...pins.map((pin) => pin.server)])].sort(), readbackState: "verified", readbackAt: now, updatedAt: now };
27811
+ persist(pinnedActivation);
27812
+ committed = true;
27813
+ try {
27814
+ await adapter.cleanup(handle, "verified-commit");
27815
+ } catch (error) {
27816
+ attempt.journal[0].state = "cleanup-pending";
27817
+ persist({ ...pinnedActivation, pending: { operation: "update", phase: "cleanup", attemptId, startedAt: now } });
27818
+ await adapter.releaseRecovery(handle);
27819
+ throw error;
27820
+ }
27821
+ attempt.phase = "completed";
27822
+ attempt.journal[0].state = "completed";
27823
+ attempt.completedAt = now;
27824
+ persist(pinnedActivation);
27825
+ const activatedRoot = observation.contentRoots[0]?.path;
27826
+ return { ...preview, changes: preview.changes.map((change) => ({ ...change, file: activatedRoot === undefined ? change.file : join35(activatedRoot, relative8(plugin.path, change.file)) })) };
27827
+ } catch (error) {
27828
+ if (!committed && handle !== undefined && applying) {
27829
+ if (!(error instanceof LifecycleHostPhaseError && error.mutationStarted === false)) {
27830
+ attempt.phase = "recovery-required";
27831
+ attempt.journal[0].state = "rollback";
27832
+ try {
27833
+ let journalError;
27834
+ try {
27835
+ pending("rollback");
27836
+ } catch (writeError) {
27837
+ journalError = writeError;
27838
+ }
27839
+ await adapter.rollback(handle);
27840
+ adapter.verifyRollback(handle, await adapter.readback(handle));
27841
+ if (journalError !== undefined)
27842
+ throw new Error(`rollback verified but its durable receipt is blocked: ${journalError.message}`);
27843
+ attempt.phase = "finalizing";
27844
+ attempt.journal[0].state = "cleanup-pending";
27845
+ persist({ ...activation, pending: { operation: "update", phase: "cleanup", attemptId, startedAt: now } });
27846
+ await adapter.cleanup(handle, "verified-rollback");
27847
+ attempt.phase = "failed";
27848
+ attempt.journal[0].state = "rolled-back";
27849
+ persist(activation);
27850
+ } catch (recoveryError) {
27851
+ await adapter.releaseRecovery(handle);
27852
+ throw new Error(`pin failed: ${error.message}; recovery remains pending: ${recoveryError.message}`);
27853
+ }
27854
+ } else {
27855
+ attempt.phase = "failed";
27856
+ attempt.journal[0].state = "not-attempted";
27857
+ attempt.mutationStarted = false;
27858
+ persist(activation);
27859
+ await adapter.cleanup(handle, "aborted-preparation");
27860
+ }
27861
+ } else if (!committed && accepted) {
27862
+ try {
27863
+ if (preparation !== undefined)
27864
+ await adapter.cleanup(preparation, "aborted-preparation");
27865
+ } finally {
27866
+ attempt.phase = "failed";
27867
+ attempt.journal[0].state = "not-attempted";
27868
+ persist(activation);
27869
+ }
27870
+ }
27871
+ throw error;
27872
+ } finally {
27873
+ try {
27874
+ if (handle !== undefined)
27875
+ await adapter.releaseRecovery(handle);
27876
+ } finally {
27877
+ rmSync15(frozenRoot, { recursive: true, force: true });
27878
+ }
27879
+ }
27880
+ }
27881
+
27088
27882
  // src/pin.ts
27089
27883
  function defaultPinTargets(all = writers) {
27090
27884
  return all.filter((w) => w.gui);
@@ -27095,13 +27889,20 @@ async function runPin(options = {}) {
27095
27889
  const selected = selectTargets(candidates, options);
27096
27890
  const findings = [];
27097
27891
  let ledgerChanged = false;
27892
+ const changedRecords = new Set;
27098
27893
  for (const host of selected) {
27099
27894
  if (!host.detect())
27100
27895
  continue;
27101
27896
  for (const plugin of host.listInstalled()) {
27102
27897
  if (plugin.path === undefined)
27103
27898
  continue;
27104
- const outcome = await host.pin(plugin, { dryRun: options.dryRun });
27899
+ let outcome;
27900
+ try {
27901
+ outcome = host.lifecyclePinAdapter === undefined ? await host.pin(plugin, { dryRun: options.dryRun }) : await runOfflineLifecyclePin(host, plugin, options.dryRun === true);
27902
+ } catch (error) {
27903
+ findings.push({ host: host.id, mark: "✗", message: `plugin '${plugin.id}' pin failed — ${error.message}` });
27904
+ continue;
27905
+ }
27105
27906
  const prefix = options.dryRun === true ? "[dry-run] " : "";
27106
27907
  for (const change of outcome.changes) {
27107
27908
  findings.push({
@@ -27119,6 +27920,8 @@ async function runPin(options = {}) {
27119
27920
  }
27120
27921
  if (outcome.changes.length === 0)
27121
27922
  continue;
27923
+ if (host.lifecyclePinAdapter !== undefined)
27924
+ continue;
27122
27925
  const record = findRecord(state, host.id, plugin.id);
27123
27926
  if (record === undefined) {
27124
27927
  findings.push({
@@ -27130,6 +27933,7 @@ async function runPin(options = {}) {
27130
27933
  }
27131
27934
  if (options.dryRun === true)
27132
27935
  continue;
27936
+ changedRecords.add(record);
27133
27937
  const pins = new Set(record.pins ?? []);
27134
27938
  for (const change of outcome.changes)
27135
27939
  pins.add(change.server);
@@ -27147,10 +27951,10 @@ async function runPin(options = {}) {
27147
27951
  }
27148
27952
  }
27149
27953
  if (ledgerChanged)
27150
- persistRecordedPins(state, options.state !== undefined);
27954
+ persistRecordedPins(state, options.state !== undefined, changedRecords);
27151
27955
  return { findings, exitCode: findings.some((f) => f.mark === "✗") ? 1 : 0 };
27152
27956
  }
27153
- function persistRecordedPins(records, legacyLedger) {
27957
+ function persistRecordedPins(records, legacyLedger, changedRecords) {
27154
27958
  const loaded = legacyLedger ? null : readLifecycleState();
27155
27959
  if (loaded === null || loaded.sourceVersion !== 2) {
27156
27960
  writeState(records);
@@ -27162,7 +27966,7 @@ function persistRecordedPins(records, legacyLedger) {
27162
27966
  stateGeneration: loaded.state.stateGeneration + 1,
27163
27967
  activations: loaded.state.activations.map((activation) => {
27164
27968
  const scope = scopes.get(activation.scopeId);
27165
- const record = records.find((candidate) => candidate.host === scope?.target.kind && candidate.id === activation.nativeId);
27969
+ const record = records.find((candidate) => changedRecords.has(candidate) && candidate.host === scope?.target.kind && candidate.id === activation.nativeId);
27166
27970
  if (record === undefined)
27167
27971
  return activation;
27168
27972
  return {
@@ -27634,7 +28438,7 @@ function legacyCommandAction(command) {
27634
28438
  // package.json
27635
28439
  var package_default = {
27636
28440
  name: "plugnz",
27637
- version: "0.0.7",
28441
+ version: "0.0.9",
27638
28442
  description: "Install, diagnose and update agent plugins (Agent Plugins spec) and MCP configs across Claude Code, Codex, Cursor, Kimi and more",
27639
28443
  license: "MIT",
27640
28444
  type: "module",
@@ -27877,10 +28681,17 @@ function withoutRetiredActivation(state, scopeId, packageId, nativeId, retiredAt
27877
28681
  if (activation === undefined)
27878
28682
  throw new Error(`activation '${packageId}' is not recorded for manual removal`);
27879
28683
  const dropped = state.activations.filter((row) => row !== activation);
28684
+ const attempts = state.attempts.map((attempt) => attempt.id !== activation.pending?.attemptId ? attempt : {
28685
+ ...attempt,
28686
+ phase: "completed",
28687
+ completedAt: retiredAt,
28688
+ updatedAt: retiredAt,
28689
+ journal: attempt.journal.map((entry) => ({ ...entry, state: "completed", updatedAt: retiredAt }))
28690
+ });
27880
28691
  const tombstone = retirementTombstone(activation, retiredAt);
27881
28692
  if (tombstone === null) {
27882
28693
  if (activation.ownership.kind === "legacy-claim" && activation.route.kind === "legacy-unverified" && (activation.ownership.prior === "plgnz" || activation.ownership.prior === "unrecorded")) {
27883
- return { ...state, stateGeneration: state.stateGeneration + 1, activations: dropped };
28694
+ return { ...state, stateGeneration: state.stateGeneration + 1, activations: dropped, attempts };
27884
28695
  }
27885
28696
  throw new Error(`activation '${packageId}' cannot retain a tombstone`);
27886
28697
  }
@@ -27888,9 +28699,47 @@ function withoutRetiredActivation(state, scopeId, packageId, nativeId, retiredAt
27888
28699
  ...state,
27889
28700
  stateGeneration: state.stateGeneration + 1,
27890
28701
  activations: dropped,
28702
+ attempts,
27891
28703
  tombstones: state.tombstones.some((row) => row.id === tombstone.id) ? state.tombstones : [...state.tombstones, tombstone]
27892
28704
  };
27893
28705
  }
28706
+ function withPendingRemoval(state, operation, now) {
28707
+ const activation = state.activations.find((row) => row.scopeId === operation.scope.id && row.nativeId === operation.nativeId && (row.packageId === operation.package || row.packageId === operation.nativeId));
28708
+ if (activation === undefined)
28709
+ throw new Error(`activation '${operation.package}' is not recorded for manual removal`);
28710
+ if (activation.pending?.operation === "remove")
28711
+ return { ...state, stateGeneration: state.stateGeneration + 1 };
28712
+ const attemptId = `remove-attempt-${state.stateGeneration}-${operation.operationId}`;
28713
+ return {
28714
+ ...state,
28715
+ stateGeneration: state.stateGeneration + 1,
28716
+ scopes: state.scopes.map((scope) => scope.id === operation.scope.id ? { ...scope, lastAttemptId: attemptId } : scope),
28717
+ activations: state.activations.map((row) => row !== activation ? row : {
28718
+ ...row,
28719
+ pending: { operation: "remove", phase: "applying", attemptId, startedAt: now }
28720
+ }),
28721
+ attempts: [...state.attempts, {
28722
+ id: attemptId,
28723
+ command: "remove",
28724
+ phase: "pruning",
28725
+ mutationStarted: true,
28726
+ scopeIds: [operation.scope.id],
28727
+ startedAt: now,
28728
+ updatedAt: now,
28729
+ journal: [{
28730
+ operationId: operation.operationId,
28731
+ scopeId: operation.scope.id,
28732
+ packageId: activation.packageId,
28733
+ nativeId: activation.nativeId,
28734
+ action: "remove",
28735
+ state: "applying",
28736
+ route: activation.route,
28737
+ startedAt: now,
28738
+ updatedAt: now
28739
+ }]
28740
+ }]
28741
+ };
28742
+ }
27894
28743
  function reportFor(command, dryRun, plan, outcomes, options = {}) {
27895
28744
  const failure = options.reason ?? outcomes.find((candidate) => candidate.result !== "succeeded")?.reason ?? null;
27896
28745
  const result = options.result ?? (failure === null && outcomes.every((candidate) => candidate.result === "succeeded") ? "converged" : "incomplete");
@@ -28598,19 +29447,13 @@ async function runRecordedLifecycleRemove(flags, target, state, selected) {
28598
29447
  const alias = writer?.persistedNativeIdMayAlias?.(activation.nativeId, target) === true;
28599
29448
  if (target !== activation.packageId && target !== activation.nativeId && !alias)
28600
29449
  return [];
28601
- return [{ scope }];
29450
+ return [{ scope, activation }];
28602
29451
  });
28603
29452
  if (matches.length === 0 || matches.some((match) => !plannerKinds.has(match.scope.target.kind)))
28604
29453
  return null;
28605
29454
  const byScope = new Map;
28606
29455
  for (const match of matches)
28607
29456
  byScope.set(match.scope.id, match.scope);
28608
- for (const scopeId of byScope.keys()) {
28609
- const inScope = state.activations.filter((activation) => activation.scopeId === scopeId).length;
28610
- const selectedInScope = matches.filter((match) => match.scope.id === scopeId).length;
28611
- if (inScope !== selectedInScope)
28612
- return null;
28613
- }
28614
29457
  const perHost = new Map;
28615
29458
  for (const match of matches) {
28616
29459
  const count = (perHost.get(match.scope.target.kind) ?? 0) + 1;
@@ -28643,7 +29486,12 @@ async function runRecordedLifecycleRemove(flags, target, state, selected) {
28643
29486
  dryRun: flags.dryRun,
28644
29487
  hosts: createLifecyclePlannerHosts(),
28645
29488
  now: new Date().toISOString(),
28646
- command: "remove"
29489
+ command: "remove",
29490
+ removalSelection: matches.map(({ activation }) => ({
29491
+ scopeId: activation.scopeId,
29492
+ packageId: activation.packageId,
29493
+ nativeId: activation.nativeId
29494
+ }))
28647
29495
  });
28648
29496
  return executed.report;
28649
29497
  }
@@ -29175,20 +30023,22 @@ async function main(argv) {
29175
30023
  preflightFailure = createLifecycleReason("internal", "internal.invariant", "install is pending; refusing removal");
29176
30024
  break;
29177
30025
  }
30026
+ const pendingOperationId = claimed?.pending?.operation === "remove" ? lifecycleState.attempts.find((attempt) => attempt.id === claimed.pending.attemptId)?.journal.find((entry) => entry.scopeId === claimed.scopeId && entry.packageId === claimed.packageId && entry.nativeId === claimed.nativeId)?.operationId : undefined;
29178
30027
  pairs.push({
29179
30028
  writer,
29180
30029
  record,
29181
30030
  nativeId: identity.nativeId,
29182
30031
  legacyNativeIds: identity.legacyNativeIds,
29183
30032
  failure: null,
29184
- operation: planOperation2({
30033
+ operation: { ...planOperation2({
29185
30034
  command: "remove",
29186
30035
  scope: selected.scope,
29187
30036
  package: identity.package,
29188
30037
  nativeId: identity.nativeId,
29189
30038
  action: "retire-orphan",
29190
- route: "managed"
29191
- })
30039
+ route: "managed",
30040
+ discriminator: lifecycleVersion === 2 ? String(lifecycleState.stateGeneration) : undefined
30041
+ }), ...pendingOperationId === undefined ? {} : { operationId: pendingOperationId } }
29192
30042
  });
29193
30043
  }
29194
30044
  if (preflightFailure !== null) {
@@ -29244,7 +30094,7 @@ async function main(argv) {
29244
30094
  throw new LifecycleCommandError(createLifecycleReason("internal", "internal.invariant", `state generation moved before removal of '${nativeId}'`));
29245
30095
  }
29246
30096
  try {
29247
- lifecycleState = withoutRetiredActivation(fresh.state, operation.scope.id, operation.package, nativeId, new Date().toISOString());
30097
+ lifecycleState = withPendingRemoval(fresh.state, operation, new Date().toISOString());
29248
30098
  } catch (error) {
29249
30099
  throw new LifecycleCommandError(createLifecycleReason("internal", "internal.invariant", unknownErrorDiagnostic(error)));
29250
30100
  }
@@ -29276,6 +30126,12 @@ async function main(argv) {
29276
30126
  }
29277
30127
  pairTerminalPhase = "finalize";
29278
30128
  if (lifecycleVersion === 2) {
30129
+ const fresh = readLifecycleState();
30130
+ if (fresh.sourceVersion !== 2 || fresh.state.stateGeneration !== lifecycleState.stateGeneration) {
30131
+ throw new LifecycleCommandError(createLifecycleReason("internal", "internal.invariant", `state generation moved before retirement finalization of '${nativeId}'`));
30132
+ }
30133
+ lifecycleState = withoutRetiredActivation(fresh.state, operation.scope.id, operation.package, nativeId, new Date().toISOString());
30134
+ writeLifecycleState(lifecycleState, { globalPreflight: "succeeded" });
29279
30135
  state = readState();
29280
30136
  } else {
29281
30137
  const finalized = state.filter((candidate) => candidate !== pending);