@agentxm/extension-lifecycle 0.29.4 → 0.30.1

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 (49) hide show
  1. package/dist/src/activation/set-activation.d.ts +22 -40
  2. package/dist/src/activation/set-activation.js +191 -336
  3. package/dist/src/demote/demote-to-external-source.d.ts +1 -1
  4. package/dist/src/demote/demote-to-external-source.js +53 -24
  5. package/dist/src/hooks/install/plan.js +6 -3
  6. package/dist/src/hooks/uninstall/plan.js +4 -3
  7. package/dist/src/index.d.ts +1 -2
  8. package/dist/src/index.js +1 -2
  9. package/dist/src/install/root-intent.d.ts +2 -2
  10. package/dist/src/install/vocabulary.d.ts +5 -4
  11. package/dist/src/knowledge/install/plan.js +2 -1
  12. package/dist/src/knowledge/uninstall/plan.js +4 -3
  13. package/dist/src/mcps/install/plan.js +2 -3
  14. package/dist/src/mcps/operations/uninstall.js +2 -1
  15. package/dist/src/mcps/uninstall/plan.js +13 -4
  16. package/dist/src/packs/graph-transition.d.ts +0 -25
  17. package/dist/src/packs/graph-transition.js +0 -101
  18. package/dist/src/packs/install/plan.d.ts +0 -13
  19. package/dist/src/packs/install/plan.js +12 -9
  20. package/dist/src/packs/member-install-step.js +8 -21
  21. package/dist/src/packs/promote-authored-pack.js +6 -3
  22. package/dist/src/packs/uninstall/plan.js +52 -20
  23. package/dist/src/rules/install/plan.js +6 -3
  24. package/dist/src/rules/uninstall/plan.js +4 -3
  25. package/dist/src/skills/install/bundled.js +4 -4
  26. package/dist/src/skills/install/plan.js +3 -2
  27. package/dist/src/skills/operations/install.d.ts +0 -1
  28. package/dist/src/skills/operations/install.js +9 -18
  29. package/dist/src/skills/uninstall/plan.js +4 -3
  30. package/dist/src/subagents/install/plan.js +3 -2
  31. package/dist/src/subagents/operations/enable.js +1 -3
  32. package/dist/src/subagents/uninstall/plan.js +4 -3
  33. package/dist/src/testing.d.ts +1 -1
  34. package/dist/src/testing.js +2 -1
  35. package/dist/src/uninstall/root-intent.d.ts +1 -1
  36. package/dist/src/uninstall/uninstall-extensions.d.ts +1 -1
  37. package/dist/src/uninstall/uninstall-extensions.js +77 -1
  38. package/dist/src/update/root-request.d.ts +2 -2
  39. package/dist/src/update/selective/skills.d.ts +1 -1
  40. package/dist/src/update/selective/skills.js +0 -1
  41. package/dist/src/update/selective/subagents.d.ts +1 -1
  42. package/dist/src/update/selective/subagents.js +3 -2
  43. package/dist/src/update/selective/use-case.d.ts +2 -2
  44. package/dist/src/update/targeted-plan.js +4 -2
  45. package/dist/src/workspace-paths.d.ts +0 -6
  46. package/dist/src/workspace-paths.js +0 -10
  47. package/package.json +22 -24
  48. package/dist/src/uninstall/retention-policy.d.ts +0 -21
  49. package/dist/src/uninstall/retention-policy.js +0 -28
@@ -10,9 +10,9 @@
10
10
  * configuration, a rule or Knowledge bundle through a shared managed region,
11
11
  * a Pack through the members it contributes.
12
12
  *
13
- * Two invariants hold across every type. Disabling never removes canonical
14
- * content or retires an accepted resolution, so re-enabling never re-resolves
15
- * or re-acquires. And a type whose projection shares a file with instruction
13
+ * Disabling a leaf preserves its canonical content and accepted resolution.
14
+ * Disabling a Pack withdraws its dependency route and retires exclusive acquired
15
+ * members. A type whose projection shares a file with instruction
16
16
  * management reconciles that file inside the same transaction, so the
17
17
  * workspace is never left with a settings change the files do not reflect.
18
18
  *
@@ -25,11 +25,13 @@ import * as Path from "effect/Path";
25
25
  import * as Schema from "effect/Schema";
26
26
  import { resolveInstalledIdentifierNameOrInput } from "@agentxm/extension-sources";
27
27
  import { SourceHostProviders, WorkspaceCatalog, } from "@agentxm/extension-sources";
