@agentxm/extension-lifecycle 0.29.2 → 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 (55) hide show
  1. package/dist/src/activation/set-activation.d.ts +22 -40
  2. package/dist/src/activation/set-activation.js +199 -331
  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/configured-agent-outcomes-provider.d.ts +4 -4
  6. package/dist/src/hooks/configured-agent-outcomes-provider.js +23 -17
  7. package/dist/src/hooks/install/plan.js +6 -3
  8. package/dist/src/hooks/uninstall/plan.js +4 -3
  9. package/dist/src/index.d.ts +1 -2
  10. package/dist/src/index.js +1 -2
  11. package/dist/src/install/root-intent.d.ts +2 -2
  12. package/dist/src/install/vocabulary.d.ts +5 -4
  13. package/dist/src/knowledge/install/plan.js +2 -1
  14. package/dist/src/knowledge/uninstall/plan.js +4 -3
  15. package/dist/src/live.d.ts +1 -1
  16. package/dist/src/live.js +1 -1
  17. package/dist/src/mcps/install/plan.js +2 -3
  18. package/dist/src/mcps/operations/disable.js +1 -1
  19. package/dist/src/mcps/operations/enable.js +20 -35
  20. package/dist/src/mcps/operations/uninstall.js +2 -1
  21. package/dist/src/mcps/uninstall/plan.js +13 -4
  22. package/dist/src/packs/graph-transition.d.ts +0 -25
  23. package/dist/src/packs/graph-transition.js +0 -101
  24. package/dist/src/packs/install/plan.d.ts +0 -13
  25. package/dist/src/packs/install/plan.js +12 -9
  26. package/dist/src/packs/member-install-step.js +8 -21
  27. package/dist/src/packs/promote-authored-pack.js +6 -3
  28. package/dist/src/packs/uninstall/plan.js +52 -20
  29. package/dist/src/rules/install/plan.js +6 -3
  30. package/dist/src/rules/uninstall/plan.js +4 -3
  31. package/dist/src/skills/install/bundled.js +4 -4
  32. package/dist/src/skills/install/plan.js +3 -2
  33. package/dist/src/skills/operations/install.d.ts +0 -1
  34. package/dist/src/skills/operations/install.js +9 -18
  35. package/dist/src/skills/uninstall/plan.js +4 -3
  36. package/dist/src/subagents/install/plan.js +3 -2
  37. package/dist/src/subagents/operations/enable.js +1 -3
  38. package/dist/src/subagents/uninstall/plan.js +4 -3
  39. package/dist/src/testing.d.ts +1 -1
  40. package/dist/src/testing.js +2 -1
  41. package/dist/src/uninstall/root-intent.d.ts +1 -1
  42. package/dist/src/uninstall/uninstall-extensions.d.ts +1 -1
  43. package/dist/src/uninstall/uninstall-extensions.js +77 -1
  44. package/dist/src/update/root-request.d.ts +2 -2
  45. package/dist/src/update/selective/skills.d.ts +1 -1
  46. package/dist/src/update/selective/skills.js +0 -1
  47. package/dist/src/update/selective/subagents.d.ts +1 -1
  48. package/dist/src/update/selective/subagents.js +3 -2
  49. package/dist/src/update/selective/use-case.d.ts +2 -2
  50. package/dist/src/update/targeted-plan.js +4 -2
  51. package/dist/src/workspace-paths.d.ts +0 -6
  52. package/dist/src/workspace-paths.js +0 -10
  53. package/package.json +22 -24
  54. package/dist/src/uninstall/retention-policy.d.ts +0 -21
  55. 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,157 +160,93 @@ 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`);
190
- }
191
- if (entry.enabled === request.enabled) {
192
- return unchanged(request, request.name, `MCP server "${request.name}" is already ${verb}`);
193
- }
194
- return {
195
- _tag: "SetActivation",
196
- type: "mcp-server",
197
- name: request.name,
198
- enabled: request.enabled,
199
- scope,
200
- artifact: settingsArtifact(scope),
201
- transition: { kind: "mcp-server" },
202
- instructions: Option.none(),
203
- blocked: Option.none(),
204
- warning: Option.none(),
205
- agentOutcomes: [],
206
- };
207
- }
208
- case "rule": {
209
- const configured = yield* ws.getConfiguredRuleEntries();
210
- const entry = configured[request.name];
211
- if (entry === undefined) {
212
- return unchanged(request, request.name, `rule "${request.name}" is not configured`);
213
- }
214
- if (entry.enabled === request.enabled) {
215
- return unchanged(request, request.name, `rule "${request.name}" is already ${verb}`);
216
- }
217
- const gate = yield* instructionGate(ws);
218
- const resolved = request.enabled
219
- ? Option.some(yield* resolveConfiguredRule(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation()))
220
- : Option.none();
221
- return {
222
- _tag: "SetActivation",
223
- type: "rule",
224
- name: request.name,
225
- enabled: request.enabled,
226
- scope,
227
- artifact: settingsArtifact(scope),
228
- transition: Option.match(resolved, {
229
- onNone: () => ({ kind: "deactivate-rule" }),
230
- onSome: ({ ref, versionRange }) => ({
231
- kind: "reinstall",
232
- ref,
233
- versionRange,
234
- }),
235
- }),
236
- instructions: gate.config,
237
- blocked: gate.blocked,
238
- warning: Option.none(),
239
- agentOutcomes: [],
240
- };
241
- }
242
- case "hook": {
243
- const configured = yield* ws.getConfiguredHookEntries();
244
- const entry = configured[request.name];
245
- if (entry === undefined) {
246
- return unchanged(request, request.name, `hooks package "${request.name}" is not configured`);
247
- }
248
- if (entry.enabled === request.enabled) {
249
- return unchanged(request, request.name, `hooks package "${request.name}" is already ${verb}`);
250
- }
251
- const lockEntry = yield* ws
252
- .getLockedHookEntry(request.name)
253
- .pipe(Effect.catch(() => Effect.succeed(Option.none())));
254
- const artifact = hookLockArtifact({
255
- lockEntry,
256
- name: request.name,
257
- scope,
258
- enabled: request.enabled,
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,
259
170
  });
260
- if (!request.enabled) {
261
- return {
262
- _tag: "SetActivation",
263
- type: "hook",
264
- name: request.name,
265
- enabled: false,
266
- scope,
267
- artifact,
268
- transition: { kind: "deactivate-hook" },
269
- instructions: Option.none(),
270
- blocked: Option.none(),
271
- warning: Option.none(),
272
- agentOutcomes: [],
273
- };
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`);
274
184
  }
