dominus-cli 2.1.0 → 2.2.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/CHANGELOG.md +141 -96
- package/README.md +24 -13
- package/dist/install-plugin.js +60 -13
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +1194 -349
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +12 -6
- package/package.json +5 -3
- package/plugin/Dominus.rbxmx +3674 -0
- package/plugin/src/BridgeConfig.lua +2 -0
- package/plugin/src/ValueCodec.lua +6 -5
- package/plugin/src/WsClient.lua +13 -0
- package/plugin/src/init.server.lua +35 -22
- package/plugin/Dominus.rbxm +0 -0
package/dist/mcp.js
CHANGED
|
@@ -8,7 +8,8 @@ import { EventEmitter } from 'events';
|
|
|
8
8
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
12
13
|
|
|
13
14
|
var DOMINUS_DIR = path3.join(os.homedir(), ".dominus");
|
|
14
15
|
var CONFIG_PATH = path3.join(DOMINUS_DIR, "config.json");
|
|
@@ -64,7 +65,6 @@ var StudioMsg = {
|
|
|
64
65
|
RESPONSE: "studio:response",
|
|
65
66
|
REFLECTION: "studio:reflection",
|
|
66
67
|
TEST_RESULTS: "studio:test:results",
|
|
67
|
-
PAIRING_REQUIRED: "server:pairing_required",
|
|
68
68
|
AUTHENTICATED: "server:authenticated",
|
|
69
69
|
AUTH_REJECTED: "server:auth_rejected"
|
|
70
70
|
};
|
|
@@ -73,11 +73,35 @@ var ControllerMsg = {
|
|
|
73
73
|
WELCOME: "controller:welcome",
|
|
74
74
|
RELAY: "controller:relay",
|
|
75
75
|
TARGET_STATE: "controller:target_state",
|
|
76
|
-
SET_ACTIVE_CONNECTION: "controller:set_active_connection"
|
|
77
|
-
LIST_PENDING: "controller:list_pending",
|
|
78
|
-
APPROVE_CONNECTION: "controller:approve_connection"
|
|
76
|
+
SET_ACTIVE_CONNECTION: "controller:set_active_connection"
|
|
79
77
|
};
|
|
80
78
|
var CliMsg = {
|
|
79
|
+
GET_EXPLORER: "cli:get:explorer",
|
|
80
|
+
GET_SCRIPT: "cli:get:script",
|
|
81
|
+
SET_SCRIPT: "cli:set:script",
|
|
82
|
+
RUN_CODE: "cli:run",
|
|
83
|
+
GET_PROPERTIES: "cli:get:properties",
|
|
84
|
+
GET_DESCENDANTS_PROPERTIES: "cli:get:descendants:properties",
|
|
85
|
+
SET_PROPERTIES: "cli:set:properties",
|
|
86
|
+
INSERT_INSTANCE: "cli:insert",
|
|
87
|
+
DELETE_INSTANCE: "cli:delete",
|
|
88
|
+
GET_SELECTION: "cli:get:selection",
|
|
89
|
+
SELECT: "cli:select",
|
|
90
|
+
SEARCH_SCRIPTS: "cli:search:scripts",
|
|
91
|
+
GET_OUTPUT: "cli:get:output",
|
|
92
|
+
RUN_TESTS: "cli:run:tests",
|
|
93
|
+
EXECUTE_RUN_TEST: "cli:test:run",
|
|
94
|
+
EXECUTE_PLAY_TEST: "cli:test:play",
|
|
95
|
+
END_TEST: "cli:test:end",
|
|
96
|
+
BUILD_UI: "cli:build:ui",
|
|
97
|
+
GET_REFLECTION: "cli:get:reflection",
|
|
98
|
+
UPLOAD_ASSET: "cli:upload:asset",
|
|
99
|
+
SERIALIZE_UI: "cli:serialize:ui",
|
|
100
|
+
MOVE_INSTANCE: "cli:move:instance",
|
|
101
|
+
UNDO: "cli:undo",
|
|
102
|
+
REDO: "cli:redo",
|
|
103
|
+
BULK_SET_PROPERTIES: "cli:bulk:set:properties",
|
|
104
|
+
FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts",
|
|
81
105
|
V2_GET_TREE: "studio:v2:get_tree",
|
|
82
106
|
V2_INSPECT: "studio:v2:inspect",
|
|
83
107
|
V2_GET_SELECTION: "studio:v2:get_selection",
|
|
@@ -126,7 +150,7 @@ function loadOrCreateBridgeToken() {
|
|
|
126
150
|
if (existing.length >= 32) return existing;
|
|
127
151
|
} catch {
|
|
128
152
|
}
|
|
129
|
-
const token = randomBytes(32).toString("base64url");
|
|
153
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
130
154
|
fs3.writeFileSync(tokenPath, `${token}
|
|
131
155
|
`, { encoding: "utf8", mode: 384 });
|
|
132
156
|
try {
|
|
@@ -139,10 +163,7 @@ function tokensEqual(actual, expected) {
|
|
|
139
163
|
if (typeof actual !== "string") return false;
|
|
140
164
|
const actualBuffer = Buffer.from(actual);
|
|
141
165
|
const expectedBuffer = Buffer.from(expected);
|
|
142
|
-
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
143
|
-
}
|
|
144
|
-
function createPairingCode() {
|
|
145
|
-
return randomInt(1e5, 1e6).toString();
|
|
166
|
+
return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
// src/transport/ws-client.ts
|
|
@@ -373,20 +394,6 @@ var DominusClient = class extends EventEmitter {
|
|
|
373
394
|
active: connection.connectionId === this.activeConnectionId
|
|
374
395
|
}));
|
|
375
396
|
}
|
|
376
|
-
async listPendingConnections() {
|
|
377
|
-
const result = await this.sendControlRequest(
|
|
378
|
-
ControllerMsg.LIST_PENDING,
|
|
379
|
-
{}
|
|
380
|
-
);
|
|
381
|
-
return result.pending;
|
|
382
|
-
}
|
|
383
|
-
async approveConnection(connectionId, pairingCode) {
|
|
384
|
-
const result = await this.sendControlRequest(
|
|
385
|
-
ControllerMsg.APPROVE_CONNECTION,
|
|
386
|
-
{ connectionId, pairingCode }
|
|
387
|
-
);
|
|
388
|
-
return result.connection;
|
|
389
|
-
}
|
|
390
397
|
getActiveConnectionId() {
|
|
391
398
|
return this.activeConnectionId;
|
|
392
399
|
}
|
|
@@ -469,14 +476,12 @@ async function isPortInUse(port) {
|
|
|
469
476
|
});
|
|
470
477
|
}
|
|
471
478
|
var HANDSHAKE_TIMEOUT_MS = 8e3;
|
|
472
|
-
var PAIRING_TTL_MS = 5 * 6e4;
|
|
473
479
|
var RATE_WINDOW_MS = 1e4;
|
|
474
480
|
var RATE_LIMIT = 250;
|
|
475
481
|
var DominusServer = class extends EventEmitter {
|
|
476
482
|
wss = null;
|
|
477
483
|
studioClients = /* @__PURE__ */ new Map();
|
|
478
484
|
wsToConnectionId = /* @__PURE__ */ new Map();
|
|
479
|
-
pendingConnections = /* @__PURE__ */ new Map();
|
|
480
485
|
controllers = /* @__PURE__ */ new Set();
|
|
481
486
|
socketRoles = /* @__PURE__ */ new Map();
|
|
482
487
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -579,7 +584,10 @@ var DominusServer = class extends EventEmitter {
|
|
|
579
584
|
if (tokensEqual(hello.token, this.token)) {
|
|
580
585
|
this.authorizeStudio(ws, hello, randomUUID());
|
|
581
586
|
} else {
|
|
582
|
-
this.
|
|
587
|
+
this.sendSafe(ws, createMessage(StudioMsg.AUTH_REJECTED, {
|
|
588
|
+
error: "Studio plugin credentials are missing or stale. Run dominus-install-plugin and restart Studio."
|
|
589
|
+
}));
|
|
590
|
+
ws.close(1008, "Unauthorized Studio plugin");
|
|
583
591
|
}
|
|
584
592
|
return;
|
|
585
593
|
}
|
|
@@ -605,32 +613,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
605
613
|
}));
|
|
606
614
|
ws.close(1008, "Invalid Dominus handshake");
|
|
607
615
|
}
|
|
608
|
-
createPendingStudio(ws, hello) {
|
|
609
|
-
const connectionId = randomUUID();
|
|
610
|
-
const requestedAt = Date.now();
|
|
611
|
-
const pending = {
|
|
612
|
-
ws,
|
|
613
|
-
pairingCode: createPairingCode(),
|
|
614
|
-
hello,
|
|
615
|
-
info: {
|
|
616
|
-
connectionId,
|
|
617
|
-
clientId: hello.clientId,
|
|
618
|
-
placeId: hello.placeId ?? 0,
|
|
619
|
-
placeName: hello.placeName,
|
|
620
|
-
studioVersion: hello.studioVersion,
|
|
621
|
-
requestedAt,
|
|
622
|
-
expiresAt: requestedAt + PAIRING_TTL_MS
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
this.pendingConnections.set(connectionId, pending);
|
|
626
|
-
this.socketRoles.set(ws, "pending");
|
|
627
|
-
this.sendSafe(ws, createMessage(StudioMsg.PAIRING_REQUIRED, {
|
|
628
|
-
connectionId,
|
|
629
|
-
pairingCode: pending.pairingCode,
|
|
630
|
-
expiresAt: pending.info.expiresAt
|
|
631
|
-
}));
|
|
632
|
-
this.emit("studio:pairing_required", pending.info);
|
|
633
|
-
}
|
|
634
616
|
authorizeStudio(ws, hello, connectionId) {
|
|
635
617
|
const connection = {
|
|
636
618
|
ws,
|
|
@@ -641,7 +623,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
641
623
|
placeId: hello.placeId ?? 0,
|
|
642
624
|
placeName: hello.placeName
|
|
643
625
|
};
|
|
644
|
-
this.pendingConnections.delete(connectionId);
|
|
645
626
|
this.studioClients.set(connectionId, connection);
|
|
646
627
|
this.wsToConnectionId.set(ws, connectionId);
|
|
647
628
|
this.socketRoles.set(ws, "studio");
|
|
@@ -682,18 +663,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
682
663
|
this.sendResponse(controllerWs, msg.id, { success: true, connectionId });
|
|
683
664
|
return;
|
|
684
665
|
}
|
|
685
|
-
if (msg.type === ControllerMsg.LIST_PENDING) {
|
|
686
|
-
this.sendResponse(controllerWs, msg.id, { success: true, pending: this.getPendingInfos() });
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
if (msg.type === ControllerMsg.APPROVE_CONNECTION) {
|
|
690
|
-
const payload = msg.payload;
|
|
691
|
-
this.approveConnection(String(payload.connectionId ?? ""), String(payload.pairingCode ?? "")).then((connection) => this.sendResponse(controllerWs, msg.id, { success: true, connection })).catch((err) => this.sendResponse(controllerWs, msg.id, {
|
|
692
|
-
success: false,
|
|
693
|
-
error: err instanceof Error ? err.message : String(err)
|
|
694
|
-
}));
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
666
|
this.sendResponse(controllerWs, msg.id, { error: `Unsupported controller message: ${msg.type}` });
|
|
698
667
|
}
|
|
699
668
|
handleStudioMessage(ws, msg) {
|
|
@@ -734,12 +703,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
734
703
|
}
|
|
735
704
|
return;
|
|
736
705
|
}
|
|
737
|
-
if (role === "pending") {
|
|
738
|
-
for (const [id, pending] of this.pendingConnections) {
|
|
739
|
-
if (pending.ws === ws) this.pendingConnections.delete(id);
|
|
740
|
-
}
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
706
|
const connectionId = this.wsToConnectionId.get(ws);
|
|
744
707
|
if (!connectionId) return;
|
|
745
708
|
const connection = this.studioClients.get(connectionId);
|
|
@@ -782,16 +745,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
782
745
|
if (this.activeConnectionId) return `Studio connection ${this.activeConnectionId} is unavailable`;
|
|
783
746
|
return "Multiple Studio sessions are connected. Call dominus_select_studio with a connectionId.";
|
|
784
747
|
}
|
|
785
|
-
getPendingInfos() {
|
|
786
|
-
const now = Date.now();
|
|
787
|
-
for (const [id, pending] of this.pendingConnections) {
|
|
788
|
-
if (pending.info.expiresAt <= now || pending.ws.readyState !== WebSocket.OPEN) {
|
|
789
|
-
this.pendingConnections.delete(id);
|
|
790
|
-
pending.ws.close(1008, "Pairing expired");
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return [...this.pendingConnections.values()].map(({ info }) => ({ ...info }));
|
|
794
|
-
}
|
|
795
748
|
toPlaceInfo(connection) {
|
|
796
749
|
return {
|
|
797
750
|
placeId: connection.placeId ?? 0,
|
|
@@ -858,20 +811,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
858
811
|
listConnections() {
|
|
859
812
|
return [...this.studioClients.values()].filter(({ ws }) => ws.readyState === WebSocket.OPEN).map((connection) => this.toPlaceInfo(connection));
|
|
860
813
|
}
|
|
861
|
-
async listPendingConnections() {
|
|
862
|
-
return this.getPendingInfos();
|
|
863
|
-
}
|
|
864
|
-
async approveConnection(connectionId, pairingCode) {
|
|
865
|
-
const pending = this.pendingConnections.get(connectionId);
|
|
866
|
-
if (!pending || pending.info.expiresAt <= Date.now()) {
|
|
867
|
-
throw new Error("Pairing request not found or expired");
|
|
868
|
-
}
|
|
869
|
-
if (pending.pairingCode !== pairingCode) {
|
|
870
|
-
throw new Error("Pairing code is incorrect");
|
|
871
|
-
}
|
|
872
|
-
const connection = this.authorizeStudio(pending.ws, pending.hello, connectionId);
|
|
873
|
-
return this.toPlaceInfo(connection);
|
|
874
|
-
}
|
|
875
814
|
getActiveConnectionId() {
|
|
876
815
|
return this.activeConnectionId;
|
|
877
816
|
}
|
|
@@ -909,7 +848,6 @@ var DominusServer = class extends EventEmitter {
|
|
|
909
848
|
this.relayRequests.clear();
|
|
910
849
|
for (const ws of this.socketRoles.keys()) ws.close(1001, "Dominus bridge shutdown");
|
|
911
850
|
this.studioClients.clear();
|
|
912
|
-
this.pendingConnections.clear();
|
|
913
851
|
this.controllers.clear();
|
|
914
852
|
this.socketRoles.clear();
|
|
915
853
|
this.wsToConnectionId.clear();
|
|
@@ -951,6 +889,8 @@ Required workflow for Studio changes:
|
|
|
951
889
|
6. Read the affected instances back and compare exact values, hierarchy, and source revisions.
|
|
952
890
|
7. Run an appropriate Studio test when behavior changed. Report verification evidence and any remaining uncertainty.
|
|
953
891
|
|
|
892
|
+
For broad tasks with independent Studio branches, prefer run_parallel_task when the MCP client supports Sampling. Dominus partitions immediate child scopes, runs proposal-only workers concurrently, rejects unknown refs and overlapping writes, applies only the coordinator-approved atomic plan, and reads the result back. Use the normal workflow when Sampling is unavailable or the task is a simple edit.
|
|
893
|
+
|
|
954
894
|
Safety rules:
|
|
955
895
|
- Never invent instance IDs or Roblox API members.
|
|
956
896
|
- Do not retry a failed write unchanged. Inspect the error and current state first.
|
|
@@ -961,31 +901,47 @@ Safety rules:
|
|
|
961
901
|
|
|
962
902
|
// src/mcp/resources.ts
|
|
963
903
|
function registerDominusResources(server, bridge) {
|
|
964
|
-
server.registerResource(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
904
|
+
server.registerResource(
|
|
905
|
+
"dominus-status",
|
|
906
|
+
"dominus://status",
|
|
907
|
+
{
|
|
908
|
+
title: "Dominus 2 Studio Status",
|
|
909
|
+
description: "Current authenticated Roblox Studio sessions and selected target.",
|
|
910
|
+
mimeType: "application/json"
|
|
911
|
+
},
|
|
912
|
+
async (uri) => ({
|
|
913
|
+
contents: [
|
|
914
|
+
{
|
|
915
|
+
uri: uri.href,
|
|
916
|
+
mimeType: "application/json",
|
|
917
|
+
text: JSON.stringify(
|
|
918
|
+
{
|
|
919
|
+
protocolVersion: 2,
|
|
920
|
+
activeConnectionId: bridge.getActiveConnectionId(),
|
|
921
|
+
connections: bridge.listConnections()
|
|
922
|
+
},
|
|
923
|
+
null,
|
|
924
|
+
2
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
})
|
|
929
|
+
);
|
|
930
|
+
server.registerPrompt(
|
|
931
|
+
"dominus-workflow",
|
|
932
|
+
{
|
|
933
|
+
title: "Dominus 2 Engineering Workflow",
|
|
934
|
+
description: "Inspect, plan, apply, verify, and test a Roblox Studio change safely."
|
|
935
|
+
},
|
|
936
|
+
async () => ({
|
|
937
|
+
messages: [
|
|
938
|
+
{
|
|
939
|
+
role: "user",
|
|
940
|
+
content: { type: "text", text: DOMINUS_MCP_INSTRUCTIONS }
|
|
941
|
+
}
|
|
942
|
+
]
|
|
943
|
+
})
|
|
944
|
+
);
|
|
989
945
|
}
|
|
990
946
|
|
|
991
947
|
// src/mcp/result.ts
|
|
@@ -1019,74 +975,77 @@ var toolOutputSchema = {
|
|
|
1019
975
|
};
|
|
1020
976
|
async function callStudio(bridge, command, payload, timeoutMs = 15e3) {
|
|
1021
977
|
try {
|
|
1022
|
-
const
|
|
1023
|
-
const result = response.payload;
|
|
1024
|
-
if (!result || typeof result !== "object") return failure("Studio returned an invalid response");
|
|
1025
|
-
if (result.success === false || typeof result.error === "string") {
|
|
1026
|
-
return failure(result.error ?? "Studio command failed", result);
|
|
1027
|
-
}
|
|
978
|
+
const result = await requestStudio(bridge, command, payload, timeoutMs);
|
|
1028
979
|
return success(result);
|
|
1029
980
|
} catch (err) {
|
|
1030
981
|
return failure(err);
|
|
1031
982
|
}
|
|
1032
983
|
}
|
|
984
|
+
async function requestStudio(bridge, command, payload, timeoutMs = 15e3) {
|
|
985
|
+
const response = await bridge.sendRequest(command, payload, timeoutMs);
|
|
986
|
+
const result = response.payload;
|
|
987
|
+
if (!result || typeof result !== "object") throw new Error("Studio returned an invalid response");
|
|
988
|
+
if (result.success === false || typeof result.error === "string") {
|
|
989
|
+
throw new Error(typeof result.error === "string" ? result.error : "Studio command failed");
|
|
990
|
+
}
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
1033
993
|
function ref(value) {
|
|
1034
994
|
return value;
|
|
1035
995
|
}
|
|
1036
996
|
|
|
1037
997
|
// src/mcp/tools/connections.ts
|
|
1038
998
|
function registerConnectionTools(server, bridge) {
|
|
1039
|
-
server.registerTool(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
connections: bridge.listConnections(),
|
|
1053
|
-
pendingConnections: pending
|
|
1054
|
-
});
|
|
1055
|
-
} catch (err) {
|
|
1056
|
-
return failure(err);
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
server.registerTool("dominus_pair_studio", {
|
|
1060
|
-
title: "Pair Roblox Studio",
|
|
1061
|
-
description: "Approve a first-time Studio plugin connection using the connection ID and six-digit code printed in Roblox Studio Output.",
|
|
1062
|
-
inputSchema: {
|
|
1063
|
-
connectionId: z.string().uuid(),
|
|
1064
|
-
pairingCode: z.string().regex(/^\d{6}$/)
|
|
999
|
+
server.registerTool(
|
|
1000
|
+
"dominus_status",
|
|
1001
|
+
{
|
|
1002
|
+
title: "Dominus Studio Status",
|
|
1003
|
+
description: "List authenticated Roblox Studio sessions, the active target, and bridge health. Call this before Studio work.",
|
|
1004
|
+
inputSchema: {},
|
|
1005
|
+
outputSchema: toolOutputSchema,
|
|
1006
|
+
annotations: {
|
|
1007
|
+
readOnlyHint: true,
|
|
1008
|
+
destructiveHint: false,
|
|
1009
|
+
idempotentHint: true,
|
|
1010
|
+
openWorldHint: false
|
|
1011
|
+
}
|
|
1065
1012
|
},
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1013
|
+
async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
return success({
|
|
1016
|
+
protocolVersion: 2,
|
|
1017
|
+
port: bridge.getPort(),
|
|
1018
|
+
activeConnectionId: bridge.getActiveConnectionId(),
|
|
1019
|
+
connections: bridge.listConnections()
|
|
1020
|
+
});
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
return failure(err);
|
|
1023
|
+
}
|
|
1074
1024
|
}
|
|
1075
|
-
|
|
1076
|
-
server.registerTool(
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1025
|
+
);
|
|
1026
|
+
server.registerTool(
|
|
1027
|
+
"dominus_select_studio",
|
|
1028
|
+
{
|
|
1029
|
+
title: "Select Studio Session",
|
|
1030
|
+
description: "Choose the exact Studio connection targeted by subsequent tools. Uses connectionId, not placeId, so duplicate windows are safe.",
|
|
1031
|
+
inputSchema: { connectionId: z.string().uuid().nullable() },
|
|
1032
|
+
outputSchema: toolOutputSchema,
|
|
1033
|
+
annotations: {
|
|
1034
|
+
readOnlyHint: false,
|
|
1035
|
+
destructiveHint: false,
|
|
1036
|
+
idempotentHint: true,
|
|
1037
|
+
openWorldHint: false
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
async ({ connectionId }) => {
|
|
1041
|
+
try {
|
|
1042
|
+
await bridge.setActiveConnectionId(connectionId);
|
|
1043
|
+
return success({ activeConnectionId: bridge.getActiveConnectionId() });
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
return failure(err);
|
|
1046
|
+
}
|
|
1088
1047
|
}
|
|
1089
|
-
|
|
1048
|
+
);
|
|
1090
1049
|
}
|
|
1091
1050
|
var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine";
|
|
1092
1051
|
var GITHUB_TREE_URL = "https://api.github.com/repos/Roblox/creator-docs/git/trees/main?recursive=1";
|
|
@@ -1509,57 +1468,834 @@ function escapeRegExp(value) {
|
|
|
1509
1468
|
// src/mcp/tools/docs.ts
|
|
1510
1469
|
var kindSchema = z.enum(["any", "class", "datatype", "enum", "global", "library"]);
|
|
1511
1470
|
function registerDocsTools(server) {
|
|
1512
|
-
server.registerTool(
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1471
|
+
server.registerTool(
|
|
1472
|
+
"roblox_search_api",
|
|
1473
|
+
{
|
|
1474
|
+
title: "Search Roblox API",
|
|
1475
|
+
description: "Search the official Roblox Creator Docs engine API mirror by exact or partial class, datatype, enum, global, or library name.",
|
|
1476
|
+
inputSchema: {
|
|
1477
|
+
query: z.string().min(1).max(100),
|
|
1478
|
+
kind: kindSchema.optional().default("any"),
|
|
1479
|
+
limit: z.number().int().min(1).max(25).optional().default(10)
|
|
1480
|
+
},
|
|
1481
|
+
outputSchema: toolOutputSchema,
|
|
1482
|
+
annotations: {
|
|
1483
|
+
readOnlyHint: true,
|
|
1484
|
+
destructiveHint: false,
|
|
1485
|
+
idempotentHint: true,
|
|
1486
|
+
openWorldHint: true
|
|
1487
|
+
}
|
|
1519
1488
|
},
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
return failure(err);
|
|
1489
|
+
async ({ query, kind, limit }) => {
|
|
1490
|
+
try {
|
|
1491
|
+
return success({ results: await searchRobloxApi(query, { kind, limit }) });
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
return failure(err);
|
|
1494
|
+
}
|
|
1527
1495
|
}
|
|
1496
|
+
);
|
|
1497
|
+
server.registerTool(
|
|
1498
|
+
"roblox_get_api",
|
|
1499
|
+
{
|
|
1500
|
+
title: "Get Roblox API Reference",
|
|
1501
|
+
description: "Read an official Roblox engine API reference including members, types, security, thread safety, inheritance, and source URL.",
|
|
1502
|
+
inputSchema: {
|
|
1503
|
+
name: z.string().min(1).max(100),
|
|
1504
|
+
kind: kindSchema.optional().default("any"),
|
|
1505
|
+
includeInherited: z.boolean().optional().default(true)
|
|
1506
|
+
},
|
|
1507
|
+
outputSchema: toolOutputSchema,
|
|
1508
|
+
annotations: {
|
|
1509
|
+
readOnlyHint: true,
|
|
1510
|
+
destructiveHint: false,
|
|
1511
|
+
idempotentHint: true,
|
|
1512
|
+
openWorldHint: true
|
|
1513
|
+
}
|
|
1514
|
+
},
|
|
1515
|
+
async ({ name, kind, includeInherited }) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const reference = await getRobloxApiReference(name, { kind, includeInherited });
|
|
1518
|
+
return success({ reference });
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
return failure(err);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
var propertiesSchema = z.record(z.unknown()).refine(
|
|
1526
|
+
(properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
|
|
1527
|
+
"At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
|
|
1528
|
+
);
|
|
1529
|
+
var mutationOperationSchema = z.discriminatedUnion("op", [
|
|
1530
|
+
z.object({
|
|
1531
|
+
op: z.literal("setProperties"),
|
|
1532
|
+
target: instanceRefSchema,
|
|
1533
|
+
properties: propertiesSchema
|
|
1534
|
+
}),
|
|
1535
|
+
z.object({
|
|
1536
|
+
op: z.literal("create"),
|
|
1537
|
+
parent: instanceRefSchema,
|
|
1538
|
+
className: z.string().min(1).max(100),
|
|
1539
|
+
name: z.string().min(1).max(256),
|
|
1540
|
+
properties: propertiesSchema.optional()
|
|
1541
|
+
}),
|
|
1542
|
+
z.object({
|
|
1543
|
+
op: z.literal("move"),
|
|
1544
|
+
target: instanceRefSchema,
|
|
1545
|
+
parent: instanceRefSchema
|
|
1546
|
+
})
|
|
1547
|
+
]);
|
|
1548
|
+
var workerProposalSchema = z.object({
|
|
1549
|
+
workerId: z.string().min(1).max(64),
|
|
1550
|
+
summary: z.string().min(1).max(4e3),
|
|
1551
|
+
evidence: z.array(
|
|
1552
|
+
z.object({
|
|
1553
|
+
source: z.string().min(1).max(500),
|
|
1554
|
+
summary: z.string().min(1).max(2e3)
|
|
1555
|
+
})
|
|
1556
|
+
).max(30).default([]),
|
|
1557
|
+
operations: z.array(mutationOperationSchema).max(50).default([]),
|
|
1558
|
+
risks: z.array(z.string().max(1e3)).max(30).default([]),
|
|
1559
|
+
verificationSuggestions: z.array(z.string().max(1e3)).max(30).default([])
|
|
1560
|
+
});
|
|
1561
|
+
var coordinatorDecisionSchema = z.object({
|
|
1562
|
+
summary: z.string().min(1).max(4e3),
|
|
1563
|
+
acceptedWorkerIds: z.array(z.string()).max(5),
|
|
1564
|
+
rejected: z.array(
|
|
1565
|
+
z.object({
|
|
1566
|
+
workerId: z.string(),
|
|
1567
|
+
reason: z.string().min(1).max(2e3)
|
|
1568
|
+
})
|
|
1569
|
+
).max(5).default([]),
|
|
1570
|
+
operationOrder: z.array(
|
|
1571
|
+
z.object({
|
|
1572
|
+
workerId: z.string(),
|
|
1573
|
+
operationIndex: z.number().int().min(0).max(49)
|
|
1574
|
+
})
|
|
1575
|
+
).max(100),
|
|
1576
|
+
verificationChecks: z.array(z.string().max(1e3)).max(50).default([])
|
|
1577
|
+
});
|
|
1578
|
+
function registerParallelTaskTool(server, bridge) {
|
|
1579
|
+
server.registerTool(
|
|
1580
|
+
"run_parallel_task",
|
|
1581
|
+
{
|
|
1582
|
+
title: "Run Coordinated Parallel Studio Task",
|
|
1583
|
+
description: "Partition a broad Studio goal into non-overlapping scopes, run proposal-only workers concurrently through MCP Sampling, reject unsafe or conflicting proposals, atomically apply coordinator-approved writes, and verify the result.",
|
|
1584
|
+
inputSchema: {
|
|
1585
|
+
goal: z.string().min(1).max(2e4),
|
|
1586
|
+
root: instanceRefSchema.optional(),
|
|
1587
|
+
rootPath: z.string().min(1).max(2e3).optional(),
|
|
1588
|
+
maxWorkers: z.number().int().min(1).max(5).optional().default(3),
|
|
1589
|
+
constraints: z.string().max(1e4).optional(),
|
|
1590
|
+
taskType: z.string().max(100).optional(),
|
|
1591
|
+
dryRun: z.boolean().optional().default(false)
|
|
1592
|
+
},
|
|
1593
|
+
outputSchema: toolOutputSchema,
|
|
1594
|
+
annotations: {
|
|
1595
|
+
readOnlyHint: false,
|
|
1596
|
+
destructiveHint: false,
|
|
1597
|
+
idempotentHint: false,
|
|
1598
|
+
openWorldHint: false
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1601
|
+
async ({ goal, root, rootPath, maxWorkers, constraints, taskType, dryRun }) => {
|
|
1602
|
+
try {
|
|
1603
|
+
const capabilities = server.server.getClientCapabilities();
|
|
1604
|
+
if (!capabilities?.sampling) {
|
|
1605
|
+
return failure(
|
|
1606
|
+
"run_parallel_task requires an MCP client that supports Sampling. Use the normal Dominus inspect-plan-apply workflow with this client.",
|
|
1607
|
+
{ requiredCapability: "sampling" }
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const rootRef = root ?? {
|
|
1611
|
+
pathSegments: parallelRootPathToSegments(rootPath ?? "Workspace")
|
|
1612
|
+
};
|
|
1613
|
+
const tree = await getTree(bridge, rootRef, 5, 1500);
|
|
1614
|
+
const rootNode = tree.roots[0];
|
|
1615
|
+
if (!rootNode) return failure("The requested root did not return a Studio tree");
|
|
1616
|
+
if (tree.truncated) {
|
|
1617
|
+
return failure(
|
|
1618
|
+
"The requested root exceeded the parallel evidence limit. Narrow root/rootPath so workers receive a complete hierarchy.",
|
|
1619
|
+
{ nodeCount: tree.nodeCount, maxNodes: 1500 }
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
const assignments = createAssignments(goal, rootNode, maxWorkers, constraints, taskType);
|
|
1623
|
+
emit(server, "multi_agent_start", { goal, workerCount: assignments.length });
|
|
1624
|
+
const settled = await Promise.allSettled(
|
|
1625
|
+
assignments.map(async (assignment) => {
|
|
1626
|
+
emit(server, "multi_agent_worker_start", {
|
|
1627
|
+
workerId: assignment.id,
|
|
1628
|
+
role: assignment.role,
|
|
1629
|
+
scopes: assignment.ownedScopes
|
|
1630
|
+
});
|
|
1631
|
+
const result = await runWorker(server, bridge, goal, assignment, constraints, taskType);
|
|
1632
|
+
emit(server, "multi_agent_worker_done", {
|
|
1633
|
+
workerId: assignment.id,
|
|
1634
|
+
summary: result.proposal?.summary ?? result.error ?? "Worker failed",
|
|
1635
|
+
proposedOperationCount: result.proposal?.operations.length ?? 0
|
|
1636
|
+
});
|
|
1637
|
+
return result;
|
|
1638
|
+
})
|
|
1639
|
+
);
|
|
1640
|
+
const workerResults = settled.map(
|
|
1641
|
+
(result, index) => result.status === "fulfilled" ? result.value : { assignment: assignments[index], error: toErrorMessage(result.reason) }
|
|
1642
|
+
);
|
|
1643
|
+
const validated = validateWorkerResults(workerResults);
|
|
1644
|
+
const conflictChecked = rejectProposalConflicts(validated.accepted);
|
|
1645
|
+
const accepted = conflictChecked.accepted;
|
|
1646
|
+
const rejected = [...validated.rejected, ...conflictChecked.rejected];
|
|
1647
|
+
for (const item of accepted) {
|
|
1648
|
+
emit(server, "multi_agent_proposal", {
|
|
1649
|
+
workerId: item.assignment.id,
|
|
1650
|
+
accepted: true,
|
|
1651
|
+
operationCount: item.proposal.operations.length
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
for (const item of rejected) {
|
|
1655
|
+
emit(server, "multi_agent_proposal", {
|
|
1656
|
+
workerId: item.workerId,
|
|
1657
|
+
accepted: false,
|
|
1658
|
+
reason: item.reason
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
const decision = accepted.length > 0 ? await runCoordinator(server, goal, accepted, rejected, constraints) : emptyDecision(rejected);
|
|
1662
|
+
const finalPlan = buildFinalPlan(decision, accepted, rejected);
|
|
1663
|
+
if (finalPlan.operations.length > 100) {
|
|
1664
|
+
return failure("Coordinator plan exceeds the 100-operation atomic limit", {
|
|
1665
|
+
operationCount: finalPlan.operations.length
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
emit(server, "multi_agent_decision", {
|
|
1669
|
+
acceptedCount: finalPlan.acceptedWorkerIds.length,
|
|
1670
|
+
rejectedCount: finalPlan.rejected.length,
|
|
1671
|
+
finalOperationCount: finalPlan.operations.length
|
|
1672
|
+
});
|
|
1673
|
+
let applied = null;
|
|
1674
|
+
let verification = null;
|
|
1675
|
+
let repairApplied = false;
|
|
1676
|
+
if (!dryRun && finalPlan.operations.length > 0) {
|
|
1677
|
+
emit(server, "multi_agent_apply_start", { operationCount: finalPlan.operations.length });
|
|
1678
|
+
applied = await requestStudio(
|
|
1679
|
+
bridge,
|
|
1680
|
+
CliMsg.V2_APPLY,
|
|
1681
|
+
{ operations: finalPlan.operations },
|
|
1682
|
+
12e4
|
|
1683
|
+
);
|
|
1684
|
+
verification = await verifyPlan(bridge, rootRef, rootNode, finalPlan.operations);
|
|
1685
|
+
if (!verification.passed) {
|
|
1686
|
+
const repairOperations = findRepairOperations(
|
|
1687
|
+
finalPlan.operations,
|
|
1688
|
+
verification,
|
|
1689
|
+
rootNode
|
|
1690
|
+
);
|
|
1691
|
+
if (repairOperations.length > 0) {
|
|
1692
|
+
await requestStudio(
|
|
1693
|
+
bridge,
|
|
1694
|
+
CliMsg.V2_APPLY,
|
|
1695
|
+
{ operations: repairOperations },
|
|
1696
|
+
12e4
|
|
1697
|
+
);
|
|
1698
|
+
repairApplied = true;
|
|
1699
|
+
verification = await verifyPlan(bridge, rootRef, rootNode, finalPlan.operations);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
emit(server, "multi_agent_apply_done", {
|
|
1703
|
+
appliedCount: finalPlan.operations.length,
|
|
1704
|
+
verificationPassed: verification.passed,
|
|
1705
|
+
repairApplied
|
|
1706
|
+
});
|
|
1707
|
+
} else {
|
|
1708
|
+
verification = await verifyPlan(bridge, rootRef, rootNode, []);
|
|
1709
|
+
}
|
|
1710
|
+
return success({
|
|
1711
|
+
goal,
|
|
1712
|
+
root: rootRef,
|
|
1713
|
+
dryRun,
|
|
1714
|
+
assignments: assignments.map(publicAssignment),
|
|
1715
|
+
workers: workerResults.map(publicWorkerResult),
|
|
1716
|
+
decision: {
|
|
1717
|
+
summary: decision.summary,
|
|
1718
|
+
acceptedWorkerIds: finalPlan.acceptedWorkerIds,
|
|
1719
|
+
rejected: finalPlan.rejected,
|
|
1720
|
+
verificationChecks: decision.verificationChecks
|
|
1721
|
+
},
|
|
1722
|
+
finalOperations: finalPlan.operations,
|
|
1723
|
+
applied,
|
|
1724
|
+
repairApplied,
|
|
1725
|
+
verification
|
|
1726
|
+
});
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
return failure(err);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
async function getTree(bridge, root, maxDepth, maxNodes) {
|
|
1734
|
+
const result = await requestStudio(bridge, CliMsg.V2_GET_TREE, { root, maxDepth, maxNodes });
|
|
1735
|
+
return {
|
|
1736
|
+
roots: Array.isArray(result.roots) ? result.roots : [],
|
|
1737
|
+
nodeCount: typeof result.nodeCount === "number" ? result.nodeCount : 0,
|
|
1738
|
+
truncated: result.truncated === true
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function createAssignments(goal, root, maxWorkers, constraints, taskType) {
|
|
1742
|
+
const candidates = root.children?.length ? root.children : [root];
|
|
1743
|
+
const workerCount = Math.max(1, Math.min(maxWorkers, candidates.length));
|
|
1744
|
+
const groups = Array.from({ length: workerCount }, () => []);
|
|
1745
|
+
candidates.forEach((node, index) => groups[index % workerCount].push(node));
|
|
1746
|
+
return groups.map((scopeNodes, index) => {
|
|
1747
|
+
const ownedScopes = scopeNodes.map((node) => node.ref);
|
|
1748
|
+
const knownRefKeys = /* @__PURE__ */ new Set();
|
|
1749
|
+
scopeNodes.forEach((node) => collectRefKeys(node, knownRefKeys));
|
|
1750
|
+
return {
|
|
1751
|
+
id: `worker-${index + 1}`,
|
|
1752
|
+
role: `${taskType ?? "general"} scope analyst ${index + 1}`,
|
|
1753
|
+
objective: `Inspect the assigned Studio branches and propose the smallest changes that advance: ${goal}`,
|
|
1754
|
+
ownedScopes,
|
|
1755
|
+
acceptanceCriteria: [
|
|
1756
|
+
"Every referenced existing instance must come from the supplied evidence.",
|
|
1757
|
+
"Every proposed mutation must remain inside an owned scope.",
|
|
1758
|
+
"Do not propose deletion, arbitrary code execution, or script replacement.",
|
|
1759
|
+
constraints?.trim() || "Preserve unrelated instances and behavior."
|
|
1760
|
+
],
|
|
1761
|
+
scopeNodes,
|
|
1762
|
+
knownRefKeys
|
|
1763
|
+
};
|
|
1528
1764
|
});
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1765
|
+
}
|
|
1766
|
+
async function runWorker(server, bridge, goal, assignment, constraints, taskType) {
|
|
1767
|
+
try {
|
|
1768
|
+
const studioInspection = await inspectScopeRoots(bridge, assignment.ownedScopes);
|
|
1769
|
+
const response = await server.server.createMessage(
|
|
1770
|
+
{
|
|
1771
|
+
systemPrompt: [
|
|
1772
|
+
"You are a read-only Dominus worker. Inspect only the Studio evidence in the request.",
|
|
1773
|
+
"You cannot write to Studio. Return one strict JSON WorkerProposal and no Markdown.",
|
|
1774
|
+
"Only propose setProperties, create, or move operations using exact instance refs copied from evidence.",
|
|
1775
|
+
"Never invent instanceId values. Never propose delete, script edits, or arbitrary execution."
|
|
1776
|
+
].join(" "),
|
|
1777
|
+
messages: [
|
|
1778
|
+
{
|
|
1779
|
+
role: "user",
|
|
1780
|
+
content: {
|
|
1781
|
+
type: "text",
|
|
1782
|
+
text: JSON.stringify(
|
|
1783
|
+
{
|
|
1784
|
+
schema: {
|
|
1785
|
+
workerId: assignment.id,
|
|
1786
|
+
summary: "string",
|
|
1787
|
+
evidence: [{ source: "string", summary: "string" }],
|
|
1788
|
+
operations: [
|
|
1789
|
+
{
|
|
1790
|
+
op: "setProperties",
|
|
1791
|
+
target: { instanceId: "uuid", pathSegments: ["..."] },
|
|
1792
|
+
properties: {}
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
op: "create",
|
|
1796
|
+
parent: { instanceId: "uuid", pathSegments: ["..."] },
|
|
1797
|
+
className: "Part",
|
|
1798
|
+
name: "Name",
|
|
1799
|
+
properties: {}
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
op: "move",
|
|
1803
|
+
target: { instanceId: "uuid", pathSegments: ["..."] },
|
|
1804
|
+
parent: { instanceId: "uuid", pathSegments: ["..."] }
|
|
1805
|
+
}
|
|
1806
|
+
],
|
|
1807
|
+
risks: ["string"],
|
|
1808
|
+
verificationSuggestions: ["string"]
|
|
1809
|
+
},
|
|
1810
|
+
globalGoal: goal,
|
|
1811
|
+
taskType: taskType ?? "general",
|
|
1812
|
+
constraints: constraints ?? "",
|
|
1813
|
+
assignment: publicAssignment(assignment),
|
|
1814
|
+
studioEvidence: {
|
|
1815
|
+
hierarchy: assignment.scopeNodes,
|
|
1816
|
+
typedRootInspection: studioInspection
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
null,
|
|
1820
|
+
2
|
|
1821
|
+
)
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
],
|
|
1825
|
+
maxTokens: 5e3,
|
|
1826
|
+
temperature: 0.2,
|
|
1827
|
+
modelPreferences: {
|
|
1828
|
+
intelligencePriority: 0.9,
|
|
1829
|
+
speedPriority: 0.3,
|
|
1830
|
+
costPriority: 0.2
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
{ timeout: 12e4 }
|
|
1834
|
+
);
|
|
1835
|
+
const proposal = workerProposalSchema.parse(parseJson(extractSamplingText(response.content)));
|
|
1836
|
+
return { assignment, proposal };
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
return { assignment, error: toErrorMessage(err) };
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
async function inspectScopeRoots(bridge, targets) {
|
|
1842
|
+
const results = [];
|
|
1843
|
+
for (let offset = 0; offset < targets.length; offset += 20) {
|
|
1844
|
+
const inspected = await requestStudio(bridge, CliMsg.V2_INSPECT, {
|
|
1845
|
+
targets: targets.slice(offset, offset + 20),
|
|
1846
|
+
compact: true
|
|
1847
|
+
});
|
|
1848
|
+
if (Array.isArray(inspected.results)) {
|
|
1849
|
+
results.push(...inspected.results);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return results;
|
|
1853
|
+
}
|
|
1854
|
+
function validateWorkerResults(results) {
|
|
1855
|
+
const accepted = [];
|
|
1856
|
+
const rejected = [];
|
|
1857
|
+
for (const result of results) {
|
|
1858
|
+
if (!result.proposal) {
|
|
1859
|
+
rejected.push({
|
|
1860
|
+
workerId: result.assignment.id,
|
|
1861
|
+
reason: result.error ?? "Worker returned no proposal"
|
|
1862
|
+
});
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (result.proposal.workerId !== result.assignment.id) {
|
|
1866
|
+
rejected.push({
|
|
1867
|
+
workerId: result.assignment.id,
|
|
1868
|
+
reason: `Proposal workerId ${result.proposal.workerId} did not match its assignment`,
|
|
1869
|
+
summary: result.proposal.summary
|
|
1870
|
+
});
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
const errors = validateOperations(result.assignment, result.proposal.operations);
|
|
1874
|
+
if (errors.length > 0) {
|
|
1875
|
+
rejected.push({
|
|
1876
|
+
workerId: result.assignment.id,
|
|
1877
|
+
reason: errors.join("; "),
|
|
1878
|
+
summary: result.proposal.summary
|
|
1879
|
+
});
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
accepted.push({ assignment: result.assignment, proposal: result.proposal });
|
|
1883
|
+
}
|
|
1884
|
+
return { accepted, rejected };
|
|
1885
|
+
}
|
|
1886
|
+
function validateOperations(assignment, operations) {
|
|
1887
|
+
const errors = [];
|
|
1888
|
+
for (const [index, operation] of operations.entries()) {
|
|
1889
|
+
const refs = operation.op === "create" ? [operation.parent] : operation.op === "move" ? [operation.target, operation.parent] : [operation.target];
|
|
1890
|
+
for (const ref2 of refs) {
|
|
1891
|
+
if (!isKnownParallelRef(ref2, assignment.knownRefKeys)) {
|
|
1892
|
+
errors.push(`Operation ${index} references an instance absent from worker evidence`);
|
|
1893
|
+
} else if (!isParallelRefWithinScopes(ref2, assignment.ownedScopes)) {
|
|
1894
|
+
errors.push(`Operation ${index} targets outside the worker owned scopes`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return errors;
|
|
1899
|
+
}
|
|
1900
|
+
function rejectProposalConflicts(accepted) {
|
|
1901
|
+
const finalAccepted = [];
|
|
1902
|
+
const rejected = [];
|
|
1903
|
+
const claims = [];
|
|
1904
|
+
for (const item of accepted) {
|
|
1905
|
+
const targets = item.proposal.operations.map(operationTarget);
|
|
1906
|
+
const conflict = claims.find(
|
|
1907
|
+
(claim) => targets.some(
|
|
1908
|
+
(target) => target.path && claim.paths.some((path4) => parallelPathsOverlap(target.path, path4)) || claim.refKeys.includes(target.key)
|
|
1909
|
+
)
|
|
1910
|
+
);
|
|
1911
|
+
if (conflict) {
|
|
1912
|
+
rejected.push({
|
|
1913
|
+
workerId: item.assignment.id,
|
|
1914
|
+
reason: `Proposal overlaps writes owned by ${conflict.workerId}`,
|
|
1915
|
+
summary: item.proposal.summary
|
|
1916
|
+
});
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
finalAccepted.push(item);
|
|
1920
|
+
claims.push({
|
|
1921
|
+
workerId: item.assignment.id,
|
|
1922
|
+
paths: targets.flatMap((target) => target.path ? [target.path] : []),
|
|
1923
|
+
refKeys: targets.map((target) => target.key)
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
return { accepted: finalAccepted, rejected };
|
|
1927
|
+
}
|
|
1928
|
+
async function runCoordinator(server, goal, accepted, rejected, constraints) {
|
|
1929
|
+
const response = await server.server.createMessage(
|
|
1930
|
+
{
|
|
1931
|
+
systemPrompt: [
|
|
1932
|
+
"You are the Dominus coordinator. Return strict JSON only.",
|
|
1933
|
+
"Workers are proposal-only. Select whole non-conflicting proposals and order their existing operations.",
|
|
1934
|
+
"Do not invent, edit, merge, or partially accept operations. The server will reject any unknown index.",
|
|
1935
|
+
"Prefer the smallest plan that satisfies the goal and preserves unrelated Studio state."
|
|
1936
|
+
].join(" "),
|
|
1937
|
+
messages: [
|
|
1938
|
+
{
|
|
1939
|
+
role: "user",
|
|
1940
|
+
content: {
|
|
1941
|
+
type: "text",
|
|
1942
|
+
text: JSON.stringify(
|
|
1943
|
+
{
|
|
1944
|
+
schema: {
|
|
1945
|
+
summary: "string",
|
|
1946
|
+
acceptedWorkerIds: ["worker-1"],
|
|
1947
|
+
rejected: [{ workerId: "worker-2", reason: "string" }],
|
|
1948
|
+
operationOrder: [{ workerId: "worker-1", operationIndex: 0 }],
|
|
1949
|
+
verificationChecks: ["string"]
|
|
1950
|
+
},
|
|
1951
|
+
goal,
|
|
1952
|
+
constraints: constraints ?? "",
|
|
1953
|
+
validProposals: accepted.map(({ assignment, proposal }) => ({
|
|
1954
|
+
workerId: assignment.id,
|
|
1955
|
+
summary: proposal.summary,
|
|
1956
|
+
evidence: proposal.evidence,
|
|
1957
|
+
operations: proposal.operations,
|
|
1958
|
+
risks: proposal.risks,
|
|
1959
|
+
verificationSuggestions: proposal.verificationSuggestions
|
|
1960
|
+
})),
|
|
1961
|
+
alreadyRejected: rejected
|
|
1962
|
+
},
|
|
1963
|
+
null,
|
|
1964
|
+
2
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
],
|
|
1969
|
+
maxTokens: 5e3,
|
|
1970
|
+
temperature: 0.1,
|
|
1971
|
+
modelPreferences: {
|
|
1972
|
+
intelligencePriority: 1,
|
|
1973
|
+
speedPriority: 0.2,
|
|
1974
|
+
costPriority: 0.1
|
|
1975
|
+
}
|
|
1536
1976
|
},
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1977
|
+
{ timeout: 12e4 }
|
|
1978
|
+
);
|
|
1979
|
+
return coordinatorDecisionSchema.parse(parseJson(extractSamplingText(response.content)));
|
|
1980
|
+
}
|
|
1981
|
+
function buildFinalPlan(decision, accepted, alreadyRejected) {
|
|
1982
|
+
const byId = new Map(accepted.map((item) => [item.assignment.id, item]));
|
|
1983
|
+
const acceptedIds = [...new Set(decision.acceptedWorkerIds)];
|
|
1984
|
+
for (const workerId of acceptedIds) {
|
|
1985
|
+
if (!byId.has(workerId)) throw new Error(`Coordinator accepted unknown worker ${workerId}`);
|
|
1986
|
+
}
|
|
1987
|
+
const operations = [];
|
|
1988
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1989
|
+
for (const entry of decision.operationOrder) {
|
|
1990
|
+
if (!acceptedIds.includes(entry.workerId)) {
|
|
1991
|
+
throw new Error(`Coordinator ordered an operation from unaccepted worker ${entry.workerId}`);
|
|
1992
|
+
}
|
|
1993
|
+
const proposal = byId.get(entry.workerId).proposal;
|
|
1994
|
+
const operation = proposal.operations[entry.operationIndex];
|
|
1995
|
+
if (!operation)
|
|
1996
|
+
throw new Error(
|
|
1997
|
+
`Coordinator selected missing operation ${entry.workerId}:${entry.operationIndex}`
|
|
1998
|
+
);
|
|
1999
|
+
const key = `${entry.workerId}:${entry.operationIndex}`;
|
|
2000
|
+
if (seen.has(key)) throw new Error(`Coordinator selected duplicate operation ${key}`);
|
|
2001
|
+
seen.add(key);
|
|
2002
|
+
operations.push(operation);
|
|
2003
|
+
}
|
|
2004
|
+
for (const workerId of acceptedIds) {
|
|
2005
|
+
const proposal = byId.get(workerId).proposal;
|
|
2006
|
+
for (let index = 0; index < proposal.operations.length; index++) {
|
|
2007
|
+
if (!seen.has(`${workerId}:${index}`)) {
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
`Coordinator partially accepted ${workerId}; operation ${index} was omitted`
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
1545
2012
|
}
|
|
2013
|
+
}
|
|
2014
|
+
const coordinatorReasons = new Map(decision.rejected.map((item) => [item.workerId, item.reason]));
|
|
2015
|
+
const rejected = [...alreadyRejected];
|
|
2016
|
+
for (const item of accepted) {
|
|
2017
|
+
if (!acceptedIds.includes(item.assignment.id)) {
|
|
2018
|
+
rejected.push({
|
|
2019
|
+
workerId: item.assignment.id,
|
|
2020
|
+
reason: coordinatorReasons.get(item.assignment.id) ?? "Coordinator did not accept this proposal",
|
|
2021
|
+
summary: item.proposal.summary
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return { acceptedWorkerIds: acceptedIds, rejected, operations };
|
|
2026
|
+
}
|
|
2027
|
+
async function verifyPlan(bridge, rootRef, originalRoot, operations) {
|
|
2028
|
+
const tree = await getTree(bridge, rootRef, 12, 5e3);
|
|
2029
|
+
const nodes = flattenTree(tree.roots);
|
|
2030
|
+
const paths = new Set(
|
|
2031
|
+
nodes.flatMap((node) => node.ref.pathSegments ? [pathKey(node.ref.pathSegments)] : [])
|
|
2032
|
+
);
|
|
2033
|
+
const knownPaths = collectInstancePaths(originalRoot);
|
|
2034
|
+
const expectedCreates = operations.flatMap((operation) => {
|
|
2035
|
+
if (operation.op !== "create") return [];
|
|
2036
|
+
const parentPath = resolveRefPath(operation.parent, knownPaths);
|
|
2037
|
+
return parentPath ? [pathKey([...parentPath, operation.name])] : [];
|
|
2038
|
+
});
|
|
2039
|
+
const missingCreates = expectedCreates.filter((path4) => !paths.has(path4));
|
|
2040
|
+
const inspectOperations = operations.filter((operation) => operation.op !== "create");
|
|
2041
|
+
const inspectFailures = [];
|
|
2042
|
+
const propertyMismatches = [];
|
|
2043
|
+
const moveMismatches = [];
|
|
2044
|
+
for (let offset = 0; offset < inspectOperations.length; offset += 20) {
|
|
2045
|
+
const batch = inspectOperations.slice(offset, offset + 20);
|
|
2046
|
+
const targets = batch.map((operation) => operation.target);
|
|
2047
|
+
const inspected = await requestStudio(bridge, CliMsg.V2_INSPECT, { targets, compact: true });
|
|
2048
|
+
const results = Array.isArray(inspected.results) ? inspected.results : [];
|
|
2049
|
+
batch.forEach((operation, index) => {
|
|
2050
|
+
const result = results[index];
|
|
2051
|
+
const targetLabel = refLabel(operation.target);
|
|
2052
|
+
if (!result || result.success === false) {
|
|
2053
|
+
inspectFailures.push(
|
|
2054
|
+
`${targetLabel}: ${String(result?.error ?? "missing inspection result")}`
|
|
2055
|
+
);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (operation.op === "setProperties") {
|
|
2059
|
+
const entries = Array.isArray(result.properties) ? result.properties : [];
|
|
2060
|
+
const actual = new Map(entries.map((entry) => [String(entry.name), entry.value]));
|
|
2061
|
+
for (const [property, expected] of Object.entries(operation.properties)) {
|
|
2062
|
+
const actualValue = actual.get(property);
|
|
2063
|
+
if (!actual.has(property) || !valuesEquivalent(expected, actualValue)) {
|
|
2064
|
+
propertyMismatches.push({
|
|
2065
|
+
target: targetLabel,
|
|
2066
|
+
property,
|
|
2067
|
+
expected,
|
|
2068
|
+
actual: actualValue
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
} else {
|
|
2073
|
+
const instanceId = operation.target.instanceId;
|
|
2074
|
+
const current = nodes.find((node) => instanceId && node.ref.instanceId === instanceId) ?? nodes.find((node) => samePath(node.ref.pathSegments, operation.target.pathSegments));
|
|
2075
|
+
const expectedParent = resolveRefPath(operation.parent, knownPaths);
|
|
2076
|
+
const actualPath = current?.ref.pathSegments;
|
|
2077
|
+
const actualParent = actualPath?.slice(0, -1);
|
|
2078
|
+
if (!expectedParent || !actualParent || !samePath(expectedParent, actualParent)) {
|
|
2079
|
+
moveMismatches.push({
|
|
2080
|
+
target: targetLabel,
|
|
2081
|
+
expectedParent: expectedParent ? expectedParent.join(".") : refLabel(operation.parent),
|
|
2082
|
+
actualParent: actualParent?.join(".")
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
passed: !tree.truncated && missingCreates.length === 0 && propertyMismatches.length === 0 && moveMismatches.length === 0 && inspectFailures.length === 0,
|
|
2090
|
+
treeNodeCount: tree.nodeCount,
|
|
2091
|
+
treeTruncated: tree.truncated,
|
|
2092
|
+
expectedCreates: expectedCreates.length,
|
|
2093
|
+
foundCreates: expectedCreates.length - missingCreates.length,
|
|
2094
|
+
missingCreates,
|
|
2095
|
+
propertyMismatches,
|
|
2096
|
+
moveMismatches,
|
|
2097
|
+
inspectFailures
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function findRepairOperations(operations, verification, originalRoot) {
|
|
2101
|
+
const knownPaths = collectInstancePaths(originalRoot);
|
|
2102
|
+
const missingCreates = new Set(verification.missingCreates);
|
|
2103
|
+
const mismatchedTargets = new Set(verification.propertyMismatches.map((item) => item.target));
|
|
2104
|
+
const mismatchedMoves = new Set(verification.moveMismatches.map((item) => item.target));
|
|
2105
|
+
return operations.filter((operation) => {
|
|
2106
|
+
if (operation.op === "create") {
|
|
2107
|
+
const parentPath = resolveRefPath(operation.parent, knownPaths);
|
|
2108
|
+
return Boolean(parentPath && missingCreates.has(pathKey([...parentPath, operation.name])));
|
|
2109
|
+
}
|
|
2110
|
+
if (operation.op === "setProperties") return mismatchedTargets.has(refLabel(operation.target));
|
|
2111
|
+
return mismatchedMoves.has(refLabel(operation.target));
|
|
1546
2112
|
});
|
|
1547
2113
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2114
|
+
function emptyDecision(rejected) {
|
|
2115
|
+
return {
|
|
2116
|
+
summary: rejected.length > 0 ? "No valid worker proposal was available to apply." : "Workers found no mutations necessary.",
|
|
2117
|
+
acceptedWorkerIds: [],
|
|
2118
|
+
rejected: [],
|
|
2119
|
+
operationOrder: [],
|
|
2120
|
+
verificationChecks: ["Read the requested root back and confirm no mutation was needed."]
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function publicAssignment(assignment) {
|
|
2124
|
+
return {
|
|
2125
|
+
id: assignment.id,
|
|
2126
|
+
role: assignment.role,
|
|
2127
|
+
objective: assignment.objective,
|
|
2128
|
+
ownedScopes: assignment.ownedScopes,
|
|
2129
|
+
acceptanceCriteria: assignment.acceptanceCriteria
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function publicWorkerResult(result) {
|
|
2133
|
+
return {
|
|
2134
|
+
workerId: result.assignment.id,
|
|
2135
|
+
summary: result.proposal?.summary,
|
|
2136
|
+
evidence: result.proposal?.evidence ?? [],
|
|
2137
|
+
proposedOperationCount: result.proposal?.operations.length ?? 0,
|
|
2138
|
+
risks: result.proposal?.risks ?? [],
|
|
2139
|
+
verificationSuggestions: result.proposal?.verificationSuggestions ?? [],
|
|
2140
|
+
error: result.error
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function collectRefKeys(node, output) {
|
|
2144
|
+
for (const key of parallelRefKeys(node.ref)) output.add(key);
|
|
2145
|
+
node.children?.forEach((child) => collectRefKeys(child, output));
|
|
2146
|
+
}
|
|
2147
|
+
function collectInstancePaths(root) {
|
|
2148
|
+
const result = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (const node of flattenTree([root])) {
|
|
2150
|
+
if (node.ref.pathSegments) {
|
|
2151
|
+
if (node.ref.instanceId) result.set(`id:${node.ref.instanceId}`, node.ref.pathSegments);
|
|
2152
|
+
result.set(`path:${pathKey(node.ref.pathSegments)}`, node.ref.pathSegments);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return result;
|
|
2156
|
+
}
|
|
2157
|
+
function flattenTree(roots) {
|
|
2158
|
+
const result = [];
|
|
2159
|
+
const visit = (node) => {
|
|
2160
|
+
result.push(node);
|
|
2161
|
+
node.children?.forEach(visit);
|
|
2162
|
+
};
|
|
2163
|
+
roots.forEach(visit);
|
|
2164
|
+
return result;
|
|
2165
|
+
}
|
|
2166
|
+
function isKnownParallelRef(ref2, known) {
|
|
2167
|
+
const keys = parallelRefKeys(ref2);
|
|
2168
|
+
return keys.length > 0 && keys.every((key) => known.has(key));
|
|
2169
|
+
}
|
|
2170
|
+
function isParallelRefWithinScopes(ref2, scopes) {
|
|
2171
|
+
if (ref2.pathSegments) {
|
|
2172
|
+
return scopes.some(
|
|
2173
|
+
(scope) => scope.pathSegments && parallelPathIsWithin(ref2.pathSegments, scope.pathSegments)
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
return scopes.some((scope) => Boolean(ref2.instanceId && scope.instanceId === ref2.instanceId));
|
|
2177
|
+
}
|
|
2178
|
+
function operationTarget(operation) {
|
|
2179
|
+
if (operation.op === "create") {
|
|
2180
|
+
const parentPath = operation.parent.pathSegments;
|
|
2181
|
+
return {
|
|
2182
|
+
key: `${refKey(operation.parent)}/create:${operation.name}`,
|
|
2183
|
+
path: parentPath ? [...parentPath, operation.name] : void 0
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
return { key: refKey(operation.target), path: operation.target.pathSegments };
|
|
2187
|
+
}
|
|
2188
|
+
function parallelRefKeys(ref2) {
|
|
2189
|
+
const keys = [];
|
|
2190
|
+
if (ref2.instanceId) keys.push(`id:${ref2.instanceId}`);
|
|
2191
|
+
if (ref2.pathSegments) keys.push(`path:${pathKey(ref2.pathSegments)}`);
|
|
2192
|
+
if (ref2.instanceId && ref2.pathSegments) {
|
|
2193
|
+
keys.push(`pair:${ref2.instanceId}:${pathKey(ref2.pathSegments)}`);
|
|
2194
|
+
}
|
|
2195
|
+
return keys;
|
|
2196
|
+
}
|
|
2197
|
+
function refKey(ref2) {
|
|
2198
|
+
return parallelRefKeys(ref2)[0] ?? "invalid-ref";
|
|
2199
|
+
}
|
|
2200
|
+
function refLabel(ref2) {
|
|
2201
|
+
return ref2.pathSegments?.join(".") ?? ref2.instanceId ?? "unknown-instance";
|
|
2202
|
+
}
|
|
2203
|
+
function resolveRefPath(ref2, known) {
|
|
2204
|
+
if (ref2.pathSegments) return ref2.pathSegments;
|
|
2205
|
+
return ref2.instanceId ? known.get(`id:${ref2.instanceId}`) : void 0;
|
|
2206
|
+
}
|
|
2207
|
+
function pathKey(path4) {
|
|
2208
|
+
return path4.join("\0");
|
|
2209
|
+
}
|
|
2210
|
+
function parallelPathIsWithin(target, scope) {
|
|
2211
|
+
return target.length >= scope.length && scope.every((segment, index) => target[index] === segment);
|
|
2212
|
+
}
|
|
2213
|
+
function parallelPathsOverlap(left, right) {
|
|
2214
|
+
return parallelPathIsWithin(left, right) || parallelPathIsWithin(right, left);
|
|
2215
|
+
}
|
|
2216
|
+
function samePath(left, right) {
|
|
2217
|
+
return Boolean(
|
|
2218
|
+
left && right && left.length === right.length && left.every((part, index) => part === right[index])
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
function parallelRootPathToSegments(path4) {
|
|
2222
|
+
const trimmed = path4.trim();
|
|
2223
|
+
const bracketSegments = [...trimmed.matchAll(/\["((?:\\.|[^"])*)"\]/g)].map(
|
|
2224
|
+
(match) => JSON.parse(`"${match[1]}"`)
|
|
2225
|
+
);
|
|
2226
|
+
if (bracketSegments.length > 0) return bracketSegments;
|
|
2227
|
+
const segments = trimmed.replace(/^game\.?/i, "").split(".").map((part) => part.trim()).filter(Boolean);
|
|
2228
|
+
if (segments.length === 0) throw new Error("rootPath must identify a Studio instance");
|
|
2229
|
+
return segments;
|
|
2230
|
+
}
|
|
2231
|
+
function parseJson(text) {
|
|
2232
|
+
const trimmed = text.trim();
|
|
2233
|
+
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1] ?? trimmed;
|
|
2234
|
+
try {
|
|
2235
|
+
return JSON.parse(fenced);
|
|
2236
|
+
} catch {
|
|
2237
|
+
const start = fenced.indexOf("{");
|
|
2238
|
+
const end = fenced.lastIndexOf("}");
|
|
2239
|
+
if (start >= 0 && end > start) return JSON.parse(fenced.slice(start, end + 1));
|
|
2240
|
+
throw new Error("Sampling response did not contain a JSON object");
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function extractSamplingText(content) {
|
|
2244
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
2245
|
+
const text = blocks.flatMap((block) => {
|
|
2246
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
2247
|
+
const value = block.text;
|
|
2248
|
+
return typeof value === "string" ? [value] : [];
|
|
2249
|
+
}
|
|
2250
|
+
return [];
|
|
2251
|
+
}).join("\n");
|
|
2252
|
+
if (!text) throw new Error("Sampling response did not contain text");
|
|
2253
|
+
return text;
|
|
2254
|
+
}
|
|
2255
|
+
function valuesEquivalent(expected, actual) {
|
|
2256
|
+
if (typeof expected === "string" && typeof actual === "string") {
|
|
2257
|
+
if (expected.startsWith("#") && actual.startsWith("#"))
|
|
2258
|
+
return expected.toUpperCase() === actual.toUpperCase();
|
|
2259
|
+
if (actual.startsWith("Enum.")) return actual.endsWith(`.${expected.split(".").at(-1)}`);
|
|
2260
|
+
return expected === actual;
|
|
2261
|
+
}
|
|
2262
|
+
if (typeof expected === "number" && typeof actual === "number")
|
|
2263
|
+
return Math.abs(expected - actual) < 1e-6;
|
|
2264
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
2265
|
+
return expected.length === actual.length && expected.every((item, index) => valuesEquivalent(item, actual[index]));
|
|
2266
|
+
}
|
|
2267
|
+
if (expected && actual && typeof expected === "object" && typeof actual === "object") {
|
|
2268
|
+
const left = expected;
|
|
2269
|
+
const right = actual;
|
|
2270
|
+
return Object.keys(left).every((key) => valuesEquivalent(left[key], right[key]));
|
|
2271
|
+
}
|
|
2272
|
+
return Object.is(expected, actual);
|
|
2273
|
+
}
|
|
2274
|
+
function emit(server, type, data) {
|
|
2275
|
+
void server.sendLoggingMessage({
|
|
2276
|
+
level: "info",
|
|
2277
|
+
logger: "dominus.multi_agent",
|
|
2278
|
+
data: { type, ...data }
|
|
2279
|
+
}).catch(() => void 0);
|
|
2280
|
+
}
|
|
2281
|
+
function toErrorMessage(error) {
|
|
2282
|
+
return error instanceof Error ? error.message : String(error);
|
|
2283
|
+
}
|
|
2284
|
+
var propertiesSchema2 = z.record(z.unknown()).refine(
|
|
2285
|
+
(properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
|
|
2286
|
+
"At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
|
|
1551
2287
|
);
|
|
1552
2288
|
var setPropertiesOperation = z.object({
|
|
1553
2289
|
op: z.literal("setProperties"),
|
|
1554
2290
|
target: instanceRefSchema,
|
|
1555
|
-
properties:
|
|
2291
|
+
properties: propertiesSchema2
|
|
1556
2292
|
});
|
|
1557
2293
|
var createOperation = z.object({
|
|
1558
2294
|
op: z.literal("create"),
|
|
1559
2295
|
parent: instanceRefSchema,
|
|
1560
2296
|
className: z.string().min(1).max(100),
|
|
1561
2297
|
name: z.string().min(1).max(256),
|
|
1562
|
-
properties:
|
|
2298
|
+
properties: propertiesSchema2.optional()
|
|
1563
2299
|
});
|
|
1564
2300
|
var moveOperation = z.object({
|
|
1565
2301
|
op: z.literal("move"),
|
|
@@ -1571,160 +2307,269 @@ var mutationOperation = z.discriminatedUnion("op", [
|
|
|
1571
2307
|
createOperation,
|
|
1572
2308
|
moveOperation
|
|
1573
2309
|
]);
|
|
1574
|
-
var uiNodeSchema = z.lazy(
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2310
|
+
var uiNodeSchema = z.lazy(
|
|
2311
|
+
() => z.object({
|
|
2312
|
+
ClassName: z.string().min(1).max(100),
|
|
2313
|
+
Name: z.string().min(1).max(256).optional(),
|
|
2314
|
+
properties: propertiesSchema2.optional(),
|
|
2315
|
+
Children: z.array(uiNodeSchema).max(1e3).optional()
|
|
2316
|
+
})
|
|
2317
|
+
);
|
|
1580
2318
|
function registerStudioTools(server, bridge) {
|
|
1581
|
-
server.registerTool(
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
2319
|
+
server.registerTool(
|
|
2320
|
+
"studio_get_tree",
|
|
2321
|
+
{
|
|
2322
|
+
title: "Inspect Studio Tree",
|
|
2323
|
+
description: "Read a deterministic Studio hierarchy and receive stable instance refs. Use refs from this result in later calls.",
|
|
2324
|
+
inputSchema: {
|
|
2325
|
+
root: instanceRefSchema.optional(),
|
|
2326
|
+
maxDepth: z.number().int().min(0).max(12).optional().default(3),
|
|
2327
|
+
maxNodes: z.number().int().min(1).max(5e3).optional().default(1e3)
|
|
2328
|
+
},
|
|
2329
|
+
outputSchema: toolOutputSchema,
|
|
2330
|
+
annotations: {
|
|
2331
|
+
readOnlyHint: true,
|
|
2332
|
+
destructiveHint: false,
|
|
2333
|
+
idempotentHint: true,
|
|
2334
|
+
openWorldHint: false
|
|
2335
|
+
}
|
|
1588
2336
|
},
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2337
|
+
({ root, maxDepth, maxNodes }) => callStudio(bridge, CliMsg.V2_GET_TREE, {
|
|
2338
|
+
root: root ? ref(root) : void 0,
|
|
2339
|
+
maxDepth,
|
|
2340
|
+
maxNodes
|
|
2341
|
+
})
|
|
2342
|
+
);
|
|
2343
|
+
server.registerTool(
|
|
2344
|
+
"studio_inspect",
|
|
2345
|
+
{
|
|
2346
|
+
title: "Inspect Studio Instances",
|
|
2347
|
+
description: "Read typed property values and immediate children for up to 20 instance refs. Returns writable metadata from live ReflectionService.",
|
|
2348
|
+
inputSchema: {
|
|
2349
|
+
targets: z.array(instanceRefSchema).min(1).max(20),
|
|
2350
|
+
compact: z.boolean().optional().default(true)
|
|
2351
|
+
},
|
|
2352
|
+
outputSchema: toolOutputSchema,
|
|
2353
|
+
annotations: {
|
|
2354
|
+
readOnlyHint: true,
|
|
2355
|
+
destructiveHint: false,
|
|
2356
|
+
idempotentHint: true,
|
|
2357
|
+
openWorldHint: false
|
|
2358
|
+
}
|
|
2359
|
+
},
|
|
2360
|
+
({ targets, compact }) => callStudio(bridge, CliMsg.V2_INSPECT, { targets, compact })
|
|
2361
|
+
);
|
|
2362
|
+
server.registerTool(
|
|
2363
|
+
"studio_get_selection",
|
|
2364
|
+
{
|
|
2365
|
+
title: "Get Studio Selection",
|
|
2366
|
+
description: "Return the user current Roblox Studio selection as stable instance refs.",
|
|
2367
|
+
inputSchema: {},
|
|
2368
|
+
outputSchema: toolOutputSchema,
|
|
2369
|
+
annotations: {
|
|
2370
|
+
readOnlyHint: true,
|
|
2371
|
+
destructiveHint: false,
|
|
2372
|
+
idempotentHint: true,
|
|
2373
|
+
openWorldHint: false
|
|
2374
|
+
}
|
|
2375
|
+
},
|
|
2376
|
+
() => callStudio(bridge, CliMsg.V2_GET_SELECTION, {})
|
|
2377
|
+
);
|
|
2378
|
+
server.registerTool(
|
|
2379
|
+
"studio_read_script",
|
|
2380
|
+
{
|
|
2381
|
+
title: "Read Studio Script",
|
|
2382
|
+
description: "Read a Script, LocalScript, or ModuleScript and receive its source plus a revision required for conflict-safe updates.",
|
|
2383
|
+
inputSchema: { target: instanceRefSchema },
|
|
2384
|
+
outputSchema: toolOutputSchema,
|
|
2385
|
+
annotations: {
|
|
2386
|
+
readOnlyHint: true,
|
|
2387
|
+
destructiveHint: false,
|
|
2388
|
+
idempotentHint: true,
|
|
2389
|
+
openWorldHint: false
|
|
2390
|
+
}
|
|
2391
|
+
},
|
|
2392
|
+
({ target }) => callStudio(bridge, CliMsg.V2_READ_SCRIPT, { target })
|
|
2393
|
+
);
|
|
2394
|
+
server.registerTool(
|
|
2395
|
+
"studio_update_script",
|
|
2396
|
+
{
|
|
2397
|
+
title: "Update Studio Script",
|
|
2398
|
+
description: "Replace script source only if expectedRevision still matches the editor. Read the script again after updating to verify it.",
|
|
2399
|
+
inputSchema: {
|
|
2400
|
+
target: instanceRefSchema,
|
|
2401
|
+
source: z.string().max(75e4),
|
|
2402
|
+
expectedRevision: z.string().regex(/^\d+:[0-9a-f]{8}$/)
|
|
2403
|
+
},
|
|
2404
|
+
outputSchema: toolOutputSchema,
|
|
2405
|
+
annotations: {
|
|
2406
|
+
readOnlyHint: false,
|
|
2407
|
+
destructiveHint: true,
|
|
2408
|
+
idempotentHint: true,
|
|
2409
|
+
openWorldHint: false
|
|
2410
|
+
}
|
|
1602
2411
|
},
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1619
|
-
}, ({ target }) => callStudio(bridge, CliMsg.V2_READ_SCRIPT, { target }));
|
|
1620
|
-
server.registerTool("studio_update_script", {
|
|
1621
|
-
title: "Update Studio Script",
|
|
1622
|
-
description: "Replace script source only if expectedRevision still matches the editor. Read the script again after updating to verify it.",
|
|
1623
|
-
inputSchema: {
|
|
1624
|
-
target: instanceRefSchema,
|
|
1625
|
-
source: z.string().max(75e4),
|
|
1626
|
-
expectedRevision: z.string().regex(/^\d+:[0-9a-f]{8}$/)
|
|
2412
|
+
({ target, source, expectedRevision }) => callStudio(bridge, CliMsg.V2_UPDATE_SCRIPT, { target, source, expectedRevision }, 6e4)
|
|
2413
|
+
);
|
|
2414
|
+
server.registerTool(
|
|
2415
|
+
"studio_apply",
|
|
2416
|
+
{
|
|
2417
|
+
title: "Apply Atomic Studio Mutations",
|
|
2418
|
+
description: "Atomically set properties, create instances, and move instances. Any failed operation cancels and undoes the entire batch.",
|
|
2419
|
+
inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
|
|
2420
|
+
outputSchema: toolOutputSchema,
|
|
2421
|
+
annotations: {
|
|
2422
|
+
readOnlyHint: false,
|
|
2423
|
+
destructiveHint: false,
|
|
2424
|
+
idempotentHint: false,
|
|
2425
|
+
openWorldHint: false
|
|
2426
|
+
}
|
|
1627
2427
|
},
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2428
|
+
({ operations }) => callStudio(bridge, CliMsg.V2_APPLY, { operations }, 6e4)
|
|
2429
|
+
);
|
|
2430
|
+
server.registerTool(
|
|
2431
|
+
"studio_delete_instances",
|
|
2432
|
+
{
|
|
2433
|
+
title: "Delete Studio Instances",
|
|
2434
|
+
description: "Destructively delete up to 50 instances in one undoable recording. Only call when the user explicitly requested deletion or cleanup.",
|
|
2435
|
+
inputSchema: {
|
|
2436
|
+
targets: z.array(instanceRefSchema).min(1).max(50),
|
|
2437
|
+
confirm: z.literal(true)
|
|
2438
|
+
},
|
|
2439
|
+
outputSchema: toolOutputSchema,
|
|
2440
|
+
annotations: {
|
|
2441
|
+
readOnlyHint: false,
|
|
2442
|
+
destructiveHint: true,
|
|
2443
|
+
idempotentHint: false,
|
|
2444
|
+
openWorldHint: false
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
2447
|
+
({ targets, confirm }) => callStudio(bridge, CliMsg.V2_DELETE, { targets, confirm }, 6e4)
|
|
2448
|
+
);
|
|
2449
|
+
server.registerTool(
|
|
2450
|
+
"studio_build_ui",
|
|
2451
|
+
{
|
|
2452
|
+
title: "Build Atomic Roblox UI",
|
|
2453
|
+
description: "Build and validate a declarative UI tree off-tree, then commit it in one undoable operation. Existing UI is replaced only when replaceExisting is true.",
|
|
2454
|
+
inputSchema: {
|
|
2455
|
+
parent: instanceRefSchema.optional(),
|
|
2456
|
+
tree: uiNodeSchema,
|
|
2457
|
+
replaceExisting: z.boolean().optional().default(false),
|
|
2458
|
+
maxDepth: z.number().int().min(1).max(50).optional().default(30),
|
|
2459
|
+
maxNodes: z.number().int().min(1).max(3e3).optional().default(1e3)
|
|
2460
|
+
},
|
|
2461
|
+
outputSchema: toolOutputSchema,
|
|
2462
|
+
annotations: {
|
|
2463
|
+
readOnlyHint: false,
|
|
2464
|
+
destructiveHint: true,
|
|
2465
|
+
idempotentHint: false,
|
|
2466
|
+
openWorldHint: false
|
|
2467
|
+
}
|
|
1649
2468
|
},
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2469
|
+
({ parent, tree, replaceExisting, maxDepth, maxNodes }) => callStudio(
|
|
2470
|
+
bridge,
|
|
2471
|
+
CliMsg.V2_BUILD_UI,
|
|
2472
|
+
{ parent, tree, replaceExisting, maxDepth, maxNodes },
|
|
2473
|
+
12e4
|
|
2474
|
+
)
|
|
2475
|
+
);
|
|
2476
|
+
server.registerTool(
|
|
2477
|
+
"studio_snapshot_ui",
|
|
2478
|
+
{
|
|
2479
|
+
title: "Snapshot Roblox UI",
|
|
2480
|
+
description: "Serialize a live UI subtree into typed, studio_build_ui-compatible data for fidelity checks and read-back verification.",
|
|
2481
|
+
inputSchema: {
|
|
2482
|
+
target: instanceRefSchema,
|
|
2483
|
+
maxDepth: z.number().int().min(0).max(50).optional().default(30)
|
|
2484
|
+
},
|
|
2485
|
+
outputSchema: toolOutputSchema,
|
|
2486
|
+
annotations: {
|
|
2487
|
+
readOnlyHint: true,
|
|
2488
|
+
destructiveHint: false,
|
|
2489
|
+
idempotentHint: true,
|
|
2490
|
+
openWorldHint: false
|
|
2491
|
+
}
|
|
1662
2492
|
},
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2493
|
+
({ target, maxDepth }) => callStudio(bridge, CliMsg.V2_SNAPSHOT_UI, { target, maxDepth }, 12e4)
|
|
2494
|
+
);
|
|
2495
|
+
server.registerTool(
|
|
2496
|
+
"studio_run_test",
|
|
2497
|
+
{
|
|
2498
|
+
title: "Run Roblox Studio Test",
|
|
2499
|
+
description: "Run a bounded StudioTestService run, play, or multiplayer scenario. The game test harness must call StudioTestService:EndTest with its result.",
|
|
2500
|
+
inputSchema: {
|
|
2501
|
+
mode: z.enum(["run", "play", "multiplayer"]).optional().default("run"),
|
|
2502
|
+
args: z.unknown().optional(),
|
|
2503
|
+
players: z.number().int().min(1).max(8).optional().default(1),
|
|
2504
|
+
timeoutMs: z.number().int().min(5e3).max(6e5).optional().default(12e4)
|
|
2505
|
+
},
|
|
2506
|
+
outputSchema: toolOutputSchema,
|
|
2507
|
+
annotations: {
|
|
2508
|
+
readOnlyHint: false,
|
|
2509
|
+
destructiveHint: false,
|
|
2510
|
+
idempotentHint: false,
|
|
2511
|
+
openWorldHint: false
|
|
2512
|
+
}
|
|
1677
2513
|
},
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2514
|
+
({ mode, args, players, timeoutMs }) => callStudio(
|
|
2515
|
+
bridge,
|
|
2516
|
+
CliMsg.V2_RUN_TEST,
|
|
2517
|
+
{ mode, args, players, timeoutMs },
|
|
2518
|
+
timeoutMs + 15e3
|
|
2519
|
+
)
|
|
2520
|
+
);
|
|
2521
|
+
server.registerTool(
|
|
2522
|
+
"studio_get_output",
|
|
2523
|
+
{
|
|
2524
|
+
title: "Read Studio Output",
|
|
2525
|
+
description: "Read the bounded Studio output buffer, optionally filtered by severity. Dominus transport messages are excluded.",
|
|
2526
|
+
inputSchema: {
|
|
2527
|
+
limit: z.number().int().min(1).max(500).optional().default(100),
|
|
2528
|
+
level: z.enum(["info", "warn", "error"]).optional()
|
|
2529
|
+
},
|
|
2530
|
+
outputSchema: toolOutputSchema,
|
|
2531
|
+
annotations: {
|
|
2532
|
+
readOnlyHint: true,
|
|
2533
|
+
destructiveHint: false,
|
|
2534
|
+
idempotentHint: true,
|
|
2535
|
+
openWorldHint: false
|
|
2536
|
+
}
|
|
1689
2537
|
},
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2538
|
+
({ limit, level }) => callStudio(bridge, CliMsg.V2_GET_OUTPUT, { limit, level })
|
|
2539
|
+
);
|
|
2540
|
+
server.registerTool(
|
|
2541
|
+
"studio_get_reflection",
|
|
2542
|
+
{
|
|
2543
|
+
title: "Read Live Roblox Reflection",
|
|
2544
|
+
description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata.",
|
|
2545
|
+
inputSchema: { className: z.string().min(1).max(100) },
|
|
2546
|
+
outputSchema: toolOutputSchema,
|
|
2547
|
+
annotations: {
|
|
2548
|
+
readOnlyHint: true,
|
|
2549
|
+
destructiveHint: false,
|
|
2550
|
+
idempotentHint: true,
|
|
2551
|
+
openWorldHint: false
|
|
2552
|
+
}
|
|
1704
2553
|
},
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}, ({ limit, level }) => callStudio(bridge, CliMsg.V2_GET_OUTPUT, { limit, level }));
|
|
1708
|
-
server.registerTool("studio_get_reflection", {
|
|
1709
|
-
title: "Read Live Roblox Reflection",
|
|
1710
|
-
description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata.",
|
|
1711
|
-
inputSchema: { className: z.string().min(1).max(100) },
|
|
1712
|
-
outputSchema: toolOutputSchema,
|
|
1713
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1714
|
-
}, ({ className }) => callStudio(bridge, CliMsg.V2_GET_REFLECTION, { className }));
|
|
2554
|
+
({ className }) => callStudio(bridge, CliMsg.V2_GET_REFLECTION, { className })
|
|
2555
|
+
);
|
|
1715
2556
|
}
|
|
1716
2557
|
|
|
1717
2558
|
// src/mcp/server.ts
|
|
1718
|
-
var VERSION = "2.
|
|
2559
|
+
var VERSION = "2.2.0";
|
|
1719
2560
|
function createDominusMcpServer(bridge) {
|
|
1720
|
-
const server = new McpServer(
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2561
|
+
const server = new McpServer(
|
|
2562
|
+
{
|
|
2563
|
+
name: "dominus-2",
|
|
2564
|
+
version: VERSION
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
instructions: DOMINUS_MCP_INSTRUCTIONS
|
|
2568
|
+
}
|
|
2569
|
+
);
|
|
1726
2570
|
registerConnectionTools(server, bridge);
|
|
1727
2571
|
registerStudioTools(server, bridge);
|
|
2572
|
+
registerParallelTaskTool(server, bridge);
|
|
1728
2573
|
registerDocsTools(server);
|
|
1729
2574
|
registerDominusResources(server, bridge);
|
|
1730
2575
|
return server;
|