28
- import { buildInstallOperation, collectRetainedMaterializeSteps, ExtensionManagers, installMcpServer, RetainedContentUnusable, SubagentManager, } from "@agentxm/extension-materialization";
29
- import { makeConfiguredReleaseAgeEvaluation, ReleaseAgePosture, resolveConfiguredHook, resolveConfiguredKnowledge, resolveConfiguredRule, } from "@agentxm/extension-resolution";
28
+ import { ExtensionManagers, HookManager, SkillManager, RuleManager, KnowledgeManager, PackManager, McpServerManager, SubagentManager, } from "@agentxm/extension-materialization";
29
+ import {} from "@agentxm/workspace-reconciliation";
30
+ import { collectMaterializeSteps, collectUnreachableRetirement, proposeDesiredState, publishDesiredState, } from "@agentxm/workspace-reconciliation";
31
+ import { ReleaseAgePosture, } from "@agentxm/extension-resolution";
30
32
  import { activeInstructionsConfig, applyProjectionPlans, CodingAgentRepository, findManagedSubagentFiles, instructionReconciliationReadiness, observeInstructions, projectionPlanExclusionWarnings, reconcileInstructionTransition, } from "@agentxm/workspace-projection";
31
- import { OperationJournal, operationPresentation, prepareExecutionCandidate, resolveExecutionCandidate, ResolvePlanInteraction, StepFailure, } from "@agentxm/workspace-operations";
32
- import { acquiredExtensionDisplayPathFromLockEntry, ConfiguredAgentOutcomesProvider, installedRowsByName, isDesiredExtensionActive, RenderedFilePathSchema, sanitizeName, WorkspaceMutations, } from "@agentxm/workspace-state";
33
+ import { OperationJournal, operationPresentation, prepareExecutionCandidate, resolveExecutionCandidate, ResolvePlanInteraction, } from "@agentxm/workspace-operations";
34
+ import { acceptedCanonicalObservation, usableAcceptedCanonical, LockfileReader, SettingsReader, settingsEntries, SettingsWriter, ConfiguredAgentOutcomesProvider, installedRowsByName, isDesiredExtensionActive, RenderedFilePathSchema, sanitizeName, WorkspaceMutations, } from "@agentxm/workspace-state";
33
35
  import { FootprintRecorder, runWorkspaceTransaction, WorkspaceTransactionScope, } from "@agentxm/workspace-transactions";
34
36
  import { ExtensionLifecycleFailed } from "../errors.js";
35
37
  import { StepFailureConversion, withAdaptedStepFailures, } from "../step-failure-conversion.js";
@@ -39,7 +41,7 @@ import { enableSkill } from "../skills/operations/enable.js";
39
41
  import { disableSkill } from "../skills/operations/disable.js";
40
42
  import { enableSubagent } from "../subagents/operations/enable.js";
41
43
  import { disableSubagent } from "../subagents/operations/disable.js";
42
- import { canonicalNodeDisplayPath, canonicalRootDisplayPath, lockfileDisplayPath, settingsDisplayPath, } from "./display-paths.js";
44
+ import { settingsDisplayPath } from "./display-paths.js";
43
45
  // -----------------------------------------------------------------------------
44
46
  // Shared vocabulary
45
47
  // -----------------------------------------------------------------------------
@@ -111,30 +113,6 @@ const instructionGate = (ws) => Effect.gen(function* () {
111
113
  })),
112
114
  };
113
115
  });
114
- const hookLockArtifact = (args) => {
115
- if (Option.isNone(args.lockEntry))
116
- return settingsArtifact(args.scope);
117
- const entry = args.lockEntry.value;
118
- const packagePath = acquiredExtensionDisplayPathFromLockEntry(canonicalRootDisplayPath(args.scope), entry, "hooks", args.name);
119
- const targets = (args.enabled
120
- ? [
121
- { path: settingsDisplayPath(args.scope), change: "updated" },
122
- { path: lockfileDisplayPath(args.scope), change: "updated" },
123
- { path: packagePath, change: "created" },
124
- ]
125
- : [
126
- { path: settingsDisplayPath(args.scope), change: "updated" },
127
- { path: packagePath, change: "removed" },
128
- ]).sort((left, right) => left.path.localeCompare(right.path));
129
- const version = entry.type === "registry" ? entry.resolvedVersion : undefined;
130
- return {
131
- path: settingsDisplayPath(args.scope),
132
- scope: args.scope,
133
- ...(args.enabled && version !== undefined ? { version } : {}),
134
- change: "updated",
135
- targets,
136
- };
137
- };
138
116
  /**
139
117
  * Settle an activation request into the change it makes.
140
118
  *
@@ -143,7 +121,7 @@ const hookLockArtifact = (args) => {
143
121
  * of offering to do nothing. A request naming a subject the workspace does
144
122
  * not hold fails with the type's own recovery route.
145
123
  */