275
- const { ref, versionRange } = yield* resolveConfiguredHook(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation());
276
- const managers = yield* ExtensionManagers;
277
- // Hook packages report per-agent outcomes because a hooks package can
278
- // be projected for one agent and not another.
279
- const agentOutcomes = managers.hook.configuredAgentOutcomesForRef === undefined
280
- ? []
281
- : yield* managers.hook.configuredAgentOutcomesForRef(ref, "projected");
282
- return {
283
- _tag: "SetActivation",
284
- type: "hook",
285
- name: request.name,
286
- enabled: true,
287
- scope,
288
- artifact,
289
- transition: { kind: "reinstall", ref, versionRange },
290
- instructions: Option.none(),
291
- blocked: Option.none(),
292
- warning: Option.none(),
293
- agentOutcomes,
294
- };
295
- }
296
- case "knowledge": {
297
- const configured = yield* ws.getConfiguredKnowledgeEntries();
298
- const entry = configured[request.name];
299
- if (entry === undefined) {
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) {
300
193
  return yield* new ExtensionLifecycleFailed({
301
- category: "not_found",
302
- detail: `Knowledge bundle "${request.name}" is not configured`,
194
+ category: "conflict",
195
+ detail: "Activation requires complete desired state",
303
196
  });
304
197
  }
305
- if (entry.enabled === request.enabled) {
306
- return unchanged(request, request.name, `Knowledge bundle "${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
+ }
307
215
  }
308
- if (!request.enabled) {
309
- return {
310
- _tag: "SetActivation",
311
- type: "knowledge",
312
- name: request.name,
313
- enabled: false,
314
- scope,
315
- artifact: settingsArtifact(scope),
316
- transition: { kind: "deactivate-knowledge" },
317
- instructions: Option.none(),
216
+ const gate = request.type === "rule"
217
+ ? yield* instructionGate(ws)
218
+ : {
219
+ config: Option.none(),
318
220
  blocked: Option.none(),
319
- warning: Option.none(),
320
- agentOutcomes: [],
321
221
  };
322
- }
323
- const { ref, versionRange } = yield* resolveConfiguredKnowledge(request.name, entry.source, yield* makeConfiguredReleaseAgeEvaluation());
222
+ const entry = settingsEntries["mcp-server"].entry(proposal.settings, name);
223
+ const managers = yield* ExtensionManagers;
224
+ const agentOutcomes = request.type === "mcp-server" && request.enabled && Option.isSome(entry)
225
+ ? yield* managers["mcp-server"].configuredAgentOutcomesForEntry({
226
+ name,
227
+ entry: entry.value,
228
+ state: "projected",
229
+ })
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
+ : [];
324
235
  return {
325
236
  _tag: "SetActivation",
326
- type: "knowledge",
327
- name: request.name,
328
- enabled: true,
237
+ type: request.type,
238
+ name,
239
+ enabled: request.enabled,
329
240
  scope,
330
241
  artifact: settingsArtifact(scope),
331
- transition: { kind: "reinstall", ref, versionRange },
332
- instructions: Option.none(),
333
- blocked: Option.none(),
242
+ transition: request.type === "mcp-server"
243
+ ? { kind: "mcp-server" }
244
+ : { kind: "aggregate", proposal },
245
+ proposal,
246
+ instructions: gate.config,
247
+ blocked: gate.blocked,
334
248
  warning: Option.none(),
335
- agentOutcomes: [],
249
+ agentOutcomes,
336
250
  };
337
251
  }
338
252
  case "pack": {
@@ -348,7 +262,14 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
348
262
  if (entry.enabled === request.enabled) {
349
263
  return unchanged(request, request.name, `Pack "${request.name}" is already ${verb}`);
350
264
  }
351
- 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;
352
273
  if (!graph.complete) {
353
274
  return yield* new ExtensionLifecycleFailed({
354
275
  category: "conflict",
@@ -370,12 +291,52 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
370
291
  node.enabled &&
371
292
  packContributesTo(node, identity) &&
372
293
  !remainsActiveWithoutPack(node, identity));
373
- 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));
374
295
  const members = request.enabled ? contributed : affected;
375
- const memberTargets = members.map((node) => ({
376
- path: canonicalNodeDisplayPath(path, ws, node),
377
- change: "unchanged",
378
- }));
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();
379
340
  return {
380
341
  _tag: "SetActivation",
381
342
  type: "pack",
@@ -386,11 +347,22 @@ export const prepareSetActivation = (request) => Effect.gen(function* () {
386
347
  path: settingsDisplayPath(scope),
387
348
  scope,
388
349
  change: "updated",
389
- fileCount: memberTargets.length + 1,
390
- 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
+ ],
391
360
  },
392
361
  transition: {
393
362
  kind: "pack",
363
+ proposal,
364
+ retirement,
365
+ ...(materialization === undefined ? {} : { materialization }),
394
366
  identity,
395
367
  members,
396
368
  aggregateTypes: new Set(members.map((node) => node.type)),
@@ -424,7 +396,7 @@ const dematerializeMember = (node) => Effect.gen(function* () {
424
396
  workspaceRoot: ws.baseDir,
425
397
  scope: ws.scope,
426
398
  serverName: node.name,
427
- disableOnly: true,
399
+ disableOnly: false,
428
400
  })
429
401
  .pipe(Effect.flatMap((outcome) => outcome._tag === "disabled" ||
430
402
  outcome._tag === "nothing-runnable" ||
@@ -508,98 +480,38 @@ const validatePackActivation = (candidate) => Effect.gen(function* () {
508
480
  });
509
481
  }
510
482
  });
511
- const NO_WARNINGS = [];
512
- /** Disabling a rule: record the preference, then remove its projection. */
513
- const deactivateRuleTransition = (candidate) => Effect.gen(function* () {
514
- const ws = yield* WorkspaceMutations;
515
- const managers = yield* ExtensionManagers;
516
- yield* ws.updateRuleEntry(candidate.name, (current) => ({ ...current, enabled: false }));
517
- yield* managers.rule.materializeDeactivate({
518
- target: { type: "rule", name: candidate.name },
519
- });
520
- return NO_WARNINGS;
521
- });
522
- /** Disabling a hooks package: record the preference, then stop projecting it. */
523
- const deactivateHookTransition = (candidate) => Effect.gen(function* () {
524
- const ws = yield* WorkspaceMutations;
525
- const managers = yield* ExtensionManagers;
526
- yield* ws.updateHookEntry(candidate.name, (current) => ({ ...current, enabled: false }));
527
- yield* managers.hook.materializeDeactivate({
528
- target: { type: "hook", name: candidate.name },
529
- });
530
- return NO_WARNINGS;
531
- });
532
- /**
533
- * Disabling a Knowledge bundle: record the preference, then re-render the
534
- * discovery region. Re-rendering is what deactivation means for Knowledge, so
535
- * this step carries the region's own exclusion report.
536
- */
537
- const deactivateKnowledgeTransition = (candidate) => Effect.gen(function* () {
538
- const ws = yield* WorkspaceMutations;
539
- const managers = yield* ExtensionManagers;
540
- yield* ws.updateKnowledgeEntry(candidate.name, (current) => ({ ...current, enabled: false }));
541
- const plans = yield* managers.knowledge.projectionPlans();
542
- yield* applyProjectionPlans(plans);
543
- 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]));
544
487
  });
545
488
  /** Enabling a Pack, or disabling it and the members it alone kept active. */
546
489
  const packTransition = (candidate, transition) => Effect.gen(function* () {
547
- const ws = yield* WorkspaceMutations;
548
- const conversion = yield* StepFailureConversion;
549
- const configured = yield* ws.getConfiguredPackEntries();
550
- const entry = configured[candidate.name];
551
- if (entry === undefined) {
552
- return yield* new ExtensionLifecycleFailed({
553
- category: "not_found",
554
- detail: `Pack "${candidate.name}" is no longer configured`,
555
- });
556
- }
557
- yield* ws.setPackEntry(candidate.name, { ...entry, enabled: candidate.enabled });
490
+ yield* publishDesiredState(transition.proposal);
558
491
  if (candidate.enabled) {
559
- // A disabled Pack contributes nothing, so the members the planner saw
560
- // still carried the old inactive state. Re-read the closure from the
561
- // graph this transition just produced: retained re-materialization
562
- // covers active members only, and they became active one line ago.
563
- const graph = yield* ws.getDesiredStateGraph();
564
- const activeMembers = graph.nodes.filter((node) => node.type !== "pack" && packContributesTo(node, transition.identity));
565
- // The capability refuses when accepted content is not usable; that is
566
- // this feature's own conflict, reported with the recovery route the
567
- // operator can actually take.
568
- const { steps } = yield* collectRetainedMaterializeSteps({
569
- nodes: activeMembers,
570
- adapter: { toStepFailure: conversion.toStepFailure },
571
- runMcpServerInstall: ({ ref }) => installMcpServer({
572
- name: "install-mcp-server",
573
- args: {
574
- ref,
575
- nonInteractive: true,
576
- force: false,
577
- allowWorkspaceSourceTransition: false,
578
- versionRange: Option.none(),
579
- skipSettings: Option.some(true),
580
- },
581
- }).pipe(withAdaptedStepFailures),
582
- }).pipe(Effect.mapError((failure) => failure instanceof RetainedContentUnusable
583
- ? new ExtensionLifecycleFailed({
584
- category: "conflict",
585
- detail: failure.detail,
586
- suggestions: [
587
- {
588
- description: "Refresh the pack and its retained members",
589
- cmd: "axm packs update --yes",
590
- },
591
- ],
592
- cause: failure,
593
- })
594
- : failure));
595
- yield* Effect.forEach(steps, (step) => step.readiness === "error"
596
- ? Effect.fail(new ExtensionLifecycleFailed({ category: "conflict", detail: step.errorMessage }))
597
- : 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
+ }
598
500
  }
599
501
  else {
600
502
  yield* Effect.forEach(transition.members.filter((node) => !AGGREGATE_UNIT_TYPES.has(node.type)), dematerializeMember, { concurrency: 1, discard: true });
601
503
  }
602
- 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;
603
515
  });
604
516
  /**
605
517
  * The step for a type whose executor owns its whole transition, including the
@@ -617,7 +529,18 @@ const executorStep = (candidate, kind) => {
617
529
  : candidate.enabled
618
530
  ? enableMcpServer({ name: "enable-mcp-server", args: { serverName: candidate.name } })
619
531
  : disableMcpServer({ name: "disable-mcp-server", args: { serverName: candidate.name } });
620
- return { label: candidate.name, readiness: "ready", run };
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);
538
+ return {
539
+ label: candidate.name,
540
+ readiness: "ready",
541
+ ...(candidate.agentOutcomes.length === 0 ? {} : { agentOutcomes: candidate.agentOutcomes }),
542
+ run: applied,
543
+ };
621
544
  };
622
545
  /**
623
546
  * The step for a type whose transition this use case composes itself: the
@@ -663,14 +586,8 @@ const activationStep = (candidate) => {
663
586
  case "subagent":
664
587
  case "mcp-server":
665
588
  return executorStep(candidate, transition.kind);
666
- case "reinstall":
667
- return reinstallStep(candidate, transition);
668
- case "deactivate-rule":
669
- return transactionStep(candidate, deactivateRuleTransition(candidate), () => Effect.void);
670
- case "deactivate-hook":
671
- return transactionStep(candidate, deactivateHookTransition(candidate), () => Effect.void);
672
- case "deactivate-knowledge":
673
- return transactionStep(candidate, deactivateKnowledgeTransition(candidate), () => Effect.void);
589
+ case "aggregate":
590
+ return transactionStep(candidate, aggregateTransition(transition.proposal, candidate.type), () => Effect.void);
674
591
  case "pack":
675
592
  return transactionStep(candidate, packTransition(candidate, transition), () => validatePackActivation({
676
593
  name: candidate.name,
@@ -679,69 +596,13 @@ const activationStep = (candidate) => {
679
596
  }));
680
597
  }
681
598
  };
682
- /**
683
- * Enabling a rule, hooks package, or Knowledge bundle re-runs the configured
684
- * install closure for the entry the workspace already accepted, so the same
685
- * publisher-trust and release-age evidence a first install carries travels
686
- * with the re-activation.
687
- */
688
- const reinstallStep = (candidate, transition) => {
689
- const { artifact } = candidate;
690
- const install = (managers, toStepFailure) => {
691
- const common = {
692
- toStepFailure,
693
- versionRange: transition.versionRange,
694
- message: `Enabled ${candidate.name}`,
695
- buildArtifact: () => Effect.succeed(artifact),
696
- };
697
- switch (transition.ref.type) {
698
- case "rule":
699
- return buildInstallOperation(managers.rule, { ...common, ref: transition.ref });
700
- case "hook":
701
- return buildInstallOperation(managers.hook, { ...common, ref: transition.ref });
702
- case "knowledge":
703
- return buildInstallOperation(managers.knowledge, { ...common, ref: transition.ref });
704
- case "skill":
705
- return buildInstallOperation(managers.skill, { ...common, ref: transition.ref });
706
- case "subagent":
707
- return buildInstallOperation(managers.subagent, { ...common, ref: transition.ref });
708
- case "mcp-server":
709
- return buildInstallOperation(managers["mcp-server"], { ...common, ref: transition.ref });
710
- case "pack":
711
- return buildInstallOperation(managers.pack, { ...common, ref: transition.ref });
712
- }
713
- };
714
- const run = Effect.gen(function* () {
715
- const ws = yield* WorkspaceMutations;
716
- const managers = yield* ExtensionManagers;
717
- const conversion = yield* StepFailureConversion;
718
- const step = install(managers, conversion.toStepFailure);
719
- if (step.readiness === "error") {
720
- return yield* new StepFailure({ category: "conflict", detail: step.errorMessage });
721
- }
722
- return yield* Option.match(candidate.instructions, {
723
- onNone: () => step.run,
724
- onSome: (config) => runWorkspaceTransaction({
725
- transition: reconcileInstructionTransition({ ws, config, transition: step.run }),
726
- validate: () => Effect.void,
727
- }).pipe(withAdaptedStepFailures),
728
- });
729
- });
730
- return {
731
- label: candidate.name,
732
- readiness: "ready",
733
- artifact,
734
- ...(candidate.agentOutcomes.length === 0 ? {} : { agentOutcomes: candidate.agentOutcomes }),
735
- run,
736
- };
737
- };
738
599
  /**
739
600
  * Preview or apply a settled activation. The whole change — the preference,
740
601
  * the projections it implies, and any instruction file it shares — runs in
741
602
  * one workspace transaction, so a projection that cannot be written never
742
603
  * leaves the preference recorded.
743
604
  */
744
- export const previewOrApplySetActivation = (candidate, execution) => Effect.gen(function* () {
605
+ const prepareActivationExecution = (candidate) => Effect.gen(function* () {
745
606
  const plan = {
746
607
  _tag: "Plan",
747
608
  name: `${candidate.enabled ? "Enable" : "Disable"} ${PLAN_SUBJECT[candidate.type]}`,
@@ -762,8 +623,15 @@ export const previewOrApplySetActivation = (candidate, execution) => Effect.gen(
762
623
  },
763
624
  ];
764
625
  const prepared = yield* prepareExecutionCandidate(plan, { configuredAgentOperations });
765
- 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);
766
633
  });
634
+ export const previewOrApplySetActivation = (candidate, execution) => resolveExecutionCandidate(candidate.execution, execution);
767
635
  /** The activation use case. */
768
636
  export const SetActivation = {
769
637
  prepare: prepareSetActivation,