@agentxm/extension-lifecycle 0.28.4-bootstrap.0

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 (84) hide show
  1. package/LICENSE +110 -0
  2. package/dist/src/configured-entry-resolution.d.ts +110 -0
  3. package/dist/src/configured-entry-resolution.js +723 -0
  4. package/dist/src/errors.d.ts +39 -0
  5. package/dist/src/errors.js +45 -0
  6. package/dist/src/failure-adapter.d.ts +35 -0
  7. package/dist/src/failure-adapter.js +23 -0
  8. package/dist/src/hooks/configured-agent-outcomes-provider.d.ts +18 -0
  9. package/dist/src/hooks/configured-agent-outcomes-provider.js +39 -0
  10. package/dist/src/hooks/manager.d.ts +15 -0
  11. package/dist/src/hooks/manager.js +751 -0
  12. package/dist/src/index.d.ts +38 -0
  13. package/dist/src/index.js +41 -0
  14. package/dist/src/internal/fs-helpers.d.ts +12 -0
  15. package/dist/src/internal/fs-helpers.js +13 -0
  16. package/dist/src/internal/integrity.d.ts +13 -0
  17. package/dist/src/internal/integrity.js +17 -0
  18. package/dist/src/knowledge/manager.d.ts +11 -0
  19. package/dist/src/knowledge/manager.js +655 -0
  20. package/dist/src/live.d.ts +18 -0
  21. package/dist/src/live.js +18 -0
  22. package/dist/src/mcps/manager.d.ts +16 -0
  23. package/dist/src/mcps/manager.js +213 -0
  24. package/dist/src/mcps/operations/artifact.d.ts +24 -0
  25. package/dist/src/mcps/operations/artifact.js +74 -0
  26. package/dist/src/mcps/operations/disable.d.ts +18 -0
  27. package/dist/src/mcps/operations/disable.js +113 -0
  28. package/dist/src/mcps/operations/enable.d.ts +18 -0
  29. package/dist/src/mcps/operations/enable.js +218 -0
  30. package/dist/src/mcps/operations/install.d.ts +59 -0
  31. package/dist/src/mcps/operations/install.js +566 -0
  32. package/dist/src/mcps/operations/sync-outcome.d.ts +10 -0
  33. package/dist/src/mcps/operations/sync-outcome.js +27 -0
  34. package/dist/src/mcps/operations/uninstall.d.ts +41 -0
  35. package/dist/src/mcps/operations/uninstall.js +189 -0
  36. package/dist/src/packs/dependency-resolution.d.ts +65 -0
  37. package/dist/src/packs/dependency-resolution.js +414 -0
  38. package/dist/src/packs/expansion.d.ts +59 -0
  39. package/dist/src/packs/expansion.js +44 -0
  40. package/dist/src/packs/manager.d.ts +16 -0
  41. package/dist/src/packs/manager.js +181 -0
  42. package/dist/src/packs/operations/install.d.ts +67 -0
  43. package/dist/src/packs/operations/install.js +156 -0
  44. package/dist/src/packs/resolved-dependency.d.ts +29 -0
  45. package/dist/src/packs/resolved-dependency.js +28 -0
  46. package/dist/src/registry-materialization.d.ts +52 -0
  47. package/dist/src/registry-materialization.js +67 -0
  48. package/dist/src/resolution-progress.d.ts +18 -0
  49. package/dist/src/resolution-progress.js +11 -0
  50. package/dist/src/resolution-timeout.d.ts +5 -0
  51. package/dist/src/resolution-timeout.js +11 -0
  52. package/dist/src/rules/manager.d.ts +20 -0
  53. package/dist/src/rules/manager.js +475 -0
  54. package/dist/src/skills/manager.d.ts +18 -0
  55. package/dist/src/skills/manager.js +346 -0
  56. package/dist/src/skills/materialization.d.ts +54 -0
  57. package/dist/src/skills/materialization.js +158 -0
  58. package/dist/src/skills/operations/disable.d.ts +31 -0
  59. package/dist/src/skills/operations/disable.js +110 -0
  60. package/dist/src/skills/operations/enable.d.ts +31 -0
  61. package/dist/src/skills/operations/enable.js +113 -0
  62. package/dist/src/skills/operations/install-result.d.ts +33 -0
  63. package/dist/src/skills/operations/install-result.js +10 -0
  64. package/dist/src/skills/operations/install.d.ts +67 -0
  65. package/dist/src/skills/operations/install.js +590 -0
  66. package/dist/src/skills/operations/source-hash.d.ts +2 -0
  67. package/dist/src/skills/operations/source-hash.js +6 -0
  68. package/dist/src/skills/operations/uninstall.d.ts +40 -0
  69. package/dist/src/skills/operations/uninstall.js +157 -0
  70. package/dist/src/skills/utils.d.ts +11 -0
  71. package/dist/src/skills/utils.js +29 -0
  72. package/dist/src/subagents/manager.d.ts +18 -0
  73. package/dist/src/subagents/manager.js +677 -0
  74. package/dist/src/subagents/operations/artifact.d.ts +18 -0
  75. package/dist/src/subagents/operations/artifact.js +43 -0
  76. package/dist/src/subagents/operations/disable.d.ts +32 -0
  77. package/dist/src/subagents/operations/disable.js +116 -0
  78. package/dist/src/subagents/operations/enable.d.ts +30 -0
  79. package/dist/src/subagents/operations/enable.js +160 -0
  80. package/dist/src/workflows/install-command/workflow.d.ts +57 -0
  81. package/dist/src/workflows/install-command/workflow.js +41 -0
  82. package/dist/src/workflows/uninstall-command/workflow.d.ts +42 -0
  83. package/dist/src/workflows/uninstall-command/workflow.js +29 -0
  84. package/package.json +62 -0