146
- export const prepareSetActivation = (request) => Effect.gen(function* () {
124
+ const settleActivation = (request) => Effect.gen(function* () {
147
125
  const ws = yield* WorkspaceMutations;
148
126
  const path = yield* Path.Path;
149
127
  const scope = ws.scope;
@@ -182,167 +160,95 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
182
160
  agentOutcomes: [],
183
161
  };
184
162
  }
185
- case "mcp-server": {
186
- const configured = yield* ws.getConfiguredMcpServerEntries();
187
- const entry = configured[request.name];
188
- if (entry === undefined) {
189
- return unchanged(request, request.name, `MCP server "${request.name}" is not configured`);
163
+ case "mcp-server":
164
+ case "rule":
165
+ case "hook":
166
+ case "knowledge": {
167
+ const name = yield* resolveInstalledIdentifierNameOrInput({
168
+ input: request.name,
169
+ resourceType: request.type,
170
+ });
171
+ const currentGraph = yield* ws.getDesiredStateGraph();
172
+ if (!currentGraph.nodes.some((node) => node.type === request.type && node.name === name)) {
173
+ if (request.type === "knowledge")
174
+ return yield* new ExtensionLifecycleFailed({
175
+ category: "not_found",
176
+ detail: `Knowledge bundle "${name}" is not configured`,
177
+ });
178
+ const label = request.type === "mcp-server"
179
+ ? "MCP server"
180
+ : request.type === "hook"
181
+ ? "hooks package"
182
+ : "rule";
183
+ return unchanged(request, name, `${label} "${name}" is not configured`);
184
+ }
185
+ const proposal = yield* proposeDesiredState([
186
+ { kind: "activation", type: request.type, name, enabled: request.enabled },
187
+ ]).pipe(Effect.catchTag("WorkspaceSyncFailed", (failure) => new ExtensionLifecycleFailed({
188
+ category: failure.category,
189
+ detail: failure.detail,
190
+ cause: failure,
191
+ })));
192
+ if (!proposal.before.complete || !proposal.after.complete) {
193
+ return yield* new ExtensionLifecycleFailed({
194
+ category: "conflict",
195
+ detail: "Activation requires complete desired state",
196
+ });
190
197
  }
191
- if (!request.enabled && entry.enabled === request.enabled) {
192
- return unchanged(request, request.name, `MCP server "${request.name}" is already ${verb}`);
198
+ const node = proposal.before.nodes.find((node) => node.type === request.type && node.name === name);
199
+ if (node === undefined)
200
+ return yield* notInstalled(request.type, name, `axm ${request.type}s list`);
201
+ if (node.enabled === request.enabled && (request.type !== "mcp-server" || !request.enabled))
202
+ return unchanged(request, name, `${TYPE_LABEL[request.type]} "${name}" is already ${verb}`);
203
+ if (request.enabled && node.authority !== "inline") {
204
+ const canonical = yield* usableAcceptedCanonical({
205
+ workspace: ws,
206
+ type: request.type,
207
+ name,
208
+ });
209
+ if (Option.isNone(canonical)) {
210
+ return yield* new ExtensionLifecycleFailed({
211
+ category: "conflict",
212
+ detail: `Accepted ${request.type} content for "${name}" is not usable; sync it before enabling`,
213
+ });
214
+ }
193
215
  }
216
+ const gate = request.type === "rule"
217
+ ? yield* instructionGate(ws)
218
+ : {
219
+ config: Option.none(),
220
+ blocked: Option.none(),
221
+ };
222
+ const entry = settingsEntries["mcp-server"].entry(proposal.settings, name);
194
223
  const managers = yield* ExtensionManagers;
195
- const agentOutcomes = request.enabled
224
+ const agentOutcomes = request.type === "mcp-server" && request.enabled && Option.isSome(entry)
196
225
  ? yield* managers["mcp-server"].configuredAgentOutcomesForEntry({
197
- name: request.name,
198
- entry,
226
+ name,
227
+ entry: entry.value,
199
228
  state: "projected",
200
229
  })
201
- : [];
230
+ : request.type === "hook" &&
231
+ request.enabled &&
232
+ managers.hook.configuredAgentOutcomes !== undefined
233
+ ? (yield* managers.hook.configuredAgentOutcomes("projected", proposal.after)).filter((outcome) => outcome.name === name)
234
+ : [];
202
235
  return {
203
236
  _tag: "SetActivation",
204
- type: "mcp-server",
205
- name: request.name,
206
- enabled: request.enabled,
207
- scope,
208
- artifact: settingsArtifact(scope),
209
- transition: { kind: "mcp-server" },
210
- instructions: Option.none(),
211
- blocked: Option.none(),
212
- warning: Option.none(),
213
- agentOutcomes,
214
- };
215
- }
216
- case "rule": {
217
- const configured = yield* ws.getConfiguredRuleEntries();
218
- const entry = configured[request.name];
219
- if (entry === undefined) {
220
- return unchanged(request, request.name, `rule "${request.name}" is not configured`);
221
- }
222
- if (entry.enabled === request.enabled) {
223
- return unchanged(request, request.name, `rule "${request.name}" is already ${verb}`);
224
- }
225
- const gate = yield* instructionGate(ws);
226
- const resolved = request.enabled
227
- ? Option.some(yield* resolveConfiguredRule(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation()))
228
- : Option.none();
229
- return {
230
- _tag: "SetActivation",
231
- type: "rule",
232
- name: request.name,
237
+ type: request.type,
238
+ name,
233
239
  enabled: request.enabled,
234
240
  scope,
235
241
  artifact: settingsArtifact(scope),
236
- transition: Option.match(resolved, {
237
- onNone: () => ({ kind: "deactivate-rule" }),
238
- onSome: ({ ref, versionRange }) => ({
239
- kind: "reinstall",
240
- ref,
241
- versionRange,
242
- }),
243
- }),
242
+ transition: request.type === "mcp-server"
243
+ ? { kind: "mcp-server" }
244
+ : { kind: "aggregate", proposal },
245
+ proposal,
244
246
  instructions: gate.config,
245
247
  blocked: gate.blocked,
246
248
  warning: Option.none(),
247
- agentOutcomes: [],
248
- };
249
- }
250
- case "hook": {
251
- const configured = yield* ws.getConfiguredHookEntries();
252
- const entry = configured[request.name];
253
- if (entry === undefined) {
254
- return unchanged(request, request.name, `hooks package "${request.name}" is not configured`);
255
- }
256
- if (entry.enabled === request.enabled) {
257
- return unchanged(request, request.name, `hooks package "${request.name}" is already ${verb}`);
258
- }
259
- const lockEntry = yield* ws
260
- .getLockedHookEntry(request.name)
261
- .pipe(Effect.catch(() => Effect.succeed(Option.none())));
262
- const artifact = hookLockArtifact({
263
- lockEntry,
264
- name: request.name,
265
- scope,
266
- enabled: request.enabled,
267
- });
268
- if (!request.enabled) {
269
- return {
270
- _tag: "SetActivation",
271
- type: "hook",
272
- name: request.name,
273
- enabled: false,
274
- scope,
275
- artifact,
276
- transition: { kind: "deactivate-hook" },
277
- instructions: Option.none(),
278
- blocked: Option.none(),
279
- warning: Option.none(),
280
- agentOutcomes: [],
281
- };
282
- }
283
- const { ref, versionRange } = yield* resolveConfiguredHook(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation());
284
- const managers = yield* ExtensionManagers;
285
- // Hook packages report per-agent outcomes because a hooks package can
286
- // be projected for one agent and not another.
287
- const agentOutcomes = managers.hook.configuredAgentOutcomesForRef === undefined
288
- ? []
289
- : yield* managers.hook.configuredAgentOutcomesForRef(ref, "projected");
290
- return {
291
- _tag: "SetActivation",
292
- type: "hook",
293
- name: request.name,
294
- enabled: true,
295
- scope,
296
- artifact,
297
- transition: { kind: "reinstall", ref, versionRange },
298
- instructions: Option.none(),
299
- blocked: Option.none(),
300
- warning: Option.none(),
301
249
  agentOutcomes,
302
250
  };
303
251
  }
304
- case "knowledge": {
305
- const configured = yield* ws.getConfiguredKnowledgeEntries();
306
- const entry = configured[request.name];
307
- if (entry === undefined) {
308
- return yield* new ExtensionLifecycleFailed({
309
- category: "not_found",
310
- detail: `Knowledge bundle "${request.name}" is not configured`,
311
- });
312
- }
313
- if (entry.enabled === request.enabled) {
314
- return unchanged(request, request.name, `Knowledge bundle "${request.name}" is already ${verb}`);
315
- }
316
- if (!request.enabled) {
317
- return {
318
- _tag: "SetActivation",
319
- type: "knowledge",
320
- name: request.name,
321
- enabled: false,
322
- scope,
323
- artifact: settingsArtifact(scope),
324
- transition: { kind: "deactivate-knowledge" },
325
- instructions: Option.none(),
326
- blocked: Option.none(),
327
- warning: Option.none(),
328
- agentOutcomes: [],
329
- };
330
- }
331
- const { ref, versionRange } = yield* resolveConfiguredKnowledge(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation());
332
- return {
333
- _tag: "SetActivation",
334
- type: "knowledge",
335
- name: request.name,
336
- enabled: true,
337
- scope,
338
- artifact: settingsArtifact(scope),
339
- transition: { kind: "reinstall", ref, versionRange },
340
- instructions: Option.none(),
341
- blocked: Option.none(),
342
- warning: Option.none(),
343
- agentOutcomes: [],
344
- };
345
- }
346
252
  case "pack": {
347
253
  const configured = yield* ws.getConfiguredPackEntries();
348
254
  const entry = configured[request.name];
@@ -356,7 +262,14 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
356
262
  if (entry.enabled === request.enabled) {
357
263
  return unchanged(request, request.name, `Pack "${request.name}" is already ${verb}`);
358
264
  }
359
- const graph = yield* ws.getDesiredStateGraph();
265
+ const proposal = yield* proposeDesiredState([
266
+ { kind: "activation", type: "pack", name: request.name, enabled: request.enabled },
267
+ ]).pipe(Effect.mapError((cause) => new ExtensionLifecycleFailed({
268
+ category: "conflict",
269
+ detail: "Cannot derive Pack activation",
270
+ cause,
271
+ })));
272
+ const graph = proposal.before;
360
273
  if (!graph.complete) {
361
274
  return yield* new ExtensionLifecycleFailed({
362
275
  category: "conflict",
@@ -378,12 +291,52 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
378
291
  node.enabled &&
379
292
  packContributesTo(node, identity) &&
380
293
  !remainsActiveWithoutPack(node, identity));
381
- const contributed = graph.nodes.filter((node) => node.type !== "pack" && packContributesTo(node, identity));
294
+ const contributed = proposal.after.nodes.filter((node) => node.type !== "pack" && packContributesTo(node, identity));
382
295
  const members = request.enabled ? contributed : affected;
383
- const memberTargets = members.map((node) => ({
384
- path: canonicalNodeDisplayPath(path, ws, node),
385
- change: "unchanged",
386
- }));
296
+ const conversion = yield* StepFailureConversion;
297
+ const adapter = {
298
+ toStepFailure: (cause) => conversion.toStepFailure(new ExtensionLifecycleFailed({
299
+ category: "conflict",
300
+ detail: "detail" in cause ? cause.detail : cause._tag,
301
+ cause,
302
+ })),
303
+ };
304
+ const retirement = request.enabled
305
+ ? Option.none()
306
+ : yield* collectUnreachableRetirement(adapter, {
307
+ resultingGraph: proposal.after,
308
+ subjects: members,
309
+ }).pipe(Effect.mapError((cause) => new ExtensionLifecycleFailed({
310
+ category: "conflict",
311
+ detail: "Cannot verify Pack member retirement",
312
+ cause,
313
+ })));
314
+ const materialization = request.enabled
315
+ ? yield* collectMaterializeSteps({
316
+ desiredState: proposal.after,
317
+ selection: { target: Option.some(identity), type: Option.none() },
318
+ adapter,
319
+ }).pipe(Effect.mapError((cause) => new ExtensionLifecycleFailed({
320
+ category: "conflict",
321
+ detail: "Cannot prepare Pack member realization",
322
+ cause,
323
+ })))
324
+ : undefined;
325
+ const retainedReferences = (yield* Effect.forEach(members, (node) => acceptedCanonicalObservation({ workspace: ws, type: node.type, name: node.name }).pipe(Effect.map((canonical) => {
326
+ if (Option.isNone(canonical) || canonical.value.observation.path === undefined)
327
+ return [];
328
+ const relative = path.relative(ws.baseDir, canonical.value.observation.path);
329
+ if (Option.isSome(retirement) &&
330
+ retirement.value.artifact?.targets?.some((target) => target.path === relative))
331
+ return [];
332
+ return [
333
+ {
334
+ path: relative,
335
+ state: canonical.value.observation.status === "usable" ? "retained" : "unknown",
336
+ reason: "canonical content required by resulting desired state",
337
+ },
338
+ ];
339
+ })))).flat();
387
340
  return {
388
341
  _tag: "SetActivation",
389
342
  type: "pack",
@@ -394,11 +347,22 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
394
347
  path: settingsDisplayPath(scope),
395
348
  scope,
396
349
  change: "updated",
397
- fileCount: memberTargets.length + 1,
398
- targets: [{ path: settingsDisplayPath(scope), change: "updated" }, ...memberTargets],
350
+ fileCount: 1 +
351
+ (Option.isSome(retirement) ? (retirement.value.artifact?.targets?.length ?? 0) : 0),
352
+ targets: [
353
+ { path: settingsDisplayPath(scope), change: "updated" },
354
+ ...(Option.isSome(retirement) ? (retirement.value.artifact?.targets ?? []) : []),
355
+ ],
356
+ references: [
357
+ ...retainedReferences,
358
+ ...(Option.isSome(retirement) ? (retirement.value.artifact?.references ?? []) : []),
359
+ ],
399
360
  },
400
361
  transition: {
401
362
  kind: "pack",
363
+ proposal,
364
+ retirement,
365
+ ...(materialization === undefined ? {} : { materialization }),
402
366
  identity,
403
367
  members,
404
368
  aggregateTypes: new Set(members.map((node) => node.type)),
@@ -516,98 +480,38 @@ const validatePackActivation = (candidate) => Effect.gen(function* () {
516
480
  });
517
481
  }
518
482
  });
519
- const NO_WARNINGS = [];
520
- /** Disabling a rule: record the preference, then remove its projection. */
521
- const deactivateRuleTransition = (candidate) => Effect.gen(function* () {
522
- const ws = yield* WorkspaceMutations;
523
- const managers = yield* ExtensionManagers;
524
- yield* ws.updateRuleEntry(candidate.name, (current) => ({ ...current, enabled: false }));
525
- yield* managers.rule.materializeDeactivate({
526
- target: { type: "rule", name: candidate.name },
527
- });
528
- return NO_WARNINGS;
529
- });
530
- /** Disabling a hooks package: record the preference, then stop projecting it. */
531
- const deactivateHookTransition = (candidate) => Effect.gen(function* () {
532
- const ws = yield* WorkspaceMutations;
533
- const managers = yield* ExtensionManagers;
534
- yield* ws.updateHookEntry(candidate.name, (current) => ({ ...current, enabled: false }));
535
- yield* managers.hook.materializeDeactivate({
536
- target: { type: "hook", name: candidate.name },
537
- });
538
- return NO_WARNINGS;
539
- });
540
- /**
541
- * Disabling a Knowledge bundle: record the preference, then re-render the
542
- * discovery region. Re-rendering is what deactivation means for Knowledge, so
543
- * this step carries the region's own exclusion report.
544
- */
545
- const deactivateKnowledgeTransition = (candidate) => Effect.gen(function* () {
546
- const ws = yield* WorkspaceMutations;
547
- const managers = yield* ExtensionManagers;
548
- yield* ws.updateKnowledgeEntry(candidate.name, (current) => ({ ...current, enabled: false }));
549
- const plans = yield* managers.knowledge.projectionPlans();
550
- yield* applyProjectionPlans(plans);
551
- return projectionPlanExclusionWarnings(plans);
483
+ /** Aggregate projections read the accepted content selected by proposed intent. */
484
+ const aggregateTransition = (proposal, type) => Effect.gen(function* () {
485
+ yield* publishDesiredState(proposal);
486
+ return yield* reconcileAggregateProjections(new Set([type]));
552
487
  });
553
488
  /** Enabling a Pack, or disabling it and the members it alone kept active. */
554
489
  const packTransition = (candidate, transition) => Effect.gen(function* () {
555
- const ws = yield* WorkspaceMutations;
556
- const conversion = yield* StepFailureConversion;
557
- const configured = yield* ws.getConfiguredPackEntries();
558
- const entry = configured[candidate.name];
559
- if (entry === undefined) {
560
- return yield* new ExtensionLifecycleFailed({
561
- category: "not_found",
562
- detail: `Pack "${candidate.name}" is no longer configured`,
563
- });
564
- }
565
- yield* ws.setPackEntry(candidate.name, { ...entry, enabled: candidate.enabled });
490
+ yield* publishDesiredState(transition.proposal);
566
491
  if (candidate.enabled) {
567
- // A disabled Pack contributes nothing, so the members the planner saw
568
- // still carried the old inactive state. Re-read the closure from the
569
- // graph this transition just produced: retained re-materialization
570
- // covers active members only, and they became active one line ago.
571
- const graph = yield* ws.getDesiredStateGraph();
572
- const activeMembers = graph.nodes.filter((node) => node.type !== "pack" && packContributesTo(node, transition.identity));
573
- // The capability refuses when accepted content is not usable; that is
574
- // this feature's own conflict, reported with the recovery route the
575
- // operator can actually take.
576
- const { steps } = yield* collectRetainedMaterializeSteps({
577
- nodes: activeMembers,
578
- adapter: { toStepFailure: conversion.toStepFailure },
579
- runMcpServerInstall: ({ ref }) => installMcpServer({
580
- name: "install-mcp-server",
581
- args: {
582
- ref,
583
- nonInteractive: true,
584
- force: false,
585
- allowWorkspaceSourceTransition: false,
586
- versionRange: Option.none(),
587
- skipSettings: Option.some(true),
588
- },
589
- }).pipe(withAdaptedStepFailures),
590
- }).pipe(Effect.mapError((failure) => failure instanceof RetainedContentUnusable
591
- ? new ExtensionLifecycleFailed({
592
- category: "conflict",
593
- detail: failure.detail,
594
- suggestions: [
595
- {
596
- description: "Refresh the pack and its retained members",
597
- cmd: "axm packs update --yes",
598
- },
599
- ],
600
- cause: failure,
601
- })
602
- : failure));
603
- yield* Effect.forEach(steps, (step) => step.readiness === "error"
604
- ? Effect.fail(new ExtensionLifecycleFailed({ category: "conflict", detail: step.errorMessage }))
605
- : step.run.pipe(Effect.asVoid), { concurrency: 1, discard: true });
492
+ for (const step of transition.materialization?.steps ?? []) {
493
+ if (step.readiness === "error")
494
+ return yield* new ExtensionLifecycleFailed({
495
+ category: "conflict",
496
+ detail: step.errorMessage,
497
+ });
498
+ yield* step.run;
499
+ }
606
500
  }
607
501
  else {
608
502
  yield* Effect.forEach(transition.members.filter((node) => !AGGREGATE_UNIT_TYPES.has(node.type)), dematerializeMember, { concurrency: 1, discard: true });
609
503
  }
610
- return yield* reconcileAggregateProjections(transition.aggregateTypes);
504
+ const warnings = yield* reconcileAggregateProjections(transition.aggregateTypes);
505
+ if (Option.isSome(transition.retirement)) {
506
+ const step = transition.retirement.value;
507
+ if (step.readiness === "error")
508
+ return yield* new ExtensionLifecycleFailed({
509
+ category: "conflict",
510
+ detail: step.errorMessage,
511
+ });
512
+ yield* step.run;
513
+ }
514
+ return warnings;
611
515
  });
612
516
  /**
613
517
  * The step for a type whose executor owns its whole transition, including the
@@ -625,11 +529,17 @@ const executorStep = (candidate, kind) => {
625
529
  : candidate.enabled
626
530
  ? enableMcpServer({ name: "enable-mcp-server", args: { serverName: candidate.name } })
627
531
  : disableMcpServer({ name: "disable-mcp-server", args: { serverName: candidate.name } });
532
+ const applied = candidate.proposal === undefined
533
+ ? run
534
+ : runWorkspaceTransaction({
535
+ transition: publishDesiredState(candidate.proposal).pipe(Effect.andThen(run)),
536
+ validate: () => Effect.void,
537
+ }).pipe(withAdaptedStepFailures);
628
538
  return {
629
539
  label: candidate.name,
630
540
  readiness: "ready",
631
541
  ...(candidate.agentOutcomes.length === 0 ? {} : { agentOutcomes: candidate.agentOutcomes }),
632
- run,
542
+ run: applied,
633
543
  };
634
544
  };
635
545
  /**
@@ -676,14 +586,8 @@ const activationStep = (candidate) => {
676
586
  case "subagent":
677
587
  case "mcp-server":
678
588
  return executorStep(candidate, transition.kind);
679
- case "reinstall":
680
- return reinstallStep(candidate, transition);
681
- case "deactivate-rule":
682
- return transactionStep(candidate, deactivateRuleTransition(candidate), () => Effect.void);
683
- case "deactivate-hook":
684
- return transactionStep(candidate, deactivateHookTransition(candidate), () => Effect.void);
685
- case "deactivate-knowledge":
686
- return transactionStep(candidate, deactivateKnowledgeTransition(candidate), () => Effect.void);
589
+ case "aggregate":
590
+ return transactionStep(candidate, aggregateTransition(transition.proposal, candidate.type), () => Effect.void);
687
591
  case "pack":
688
592
  return transactionStep(candidate, packTransition(candidate, transition), () => validatePackActivation({
689
593
  name: candidate.name,
@@ -692,69 +596,13 @@ const activationStep = (candidate) => {
692
596
  }));
693
597
  }
694
598
  };
695
- /**
696
- * Enabling a rule, hooks package, or Knowledge bundle re-runs the configured
697
- * install closure for the entry the workspace already accepted, so the same
698
- * publisher-trust and release-age evidence a first install carries travels
699
- * with the re-activation.
700
- */
701
- const reinstallStep = (candidate, transition) => {
702
- const { artifact } = candidate;
703
- const install = (managers, toStepFailure) => {
704
- const common = {
705
- toStepFailure,
706
- versionRange: transition.versionRange,
707
- message: `Enabled ${candidate.name}`,
708
- buildArtifact: () => Effect.succeed(artifact),
709
- };
710
- switch (transition.ref.type) {
711
- case "rule":
712
- return buildInstallOperation(managers.rule, { ...common, ref: transition.ref });
713
- case "hook":
714
- return buildInstallOperation(managers.hook, { ...common, ref: transition.ref });
715
- case "knowledge":
716
- return buildInstallOperation(managers.knowledge, { ...common, ref: transition.ref });
717
- case "skill":
718
- return buildInstallOperation(managers.skill, { ...common, ref: transition.ref });
719
- case "subagent":
720
- return buildInstallOperation(managers.subagent, { ...common, ref: transition.ref });
721
- case "mcp-server":
722
- return buildInstallOperation(managers["mcp-server"], { ...common, ref: transition.ref });
723
- case "pack":
724
- return buildInstallOperation(managers.pack, { ...common, ref: transition.ref });
725
- }
726
- };
727
- const run = Effect.gen(function* () {
728
- const ws = yield* WorkspaceMutations;
729
- const managers = yield* ExtensionManagers;
730
- const conversion = yield* StepFailureConversion;
731
- const step = install(managers, conversion.toStepFailure);
732
- if (step.readiness === "error") {
733
- return yield* new StepFailure({ category: "conflict", detail: step.errorMessage });
734
- }
735
- return yield* Option.match(candidate.instructions, {
736
- onNone: () => step.run,
737
- onSome: (config) => runWorkspaceTransaction({
738
- transition: reconcileInstructionTransition({ ws, config, transition: step.run }),
739
- validate: () => Effect.void,
740
- }).pipe(withAdaptedStepFailures),
741
- });
742
- });
743
- return {
744
- label: candidate.name,
745
- readiness: "ready",
746
- artifact,
747
- ...(candidate.agentOutcomes.length === 0 ? {} : { agentOutcomes: candidate.agentOutcomes }),
748
- run,
749
- };
750
- };
751
599
  /**
752
600
  * Preview or apply a settled activation. The whole change — the preference,
753
601
  * the projections it implies, and any instruction file it shares — runs in
754
602
  * one workspace transaction, so a projection that cannot be written never
755
603
  * leaves the preference recorded.
756
604
  */
757
- export const previewOrApplySetActivation = (candidate, execution) => Effect.gen(function* () {
605
+ const prepareActivationExecution = (candidate) => Effect.gen(function* () {
758
606
  const plan = {
759
607
  _tag: "Plan",
760
608
  name: `${candidate.enabled ? "Enable" : "Disable"} ${PLAN_SUBJECT[candidate.type]}`,
@@ -775,8 +623,15 @@ export const previewOrApplySetActivation = (candidate, execution) => Effect.gen(
775
623
  },
776
624
  ];
777
625
  const prepared = yield* prepareExecutionCandidate(plan, { configuredAgentOperations });
778
- return yield* resolveExecutionCandidate(prepared, execution);
626
+ return { ...candidate, execution: prepared };
627
+ });
628
+ export const prepareSetActivation = (request) => Effect.gen(function* () {
629
+ const candidate = yield* settleActivation(request);
630
+ if (candidate._tag === "Unchanged")
631
+ return candidate;
632
+ return yield* prepareActivationExecution(candidate);
779
633
  });
634
+ export const previewOrApplySetActivation = (candidate, execution) => resolveExecutionCandidate(candidate.execution, execution);
780
635
  /** The activation use case. */
781
636
  export const SetActivation = {
782
637
  prepare: prepareSetActivation,