@agentxm/workspace-sync 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.
- package/LICENSE +110 -0
- package/dist/src/errors.d.ts +29 -0
- package/dist/src/errors.js +25 -0
- package/dist/src/failure-adapter.d.ts +18 -0
- package/dist/src/failure-adapter.js +11 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +14 -0
- package/dist/src/materialize.d.ts +125 -0
- package/dist/src/materialize.js +547 -0
- package/dist/src/plan.d.ts +100 -0
- package/dist/src/plan.js +456 -0
- package/dist/src/rendered-file-cleanup.d.ts +45 -0
- package/dist/src/rendered-file-cleanup.js +381 -0
- package/package.json +55 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desired-state materialization planning: select desired nodes, judge
|
|
3
|
+
* observed-materialization currency, and assemble the per-extension
|
|
4
|
+
* materialize steps of a sync plan. Lifecycle-owned decisions — configured
|
|
5
|
+
* entry resolution and the MCP server install operation — enter as
|
|
6
|
+
* application-supplied capabilities, so this feature never executes another
|
|
7
|
+
* feature's policy.
|
|
8
|
+
*
|
|
9
|
+
* @experimental All exports from this module are unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as DateTime from "effect/DateTime";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as FileSystem from "effect/FileSystem";
|
|
14
|
+
import * as Option from "effect/Option";
|
|
15
|
+
import * as Path from "effect/Path";
|
|
16
|
+
import * as ServiceMap from "effect/Context";
|
|
17
|
+
import * as semver from "semver";
|
|
18
|
+
import { CodingAgentRepository, HookManager, KnowledgeManager, RuleManager, SkillManager, SubagentManager, buildMaterializeOperation, collectManagedAgentMcpServers, enabledConfiguredEntries, extensionConstraintFactText, inspectMcpServerAcrossAgents, isConfiguredEntryEnabled, makeExtensionConstraintInvariantFact, planExtensionConstraintFact, pruneManagedMcpServersForAgent, skillArtifactFromTargets, targetFromRef, toStepKey, } from "@agentxm/extension-workspace";
|
|
19
|
+
import { normalizeReleaseAgeRecords, } from "@agentxm/registry-protocol/unstable/registry/release-age-policy";
|
|
20
|
+
import {} from "@agentxm/extension-model/unstable/extensions/release-age";
|
|
21
|
+
import { sanitizeName, acceptedResolutionRef, acceptedCanonicalObservation, isSourcedDesiredExtension, desiredStateProblemsText, WorkspaceMutations, usableAcceptedCanonical, } from "@agentxm/workspace-state";
|
|
22
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/extension-ref";
|
|
23
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/skill";
|
|
24
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/mcp-server";
|
|
25
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/hook";
|
|
26
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/knowledge";
|
|
27
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/rule";
|
|
28
|
+
import {} from "@agentxm/extension-model/unstable/extensions/refs/subagent";
|
|
29
|
+
import { parseExtensionFqnParts, } from "@agentxm/extension-model/unstable/extensions";
|
|
30
|
+
import { StepFailure, } from "@agentxm/workspace-operations";
|
|
31
|
+
import { WorkspaceSyncFailed } from "./errors.js";
|
|
32
|
+
import { isInlineMcpServerEntry, SYNC_RECOVERY_IDS, buildInlineMcpServerSyncOperation, buildMcpServerPruneOperation, } from "./plan.js";
|
|
33
|
+
export const normalizedIdentity = (identity) => identity.startsWith("workspace:") ? identity.slice("workspace:".length) : identity;
|
|
34
|
+
const sourceTransitionIdentity = (authority, identity) => authority === "workspace"
|
|
35
|
+
? "workspace"
|
|
36
|
+
: identity.startsWith(`${authority}:`)
|
|
37
|
+
? identity
|
|
38
|
+
: `${authority}:${identity}`;
|
|
39
|
+
const selectedDesiredNodes = (graph, selection) => {
|
|
40
|
+
if (Option.isSome(selection.target)) {
|
|
41
|
+
const target = selection.target.value;
|
|
42
|
+
const parsed = parseExtensionFqnParts(target);
|
|
43
|
+
if (parsed === undefined)
|
|
44
|
+
return [];
|
|
45
|
+
if (parsed.type === "pack") {
|
|
46
|
+
return graph.nodes.filter((node) => node.type !== "pack" &&
|
|
47
|
+
node.origins.some((origin) => origin.type === "pack" && normalizedIdentity(origin.pack) === target));
|
|
48
|
+
}
|
|
49
|
+
return graph.nodes.filter((node) => node.type === parsed.type && normalizedIdentity(node.identity) === target);
|
|
50
|
+
}
|
|
51
|
+
if (Option.isSome(selection.type)) {
|
|
52
|
+
const type = selection.type.value;
|
|
53
|
+
return graph.nodes.filter((node) => node.type === type);
|
|
54
|
+
}
|
|
55
|
+
return graph.nodes;
|
|
56
|
+
};
|
|
57
|
+
export const scopedProblems = (graph, selection) => {
|
|
58
|
+
if (Option.isNone(selection.target) && Option.isNone(selection.type))
|
|
59
|
+
return graph.problems;
|
|
60
|
+
if (Option.isSome(selection.type)) {
|
|
61
|
+
const type = selection.type.value;
|
|
62
|
+
return graph.problems.filter((problem) => problem.type.startsWith("pack-") ||
|
|
63
|
+
("extensionType" in problem && problem.extensionType === type));
|
|
64
|
+
}
|
|
65
|
+
if (Option.isNone(selection.target))
|
|
66
|
+
return graph.problems;
|
|
67
|
+
const target = selection.target.value;
|
|
68
|
+
const parsed = parseExtensionFqnParts(target);
|
|
69
|
+
if (parsed === undefined)
|
|
70
|
+
return graph.problems;
|
|
71
|
+
if (parsed.type === "pack") {
|
|
72
|
+
return graph.problems.filter((problem) => "pack" in problem && normalizedIdentity(problem.pack) === target);
|
|
73
|
+
}
|
|
74
|
+
return graph.problems.filter((problem) => "extensionType" in problem &&
|
|
75
|
+
problem.extensionType === parsed.type &&
|
|
76
|
+
problem.name === parsed.name);
|
|
77
|
+
};
|
|
78
|
+
export const recoverableExternalPackName = (graph, problem) => {
|
|
79
|
+
if (!("pack" in problem))
|
|
80
|
+
return undefined;
|
|
81
|
+
const identity = normalizedIdentity(problem.pack);
|
|
82
|
+
const node = graph.nodes.find((candidate) => candidate.type === "pack" && normalizedIdentity(candidate.identity) === identity);
|
|
83
|
+
if (node === undefined || node.identity.startsWith("workspace:"))
|
|
84
|
+
return undefined;
|
|
85
|
+
return node.name;
|
|
86
|
+
};
|
|
87
|
+
const configuredReleaseAge = (resolved) => "releaseAge" in resolved ? resolved.releaseAge : undefined;
|
|
88
|
+
const registryVersion = (ref) => ref.refType === "registry" ? ref.version : undefined;
|
|
89
|
+
const skillSyncArtifact = (args) => Effect.gen(function* () {
|
|
90
|
+
const materializationAgents = args.materializationAgentIds === undefined
|
|
91
|
+
? yield* args.agentRepo
|
|
92
|
+
.getMaterializationAgents()
|
|
93
|
+
.pipe(Effect.provideService(WorkspaceMutations, args.ws))
|
|
94
|
+
: yield* args.agentRepo.all.pipe(Effect.map((agents) => agents.filter((agent) => args.materializationAgentIds?.includes(agent.id) === true)));
|
|
95
|
+
const resolved = yield* Effect.forEach(materializationAgents, (agent) => agent.resolveEffectiveSkillsDir({ workspaceRoot: args.ws.baseDir }).pipe(Effect.provideService(FileSystem.FileSystem, args.fs), Effect.provideService(Path.Path, args.path), Effect.map((outcome) => ({ agent, outcome }))), { concurrency: "unbounded" });
|
|
96
|
+
const targets = resolved.flatMap(({ agent, outcome }) => outcome._tag === "supported" ? [{ agentId: agent.id, targetDir: outcome.dir }] : []);
|
|
97
|
+
const artifact = yield* skillArtifactFromTargets({
|
|
98
|
+
targets,
|
|
99
|
+
workspaceRoot: args.ws.baseDir,
|
|
100
|
+
sanitizedName: sanitizeName(args.ref.skill.name),
|
|
101
|
+
scope: args.ws.scope,
|
|
102
|
+
change: "updated",
|
|
103
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, args.fs), Effect.provideService(Path.Path, args.path));
|
|
104
|
+
const version = registryVersion(args.ref);
|
|
105
|
+
return {
|
|
106
|
+
...artifact,
|
|
107
|
+
...(version === undefined ? {} : { version }),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const subagentSyncArtifact = (args) => Effect.sync(() => {
|
|
111
|
+
const version = registryVersion(args.ref);
|
|
112
|
+
return {
|
|
113
|
+
path: args.ref.subagent.name,
|
|
114
|
+
scope: args.ws.scope,
|
|
115
|
+
...(version === undefined ? {} : { version }),
|
|
116
|
+
change: "updated",
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const buildMcpServerSyncOperation = ({ ref, force, transitionLabel, runMcpServerInstall, }) => {
|
|
120
|
+
const target = targetFromRef(ref);
|
|
121
|
+
return {
|
|
122
|
+
key: toStepKey(target),
|
|
123
|
+
label: transitionLabel,
|
|
124
|
+
readiness: "ready",
|
|
125
|
+
run: runMcpServerInstall({ ref, force }),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const isObservedMaterializationCurrent = (ws, node, configuredAgents, agentRepo, subagentManager, resolvedRef, fs, path) => ws.records
|
|
129
|
+
.getExtensionInventory(node.type, {
|
|
130
|
+
...(configuredAgents.length > 0 &&
|
|
131
|
+
(node.type === "skill" || node.type === "mcp-server" || node.type === "subagent")
|
|
132
|
+
? { agents: configuredAgents }
|
|
133
|
+
: {}),
|
|
134
|
+
})
|
|
135
|
+
.pipe(Effect.flatMap((inventory) => {
|
|
136
|
+
const observed = inventory.items.find((item) => item.name === node.name && item.installed);
|
|
137
|
+
if (observed === undefined)
|
|
138
|
+
return Effect.succeed(false);
|
|
139
|
+
if (node.type !== "skill" && node.type !== "mcp-server" && node.type !== "subagent") {
|
|
140
|
+
// Rule, hook, and knowledge outputs are aggregate units whose
|
|
141
|
+
// currency is judged by reading the unit back (collectInstructionStep,
|
|
142
|
+
// collectHooksStep, collectKnowledgeStep). Canonical presence decides
|
|
143
|
+
// only whether this node needs canonical rematerialization.
|
|
144
|
+
return Effect.succeed(true);
|
|
145
|
+
}
|
|
146
|
+
if (configuredAgents.length === 0 && node.type !== "skill")
|
|
147
|
+
return Effect.succeed(true);
|
|
148
|
+
if (node.type === "subagent") {
|
|
149
|
+
return resolvedRef.type === "subagent"
|
|
150
|
+
? subagentManager
|
|
151
|
+
.projectionObservation(resolvedRef)
|
|
152
|
+
.pipe(Effect.map(({ current }) => current))
|
|
153
|
+
: Effect.succeed(false);
|
|
154
|
+
}
|
|
155
|
+
const hasProjectionOrigin = (() => {
|
|
156
|
+
switch (node.type) {
|
|
157
|
+
case "skill":
|
|
158
|
+
return observed.origins.includes("agent-skill-dir");
|
|
159
|
+
case "mcp-server":
|
|
160
|
+
return (observed.origins.includes("workspace-mcp-config") ||
|
|
161
|
+
observed.origins.includes("agent-mcp-config"));
|
|
162
|
+
default:
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
if (!hasProjectionOrigin)
|
|
167
|
+
return Effect.succeed(false);
|
|
168
|
+
if (node.type !== "skill") {
|
|
169
|
+
if (node.type === "mcp-server") {
|
|
170
|
+
return collectManagedAgentMcpServers({
|
|
171
|
+
workspaceRoot: ws.baseDir,
|
|
172
|
+
scope: ws.scope,
|
|
173
|
+
agentIds: configuredAgents,
|
|
174
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.map((managed) => configuredAgents.every((agentId) => observed.agents.includes(agentId) ||
|
|
175
|
+
managed.some((entry) => entry.agentId === agentId && entry.serverName === node.name))));
|
|
176
|
+
}
|
|
177
|
+
return Effect.succeed(configuredAgents.every((agentId) => observed.agents.includes(agentId)));
|
|
178
|
+
}
|
|
179
|
+
return agentRepo.all.pipe(Effect.flatMap((agents) => {
|
|
180
|
+
const configured = agents.filter((agent) => configuredAgents.includes(agent.id));
|
|
181
|
+
if (configured.length !== configuredAgents.length)
|
|
182
|
+
return Effect.succeed(false);
|
|
183
|
+
return Effect.forEach(configured, (agent) => agent.resolveEffectiveSkillsDir({ workspaceRoot: ws.baseDir }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.map((outcome) => {
|
|
184
|
+
if (outcome._tag === "unsupported" || outcome._tag === "disabled")
|
|
185
|
+
return true;
|
|
186
|
+
if (outcome._tag === "misconfigured")
|
|
187
|
+
return false;
|
|
188
|
+
const expectedPath = path.relative(ws.baseDir, path.join(outcome.dir, sanitizeName(node.name)));
|
|
189
|
+
return observed.paths.includes(expectedPath);
|
|
190
|
+
})), { concurrency: "unbounded" }).pipe(Effect.map((results) => results.every(Boolean)));
|
|
191
|
+
}));
|
|
192
|
+
}));
|
|
193
|
+
export const collectMaterializeSteps = (args) => Effect.gen(function* () {
|
|
194
|
+
const skillManager = yield* SkillManager;
|
|
195
|
+
const subagentManager = yield* SubagentManager;
|
|
196
|
+
const ruleManager = yield* RuleManager;
|
|
197
|
+
const hookManager = yield* HookManager;
|
|
198
|
+
const knowledgeManager = yield* KnowledgeManager;
|
|
199
|
+
const agentRepo = yield* CodingAgentRepository;
|
|
200
|
+
const ws = yield* WorkspaceMutations;
|
|
201
|
+
const fs = yield* FileSystem.FileSystem;
|
|
202
|
+
const path = yield* Path.Path;
|
|
203
|
+
const releaseAgeEvaluation = args.releaseAgeEvaluation;
|
|
204
|
+
const configuredMcpServerEntries = yield* ws.getConfiguredMcpServerEntries();
|
|
205
|
+
const configuredAgents = args.configuredAgents ?? (yield* ws.getConfiguredAgents());
|
|
206
|
+
const desiredState = yield* ws.getDesiredStateGraph();
|
|
207
|
+
const selection = args.selection ?? { target: Option.none(), type: Option.none() };
|
|
208
|
+
const isScoped = Option.isSome(selection.target) || Option.isSome(selection.type);
|
|
209
|
+
const problems = scopedProblems(desiredState, selection);
|
|
210
|
+
const blockers = problems.filter((problem) => {
|
|
211
|
+
const name = recoverableExternalPackName(desiredState, problem);
|
|
212
|
+
return name === undefined || args.packRecovery?.packNames.has(name) !== true;
|
|
213
|
+
});
|
|
214
|
+
if (blockers.length > 0) {
|
|
215
|
+
return yield* new WorkspaceSyncFailed({
|
|
216
|
+
category: "conflict",
|
|
217
|
+
detail: `Cannot reconcile the selected incomplete desired extension graph: ${desiredStateProblemsText(blockers)}`,
|
|
218
|
+
suggestions: [
|
|
219
|
+
{
|
|
220
|
+
description: "Inspect workspace facts",
|
|
221
|
+
cmd: "axm lint",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const packRecoverySteps = args.packRecovery?.steps ?? [];
|
|
227
|
+
if (Option.isSome(selection.target) &&
|
|
228
|
+
selectedDesiredNodes(desiredState, selection).length === 0) {
|
|
229
|
+
return yield* new WorkspaceSyncFailed({
|
|
230
|
+
category: "not_found",
|
|
231
|
+
detail: `No desired extension nodes matched ${selection.target.value}`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const reconciled = yield* Effect.forEach(selectedDesiredNodes(desiredState, selection)
|
|
235
|
+
.filter(isSourcedDesiredExtension)
|
|
236
|
+
.filter((node) => node.enabled && node.type !== "pack"), (node) => Effect.gen(function* () {
|
|
237
|
+
const canonical = yield* acceptedCanonicalObservation({
|
|
238
|
+
workspace: ws,
|
|
239
|
+
type: node.type,
|
|
240
|
+
name: node.name,
|
|
241
|
+
});
|
|
242
|
+
const observation = Option.isSome(canonical)
|
|
243
|
+
? canonical.value.observation
|
|
244
|
+
: { type: node.type, name: node.name, status: "missing-resolution" };
|
|
245
|
+
const accepted = Option.isSome(canonical) ? canonical.value.accepted : undefined;
|
|
246
|
+
const constraintFact = observation.status === "constraint-mismatch"
|
|
247
|
+
? makeExtensionConstraintInvariantFact(node, observation)
|
|
248
|
+
: undefined;
|
|
249
|
+
const forceCanonical = args.retainedOnly === true ? false : observation.status !== "usable";
|
|
250
|
+
const resolved = yield* Effect.gen(function* () {
|
|
251
|
+
if (observation.status === "usable") {
|
|
252
|
+
const usable = yield* usableAcceptedCanonical({
|
|
253
|
+
workspace: ws,
|
|
254
|
+
type: node.type,
|
|
255
|
+
name: node.name,
|
|
256
|
+
});
|
|
257
|
+
if (Option.isSome(usable)) {
|
|
258
|
+
return { ref: usable.value.ref, versionRange: Option.none() };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (accepted !== undefined && constraintFact === undefined) {
|
|
262
|
+
const immutable = yield* acceptedResolutionRef({
|
|
263
|
+
workspace: ws,
|
|
264
|
+
type: node.type,
|
|
265
|
+
name: node.name,
|
|
266
|
+
});
|
|
267
|
+
if (Option.isSome(immutable)) {
|
|
268
|
+
return { ref: immutable.value, versionRange: Option.none() };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (args.retainedOnly === true) {
|
|
272
|
+
return yield* new WorkspaceSyncFailed({
|
|
273
|
+
category: "conflict",
|
|
274
|
+
detail: `Cannot rematerialize retained ${node.type} ${node.name}: canonical content is ${observation.status}`,
|
|
275
|
+
suggestions: [
|
|
276
|
+
{
|
|
277
|
+
description: "Refresh the pack and its retained members",
|
|
278
|
+
cmd: "axm packs update --yes",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return yield* args.resolveDesiredRef(node, observation.status, constraintFact === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: extensionConstraintFactText(constraintFact));
|
|
286
|
+
});
|
|
287
|
+
const ref = resolved.ref;
|
|
288
|
+
const materializationCurrent = yield* isObservedMaterializationCurrent(ws, node, configuredAgents, agentRepo, subagentManager, ref, fs, path);
|
|
289
|
+
const materialize = observation.status !== "usable" || !materializationCurrent;
|
|
290
|
+
const resolvedVersion = ref.refType === "registry" || ref.refType === "workspace" ? ref.version : undefined;
|
|
291
|
+
const constraintDecision = constraintFact === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: planExtensionConstraintFact(constraintFact, resolvedVersion);
|
|
294
|
+
if (constraintFact !== undefined && constraintDecision?.readiness === "blocked") {
|
|
295
|
+
return yield* new WorkspaceSyncFailed({
|
|
296
|
+
category: "conflict",
|
|
297
|
+
detail: `${extensionConstraintFactText(constraintFact)}; decision=blocked; reason=${constraintDecision.reason}${constraintDecision.candidateVersion === undefined ? "" : `; candidate version=${constraintDecision.candidateVersion}`}`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const constraintTransition = constraintFact !== undefined && constraintDecision?.readiness === "ready"
|
|
301
|
+
? `${extensionConstraintFactText(constraintFact)}; decision=reconcilable; proposed version=${constraintDecision.version}`
|
|
302
|
+
: undefined;
|
|
303
|
+
const releaseAge = configuredReleaseAge(resolved);
|
|
304
|
+
return {
|
|
305
|
+
ref,
|
|
306
|
+
force: forceCanonical,
|
|
307
|
+
materialize,
|
|
308
|
+
transitionLabel: [
|
|
309
|
+
node.name,
|
|
310
|
+
`previous source=${accepted === undefined
|
|
311
|
+
? "none"
|
|
312
|
+
: sourceTransitionIdentity(accepted.type, node.identity)}`,
|
|
313
|
+
`proposed source=${sourceTransitionIdentity(ref.source.type, node.identity)}`,
|
|
314
|
+
`previous version=${accepted?.type === "registry" ? accepted.resolvedVersion : "none"}`,
|
|
315
|
+
`proposed version=${ref.refType === "registry" || ref.refType === "workspace" ? ref.version : "unversioned"}`,
|
|
316
|
+
`reason=${constraintFact === undefined
|
|
317
|
+
? observation.status !== "usable"
|
|
318
|
+
? observation.status
|
|
319
|
+
: "stale-projection"
|
|
320
|
+
: constraintTransition}`,
|
|
321
|
+
`downgrade=${accepted?.type === "registry" &&
|
|
322
|
+
(ref.refType === "registry" || ref.refType === "workspace") &&
|
|
323
|
+
semver.gt(accepted.resolvedVersion, ref.version)
|
|
324
|
+
? "yes"
|
|
325
|
+
: "no"}`,
|
|
326
|
+
].join("; "),
|
|
327
|
+
releaseAge,
|
|
328
|
+
};
|
|
329
|
+
}), { concurrency: "unbounded" });
|
|
330
|
+
const skillRefs = [];
|
|
331
|
+
const mcpServerRefs = [];
|
|
332
|
+
const subagentRefs = [];
|
|
333
|
+
const ruleRefs = [];
|
|
334
|
+
const hookRefs = [];
|
|
335
|
+
const knowledgeRefs = [];
|
|
336
|
+
for (const item of reconciled) {
|
|
337
|
+
switch (item.ref.type) {
|
|
338
|
+
case "skill":
|
|
339
|
+
skillRefs.push({
|
|
340
|
+
ref: item.ref,
|
|
341
|
+
force: item.force,
|
|
342
|
+
materialize: item.materialize,
|
|
343
|
+
transitionLabel: item.transitionLabel,
|
|
344
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
345
|
+
});
|
|
346
|
+
break;
|
|
347
|
+
case "mcp-server":
|
|
348
|
+
mcpServerRefs.push({
|
|
349
|
+
ref: item.ref,
|
|
350
|
+
force: item.force,
|
|
351
|
+
materialize: item.materialize,
|
|
352
|
+
transitionLabel: item.transitionLabel,
|
|
353
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
354
|
+
});
|
|
355
|
+
break;
|
|
356
|
+
case "subagent":
|
|
357
|
+
subagentRefs.push({
|
|
358
|
+
ref: item.ref,
|
|
359
|
+
force: item.force,
|
|
360
|
+
materialize: item.materialize,
|
|
361
|
+
transitionLabel: item.transitionLabel,
|
|
362
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
363
|
+
});
|
|
364
|
+
break;
|
|
365
|
+
case "rule":
|
|
366
|
+
ruleRefs.push({
|
|
367
|
+
ref: item.ref,
|
|
368
|
+
force: item.force,
|
|
369
|
+
materialize: item.materialize,
|
|
370
|
+
transitionLabel: item.transitionLabel,
|
|
371
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
case "hook":
|
|
375
|
+
hookRefs.push({
|
|
376
|
+
ref: item.ref,
|
|
377
|
+
force: item.force,
|
|
378
|
+
materialize: item.materialize,
|
|
379
|
+
transitionLabel: item.transitionLabel,
|
|
380
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
case "knowledge":
|
|
384
|
+
knowledgeRefs.push({
|
|
385
|
+
ref: item.ref,
|
|
386
|
+
force: item.force,
|
|
387
|
+
materialize: item.materialize,
|
|
388
|
+
transitionLabel: item.transitionLabel,
|
|
389
|
+
...(item.releaseAge === undefined ? {} : { releaseAge: item.releaseAge }),
|
|
390
|
+
});
|
|
391
|
+
break;
|
|
392
|
+
case "pack":
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const declaredMcpServerNames = new Set([
|
|
397
|
+
...enabledConfiguredEntries(configuredMcpServerEntries).map(([name]) => name),
|
|
398
|
+
...mcpServerRefs.map(({ ref }) => ref.server.name),
|
|
399
|
+
]);
|
|
400
|
+
const inlineMcpServerSteps = yield* Effect.forEach(Object.entries(configuredMcpServerEntries).filter(([name, entry]) => isConfiguredEntryEnabled(entry) &&
|
|
401
|
+
isInlineMcpServerEntry(entry) &&
|
|
402
|
+
(Option.isNone(selection.type) || selection.type.value === "mcp-server") &&
|
|
403
|
+
(Option.isNone(selection.target) ||
|
|
404
|
+
(parseExtensionFqnParts(selection.target.value)?.type === "mcp-server" &&
|
|
405
|
+
parseExtensionFqnParts(selection.target.value)?.name === name))), ([name, entry]) => Effect.gen(function* () {
|
|
406
|
+
const inspections = yield* inspectMcpServerAcrossAgents({
|
|
407
|
+
workspaceRoot: ws.baseDir,
|
|
408
|
+
scope: ws.scope,
|
|
409
|
+
agentIds: configuredAgents,
|
|
410
|
+
serverName: name,
|
|
411
|
+
entry,
|
|
412
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
413
|
+
const current = inspections.every((inspection) => inspection.status === "match" ||
|
|
414
|
+
inspection.status === "unsupported" ||
|
|
415
|
+
inspection.status === "not-applicable");
|
|
416
|
+
if (current)
|
|
417
|
+
return Option.none();
|
|
418
|
+
const conflicts = inspections.filter((inspection) => inspection.status === "unmanaged");
|
|
419
|
+
if (conflicts.length > 0) {
|
|
420
|
+
return Option.some({
|
|
421
|
+
key: `${SYNC_RECOVERY_IDS.inlineMcpCollision}:${name}`,
|
|
422
|
+
label: `mcp-server ${name}`,
|
|
423
|
+
readiness: "error",
|
|
424
|
+
errorMessage: `Inline MCP server ${name} collides with unowned native config at ${conflicts
|
|
425
|
+
.map((inspection) => inspection.path)
|
|
426
|
+
.join(", ")}; move, remove, or adopt the unowned entry before rerunning axm sync`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return Option.some(buildInlineMcpServerSyncOperation({
|
|
430
|
+
name,
|
|
431
|
+
entry,
|
|
432
|
+
agentIds: configuredAgents,
|
|
433
|
+
force: inspections.some((inspection) => inspection.status === "drift"),
|
|
434
|
+
ws,
|
|
435
|
+
adapter: args.adapter,
|
|
436
|
+
}));
|
|
437
|
+
}), { concurrency: "unbounded" }).pipe(Effect.map((steps) => steps.flatMap((step) => (Option.isSome(step) ? [step.value] : []))));
|
|
438
|
+
const needsMcpServerPrune = problems.length === 0 &&
|
|
439
|
+
!isScoped &&
|
|
440
|
+
configuredAgents.length > 0 &&
|
|
441
|
+
(yield* Effect.forEach(configuredAgents, (agentId) => pruneManagedMcpServersForAgent(agentId, {
|
|
442
|
+
workspaceRoot: ws.baseDir,
|
|
443
|
+
declaredServerNames: declaredMcpServerNames,
|
|
444
|
+
scope: ws.scope,
|
|
445
|
+
dryRun: true,
|
|
446
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)), { concurrency: "unbounded" }).pipe(Effect.map((outcomes) => outcomes.some((outcome) => outcome._tag === "success" && outcome.targets !== undefined))));
|
|
447
|
+
const skillMaterializeStep = ({ ref, force, transitionLabel }) => Effect.gen(function* () {
|
|
448
|
+
const buildArtifact = () => skillSyncArtifact({
|
|
449
|
+
ref,
|
|
450
|
+
agentRepo,
|
|
451
|
+
fs,
|
|
452
|
+
...(args.configuredAgents === undefined
|
|
453
|
+
? {}
|
|
454
|
+
: { materializationAgentIds: configuredAgents }),
|
|
455
|
+
path,
|
|
456
|
+
ws,
|
|
457
|
+
});
|
|
458
|
+
const artifact = yield* buildArtifact();
|
|
459
|
+
return {
|
|
460
|
+
...buildMaterializeOperation(skillManager, {
|
|
461
|
+
toStepFailure: args.adapter.toStepFailure,
|
|
462
|
+
ref,
|
|
463
|
+
force,
|
|
464
|
+
label: transitionLabel,
|
|
465
|
+
message: `Synced skill ${ref.skill.name}`,
|
|
466
|
+
buildArtifact,
|
|
467
|
+
}),
|
|
468
|
+
artifact,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
const subagentMaterializeStep = ({ ref, force, transitionLabel, }) => buildMaterializeOperation(subagentManager, {
|
|
472
|
+
toStepFailure: args.adapter.toStepFailure,
|
|
473
|
+
ref,
|
|
474
|
+
force,
|
|
475
|
+
label: transitionLabel,
|
|
476
|
+
message: `Synced subagent ${ref.subagent.name}`,
|
|
477
|
+
buildArtifact: () => subagentSyncArtifact({ ref, ws }),
|
|
478
|
+
});
|
|
479
|
+
const knowledgeMaterializeStep = ({ ref, force, transitionLabel, }) => buildMaterializeOperation(knowledgeManager, {
|
|
480
|
+
toStepFailure: args.adapter.toStepFailure,
|
|
481
|
+
ref,
|
|
482
|
+
force,
|
|
483
|
+
label: transitionLabel,
|
|
484
|
+
message: `Synced knowledge ${ref.knowledge.name}`,
|
|
485
|
+
});
|
|
486
|
+
const skillSteps = yield* Effect.forEach(skillRefs.filter(({ materialize }) => materialize), skillMaterializeStep, { concurrency: "unbounded" });
|
|
487
|
+
return {
|
|
488
|
+
cleanupSafe: problems.length === 0,
|
|
489
|
+
knowledgeMayChange: packRecoverySteps.length > 0 || knowledgeRefs.some(({ materialize }) => materialize),
|
|
490
|
+
serialMaterialization: packRecoverySteps.length > 0,
|
|
491
|
+
expectedSkillNames: new Set(skillRefs.map(({ ref }) => ref.skill.name)),
|
|
492
|
+
expectedSubagentNames: new Set(subagentRefs.map(({ ref }) => ref.subagent.name)),
|
|
493
|
+
releaseAge: {
|
|
494
|
+
evaluatedAt: DateTime.formatIso(releaseAgeEvaluation.evaluatedAt),
|
|
495
|
+
holdbacks: normalizeReleaseAgeRecords([
|
|
496
|
+
...reconciled.flatMap((item) => item.releaseAge?.holdbacks ?? []),
|
|
497
|
+
...(args.packRecovery?.releaseAge?.holdbacks ?? []),
|
|
498
|
+
]),
|
|
499
|
+
bypasses: normalizeReleaseAgeRecords([
|
|
500
|
+
...reconciled.flatMap((item) => item.releaseAge?.bypasses ?? []),
|
|
501
|
+
...(args.packRecovery?.releaseAge?.bypasses ?? []),
|
|
502
|
+
]),
|
|
503
|
+
},
|
|
504
|
+
steps: [
|
|
505
|
+
...packRecoverySteps,
|
|
506
|
+
...skillSteps,
|
|
507
|
+
...mcpServerRefs
|
|
508
|
+
.filter(({ materialize }) => materialize)
|
|
509
|
+
.map(({ ref, force, transitionLabel }) => buildMcpServerSyncOperation({
|
|
510
|
+
ref,
|
|
511
|
+
force,
|
|
512
|
+
transitionLabel,
|
|
513
|
+
runMcpServerInstall: args.runMcpServerInstall,
|
|
514
|
+
})),
|
|
515
|
+
...inlineMcpServerSteps,
|
|
516
|
+
...(needsMcpServerPrune
|
|
517
|
+
? [
|
|
518
|
+
buildMcpServerPruneOperation({
|
|
519
|
+
declaredServerNames: declaredMcpServerNames,
|
|
520
|
+
agentIds: configuredAgents,
|
|
521
|
+
ws,
|
|
522
|
+
adapter: args.adapter,
|
|
523
|
+
}),
|
|
524
|
+
]
|
|
525
|
+
: []),
|
|
526
|
+
...subagentRefs.filter(({ materialize }) => materialize).map(subagentMaterializeStep),
|
|
527
|
+
...ruleRefs
|
|
528
|
+
.filter(({ materialize }) => materialize)
|
|
529
|
+
.map(({ ref, force, transitionLabel }) => buildMaterializeOperation(ruleManager, {
|
|
530
|
+
toStepFailure: args.adapter.toStepFailure,
|
|
531
|
+
ref,
|
|
532
|
+
force,
|
|
533
|
+
label: transitionLabel,
|
|
534
|
+
})),
|
|
535
|
+
...hookRefs
|
|
536
|
+
.filter(({ materialize }) => materialize)
|
|
537
|
+
.map(({ ref, force, transitionLabel }) => buildMaterializeOperation(hookManager, {
|
|
538
|
+
toStepFailure: args.adapter.toStepFailure,
|
|
539
|
+
ref,
|
|
540
|
+
force,
|
|
541
|
+
label: transitionLabel,
|
|
542
|
+
})),
|
|
543
|
+
...knowledgeRefs.filter(({ materialize }) => materialize).map(knowledgeMaterializeStep),
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
//# sourceMappingURL=materialize.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync plan policy: recovery identities, aggregate-unit reconciliation steps
|
|
3
|
+
* (knowledge discovery, managed hook projections, instruction files, stale
|
|
4
|
+
* managed-projection cleanup, inline MCP servers and managed-entry pruning),
|
|
5
|
+
* and the plan-assembly ordering that realizes desired state. The CLI keeps
|
|
6
|
+
* argument parsing, confirmation, rendering, and plan execution.
|
|
7
|
+
*
|
|
8
|
+
* The application supplies a {@link SyncFailureAdapter}: its boundary mapping
|
|
9
|
+
* from typed failures to the kernel's `StepFailure`, so step categories and
|
|
10
|
+
* details stay byte-identical with the boundary's own rendering.
|
|
11
|
+
*
|
|
12
|
+
* @experimental This API is unstable and may change without notice.
|
|
13
|
+
*/
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import * as FileSystem from "effect/FileSystem";
|
|
16
|
+
import * as Option from "effect/Option";
|
|
17
|
+
import * as Path from "effect/Path";
|
|
18
|
+
import type * as ServiceMap from "effect/Context";
|
|
19
|
+
import { CodingAgentRepository, HookManager, KnowledgeManager, RuleManager, type ProjectionInvariantFact } from "@agentxm/extension-workspace";
|
|
20
|
+
import { StepFailure, type JobStepResult, type OperationPresentation, type Plan, type PlannedJobStep } from "@agentxm/workspace-operations";
|
|
21
|
+
import type { ReleaseAgeOperationEvidence } from "@agentxm/registry-protocol/unstable/registry/release-age-policy";
|
|
22
|
+
import { WorkspaceMutations, type McpServerEntry } from "@agentxm/workspace-state";
|
|
23
|
+
import type { SyncFailureAdapter } from "./failure-adapter.js";
|
|
24
|
+
export declare const SYNC_RECOVERY_IDS: {
|
|
25
|
+
readonly packManifestDivergence: "pack:manifest-divergence";
|
|
26
|
+
readonly extensionConstraintMismatch: "extension:constraint-mismatch";
|
|
27
|
+
readonly inlineMcpCollision: "mcp-server:inline";
|
|
28
|
+
readonly hookProjections: "hook:projections";
|
|
29
|
+
readonly instructionReconcile: "instruction:reconcile";
|
|
30
|
+
};
|
|
31
|
+
/** Executable sync recovery and blocker identities covered by recovery conformance. */
|
|
32
|
+
export declare const syncRecoveryIdentifiers: readonly ["pack:manifest-divergence", "extension:constraint-mismatch", "mcp-server:inline", "hook:projections", "instruction:reconcile"];
|
|
33
|
+
export declare const SYNC_PLAN_NAME = "Sync workspace";
|
|
34
|
+
export declare const SYNC_PLAN_DESCRIPTION = "Workspace-wide materialization from settings and on-disk extension content";
|
|
35
|
+
export declare const SYNC_PRESENTATION: OperationPresentation;
|
|
36
|
+
/** Services the feature's own plan steps require at execution time. */
|
|
37
|
+
export type SyncStepRequirements = FileSystem.FileSystem | Path.Path | WorkspaceMutations | CodingAgentRepository;
|
|
38
|
+
export declare const projectionFactsNeedReconciliation: (facts: ReadonlyArray<ProjectionInvariantFact>) => boolean;
|
|
39
|
+
export declare const projectionDivergenceLabel: (label: string, facts: ReadonlyArray<ProjectionInvariantFact>) => string;
|
|
40
|
+
export declare const isInlineMcpServerEntry: (entry: McpServerEntry) => boolean;
|
|
41
|
+
export declare const buildInlineMcpServerSyncOperation: ({ name, entry, agentIds, force, ws, adapter, }: {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly entry: McpServerEntry;
|
|
44
|
+
readonly agentIds: ReadonlyArray<string>;
|
|
45
|
+
readonly force: boolean;
|
|
46
|
+
readonly ws: ServiceMap.Service.Shape<typeof WorkspaceMutations>;
|
|
47
|
+
readonly adapter: SyncFailureAdapter;
|
|
48
|
+
}) => PlannedJobStep<SyncStepRequirements>;
|
|
49
|
+
export declare const buildMcpServerPruneOperation: ({ declaredServerNames, agentIds, ws, adapter, }: {
|
|
50
|
+
readonly declaredServerNames: ReadonlySet<string>;
|
|
51
|
+
readonly agentIds: ReadonlyArray<string>;
|
|
52
|
+
readonly ws: ServiceMap.Service.Shape<typeof WorkspaceMutations>;
|
|
53
|
+
readonly adapter: SyncFailureAdapter;
|
|
54
|
+
}) => PlannedJobStep<SyncStepRequirements>;
|
|
55
|
+
export declare const collectKnowledgeStep: (args: {
|
|
56
|
+
readonly adapter: SyncFailureAdapter;
|
|
57
|
+
readonly deferPreview?: boolean;
|
|
58
|
+
readonly facts?: ReadonlyArray<ProjectionInvariantFact>;
|
|
59
|
+
}) => Effect.Effect<Option.Option<PlannedJobStep<SyncStepRequirements>> | Option.Option<{
|
|
60
|
+
run: Effect.Effect<JobStepResult, StepFailure, never>;
|
|
61
|
+
message?: string;
|
|
62
|
+
key: string;
|
|
63
|
+
label: string;
|
|
64
|
+
readiness: "ready";
|
|
65
|
+
artifact: {
|
|
66
|
+
path: string;
|
|
67
|
+
scope: "project" | "user";
|
|
68
|
+
change: "updated" | "unchanged";
|
|
69
|
+
managedRegions: {
|
|
70
|
+
unitId: "skill:agent-skill-directory" | "mcp-server:native-config-entry" | "subagent:native-profile" | "hook:agent-hook-entries" | "hook:fallback-region" | "rule:instructions-region" | "knowledge:discovery-region";
|
|
71
|
+
path: string;
|
|
72
|
+
owner: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
}>, import("@agentxm/workspace-state").WorkspaceSettingsReadFailure, WorkspaceMutations | KnowledgeManager>;
|
|
76
|
+
export declare const collectCleanupStep: (args: {
|
|
77
|
+
readonly expectedSkillNames: ReadonlySet<string>;
|
|
78
|
+
readonly expectedSubagentNames: ReadonlySet<string>;
|
|
79
|
+
readonly adapter: SyncFailureAdapter;
|
|
80
|
+
}) => Effect.Effect<Option.Option<PlannedJobStep<SyncStepRequirements>>, import("./errors.ts").WorkspaceSyncCleanupFailure, Path.Path | FileSystem.FileSystem | CodingAgentRepository | WorkspaceMutations>;
|
|
81
|
+
export declare const collectHooksStep: (args: {
|
|
82
|
+
readonly facts: ReadonlyArray<ProjectionInvariantFact>;
|
|
83
|
+
readonly adapter: SyncFailureAdapter;
|
|
84
|
+
}) => Effect.Effect<Option.Option<PlannedJobStep<SyncStepRequirements>>, import("@agentxm/extension-workspace").ExtensionManagerFailure, WorkspaceMutations | HookManager>;
|
|
85
|
+
export declare const collectInstructionStep: (args: {
|
|
86
|
+
readonly projectionFacts: ReadonlyArray<ProjectionInvariantFact>;
|
|
87
|
+
readonly adapter: SyncFailureAdapter;
|
|
88
|
+
}) => Effect.Effect<Option.Option<PlannedJobStep<SyncStepRequirements>>, import("@agentxm/workspace-state").WorkspaceSettingsReadFailure, Path.Path | FileSystem.FileSystem | WorkspaceMutations | RuleManager>;
|
|
89
|
+
export declare const makeSyncPlan: <R>({ materializeSteps, knowledgeStep, hooksStep, cleanupStep, instructionStep, releaseAge, serialMaterialization, name, description, }: {
|
|
90
|
+
readonly materializeSteps: ReadonlyArray<PlannedJobStep<R>>;
|
|
91
|
+
readonly knowledgeStep: Option.Option<PlannedJobStep<R>>;
|
|
92
|
+
readonly hooksStep: Option.Option<PlannedJobStep<R>>;
|
|
93
|
+
readonly cleanupStep: Option.Option<PlannedJobStep<R>>;
|
|
94
|
+
readonly instructionStep: Option.Option<PlannedJobStep<R>>;
|
|
95
|
+
readonly releaseAge: ReleaseAgeOperationEvidence;
|
|
96
|
+
readonly serialMaterialization?: boolean;
|
|
97
|
+
readonly name?: string;
|
|
98
|
+
readonly description?: string;
|
|
99
|
+
}) => Plan<R>;
|
|
100
|
+
//# sourceMappingURL=plan.d.ts.map
|