@@ -0,0 +1,677 @@
1
+ /**
2
+ * Subagent extension manager service.
3
+ *
4
+ * Implements ExtensionManager<SubagentExtensionRef> with canonical source
5
+ * materialization, per-agent rendering via CodingAgent.addSubagent(),
6
+ * and source-hash-based skip logic.
7
+ *
8
+ * @experimental This API is unstable and may change without notice.
9
+ */
10
+ import * as FileSystem from "effect/FileSystem";
11
+ import * as HttpClient from "effect/unstable/http/HttpClient";
12
+ import * as Path from "effect/Path";
13
+ import * as Effect from "effect/Effect";
14
+ import * as Layer from "effect/Layer";
15
+ import * as Option from "effect/Option";
16
+ import * as Schema from "effect/Schema";
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";
19
+ import { copyExtensionDirectory } from "@agentxm/extension-workspace";
20
+ import { sanitizeName } from "@agentxm/workspace-state";
21
+ import { stripFileProtocol } from "../internal/fs-helpers.js";
22
+ import { makeWorkspaceRelativeSourcePath } from "@agentxm/extension-model/unstable/path-types";
23
+ import { removeIfExists } from "@agentxm/workspace-state";
24
+ import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
25
+ import { parseSubagentMd } from "@agentxm/registry-protocol/unstable/content/subagent-content";
26
+ import { configuredSubagentsToDiskRefs } from "@agentxm/extension-workspace";
27
+ import { acceptedRegistryVersionForRef, validateExactResolvedVersion, } from "@agentxm/workspace-state";
28
+ import { canReuseInstalledPackage, materializeExternalPackageWithTreeIntegrity, } from "@agentxm/extension-workspace";
29
+ import { materializeRegistryPackageWithTreeIntegrity } from "../registry-materialization.js";
30
+ import { insertManagedFileBanner } from "@agentxm/extension-workspace";
31
+ import { SubagentManager } from "@agentxm/extension-workspace";
32
+ import { computePackageContentHash } from "@agentxm/workspace-state";
33
+ import { computeSourceHash, RenderedFilePathSchema } from "@agentxm/workspace-state";
34
+ import {} from "@agentxm/extension-model/unstable/sources/source-hash";
35
+ import { MANIFEST_FILENAME, SubagentManifestSchema, } from "@agentxm/extension-model/unstable/subagents/manifest-schema";
36
+ import { configuredRowsByName } from "@agentxm/workspace-state";
37
+ import { isObservedInstalled } from "@agentxm/workspace-state";
38
+ import { acceptedCanonicalObservation, prepareAcceptedCanonicalTransition, removableAcceptedCanonicalPath, } from "@agentxm/workspace-state";
39
+ import { protectWorkspacePath } from "@agentxm/workspace-state";
40
+ const decodeSubagentManifest = Schema.decodeUnknownSync(SubagentManifestSchema);
41
+ const decodeRenderedFilePath = Schema.decodeUnknownSync(RenderedFilePathSchema);
42
+ const parseRooModes = (content) => {
43
+ try {
44
+ const parsed = JSON.parse(content);
45
+ if (typeof parsed !== "object" || parsed === null || !("customModes" in parsed))
46
+ return [];
47
+ const customModes = parsed.customModes;
48
+ if (!Array.isArray(customModes))
49
+ return [];
50
+ return customModes.filter((mode) => typeof mode === "object" && mode !== null && !Array.isArray(mode));
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ };
56
+ /**
57
+ * Strip the meta-only `agentOverrides` key from a frontmatter map so it does
58
+ * not leak into rendered files. The map is treated as opaque otherwise.
59
+ */
60
+ const stripAgentOverrides = (fm) => {
61
+ if (!("agentOverrides" in fm))
62
+ return fm;
63
+ const { agentOverrides: _agentOverrides, ...rest } = fm;
64
+ return rest;
65
+ };
66
+ // -----------------------------------------------------------------------------
67
+ // Live Layer
68
+ // -----------------------------------------------------------------------------
69
+ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(function* () {
70
+ const ws = yield* WorkspaceMutations;
71
+ const fs = yield* FileSystem.FileSystem;
72
+ const httpClient = yield* HttpClient.HttpClient;
73
+ const path = yield* Path.Path;
74
+ const agentRepo = yield* CodingAgentRepository;
75
+ const baseDir = ws.baseDir;
76
+ // Build a layer to provide FileSystem + Path to inner effects
77
+ const fsPathLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(HttpClient.HttpClient, httpClient), Layer.succeed(Path.Path, path));
78
+ const provide = (effect) => Effect.provide(effect, fsPathLayer);
79
+ const roleSkillContent = (args) => insertManagedFileBanner(`---\nname: ${args.name}\ndescription: ${args.description}\n---\n\n# ${args.name} role\n\nAdopt this role for the current task. This is an advisory role-skill fallback because ${args.agentId} has no native subagent surface.\n\n${args.body.trim()}\n`, {
80
+ ...args.managedFile,
81
+ helpTopic: "subagents",
82
+ format: "markdown",
83
+ });
84
+ const jsonValuesEqual = (left, right) => {
85
+ if (left === right)
86
+ return true;
87
+ if (Array.isArray(left) && Array.isArray(right)) {
88
+ return (left.length === right.length &&
89
+ left.every((value, index) => jsonValuesEqual(value, right[index])));
90
+ }
91
+ if (typeof left === "object" &&
92
+ left !== null &&
93
+ !Array.isArray(left) &&
94
+ typeof right === "object" &&
95
+ right !== null &&
96
+ !Array.isArray(right)) {
97
+ const leftEntries = Object.entries(left).sort(([a], [b]) => a.localeCompare(b));
98
+ const rightEntries = Object.entries(right).sort(([a], [b]) => a.localeCompare(b));
99
+ return (leftEntries.length === rightEntries.length &&
100
+ leftEntries.every(([key, value], index) => key === rightEntries[index]?.[0] && jsonValuesEqual(value, rightEntries[index]?.[1])));
101
+ }
102
+ return false;
103
+ };
104
+ const materializeRoleSkillFallback = (args) => Effect.gen(function* () {
105
+ const polyfillHash = computeSourceHash(JSON.stringify({ agent: args.agentId, name: args.name, body: args.body }));
106
+ const polyfillDir = path.join(baseDir, ".axm", "build", "polyfills", "subagents", args.sanitized, polyfillHash);
107
+ const skillMdPath = path.join(polyfillDir, "SKILL.md");
108
+ const skillContent = roleSkillContent(args);
109
+ yield* protectWorkspacePath(polyfillDir);
110
+ yield* fs.makeDirectory(polyfillDir, { recursive: true });
111
+ const current = yield* fs.readFileString(skillMdPath).pipe(Effect.option);
112
+ if (Option.isNone(current) || current.value !== skillContent) {
113
+ yield* fs.writeFileString(skillMdPath, skillContent);
114
+ }
115
+ const targetPath = path.join(path.normalize(args.targetDir), args.sanitized);
116
+ yield* protectWorkspacePath(targetPath);
117
+ yield* fs.remove(targetPath, { recursive: true, force: true });
118
+ yield* provide(copyExtensionDirectory(polyfillDir, targetPath, {
119
+ forAgentArtifact: true,
120
+ }));
121
+ yield* Effect.logWarning(`Degraded subagent ${args.name} to a role skill for ${args.agentId}`);
122
+ return {
123
+ _tag: "success",
124
+ renderedFilePaths: [targetPath],
125
+ warnings: [`Degraded subagent ${args.name} to a role skill for ${args.agentId}`],
126
+ };
127
+ }).pipe(Effect.mapError((cause) => cause._tag === "WorkspaceSnapshotError"
128
+ ? cause
129
+ : new SubagentIoFailed({
130
+ detail: `Failed to materialize subagent fallback for ${args.agentId}`,
131
+ cause,
132
+ })));
133
+ const lastInstallState = new Map();
134
+ const lastUninstallState = new Map();
135
+ // Compute canonical paths for a subagent ref
136
+ const getCanonicalPaths = (ref) => {
137
+ const sanitized = sanitizeName(ref.subagent.name);
138
+ const source = ref;
139
+ const paths = computeSubagentPathsForLayout(path.join, ws.layout, source, sanitized);
140
+ return { sanitized, paths };
141
+ };
142
+ // Read and parse subagent content from canonical source
143
+ const readSubagentContent = (subagentSrcPath, name) => Effect.gen(function* () {
144
+ const expectedFilename = subagentContentFilename(name);
145
+ const contentPath = subagentContentPath(path.join, subagentSrcPath, name);
146
+ const rawContent = yield* fs.readFileString(contentPath).pipe(Effect.mapError((cause) => new SubagentContentUnreadable({
147
+ expectedFilename,
148
+ subagentSrcPath,
149
+ contentPath,
150
+ cause,
151
+ })));
152
+ const parsed = yield* parseSubagentMd(rawContent, name);
153
+ return { rawContent, parsed };
154
+ });
155
+ // Copy source to canonical location
156
+ const copyToCanonical = (sourcePath, targetPath) => provide(materializeExternalPackageWithTreeIntegrity({
157
+ baseDir,
158
+ canonicalPath: targetPath,
159
+ sourceLocation: sourcePath,
160
+ copyFailureCode: "internal",
161
+ copyFailureDetail: (target) => `Failed to copy subagent files to ${target}`,
162
+ }));
163
+ // Materialize from registry
164
+ const materializeFromRegistry = (ref, canonicalPath, force) => Effect.gen(function* () {
165
+ const lockedEntry = yield* ws.getLockedSubagent(ref.subagent.name);
166
+ const lockedVersion = acceptedRegistryVersionForRef(lockedEntry, ref);
167
+ const useExisting = yield* provide(canReuseInstalledPackage({
168
+ installedPath: canonicalPath,
169
+ force,
170
+ refVersion: ref.version,
171
+ hasIntegrity: Option.isSome(ref.integrity),
172
+ ...(lockedVersion === undefined ? {} : { lockedVersion }),
173
+ existsFailureDetail: (target) => `Failed to check if canonical path exists: ${target}`,
174
+ }));
175
+ if (useExisting && Option.isSome(lockedEntry)) {
176
+ const observedTree = yield* provide(computeMaterializedTreeIntegrity(canonicalPath));
177
+ if (observedTree === lockedEntry.value.treeIntegrity) {
178
+ return lockedEntry.value.treeIntegrity;
179
+ }
180
+ }
181
+ const materialized = yield* provide(materializeRegistryPackageWithTreeIntegrity({
182
+ baseDir,
183
+ destinationPath: canonicalPath,
184
+ sourceLocation: ref.source.location,
185
+ owner: ref.owner,
186
+ type: "subagent",
187
+ name: ref.name,
188
+ version: ref.version,
189
+ integrity: ref.integrity,
190
+ messages: {
191
+ integrityMismatchDetail: `Integrity mismatch for ${ref.name}@${ref.version}`,
192
+ },
193
+ }));
194
+ return materialized.treeIntegrity;
195
+ });
196
+ // Materialize canonical source for any ref type
197
+ const materializeCanonical = (ref, sanitized, canonicalPath, subagentSrcPath, force = false) => Effect.gen(function* () {
198
+ switch (ref.refType) {
199
+ case "git-hosted": {
200
+ const packageRoot = stripFileProtocol(ref.location);
201
+ const sourcePath = ws.layout.scope === "project" ? packageRoot : path.join(packageRoot, "src");
202
+ const targetPath = ws.layout.scope === "project" ? canonicalPath : subagentSrcPath;
203
+ const isSelfCopy = path.resolve(sourcePath) === path.resolve(targetPath);
204
+ if (!isSelfCopy) {
205
+ const materialized = yield* copyToCanonical(sourcePath, targetPath);
206
+ return materialized.treeIntegrity;
207
+ }
208
+ return yield* provide(computeMaterializedTreeIntegrity(targetPath));
209
+ }
210
+ case "local": {
211
+ const packageRoot = stripFileProtocol(ref.location);
212
+ const sourcePath = ws.layout.scope === "project" ? packageRoot : path.join(packageRoot, "src");
213
+ const targetPath = ws.layout.scope === "project" ? canonicalPath : subagentSrcPath;
214
+ const isSelfCopy = path.resolve(sourcePath) === path.resolve(targetPath);
215
+ if (!isSelfCopy) {
216
+ const materialized = yield* copyToCanonical(sourcePath, targetPath);
217
+ return materialized.treeIntegrity;
218
+ }
219
+ return yield* provide(computeMaterializedTreeIntegrity(targetPath));
220
+ }
221
+ case "registry": {
222
+ return yield* materializeFromRegistry(ref, canonicalPath, force);
223
+ }
224
+ case "workspace": {
225
+ if (ref.scope !== ws.scope ||
226
+ path.resolve(ref.location) !== path.resolve(canonicalPath)) {
227
+ return yield* new SubagentDefinitionInvalid({
228
+ detail: `Invalid workspace subagent source location: ${ref.location}`,
229
+ });
230
+ }
231
+ const exists = yield* fs.exists(subagentSrcPath).pipe(Effect.mapError((cause) => new SubagentIoFailed({
232
+ detail: `Failed to inspect workspace subagent source: ${subagentSrcPath}`,
233
+ cause,
234
+ })));
235
+ if (!exists) {
236
+ return yield* new SubagentDefinitionInvalid({
237
+ detail: `Workspace subagent source is missing: ${subagentSrcPath}`,
238
+ });
239
+ }
240
+ return yield* provide(computeMaterializedTreeIntegrity(canonicalPath));
241
+ }
242
+ }
243
+ });
244
+ const materializeInstall = Effect.fn("SubagentManager.materializeInstall")(function* ({ ref, force }) {
245
+ const { sanitized, paths } = getCanonicalPaths(ref);
246
+ const { canonicalPath, subagentSrcPath } = paths;
247
+ // --- Materialize canonical source ---
248
+ const treeIntegrity = yield* materializeCanonical(ref, sanitized, canonicalPath, subagentSrcPath, force === true);
249
+ const manifestRaw = yield* fs
250
+ .readFileString(path.join(canonicalPath, MANIFEST_FILENAME))
251
+ .pipe(Effect.option);
252
+ const manifestFallback = Option.isNone(manifestRaw)
253
+ ? undefined
254
+ : yield* Effect.try({
255
+ try: () => decodeSubagentManifest(JSON.parse(manifestRaw.value)).fallback,
256
+ catch: (cause) => new SubagentDefinitionInvalid({
257
+ detail: `Failed to parse ${MANIFEST_FILENAME}`,
258
+ cause,
259
+ }),
260
+ });
261
+ // --- Read content file ---
262
+ const contentPath = subagentContentPath(path.join, subagentSrcPath, ref.subagent.name);
263
+ const sourcePath = makeWorkspaceRelativeSourcePath(path, baseDir, contentPath);
264
+ if (Option.isNone(sourcePath)) {
265
+ return yield* new SubagentIoFailed({
266
+ detail: `Subagent source path escapes workspace root: ${contentPath}`,
267
+ });
268
+ }
269
+ const managedFile = managedSubagentFile(ref, sourcePath.value);
270
+ const { parsed } = yield* readSubagentContent(subagentSrcPath, ref.subagent.name);
271
+ // --- Resolve configured agents ---
272
+ const configuredAgents = yield* agentRepo
273
+ .getConfiguredAgents()
274
+ .pipe(Effect.provideService(WorkspaceMutations, ws));
275
+ // --- Extract frontmatter fields ---
276
+ const frontmatter = Option.getOrElse(parsed.frontmatter, () => ({}));
277
+ const agentOverrides = Option.getOrUndefined(parsed.agentOverrides);
278
+ const renderFrontmatter = stripAgentOverrides(frontmatter);
279
+ // --- Warn on overrides for agents not configured for this workspace ---
280
+ yield* warnOnOrphanOverrides(`Subagent "${ref.subagent.name}"`, agentOverrides, configuredAgents.map((a) => a.id));
281
+ // --- Render to all agents concurrently ---
282
+ const renderResults = yield* applyProjectionPlansWithResults(configuredAgents.map((agent) => planSingletonProjection({
283
+ unitId: "subagent:native-profile",
284
+ // Some adapters co-locate multiple agent profiles. A shared key
285
+ // deliberately serializes until the adapter exposes its exact file.
286
+ targetFile: `subagent:${ref.subagent.name}:configured-agents`,
287
+ contributor: ref,
288
+ adapter: {
289
+ observe: () => Effect.succeed({
290
+ unitId: "subagent:native-profile",
291
+ path: `${agent.id}:${ref.subagent.name}`,
292
+ present: false,
293
+ current: false,
294
+ expectedContributors: [ref.subagent.name],
295
+ observedContributors: [],
296
+ }),
297
+ apply: () => agent
298
+ .addSubagent({
299
+ workspaceRoot: baseDir,
300
+ scope: ws.scope,
301
+ managedFile,
302
+ input: {
303
+ agentId: agent.id,
304
+ name: ref.subagent.name,
305
+ body: parsed.body,
306
+ frontmatter: renderFrontmatter,
307
+ agentOverrides: agentOverrides?.[agent.id],
308
+ },
309
+ force: false,
310
+ })
311
+ .pipe(Effect.provide(fsPathLayer), Effect.flatMap((outcome) => {
312
+ if (outcome._tag !== "unsupported") {
313
+ return Effect.succeed(outcome);
314
+ }
315
+ if ((ref.fallback ?? manifestFallback) === "none") {
316
+ return new SubagentDefinitionInvalid({
317
+ detail: `Subagent ${ref.subagent.name} requires native subagent support for ${agent.id} because fallback is none`,
318
+ });
319
+ }
320
+ return agent.resolveEffectiveSkillsDir({ workspaceRoot: baseDir }).pipe(Effect.provide(fsPathLayer), Effect.flatMap((skillsOutcome) => {
321
+ if (skillsOutcome._tag !== "supported") {
322
+ return Effect.succeed(outcome);
323
+ }
324
+ const description = Option.getOrElse(ref.subagent.description, () => `Adopt the ${ref.subagent.name} role`);
325
+ return materializeRoleSkillFallback({
326
+ agentId: agent.id,
327
+ name: ref.subagent.name,
328
+ sanitized,
329
+ body: parsed.body,
330
+ description,
331
+ targetDir: skillsOutcome.dir,
332
+ managedFile,
333
+ }).pipe(Effect.mapError((cause) => new SubagentIoFailed({
334
+ detail: `Failed to materialize subagent fallback for ${agent.id}`,
335
+ cause,
336
+ })));
337
+ }));
338
+ }), Effect.map((outcome) => ({ agentId: agent.id, outcome }))),
339
+ },
340
+ })));
341
+ yield* Effect.forEach(renderResults, ({ agentId, outcome }) => {
342
+ if (outcome._tag !== "success")
343
+ return Effect.void;
344
+ return Effect.logDebug(`Rendered ${ref.subagent.name} for ${agentId}`);
345
+ });
346
+ const successfulResults = renderResults.filter(({ outcome }) => outcome._tag === "success");
347
+ const agentIdsByPath = new Map();
348
+ for (const { agentId, outcome } of successfulResults) {
349
+ if (outcome._tag !== "success")
350
+ continue;
351
+ for (const renderedFilePath of outcome.renderedFilePaths) {
352
+ const relativePath = path.isAbsolute(renderedFilePath)
353
+ ? path.relative(baseDir, renderedFilePath)
354
+ : path.normalize(renderedFilePath);
355
+ const agentIds = agentIdsByPath.get(relativePath) ?? [];
356
+ if (!agentIds.includes(agentId))
357
+ agentIds.push(agentId);
358
+ agentIdsByPath.set(relativePath, agentIds);
359
+ }
360
+ }
361
+ lastInstallState.set(ref.subagent.name, {
362
+ sourceHash: yield* Effect.provide(computePackageContentHash(canonicalPath), fsPathLayer),
363
+ treeIntegrity,
364
+ materialization: {
365
+ agents: successfulResults.map(({ agentId }) => agentId),
366
+ targets: Array.from(agentIdsByPath.entries())
367
+ .sort(([left], [right]) => left.localeCompare(right))
368
+ .map(([targetPath, agentIds]) => ({
369
+ path: targetPath,
370
+ agentIds,
371
+ })),
372
+ },
373
+ });
374
+ });
375
+ const makeMaterializeRemoval = (retainCanonical) => Effect.fn("SubagentManager.materializeRemoval")(function* ({ target }) {
376
+ const sanitized = sanitizeName(target.name);
377
+ const configuredAgents = yield* agentRepo
378
+ .getConfiguredAgents()
379
+ .pipe(Effect.provideService(WorkspaceMutations, ws));
380
+ const removals = yield* applyProjectionPlansWithResults(configuredAgents.map((agent) => planSingletonProjection({
381
+ unitId: "subagent:native-profile",
382
+ targetFile: `subagent:${target.name}:configured-agents`,
383
+ contributor: target,
384
+ adapter: {
385
+ observe: () => Effect.succeed({
386
+ unitId: "subagent:native-profile",
387
+ path: `${agent.id}:${target.name}`,
388
+ present: true,
389
+ current: false,
390
+ expectedContributors: [],
391
+ observedContributors: [target.name],
392
+ }),
393
+ apply: () => Effect.gen(function* () {
394
+ const removedPaths = [];
395
+ const resolved = yield* agent.resolveEffectiveSubagentsDir({
396
+ workspaceRoot: baseDir,
397
+ scope: ws.scope,
398
+ });
399
+ if (resolved._tag === "supported") {
400
+ const renderedFilePaths = yield* findManagedSubagentFiles(resolved.dir, sanitized).pipe(Effect.provide(fsPathLayer));
401
+ yield* agent.removeSubagent({
402
+ workspaceRoot: baseDir,
403
+ scope: ws.scope,
404
+ subagentName: target.name,
405
+ renderedFilePaths: renderedFilePaths.map((filePath) => decodeRenderedFilePath(path.relative(baseDir, filePath))),
406
+ });
407
+ removedPaths.push(...renderedFilePaths);
408
+ }
409
+ const skills = yield* agent.resolveEffectiveSkillsDir({
410
+ workspaceRoot: baseDir,
411
+ });
412
+ if (skills._tag === "supported") {
413
+ const fallbackPath = path.join(path.normalize(skills.dir), sanitized);
414
+ const fallbackContent = yield* fs
415
+ .readFileString(path.join(fallbackPath, "SKILL.md"))
416
+ .pipe(Effect.option);
417
+ if (Option.isSome(fallbackContent) &&
418
+ hasAxmManagedMarker(fallbackContent.value)) {
419
+ yield* protectWorkspacePath(fallbackPath);
420
+ yield* fs.remove(fallbackPath, { recursive: true, force: true }).pipe(Effect.mapError((cause) => new SubagentIoFailed({
421
+ detail: `Failed to remove subagent fallback artifact: ${fallbackPath}`,
422
+ cause,
423
+ })));
424
+ removedPaths.push(fallbackPath);
425
+ }
426
+ }
427
+ return { agentId: agent.id, removedPaths };
428
+ }).pipe(Effect.provide(fsPathLayer)),
429
+ },
430
+ })));
431
+ const agentIdsByPath = new Map();
432
+ for (const removal of removals) {
433
+ for (const removedPath of removal.removedPaths) {
434
+ const relativePath = path.relative(baseDir, removedPath);
435
+ const agentIds = agentIdsByPath.get(relativePath) ?? [];
436
+ if (!agentIds.includes(removal.agentId))
437
+ agentIds.push(removal.agentId);
438
+ agentIdsByPath.set(relativePath, agentIds);
439
+ }
440
+ }
441
+ lastUninstallState.set(target.name, {
442
+ agents: removals
443
+ .filter((removal) => removal.removedPaths.length > 0)
444
+ .map((removal) => removal.agentId)
445
+ .sort(),
446
+ targets: Array.from(agentIdsByPath.entries())
447
+ .sort(([left], [right]) => left.localeCompare(right))
448
+ .map(([targetPath, agentIds]) => ({
449
+ path: targetPath,
450
+ agentIds: [...agentIds].sort(),
451
+ })),
452
+ });
453
+ // --- Remove canonical source directory ---
454
+ if (!retainCanonical) {
455
+ const canonical = yield* provide(acceptedCanonicalObservation({ workspace: ws, type: "subagent", name: target.name }));
456
+ const packageRoot = removableAcceptedCanonicalPath(canonical);
457
+ if (Option.isSome(packageRoot))
458
+ yield* removeIfExists(fs, packageRoot.value);
459
+ }
460
+ });
461
+ const materializeUninstall = makeMaterializeRemoval(false);
462
+ const materializeDeactivate = makeMaterializeRemoval(true);
463
+ const projectionObservation = Effect.fn("SubagentManager.projectionObservation")(function* (ref) {
464
+ const { sanitized, paths } = getCanonicalPaths(ref);
465
+ const manifestRaw = yield* fs
466
+ .readFileString(path.join(paths.canonicalPath, MANIFEST_FILENAME))
467
+ .pipe(Effect.option);
468
+ const manifestFallback = Option.isNone(manifestRaw)
469
+ ? undefined
470
+ : yield* Effect.try({
471
+ try: () => decodeSubagentManifest(JSON.parse(manifestRaw.value)).fallback,
472
+ catch: (cause) => new SubagentDefinitionInvalid({
473
+ detail: `Failed to parse ${MANIFEST_FILENAME}`,
474
+ cause,
475
+ }),
476
+ });
477
+ const contentPath = subagentContentPath(path.join, paths.subagentSrcPath, ref.subagent.name);
478
+ const sourcePath = makeWorkspaceRelativeSourcePath(path, baseDir, contentPath);
479
+ if (Option.isNone(sourcePath))
480
+ return { present: false, current: false };
481
+ const managedFile = managedSubagentFile(ref, sourcePath.value);
482
+ const { parsed } = yield* readSubagentContent(paths.subagentSrcPath, ref.subagent.name);
483
+ const frontmatter = Option.getOrElse(parsed.frontmatter, () => ({}));
484
+ const agentOverrides = Option.getOrUndefined(parsed.agentOverrides);
485
+ const renderFrontmatter = stripAgentOverrides(frontmatter);
486
+ const configuredAgents = yield* agentRepo
487
+ .getConfiguredAgents()
488
+ .pipe(Effect.provideService(WorkspaceMutations, ws));
489
+ const current = yield* Effect.forEach(configuredAgents, (agent) => agent.resolveEffectiveSubagentsDir({ workspaceRoot: baseDir, scope: ws.scope }).pipe(Effect.provide(fsPathLayer), Effect.flatMap((resolved) => {
490
+ if (resolved._tag === "disabled") {
491
+ return Effect.succeed({ present: true, current: true });
492
+ }
493
+ if (resolved._tag === "misconfigured") {
494
+ return Effect.succeed({ present: false, current: false });
495
+ }
496
+ if (resolved._tag === "supported") {
497
+ const rendered = renderManagedSubagentOutputs({
498
+ workspaceRoot: baseDir,
499
+ scope: ws.scope,
500
+ force: false,
501
+ managedFile,
502
+ input: {
503
+ agentId: agent.id,
504
+ name: ref.subagent.name,
505
+ body: parsed.body,
506
+ frontmatter: renderFrontmatter,
507
+ agentOverrides: agentOverrides?.[agent.id],
508
+ },
509
+ });
510
+ if (rendered === undefined) {
511
+ const expected = buildRooModeEntry({
512
+ agentId: agent.id,
513
+ name: ref.subagent.name,
514
+ body: parsed.body,
515
+ frontmatter: renderFrontmatter,
516
+ agentOverrides: agentOverrides?.[agent.id],
517
+ }).entry;
518
+ return fs.readFileString(resolved.dir).pipe(Effect.option, Effect.map((content) => {
519
+ const observed = Option.isNone(content)
520
+ ? undefined
521
+ : parseRooModes(content.value).find((mode) => mode["slug"] === ref.subagent.name);
522
+ return {
523
+ present: observed !== undefined,
524
+ current: observed !== undefined && jsonValuesEqual(observed, expected),
525
+ };
526
+ }));
527
+ }
528
+ if (rendered._tag === "Skipped") {
529
+ return Effect.succeed({ present: true, current: true });
530
+ }
531
+ return Effect.forEach(rendered.outputs, (output) => fs.readFileString(path.resolve(baseDir, output.path)).pipe(Effect.option)).pipe(Effect.map((contents) => ({
532
+ present: contents.every(Option.isSome),
533
+ current: contents.every((content, index) => Option.isSome(content) && content.value === rendered.outputs[index]?.content),
534
+ })));
535
+ }
536
+ if ((ref.fallback ?? manifestFallback) === "none") {
537
+ return Effect.succeed({ present: false, current: false });
538
+ }
539
+ return agent.resolveEffectiveSkillsDir({ workspaceRoot: baseDir }).pipe(Effect.provide(fsPathLayer), Effect.flatMap((skills) => {
540
+ if (skills._tag === "disabled" || skills._tag === "unsupported") {
541
+ return Effect.succeed({ present: true, current: true });
542
+ }
543
+ if (skills._tag === "misconfigured") {
544
+ return Effect.succeed({ present: false, current: false });
545
+ }
546
+ const description = Option.getOrElse(ref.subagent.description, () => `Adopt the ${ref.subagent.name} role`);
547
+ const expected = roleSkillContent({
548
+ agentId: agent.id,
549
+ name: ref.subagent.name,
550
+ body: parsed.body,
551
+ description,
552
+ managedFile,
553
+ });
554
+ return fs
555
+ .readFileString(path.join(path.normalize(skills.dir), sanitized, "SKILL.md"))
556
+ .pipe(Effect.option, Effect.map((content) => ({
557
+ present: Option.isSome(content),
558
+ current: Option.exists(content, (value) => value === expected),
559
+ })));
560
+ }));
561
+ })));
562
+ return {
563
+ present: current.every(({ present }) => present),
564
+ current: current.every((observation) => observation.current),
565
+ };
566
+ });
567
+ return {
568
+ type: "subagent",
569
+ projectionObservation,
570
+ runTransaction: ws.runTransaction,
571
+ isInstalled: Effect.fn("SubagentManager.isInstalled")(function* ({ target, }) {
572
+ return yield* isObservedInstalled(ws, "subagent", target.name);
573
+ }),
574
+ materializeInstall,
575
+ prepareSourceTransition: ({ ref }) => provide(prepareAcceptedCanonicalTransition({
576
+ workspace: ws,
577
+ type: "subagent",
578
+ name: ref.subagent.name,
579
+ ref,
580
+ })),
581
+ getLastMaterialization: ({ target }) => Effect.succeed(lastInstallState.get(target.name)?.materialization ?? { agents: [], targets: [] }),
582
+ getConfiguredSource: Effect.fn("SubagentManager.getConfiguredSource")(function* ({ target }) {
583
+ const configured = yield* ws.getConfiguredSubagentEntries();
584
+ return Option.fromUndefinedOr(configured[target.name]?.source);
585
+ }),
586
+ listMaterializable: Effect.fn("SubagentManager.listMaterializable")(function* () {
587
+ const configured = yield* ws.records
588
+ .rows("subagent")
589
+ .pipe(Effect.map(configuredRowsByName));
590
+ return yield* configuredSubagentsToDiskRefs({ fs, path, baseDir, scope: ws.scope, layout: ws.layout }, configured);
591
+ }),
592
+ materializeUninstall,
593
+ materializeDeactivate,
594
+ getLastUnmaterialization: ({ target }) => Effect.succeed(lastUninstallState.get(target.name) ?? { agents: [], targets: [] }),
595
+ upsertSettingsEntry: Effect.fn("SubagentManager.upsertSettingsEntry")(function* ({ ref, versionRange, }) {
596
+ const workspaceRelativeLocalSourcePath = ref.refType === "local"
597
+ ? makeWorkspaceRelativeSourcePath(path, baseDir, ref.sourcePath ?? stripFileProtocol(ref.location))
598
+ : Option.none();
599
+ if (ref.refType === "local" && Option.isNone(workspaceRelativeLocalSourcePath)) {
600
+ return yield* new SubagentDefinitionInvalid({
601
+ detail: `Local subagent source path must stay within the workspace root: ${ref.source.path}`,
602
+ });
603
+ }
604
+ const state = lastInstallState.get(ref.subagent.name);
605
+ if (ref.refType === "workspace") {
606
+ return yield* ws.setSubagentEntry(ref.subagent.name, {
607
+ source: "workspace",
608
+ enabled: true,
609
+ });
610
+ }
611
+ if (state === undefined) {
612
+ return yield* new SubagentInstallStateMissing({
613
+ name: ref.subagent.name,
614
+ kind: "content-identity",
615
+ });
616
+ }
617
+ const lockEntry = buildSubagentLockEntry(ref, state.sourceHash, state.treeIntegrity, workspaceRelativeLocalSourcePath);
618
+ if (lockEntry === undefined) {
619
+ return yield* new SubagentInstallStateMissing({
620
+ name: ref.subagent.name,
621
+ kind: "external-resolution",
622
+ });
623
+ }
624
+ if (lockEntry.type === "registry") {
625
+ yield* validateExactResolvedVersion(`subagents.${ref.subagent.name}.resolvedVersion`, lockEntry.resolvedVersion);
626
+ }
627
+ return yield* ws.setSubagent({
628
+ name: ref.subagent.name,
629
+ lockEntry,
630
+ versionRange,
631
+ });
632
+ }),
633
+ removeSettingsEntry: ({ target }) => ws
634
+ .removeSubagentSettings(target.name)
635
+ .pipe(Effect.withSpan("SubagentManager.removeSettingsEntry")),
636
+ upsertLockfileEntry: Effect.fn("SubagentManager.upsertLockfileEntry")(function* ({ ref, }) {
637
+ const workspaceRelativeLocalSourcePath = ref.refType === "local"
638
+ ? makeWorkspaceRelativeSourcePath(path, baseDir, ref.sourcePath ?? stripFileProtocol(ref.location))
639
+ : Option.none();
640
+ if (ref.refType === "local" && Option.isNone(workspaceRelativeLocalSourcePath)) {
641
+ return yield* new SubagentDefinitionInvalid({
642
+ detail: `Local subagent source path must stay within the workspace root: ${ref.source.path}`,
643
+ });
644
+ }
645
+ if (ref.refType === "workspace") {
646
+ yield* ws.removeSubagentLock(ref.subagent.name);
647
+ return;
648
+ }
649
+ const state = lastInstallState.get(ref.subagent.name);
650
+ if (state === undefined) {
651
+ return yield* new SubagentInstallStateMissing({
652
+ name: ref.subagent.name,
653
+ kind: "content-identity",
654
+ });
655
+ }
656
+ const lockEntry = buildSubagentLockEntry(ref, state.sourceHash, state.treeIntegrity, workspaceRelativeLocalSourcePath);
657
+ if (lockEntry === undefined) {
658
+ return yield* new SubagentInstallStateMissing({
659
+ name: ref.subagent.name,
660
+ kind: "external-resolution",
661
+ });
662
+ }
663
+ if (lockEntry.type === "registry") {
664
+ yield* validateExactResolvedVersion(`subagents.${ref.subagent.name}.resolvedVersion`, lockEntry.resolvedVersion);
665
+ }
666
+ return yield* ws.setSubagentLock({
667
+ name: ref.subagent.name,
668
+ lockEntry,
669
+ versionRange: Option.none(),
670
+ });
671
+ }),
672
+ removeLockfileEntry: ({ target }) => ws
673
+ .removeSubagentLock(target.name)
674
+ .pipe(Effect.withSpan("SubagentManager.removeLockfileEntry")),
675
+ };
676
+ }));
677
+ //# sourceMappingURL=manager.js.map