@agentxm/extension-lifecycle 0.28.4-bootstrap.0 → 0.28.5

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.
@@ -122,7 +122,9 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
122
122
  detail: `Configured ${expectedType} "${name}" uses a ${pluralType} Registry source`,
123
123
  });
124
124
  }
125
- if (parsedPattern?.name !== undefined && parsedPattern.name !== name) {
125
+ if (expectedType !== "mcp-server" &&
126
+ parsedPattern?.name !== undefined &&
127
+ parsedPattern.name !== name) {
126
128
  return yield* new ExtensionLifecycleFailed({
127
129
  category: "validation",
128
130
  detail: `Configured ${expectedType} "${name}" points to Registry extension "${parsedPattern.name}"`,
@@ -136,13 +138,14 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
136
138
  });
137
139
  }
138
140
  const versionRange = Option.fromUndefinedOr(parsedPattern?.versionRange);
141
+ const registryName = expectedType === "mcp-server" ? (parsedPattern?.name ?? name) : name;
139
142
  const workspace = yield* WorkspaceMutations;
140
143
  const acceptedRef = yield* acceptedResolutionRef({
141
144
  workspace,
142
145
  type: expectedType,
143
146
  name,
144
147
  });
145
- const accepted = Option.flatMap(acceptedRef, (ref) => ref.refType === "registry" && ref.owner === owner && ref.name === name
148
+ const accepted = Option.flatMap(acceptedRef, (ref) => ref.refType === "registry" && ref.owner === owner && ref.name === registryName
146
149
  ? Option.some({
147
150
  version: ref.version,
148
151
  publisherBindingId: ref.publisherBindingId,
@@ -150,7 +153,7 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
150
153
  : Option.none());
151
154
  const providers = yield* SourceHostProviders;
152
155
  const resolution = yield* providers.resolveNamedRegistry(resolvedSource, {
153
- name,
156
+ name: registryName,
154
157
  type: expectedType,
155
158
  owner,
156
159
  versionRange,
@@ -11,7 +11,7 @@ import * as Option from "effect/Option";
11
11
  import * as Path from "effect/Path";
12
12
  import * as Schema from "effect/Schema";
13
13
  import * as Semaphore from "effect/Semaphore";
14
- import { HookConfigInvalid, HookDefinitionInvalid, HookInstallStateMissing, HookIoFailed, activeContributors, applyProjectionPlans, planAggregateProjection, runWithTransientFileBackup, reconcileManagedRegionFile, managedHookCommands, readManagedHookCommands, updateHooksJson, evaluateHookAgentOutcome, WriteBackupRetained, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
14
+ import { HookConfigInvalid, HookDefinitionInvalid, HookInstallStateMissing, HookIoFailed, activeContributors, applyProjectionPlans, planAggregateProjection, runWithTransientFileBackup, reconcileManagedRegionFile, projectionGeneration, managedHookCommands, readManagedHookCommands, updateHooksJson, evaluateHookAgentOutcome, WriteBackupRetained, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
15
15
  import { AGENTS as CAPABILITY_AGENTS, installable, } from "@agentxm/extension-model/unstable/agent-capabilities";
16
16
  import { computePackageContentHash } from "@agentxm/workspace-state";
17
17
  import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
@@ -437,12 +437,25 @@ export const HookManagerLive = Layer.effect(HookManager, Effect.gen(function* ()
437
437
  const rendered = input.contributors
438
438
  .map((hook) => `### ${hook.manifest.title ?? hook.name}\n\nFor agents without a usable native hook mapping (${hook.fallbackAgentIds.join(", ")}), treat this as a managed advisory rule. After the matching lifecycle event (${hook.manifest.bindings.map((binding) => binding.on).join(", ")}), run \`${hook.command}\` and address any findings before continuing.`)
439
439
  .join("\n\n");
440
+ const generation = projectionGeneration([
441
+ "hook-fallback-region-v1",
442
+ target.workspaceRelative,
443
+ HOOK_FALLBACKS_REGION_OWNER,
444
+ ...input.contributors.flatMap((contributor) => [
445
+ contributor.name,
446
+ contributor.marker,
447
+ contributor.command,
448
+ JSON.stringify(contributor.fallbackAgentIds),
449
+ JSON.stringify(contributor.manifest),
450
+ ]),
451
+ ]);
440
452
  const { changed, observedRegion } = yield* provide(reconcileManagedRegionFile({
441
453
  targetPath: target.targetPath,
442
454
  displayPath: target.workspaceRelative,
443
455
  region: HOOK_FALLBACKS_REGION,
444
456
  owner: HOOK_FALLBACKS_REGION_OWNER,
445
457
  rendered,
458
+ generation,
446
459
  ...(options?.dryRun === undefined ? {} : { dryRun: options.dryRun }),
447
460
  unsupportedTargetDetail: `Hook fallback target does not support managed regions: ${target.workspaceRelative}`,
448
461
  }));
@@ -464,12 +477,6 @@ export const HookManagerLive = Layer.effect(HookManager, Effect.gen(function* ()
464
477
  present: Option.isSome(observedRegion),
465
478
  current: !changed,
466
479
  expectedContributors: input.contributors.map(({ marker }) => marker),
467
- observedContributors: Option.match(observedRegion, {
468
- onNone: () => [],
469
- onSome: (region) => input.contributors
470
- .filter(({ command }) => region.includes(command))
471
- .map(({ marker }) => marker),
472
- }),
473
480
  },
474
481
  };
475
482
  });
@@ -23,7 +23,7 @@ export type { InstallResult } from "./skills/operations/install-result.js";
23
23
  export { uninstallSkill, type UninstallSkillOperation, type UninstallSkillOperationArgs, } from "./skills/operations/uninstall.js";
24
24
  export { enableSkill, type EnableSkillOperation } from "./skills/operations/enable.js";
25
25
  export { disableSkill, type DisableSkillOperation } from "./skills/operations/disable.js";
26
- export { installMcpServer, type InstallMcpServerOperation, type InstallMcpServerOperationArgs, } from "./mcps/operations/install.js";
26
+ export { collectSecretInputNames, deleteMcpSecrets, installMcpServer, mcpSecretAccount, readMcpServerManifest, type InstallMcpServerOperation, type InstallMcpServerOperationArgs, } from "./mcps/operations/install.js";
27
27
  export { uninstallMcpServer, type UninstallMcpServerOperation, type UninstallMcpServerOperationArgs, } from "./mcps/operations/uninstall.js";
28
28
  export { enableMcpServer, type EnableMcpServerOperation } from "./mcps/operations/enable.js";
29
29
  export { disableMcpServer, type DisableMcpServerOperation } from "./mcps/operations/disable.js";
package/dist/src/index.js CHANGED
@@ -24,7 +24,7 @@ export { uninstallSkill, } from "./skills/operations/uninstall.js";
24
24
  export { enableSkill } from "./skills/operations/enable.js";
25
25
  export { disableSkill } from "./skills/operations/disable.js";
26
26
  // MCP server lifecycle operations
27
- export { installMcpServer, } from "./mcps/operations/install.js";
27
+ export { collectSecretInputNames, deleteMcpSecrets, installMcpServer, mcpSecretAccount, readMcpServerManifest, } from "./mcps/operations/install.js";
28
28
  export { uninstallMcpServer, } from "./mcps/operations/uninstall.js";
29
29
  export { enableMcpServer } from "./mcps/operations/enable.js";
30
30
  export { disableMcpServer } from "./mcps/operations/disable.js";
@@ -11,7 +11,7 @@ import * as Schema from "effect/Schema";
11
11
  import * as Scope from "effect/Scope";
12
12
  import * as Result from "effect/Result";
13
13
  import { PlatformError } from "effect/PlatformError";
14
- import { KnowledgeDefinitionInvalid, KnowledgeDesiredStateUnreconcilable, KnowledgeInstallStateMissing, KnowledgeIoFailed, KnowledgeObservableContractViolated, KnowledgeResolutionMissing, KnowledgeUnavailable, applyProjectionPlans, planAggregateProjection, requireCompleteGraph, KNOWLEDGE_REGION_OWNER, reconcileKnowledgeDiscovery, observedKnowledgeContributors, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
14
+ import { KnowledgeDefinitionInvalid, KnowledgeDesiredStateUnreconcilable, KnowledgeInstallStateMissing, KnowledgeIoFailed, KnowledgeObservableContractViolated, KnowledgeResolutionMissing, KnowledgeUnavailable, applyProjectionPlans, planAggregateProjection, requireCompleteGraph, KNOWLEDGE_REGION_OWNER, reconcileKnowledgeDiscovery, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
15
15
  import { canReuseInstalledPackage, materializeExternalPackage } from "@agentxm/extension-workspace";
16
16
  import { materializeRegistryPackage } from "../registry-materialization.js";
17
17
  import { computeExtensionPathsForLayout, extensionPathSourceFromLockEntry, } from "@agentxm/workspace-state";
@@ -351,10 +351,6 @@ export const KnowledgeManagerLive = Layer.effect(KnowledgeManager, Effect.gen(fu
351
351
  present: Option.isSome(result.observedRegion),
352
352
  current: !result.changed,
353
353
  expectedContributors: input.contributors.map(({ owner, name }) => `${owner}/knowledge/${name}`),
354
- observedContributors: Option.match(result.observedRegion, {
355
- onNone: () => [],
356
- onSome: observedKnowledgeContributors,
357
- }),
358
354
  }))),
359
355
  apply: (input) => runKnowledgeProjectionAdapter({
360
356
  bundles: input.contributors,
@@ -15,7 +15,7 @@ import * as Option from "effect/Option";
15
15
  import { McpConfigIoFailed, McpInstallStateMissing, McpRegistryOnlyInstall, removeMcpServerFromManifest, applyProjectionPlans, planSingletonProjection, } from "@agentxm/extension-workspace";
16
16
  import { McpServerManager } from "@agentxm/extension-workspace";
17
17
  import { configuredMcpServersToDiskRefs } from "@agentxm/extension-workspace";
18
- import { WorkspaceMutations } from "@agentxm/workspace-state";
18
+ import { mcpRegistryResolutionKey, WorkspaceMutations } from "@agentxm/workspace-state";
19
19
  import { canReuseInstalledPackage } from "@agentxm/extension-workspace";
20
20
  import { materializeRegistryPackageWithTreeIntegrity } from "../registry-materialization.js";
21
21
  import { computeExtensionPathsForLayout } from "@agentxm/workspace-state";
@@ -55,6 +55,7 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
55
55
  const fsPathLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path), Layer.succeed(HttpClient.HttpClient, httpClient));
56
56
  const provide = (effect) => Effect.provide(effect, fsPathLayer);
57
57
  const lastTreeIntegrities = new Map();
58
+ const pendingRemoval = new Map();
58
59
  const materializeInstall = Effect.fn("McpServerManager.materializeInstall")(function* ({ ref, force }) {
59
60
  if (ref.refType !== "registry") {
60
61
  return yield* new McpRegistryOnlyInstall({
@@ -64,7 +65,11 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
64
65
  }
65
66
  const registryRef = ref;
66
67
  const canonicalPath = computeExtensionPathsForLayout(path.join, ws.layout, registryRef, "mcps", registryRef.name).canonicalPath;
67
- const lockedEntry = yield* ws.getLockedMcpServer(registryRef.server.name);
68
+ const lockedEntry = yield* ws.getLockedMcpServer(mcpRegistryResolutionKey({
69
+ authority: registryRef.source.location,
70
+ owner: registryRef.owner,
71
+ name: registryRef.server.name,
72
+ }));
68
73
  const lockedVersion = acceptedRegistryVersionForRef(lockedEntry, registryRef);
69
74
  const useExisting = yield* provide(canReuseInstalledPackage({
70
75
  installedPath: canonicalPath,
@@ -97,6 +102,18 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
97
102
  lastTreeIntegrities.set(registryRef.server.name, materialized.treeIntegrity);
98
103
  }, Effect.asVoid);
99
104
  const makeMaterializeRemoval = (retainCanonical) => Effect.fn("McpServerManager.materializeRemoval")(function* ({ target }) {
105
+ const graph = yield* ws.getDesiredStateGraph();
106
+ const desiredNode = graph.nodes.find((node) => node.type === "mcp-server" && node.name === target.name);
107
+ const closure = desiredNode === undefined || desiredNode.authority === "inline"
108
+ ? undefined
109
+ : graph.mcpSourceClosures.find((candidate) => candidate.identity === desiredNode.identity);
110
+ const retainShared = closure !== undefined && closure.localNames.some((name) => name !== target.name);
111
+ pendingRemoval.set(target.name, {
112
+ ...(desiredNode === undefined || desiredNode.authority === "inline"
113
+ ? {}
114
+ : { resolutionKey: desiredNode.identity }),
115
+ retainShared,
116
+ });
100
117
  const configuredAgents = yield* ws.getConfiguredAgents();
101
118
  yield* applyProjectionPlans(configuredAgents.map((agentId) => planSingletonProjection({
102
119
  unitId: "mcp-server:native-config-entry",
@@ -119,7 +136,7 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
119
136
  })).pipe(Effect.asVoid),
120
137
  },
121
138
  })));
122
- if (retainCanonical)
139
+ if (retainCanonical || retainShared)
123
140
  return;
124
141
  const canonical = yield* provide(acceptedCanonicalObservation({
125
142
  workspace: ws,
@@ -178,6 +195,11 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
178
195
  const lockEntry = buildMcpServerLockEntry(registryRef, treeIntegrity);
179
196
  return ws.setMcpServer({
180
197
  name: ref.server.name,
198
+ resolutionKey: mcpRegistryResolutionKey({
199
+ authority: registryRef.source.location,
200
+ owner: registryRef.owner,
201
+ name: registryRef.server.name,
202
+ }),
181
203
  lockEntry,
182
204
  versionRange,
183
205
  });
@@ -200,14 +222,26 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
200
222
  const lockEntry = buildMcpServerLockEntry(registryRef, treeIntegrity);
201
223
  return ws.setMcpServerLock({
202
224
  name: ref.server.name,
225
+ resolutionKey: mcpRegistryResolutionKey({
226
+ authority: registryRef.source.location,
227
+ owner: registryRef.owner,
228
+ name: registryRef.server.name,
229
+ }),
203
230
  lockEntry,
204
231
  versionRange: Option.none(),
205
232
  });
206
233
  }), Effect.withSpan("McpServerManager.upsertLockfileEntry"));
207
234
  },
208
- removeLockfileEntry: ({ target }) => ws
209
- .removeMcpServerLock(target.name)
210
- .pipe(Effect.withSpan("McpServerManager.removeLockfileEntry")),
235
+ removeLockfileEntry: ({ target }) => {
236
+ const removal = pendingRemoval.get(target.name);
237
+ pendingRemoval.delete(target.name);
238
+ if (removal?.retainShared === true || removal?.resolutionKey === undefined) {
239
+ return Effect.void.pipe(Effect.withSpan("McpServerManager.removeLockfileEntry"));
240
+ }
241
+ return ws
242
+ .removeMcpServerLock(removal.resolutionKey)
243
+ .pipe(Effect.withSpan("McpServerManager.removeLockfileEntry"));
244
+ },
211
245
  };
212
246
  }));
213
247
  //# sourceMappingURL=manager.js.map
@@ -17,12 +17,15 @@ import type { StepFailure } from "@agentxm/workspace-operations";
17
17
  import type { JobStepResult, Operation } from "@agentxm/workspace-operations";
18
18
  import { WorkspaceMutations } from "@agentxm/workspace-state";
19
19
  import type { McpServerExtensionRef } from "@agentxm/extension-model/unstable/extensions/refs/mcp-server";
20
+ import { type McpServerManifest } from "@agentxm/extension-model/unstable/mcps/manifest-schema";
20
21
  import { LifecycleFailureAdapter } from "../../failure-adapter.js";
21
22
  /**
22
23
  * Args for the install-mcp-server operation.
23
24
  */
24
25
  export type InstallMcpServerOperationArgs = {
25
26
  readonly ref: McpServerExtensionRef;
27
+ /** Local connection identity and exact agent-native MCP key. */
28
+ readonly localName?: string;
26
29
  readonly force: boolean;
27
30
  /** Explicitly permit a workspace-authored relocation during reconciliation. */
28
31
  readonly allowWorkspaceSourceTransition?: boolean;
@@ -49,6 +52,25 @@ export type InstallMcpServerOperationArgs = {
49
52
  * @experimental This API is unstable and may change without notice.
50
53
  */
51
54
  export type InstallMcpServerOperation = Operation<"install-mcp-server", InstallMcpServerOperationArgs>;
55
+ export declare const mcpSecretAccount: (args: {
56
+ readonly scopeRoot: string;
57
+ readonly localName: string;
58
+ readonly sourceIdentity: string;
59
+ readonly inputName: string;
60
+ }) => string;
61
+ export type McpSecretDeletionOutcome = {
62
+ readonly _tag: "deleted";
63
+ readonly inputName: string;
64
+ } | {
65
+ readonly _tag: "absent";
66
+ readonly inputName: string;
67
+ } | {
68
+ readonly _tag: "failed";
69
+ readonly inputName: string;
70
+ };
71
+ export declare const collectSecretInputNames: (manifest: McpServerManifest) => ReadonlySet<string>;
72
+ export declare const readMcpServerManifest: (canonicalPath: string) => Effect.Effect<Option.Option<McpServerManifest>, never, FileSystem.FileSystem | Path.Path>;
73
+ export declare const deleteMcpSecrets: (identity: Omit<Parameters<typeof mcpSecretAccount>[0], "inputName">, secretNames: ReadonlySet<string>) => Effect.Effect<ReadonlyArray<McpSecretDeletionOutcome>>;
52
74
  /**
53
75
  * Install-mcp-server operation handler.
54
76
  *
@@ -20,8 +20,9 @@ import * as Array from "effect/Array";
20
20
  import * as Effect from "effect/Effect";
21
21
  import * as Option from "effect/Option";
22
22
  import * as Schema from "effect/Schema";
23
+ import { createHash } from "node:crypto";
23
24
  import { CodingAgentRepository, McpSharedTargetConflict, applyProjectionPlansWithResults, planSingletonProjection, inspectAgentMcpServer, sharedMcpTargetPolicyConflict, } from "@agentxm/extension-workspace";
24
- import { isPathSafe } from "@agentxm/workspace-state";
25
+ import { isPathSafe, mcpRegistryResolutionKey } from "@agentxm/workspace-state";
25
26
  import { acceptedRegistryVersionForRef, validateExactResolvedVersion, } from "@agentxm/workspace-state";
26
27
  import { appendWarningsToMessage } from "@agentxm/workspace-operations";
27
28
  import { WorkspaceMutations } from "@agentxm/workspace-state";
@@ -55,7 +56,9 @@ const buildLockEntry = (ref, treeIntegrity) => ({
55
56
  treeIntegrity,
56
57
  });
57
58
  const MCP_SECRET_SERVICE = "axm-mcp";
58
- const mcpSecretAccount = (serverName, inputName) => `${serverName}:${inputName}`;
59
+ export const mcpSecretAccount = (args) => createHash("sha256")
60
+ .update([args.scopeRoot, args.localName, args.sourceIdentity, args.inputName].join("\0"))
61
+ .digest("hex");
59
62
  const keyringModuleSpecifier = ["@napi-rs", "keyring"].join("/");
60
63
  const loadKeyringEntry = Effect.tryPromise({
61
64
  try: async () => {
@@ -64,22 +67,22 @@ const loadKeyringEntry = Effect.tryPromise({
64
67
  },
65
68
  catch: () => undefined,
66
69
  });
67
- const saveMcpSecret = (serverName, inputName, value) => Effect.gen(function* () {
70
+ const saveMcpSecret = (account, inputName, value) => Effect.gen(function* () {
68
71
  const Entry = yield* loadKeyringEntry;
69
72
  return yield* Effect.try({
70
73
  try: () => {
71
- const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount(serverName, inputName));
74
+ const entry = new Entry(MCP_SECRET_SERVICE, account);
72
75
  entry.setPassword(value);
73
76
  return { _tag: "saved", inputName };
74
77
  },
75
78
  catch: () => undefined,
76
79
  });
77
80
  }).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName })));
78
- const loadMcpSecret = (serverName, inputName) => Effect.gen(function* () {
81
+ const loadMcpSecret = (account) => Effect.gen(function* () {
79
82
  const Entry = yield* loadKeyringEntry;
80
83
  return yield* Effect.try({
81
84
  try: () => {
82
- const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount(serverName, inputName));
85
+ const entry = new Entry(MCP_SECRET_SERVICE, account);
83
86
  return Option.fromNullOr(entry.getPassword());
84
87
  },
85
88
  catch: () => undefined,
@@ -116,7 +119,7 @@ const collectRequiredInputNames = (manifest) => {
116
119
  }
117
120
  return names;
118
121
  };
119
- const collectSecretInputNames = (manifest) => {
122
+ export const collectSecretInputNames = (manifest) => {
120
123
  const names = new Set();
121
124
  const add = (input) => {
122
125
  const name = maybeSecretInputName(input);
@@ -179,7 +182,7 @@ const installFromRegistry = (ref, reuse) => Effect.gen(function* () {
179
182
  }
180
183
  return canonicalPath;
181
184
  });
182
- const readManifest = (canonicalPath) => Effect.gen(function* () {
185
+ export const readMcpServerManifest = (canonicalPath) => Effect.gen(function* () {
183
186
  const fs = yield* FileSystem.FileSystem;
184
187
  const path = yield* Path.Path;
185
188
  const manifestPath = path.join(canonicalPath, MCP_SERVER_MANIFEST_FILENAME);
@@ -204,8 +207,8 @@ const isNothingRunnableManifest = (manifest) => Option.match(manifest, {
204
207
  onSome: (value) => (value.server.packages === undefined || value.server.packages.length === 0) &&
205
208
  (value.server.remotes === undefined || value.server.remotes.length === 0),
206
209
  });
207
- const loadStoredMcpSecrets = (serverName, secretNames) => Effect.gen(function* () {
208
- const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(serverName, name).pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
210
+ const loadStoredMcpSecrets = (identity, secretNames) => Effect.gen(function* () {
211
+ const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(mcpSecretAccount({ ...identity, inputName: name })).pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
209
212
  const loaded = {};
210
213
  for (const { name, value } of entries) {
211
214
  if (Option.isSome(value))
@@ -213,12 +216,24 @@ const loadStoredMcpSecrets = (serverName, secretNames) => Effect.gen(function* (
213
216
  }
214
217
  return loaded;
215
218
  });
216
- const persistMcpSecrets = (serverName, secretNames, values) => Effect.forEach(secretNames, (name) => {
219
+ const persistMcpSecrets = (identity, secretNames, values) => Effect.forEach(secretNames, (name) => {
217
220
  const value = values[name];
218
221
  return value === undefined
219
222
  ? Effect.succeed({ _tag: "skipped", inputName: name })
220
- : saveMcpSecret(serverName, name, value);
223
+ : saveMcpSecret(mcpSecretAccount({ ...identity, inputName: name }), name, value);
221
224
  }, { concurrency: "unbounded" });
225
+ export const deleteMcpSecrets = (identity, secretNames) => Effect.forEach(secretNames, (inputName) => Effect.gen(function* () {
226
+ const Entry = yield* loadKeyringEntry;
227
+ return yield* Effect.try({
228
+ try: () => {
229
+ const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount({ ...identity, inputName }));
230
+ return entry.deletePassword()
231
+ ? { _tag: "deleted", inputName }
232
+ : { _tag: "absent", inputName };
233
+ },
234
+ catch: () => undefined,
235
+ });
236
+ }).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName }))), { concurrency: "unbounded" });
222
237
  const redactSettingsEnv = (values, secretNames) => {
223
238
  const redacted = {};
224
239
  for (const [name, value] of Object.entries(values)) {
@@ -398,9 +413,10 @@ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
398
413
  * then update lockfile/settings.
399
414
  */
400
415
  export const installMcpServer = (op) => Effect.gen(function* () {
401
- const adapter = yield* LifecycleFailureAdapter;
402
416
  const ws = yield* WorkspaceMutations;
417
+ const path = yield* Path.Path;
403
418
  const { ref } = op.args;
419
+ const localName = op.args.localName ?? ref.server.name;
404
420
  if (ref.refType !== "registry" && ref.refType !== "workspace") {
405
421
  return yield* new ExtensionLifecycleFailed({
406
422
  category: "usage",
@@ -415,8 +431,26 @@ export const installMcpServer = (op) => Effect.gen(function* () {
415
431
  }
416
432
  const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
417
433
  const env = Option.getOrElse(op.args.env ?? Option.none(), () => ({}));
434
+ const resolutionKey = ref.refType === "registry"
435
+ ? mcpRegistryResolutionKey({
436
+ authority: ref.source.location,
437
+ owner: ref.owner,
438
+ name: ref.server.name,
439
+ })
440
+ : undefined;
441
+ const sourceIdentity = resolutionKey ?? `workspace:${ref.owner}/mcps/${ref.server.name}`;
442
+ const desiredGraph = yield* ws.getDesiredStateGraph();
443
+ const existingLocalNode = desiredGraph.nodes.find((node) => node.type === "mcp-server" && node.name === localName);
444
+ if (existingLocalNode !== undefined &&
445
+ (existingLocalNode.authority === "inline" || existingLocalNode.identity !== sourceIdentity)) {
446
+ return yield* new ExtensionLifecycleFailed({
447
+ category: "conflict",
448
+ detail: `Local MCP name "${localName}" is already owned by a different source`,
449
+ });
450
+ }
451
+ const existingClosure = desiredGraph.mcpSourceClosures.find((closure) => closure.identity === sourceIdentity);
418
452
  const lockedVersion = ref.refType === "registry"
419
- ? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(ref.server.name), ref)
453
+ ? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(resolutionKey ?? ""), ref)
420
454
  : undefined;
421
455
  const canonicalPath = ref.refType === "registry"
422
456
  ? yield* installFromRegistry(ref, { force: op.args.force, lockedVersion })
@@ -446,7 +480,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
446
480
  }
447
481
  return ref.location;
448
482
  });
449
- const manifest = yield* readManifest(canonicalPath);
483
+ const manifest = yield* readMcpServerManifest(canonicalPath);
450
484
  const nothingRunnable = isNothingRunnableManifest(manifest);
451
485
  const secretNames = Option.match(manifest, {
452
486
  onNone: () => new Set(),
@@ -459,8 +493,13 @@ export const installMcpServer = (op) => Effect.gen(function* () {
459
493
  ? buildLockEntry(ref, yield* computeMaterializedTreeIntegrity(canonicalPath))
460
494
  : undefined;
461
495
  const currentMcpServers = yield* ws.getConfiguredMcpServerEntries();
462
- const currentEntry = currentMcpServers[ref.server.name];
463
- const storedSecrets = yield* loadStoredMcpSecrets(ref.server.name, secretNames);
496
+ const currentEntry = currentMcpServers[localName];
497
+ const secretIdentity = {
498
+ scopeRoot: path.resolve(ws.baseDir),
499
+ localName,
500
+ sourceIdentity,
501
+ };
502
+ const storedSecrets = yield* loadStoredMcpSecrets(secretIdentity, secretNames);
464
503
  const mergedEnv = { ...storedSecrets, ...(currentEntry?.env ?? {}), ...env };
465
504
  // Under --non-interactive there is nobody to prompt, so a required input
466
505
  // that nothing supplied would otherwise install a server that cannot start.
@@ -475,7 +514,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
475
514
  if (missingInputs.length > 0 && op.args.nonInteractive) {
476
515
  return yield* new ExtensionLifecycleFailed({
477
516
  category: "usage",
478
- detail: `${ref.server.name} needs ${missingInputs.join(", ")}, and --non-interactive cannot prompt for them`,
517
+ detail: `${localName} needs ${missingInputs.join(", ")}, and --non-interactive cannot prompt for them`,
479
518
  suggestions: [
480
519
  {
481
520
  description: "Supply each required input on the command line",
@@ -494,50 +533,78 @@ export const installMcpServer = (op) => Effect.gen(function* () {
494
533
  enabled,
495
534
  ...(agents === undefined ? {} : { agents }),
496
535
  };
497
- const agentSync = yield* syncConfiguredAgentsOnInstall({
498
- wsBaseDir: ws.baseDir,
499
- scope: ws.scope,
500
- strict: strictAgentSync,
501
- serverName: ref.server.name,
502
- canonicalPath,
503
- owner: ref.owner,
504
- resolvedVersion: ref.version,
505
- nothingRunnable,
506
- enabled,
507
- configValues: preserveSecretReferences(mergedEnv, secretNames),
508
- entry: settingsEntry,
509
- });
510
- const secretPersistence = yield* persistMcpSecrets(ref.server.name, secretNames, mergedEnv);
511
- const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
512
- ? [`${outcome.inputName} could not be saved to the system keychain`]
513
- : []);
514
536
  const writeEffect = op.args.skipStateWrites === true
515
537
  ? Effect.void
516
538
  : Option.getOrElse(op.args.skipSettings, () => false)
517
539
  ? lockEntry === undefined
518
540
  ? Effect.void
519
541
  : ws.setMcpServerLock({
520
- name: ref.server.name,
542
+ name: resolutionKey ?? ref.server.name,
543
+ resolutionKey: resolutionKey ?? ref.server.name,
521
544
  lockEntry,
522
545
  versionRange: Option.none(),
523
546
  })
524
547
  : lockEntry === undefined
525
- ? ws.setMcpServerEntry(ref.server.name, {
548
+ ? ws.setMcpServerEntry(localName, {
526
549
  ...settingsEntry,
527
550
  })
528
551
  : ws.setMcpServer({
529
- name: ref.server.name,
552
+ name: localName,
553
+ resolutionKey: resolutionKey ?? localName,
530
554
  lockEntry,
531
555
  versionRange: op.args.versionRange,
532
556
  env: persistedEnv,
533
557
  enabled,
534
558
  ...(agents === undefined ? {} : { agents }),
535
559
  });
536
- const writeWarning = yield* writeEffect.pipe(Effect.as(Option.none()), Effect.catch((e) => Effect.succeed(Option.some(`MCP server update failed: ${adapter.describeFailure(e)}`))));
537
- const warnings = Option.match(writeWarning, {
538
- onNone: () => [...secretWarnings, ...agentSync.warnings],
539
- onSome: (warning) => [warning, ...secretWarnings, ...agentSync.warnings],
540
- });
560
+ yield* writeEffect;
561
+ const projectionNames = ref.refType === "registry" && lockedVersion !== undefined && lockedVersion !== ref.version
562
+ ? [...new Set([...(existingClosure?.localNames ?? []), localName])].sort()
563
+ : [localName];
564
+ const agentSyncResults = yield* Effect.forEach(projectionNames, (projectionName) => Effect.gen(function* () {
565
+ const projectionEntry = projectionName === localName ? settingsEntry : currentMcpServers[projectionName];
566
+ if (projectionEntry === undefined || projectionEntry.kind === "inline") {
567
+ return undefined;
568
+ }
569
+ const projectionSecretIdentity = {
570
+ scopeRoot: path.resolve(ws.baseDir),
571
+ localName: projectionName,
572
+ sourceIdentity,
573
+ };
574
+ const projectionStoredSecrets = yield* loadStoredMcpSecrets(projectionSecretIdentity, secretNames);
575
+ const projectionEnv = projectionName === localName
576
+ ? mergedEnv
577
+ : { ...projectionStoredSecrets, ...projectionEntry.env };
578
+ return yield* syncConfiguredAgentsOnInstall({
579
+ wsBaseDir: ws.baseDir,
580
+ scope: ws.scope,
581
+ strict: strictAgentSync,
582
+ serverName: projectionName,
583
+ canonicalPath,
584
+ owner: ref.owner,
585
+ resolvedVersion: ref.version,
586
+ nothingRunnable,
587
+ enabled: projectionEntry.enabled,
588
+ configValues: preserveSecretReferences(projectionEnv, secretNames),
589
+ entry: projectionEntry,
590
+ });
591
+ }), { concurrency: 1 });
592
+ const agentSyncSummaries = agentSyncResults.filter((summary) => summary !== undefined);
593
+ const agentSync = {
594
+ status: agentSyncSummaries.some((summary) => summary.status === "degraded")
595
+ ? "degraded"
596
+ : "green",
597
+ details: agentSyncSummaries.flatMap((summary) => summary.details),
598
+ warnings: agentSyncSummaries.flatMap((summary) => summary.warnings),
599
+ outcomes: agentSyncSummaries.flatMap((summary) => summary.outcomes),
600
+ };
601
+ const secretPersistence = yield* persistMcpSecrets(secretIdentity, secretNames, mergedEnv);
602
+ const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
603
+ ? [
604
+ `${outcome.inputName} could not be saved to the system keychain; AXM state was applied and credential action is required`,
605
+ ]
606
+ : []);
607
+ const warnings = [...secretWarnings, ...agentSync.warnings];
541
608
  const change = currentEntry === undefined ? "created" : "updated";
542
609
  const agentOutcomes = agentSync.outcomes.flatMap(({ agentId, outcome }) => outcome._tag === "success" || outcome._tag === "fallback"
543
610
  ? [
@@ -549,7 +616,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
549
616
  : []);
550
617
  return {
551
618
  result: "success",
552
- message: appendWarningsToMessage(`Installed ${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
619
+ message: appendWarningsToMessage(`Installed ${localName} from ${ref.owner}/mcps/${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
553
620
  artifact: mcpServerArtifact({
554
621
  lockEntry,
555
622
  scope: ws.scope,
@@ -20,6 +20,7 @@ import { acceptedCanonicalObservation, acceptedLockedCanonicalPath, removableAcc
20
20
  import { agentConfigTarget, mcpServerArtifact, mcpSettingsTarget } from "./artifact.js";
21
21
  import { LifecycleFailureAdapter, withAdaptedStepFailures } from "../../failure-adapter.js";
22
22
  import { ExtensionLifecycleFailed } from "../../errors.js";
23
+ import { collectSecretInputNames, deleteMcpSecrets, readMcpServerManifest } from "./install.js";
23
24
  const REQUIRED_AGENT_IDS = new Set([
24
25
  "claude-code",
25
26
  "opencode",
@@ -119,8 +120,8 @@ const syncConfiguredAgentsOnUninstall = (args) => Effect.gen(function* () {
119
120
  * 3. Remove settings and accepted resolution
120
121
  */
121
122
  export const uninstallMcpServer = (op) => Effect.gen(function* () {
122
- const adapter = yield* LifecycleFailureAdapter;
123
123
  const fs = yield* FileSystem.FileSystem;
124
+ const path = yield* Path.Path;
124
125
  const ws = yield* WorkspaceMutations;
125
126
  const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
126
127
  const desired = yield* ws.getDesiredStateGraph();
@@ -132,6 +133,12 @@ export const uninstallMcpServer = (op) => Effect.gen(function* () {
132
133
  });
133
134
  }
134
135
  const desiredNode = desired.nodes.find((node) => node.type === "mcp-server" && node.name === op.args.serverName);
136
+ const sourceClosure = desiredNode === undefined || desiredNode.authority === "inline"
137
+ ? undefined
138
+ : desired.mcpSourceClosures.find((closure) => closure.identity === desiredNode.identity);
139
+ const keepSharedResolution = sourceClosure !== undefined &&
140
+ (sourceClosure.localNames.some((name) => name !== op.args.serverName) ||
141
+ sourceClosure.origins.some((origin) => origin.type === "pack"));
135
142
  const acceptedCanonical = yield* acceptedCanonicalObservation({
136
143
  workspace: ws,
137
144
  type: "mcp-server",
@@ -150,31 +157,43 @@ export const uninstallMcpServer = (op) => Effect.gen(function* () {
150
157
  if (desiredNode === undefined && !installedOnDisk) {
151
158
  return { result: "success", message: "not installed" };
152
159
  }
153
- if (desiredNode?.origins.some((origin) => origin.type === "pack") === true) {
154
- yield* ws.removeMcpServerSettings(op.args.serverName);
155
- return {
156
- result: "success",
157
- message: "Kept on disk because dependency is still required by an installed pack",
158
- };
159
- }
160
- if (Option.isSome(removableCanonical))
160
+ const manifest = yield* Option.match(removableCanonical, {
161
+ onNone: () => Effect.succeed(Option.none()),
162
+ onSome: readMcpServerManifest,
163
+ });
164
+ if (!keepSharedResolution && Option.isSome(removableCanonical)) {
161
165
  yield* removeIfExists(fs, removableCanonical.value);
162
- // Remove from settings + lockfile (best-effort; preserve warning in result).
163
- const removeWarning = yield* ws.removeMcpServer(op.args.serverName).pipe(Effect.as(Option.none()), Effect.catch((e) => Effect.succeed(Option.some(`MCP server removal from settings failed: ${adapter.describeFailure(e)}`))));
166
+ }
167
+ // Workspace files and projections are transaction-owned. Keychain cleanup
168
+ // intentionally runs afterward and reports recoverable credential residue.
169
+ yield* ws.removeMcpServer(op.args.serverName);
164
170
  const agentSync = yield* syncConfiguredAgentsOnUninstall({
165
171
  wsBaseDir: ws.baseDir,
166
172
  scope: ws.scope,
167
173
  strict: strictAgentSync,
168
174
  serverName: op.args.serverName,
169
175
  });
170
- const warnings = Option.match(removeWarning, {
171
- onNone: () => agentSync.warnings,
172
- onSome: (warning) => [warning, ...agentSync.warnings],
176
+ const secretNames = Option.match(manifest, {
177
+ onNone: () => new Set(),
178
+ onSome: collectSecretInputNames,
173
179
  });
180
+ const secretDeletion = desiredNode === undefined || desiredNode.authority === "inline"
181
+ ? []
182
+ : yield* deleteMcpSecrets({
183
+ scopeRoot: path.resolve(ws.baseDir),
184
+ localName: op.args.serverName,
185
+ sourceIdentity: desiredNode.identity,
186
+ }, secretNames);
187
+ const secretWarnings = secretDeletion.flatMap((outcome) => outcome._tag === "failed"
188
+ ? [
189
+ `${outcome.inputName} could not be deleted from the system keychain; AXM state was applied and credential cleanup is required`,
190
+ ]
191
+ : []);
192
+ const warnings = [...secretWarnings, ...agentSync.warnings];
174
193
  const agentTarget = agentConfigTarget("removed", agentSync.agentIds);
175
194
  return {
176
195
  result: "success",
177
- message: appendWarningsToMessage(`Uninstalled ${op.args.serverName} (canonical=success, agent-sync=${agentSync.status})`, warnings),
196
+ message: appendWarningsToMessage(`Uninstalled ${op.args.serverName} (canonical=${keepSharedResolution ? "retained-shared" : "success"}, agent-sync=${agentSync.status})`, warnings),
178
197
  artifact: mcpServerArtifact({
179
198
  lockEntry: undefined,
180
199
  scope: ws.scope,
@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";
10
10
  import * as Option from "effect/Option";
11
11
  import * as Path from "effect/Path";
12
12
  import * as Schema from "effect/Schema";
13
- import { RuleDefinitionInvalid, RuleInstallStateMissing, activeContributors, applyProjectionPlans, planAggregateProjection, reconcileManagedRegionFile, MARKER_KIND_POINT, MARKER_VERSION, parseMarker, serializeMarker, assertInstructionTargetsSafe, assertInstructionsGitignoreSafe, observeInstructionProjection, reconcileInstructionTargets, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
13
+ import { RuleDefinitionInvalid, RuleInstallStateMissing, activeContributors, applyProjectionPlans, planAggregateProjection, reconcileManagedRegionFile, projectionGeneration, MARKER_KIND_POINT, MARKER_VERSION, serializeMarker, assertInstructionTargetsSafe, assertInstructionsGitignoreSafe, observeInstructionProjection, reconcileInstructionTargets, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
14
14
  import { decodeExtensionNameSync, formatFqn } from "@agentxm/extension-model/unstable/extensions";
15
15
  import { canReuseInstalledPackage, enabledConfiguredEntries, materializeExternalPackageWithTreeIntegrity, } from "@agentxm/extension-workspace";
16
16
  import { materializeRegistryPackageWithTreeIntegrity } from "../registry-materialization.js";
@@ -252,20 +252,21 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
252
252
  });
253
253
  return sorted;
254
254
  }));
255
- const observedRuleContributors = (content) => content.split(/\r?\n/u).flatMap((line) => {
256
- const parsed = parseMarker(line, { kind: "block", open: "<!--", close: "-->" });
257
- if (parsed.state !== "complete" ||
258
- parsed.marker.kind !== MARKER_KIND_POINT ||
259
- parsed.marker.pointKind !== "rule") {
260
- return [];
261
- }
262
- const separator = parsed.marker.ext.lastIndexOf("@");
263
- return separator > 0 ? [parsed.marker.ext.slice(0, separator)] : [];
264
- });
265
255
  const reconcileRulesRegion = (args) => Effect.gen(function* () {
266
256
  const { target } = args;
267
257
  const contributors = args.input.contributors;
268
258
  const rendered = contributors.map(renderRuleBlock).join("\n\n");
259
+ const generation = projectionGeneration([
260
+ "rule-instructions-region-v1",
261
+ target.relative,
262
+ RULES_REGION_OWNER,
263
+ ...contributors.flatMap((contributor) => [
264
+ contributor.name,
265
+ contributor.marker,
266
+ contributor.body,
267
+ JSON.stringify(contributor.manifest),
268
+ ]),
269
+ ]);
269
270
  const instructions = args.instructions;
270
271
  if (args.dryRun !== true && Option.isSome(instructions)) {
271
272
  yield* provide(Effect.gen(function* () {
@@ -285,6 +286,7 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
285
286
  region: RULES_REGION,
286
287
  owner: RULES_REGION_OWNER,
287
288
  rendered,
289
+ generation,
288
290
  ...(args.dryRun === undefined ? {} : { dryRun: args.dryRun }),
289
291
  writeWhenMissing: true,
290
292
  unsupportedTargetDetail: `Instruction source does not support managed regions: ${target.relative}`,
@@ -302,10 +304,6 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
302
304
  present: Option.isSome(observedRegion),
303
305
  current: !changed,
304
306
  expectedContributors: contributors.map(({ marker }) => marker),
305
- observedContributors: Option.match(observedRegion, {
306
- onNone: () => [],
307
- onSome: observedRuleContributors,
308
- }),
309
307
  };
310
308
  if (args.dryRun === true) {
311
309
  return {
@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";
15
15
  import * as Option from "effect/Option";
16
16
  import * as Schema from "effect/Schema";
17
17
  import { WorkspaceMutations } from "@agentxm/workspace-state";
18
- import { CodingAgentRepository, renderManagedSubagentOutputs, computeSubagentPathsForLayout, subagentContentFilename, subagentContentPath, SubagentContentUnreadable, SubagentDefinitionInvalid, SubagentInstallStateMissing, SubagentIoFailed, warnOnOrphanOverrides, buildRooModeEntry, buildSubagentLockEntry, findManagedSubagentFiles, hasAxmManagedMarker, applyProjectionPlansWithResults, planSingletonProjection, managedSubagentFile, } from "@agentxm/extension-workspace";
18
+ import { CodingAgentRepository, renderManagedSubagentOutputs, managedFileFormatForPath, managedFileMarker, projectionGeneration, computeSubagentPathsForLayout, subagentContentFilename, subagentContentPath, SubagentContentUnreadable, SubagentDefinitionInvalid, SubagentInstallStateMissing, SubagentIoFailed, warnOnOrphanOverrides, buildRooModeEntry, buildSubagentLockEntry, findManagedSubagentFiles, hasAxmManagedMarker, applyProjectionPlansWithResults, planSingletonProjection, managedSubagentFile, } from "@agentxm/extension-workspace";
19
19
  import { copyExtensionDirectory } from "@agentxm/extension-workspace";
20
20
  import { sanitizeName } from "@agentxm/workspace-state";
21
21
  import { stripFileProtocol } from "../internal/fs-helpers.js";
@@ -80,6 +80,16 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
80
80
  ...args.managedFile,
81
81
  helpTopic: "subagents",
82
82
  format: "markdown",
83
+ generation: projectionGeneration([
84
+ "subagent-role-skill-v1",
85
+ args.managedFile.ext,
86
+ args.managedFile.source.kind,
87
+ args.managedFile.source.path,
88
+ args.agentId,
89
+ args.name,
90
+ args.description,
91
+ args.body,
92
+ ]),
83
93
  });
84
94
  const jsonValuesEqual = (left, right) => {
85
95
  if (left === right)
@@ -101,6 +111,31 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
101
111
  }
102
112
  return false;
103
113
  };
114
+ const serializedJsonValuesEqual = (left, right) => {
115
+ try {
116
+ const leftValue = JSON.parse(left);
117
+ const rightValue = JSON.parse(right);
118
+ return jsonValuesEqual(leftValue, rightValue);
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ };
124
+ const generatedFileCurrent = (args) => {
125
+ const format = managedFileFormatForPath(args.outputPath);
126
+ if (format === undefined) {
127
+ return args.outputPath.endsWith(".json")
128
+ ? serializedJsonValuesEqual(args.content, args.expected)
129
+ : args.content === args.expected;
130
+ }
131
+ const actualMarker = managedFileMarker(args.content, format);
132
+ const expectedMarker = managedFileMarker(args.expected, format);
133
+ return (Option.isSome(actualMarker) &&
134
+ Option.isSome(expectedMarker) &&
135
+ actualMarker.value.ext === expectedMarker.value.ext &&
136
+ actualMarker.value.src === expectedMarker.value.src &&
137
+ actualMarker.value.generation === expectedMarker.value.generation);
138
+ };
104
139
  const materializeRoleSkillFallback = (args) => Effect.gen(function* () {
105
140
  const polyfillHash = computeSourceHash(JSON.stringify({ agent: args.agentId, name: args.name, body: args.body }));
106
141
  const polyfillDir = path.join(baseDir, ".axm", "build", "polyfills", "subagents", args.sanitized, polyfillHash);
@@ -530,7 +565,13 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
530
565
  }
531
566
  return Effect.forEach(rendered.outputs, (output) => fs.readFileString(path.resolve(baseDir, output.path)).pipe(Effect.option)).pipe(Effect.map((contents) => ({
532
567
  present: contents.every(Option.isSome),
533
- current: contents.every((content, index) => Option.isSome(content) && content.value === rendered.outputs[index]?.content),
568
+ current: contents.every((content, index) => Option.isSome(content) &&
569
+ rendered.outputs[index] !== undefined &&
570
+ generatedFileCurrent({
571
+ content: content.value,
572
+ expected: rendered.outputs[index].content,
573
+ outputPath: rendered.outputs[index].path,
574
+ })),
534
575
  })));
535
576
  }
536
577
  if ((ref.fallback ?? manifestFallback) === "none") {
@@ -555,7 +596,11 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
555
596
  .readFileString(path.join(path.normalize(skills.dir), sanitized, "SKILL.md"))
556
597
  .pipe(Effect.option, Effect.map((content) => ({
557
598
  present: Option.isSome(content),
558
- current: Option.exists(content, (value) => value === expected),
599
+ current: Option.exists(content, (value) => generatedFileCurrent({
600
+ content: value,
601
+ expected,
602
+ outputPath: "SKILL.md",
603
+ })),
559
604
  })));
560
605
  }));
561
606
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentxm/extension-lifecycle",
3
- "version": "0.28.4-bootstrap.0",
3
+ "version": "0.28.5",
4
4
  "description": "AXM extension-lifecycle feature: install, update, uninstall, enable, and disable policy across root and type-specific command forms for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-MIT",
@@ -18,10 +18,12 @@
18
18
  "exports": {
19
19
  ".": {
20
20
  "types": "./dist/src/index.d.ts",
21
+ "axm-source": "./src/index.ts",
21
22
  "default": "./dist/src/index.js"
22
23
  },
23
24
  "./live": {
24
25
  "types": "./dist/src/live.d.ts",
26
+ "axm-source": "./src/live.ts",
25
27
  "default": "./dist/src/live.js"
26
28
  }
27
29
  },
@@ -41,13 +43,13 @@
41
43
  "dependencies": {
42
44
  "effect": "4.0.0-rc.112",
43
45
  "semver": "^7.8.5",
44
- "@agentxm/registry-client": "^0.28.4-bootstrap.0",
45
- "@agentxm/extension-sources": "^0.28.4-bootstrap.0",
46
- "@agentxm/extension-model": "^0.28.4-bootstrap.0",
47
- "@agentxm/workspace-state": "^0.28.4-bootstrap.0",
48
- "@agentxm/registry-protocol": "^0.28.4-bootstrap.0",
49
- "@agentxm/extension-workspace": "^0.28.4-bootstrap.0",
50
- "@agentxm/workspace-operations": "^0.28.4-bootstrap.0"
46
+ "@agentxm/extension-model": "^0.28.5",
47
+ "@agentxm/extension-workspace": "^0.28.5",
48
+ "@agentxm/registry-client": "^0.28.5",
49
+ "@agentxm/registry-protocol": "^0.28.5",
50
+ "@agentxm/extension-sources": "^0.28.5",
51
+ "@agentxm/workspace-operations": "^0.28.5",
52
+ "@agentxm/workspace-state": "^0.28.5"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@effect/platform-node": "4.0.0-rc.112",