dominus-cli 2.1.1 → 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/dist/mcp.js CHANGED
@@ -76,6 +76,32 @@ var ControllerMsg = {
76
76
  SET_ACTIVE_CONNECTION: "controller:set_active_connection"
77
77
  };
78
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",
79
105
  V2_GET_TREE: "studio:v2:get_tree",
80
106
  V2_INSPECT: "studio:v2:inspect",
81
107
  V2_GET_SELECTION: "studio:v2:get_selection",
@@ -863,6 +889,8 @@ Required workflow for Studio changes:
863
889
  6. Read the affected instances back and compare exact values, hierarchy, and source revisions.
864
890
  7. Run an appropriate Studio test when behavior changed. Report verification evidence and any remaining uncertainty.
865
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
+
866
894
  Safety rules:
867
895
  - Never invent instance IDs or Roblox API members.
868
896
  - Do not retry a failed write unchanged. Inspect the error and current state first.
@@ -873,30 +901,47 @@ Safety rules:
873
901
 
874
902
  // src/mcp/resources.ts
875
903
  function registerDominusResources(server, bridge) {
876
- server.registerResource("dominus-status", "dominus://status", {
877
- title: "Dominus 2 Studio Status",
878
- description: "Current authenticated Roblox Studio sessions and selected target.",
879
- mimeType: "application/json"
880
- }, async (uri) => ({
881
- contents: [{
882
- uri: uri.href,
883
- mimeType: "application/json",
884
- text: JSON.stringify({
885
- protocolVersion: 2,
886
- activeConnectionId: bridge.getActiveConnectionId(),
887
- connections: bridge.listConnections()
888
- }, null, 2)
889
- }]
890
- }));
891
- server.registerPrompt("dominus-workflow", {
892
- title: "Dominus 2 Engineering Workflow",
893
- description: "Inspect, plan, apply, verify, and test a Roblox Studio change safely."
894
- }, async () => ({
895
- messages: [{
896
- role: "user",
897
- content: { type: "text", text: DOMINUS_MCP_INSTRUCTIONS }
898
- }]
899
- }));
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
+ );
900
945
  }
901
946
 
902
947
  // src/mcp/result.ts
@@ -930,55 +975,77 @@ var toolOutputSchema = {
930
975
  };
931
976
  async function callStudio(bridge, command, payload, timeoutMs = 15e3) {
932
977
  try {
933
- const response = await bridge.sendRequest(command, payload, timeoutMs);
934
- const result = response.payload;
935
- if (!result || typeof result !== "object") return failure("Studio returned an invalid response");
936
- if (result.success === false || typeof result.error === "string") {
937
- return failure(result.error ?? "Studio command failed", result);
938
- }
978
+ const result = await requestStudio(bridge, command, payload, timeoutMs);
939
979
  return success(result);
940
980
  } catch (err) {
941
981
  return failure(err);
942
982
  }
943
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
+ }
944
993
  function ref(value) {
945
994
  return value;
946
995
  }
947
996
 
948
997
  // src/mcp/tools/connections.ts
949
998
  function registerConnectionTools(server, bridge) {
950
- server.registerTool("dominus_status", {
951
- title: "Dominus Studio Status",
952
- description: "List authenticated Roblox Studio sessions, the active target, and bridge health. Call this before Studio work.",
953
- inputSchema: {},
954
- outputSchema: toolOutputSchema,
955
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
956
- }, async () => {
957
- try {
958
- return success({
959
- protocolVersion: 2,
960
- port: bridge.getPort(),
961
- activeConnectionId: bridge.getActiveConnectionId(),
962
- connections: bridge.listConnections()
963
- });
964
- } catch (err) {
965
- return failure(err);
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
+ }
1012
+ },
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
+ }
966
1024
  }
967
- });
968
- server.registerTool("dominus_select_studio", {
969
- title: "Select Studio Session",
970
- description: "Choose the exact Studio connection targeted by subsequent tools. Uses connectionId, not placeId, so duplicate windows are safe.",
971
- inputSchema: { connectionId: z.string().uuid().nullable() },
972
- outputSchema: toolOutputSchema,
973
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
974
- }, async ({ connectionId }) => {
975
- try {
976
- await bridge.setActiveConnectionId(connectionId);
977
- return success({ activeConnectionId: bridge.getActiveConnectionId() });
978
- } catch (err) {
979
- return failure(err);
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
+ }
980
1047
  }
981
- });
1048
+ );
982
1049
  }
983
1050
  var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine";
984
1051
  var GITHUB_TREE_URL = "https://api.github.com/repos/Roblox/creator-docs/git/trees/main?recursive=1";
@@ -1401,57 +1468,834 @@ function escapeRegExp(value) {
1401
1468
  // src/mcp/tools/docs.ts
1402
1469
  var kindSchema = z.enum(["any", "class", "datatype", "enum", "global", "library"]);
1403
1470
  function registerDocsTools(server) {
1404
- server.registerTool("roblox_search_api", {
1405
- title: "Search Roblox API",
1406
- description: "Search the official Roblox Creator Docs engine API mirror by exact or partial class, datatype, enum, global, or library name.",
1407
- inputSchema: {
1408
- query: z.string().min(1).max(100),
1409
- kind: kindSchema.optional().default("any"),
1410
- limit: z.number().int().min(1).max(25).optional().default(10)
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
+ }
1411
1488
  },
1412
- outputSchema: toolOutputSchema,
1413
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1414
- }, async ({ query, kind, limit }) => {
1415
- try {
1416
- return success({ results: await searchRobloxApi(query, { kind, limit }) });
1417
- } catch (err) {
1418
- 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
+ }
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
+ }
1419
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
+ };
1420
1764
  });
1421
- server.registerTool("roblox_get_api", {
1422
- title: "Get Roblox API Reference",
1423
- description: "Read an official Roblox engine API reference including members, types, security, thread safety, inheritance, and source URL.",
1424
- inputSchema: {
1425
- name: z.string().min(1).max(100),
1426
- kind: kindSchema.optional().default("any"),
1427
- includeInherited: z.boolean().optional().default(true)
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
+ }
1428
1976
  },
1429
- outputSchema: toolOutputSchema,
1430
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1431
- }, async ({ name, kind, includeInherited }) => {
1432
- try {
1433
- const reference = await getRobloxApiReference(name, { kind, includeInherited });
1434
- return success({ reference });
1435
- } catch (err) {
1436
- return failure(err);
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
+ }
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])));
1437
2109
  }
2110
+ if (operation.op === "setProperties") return mismatchedTargets.has(refLabel(operation.target));
2111
+ return mismatchedMoves.has(refLabel(operation.target));
1438
2112
  });
1439
2113
  }
1440
- var propertiesSchema = z.record(z.unknown()).refine(
1441
- (properties) => Object.keys(properties).length <= 100,
1442
- "At most 100 properties are allowed per operation"
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"
1443
2287
  );
1444
2288
  var setPropertiesOperation = z.object({
1445
2289
  op: z.literal("setProperties"),
1446
2290
  target: instanceRefSchema,
1447
- properties: propertiesSchema
2291
+ properties: propertiesSchema2
1448
2292
  });
1449
2293
  var createOperation = z.object({
1450
2294
  op: z.literal("create"),
1451
2295
  parent: instanceRefSchema,
1452
2296
  className: z.string().min(1).max(100),
1453
2297
  name: z.string().min(1).max(256),
1454
- properties: propertiesSchema.optional()
2298
+ properties: propertiesSchema2.optional()
1455
2299
  });
1456
2300
  var moveOperation = z.object({
1457
2301
  op: z.literal("move"),
@@ -1463,160 +2307,269 @@ var mutationOperation = z.discriminatedUnion("op", [
1463
2307
  createOperation,
1464
2308
  moveOperation
1465
2309
  ]);
1466
- var uiNodeSchema = z.lazy(() => z.object({
1467
- ClassName: z.string().min(1).max(100),
1468
- Name: z.string().min(1).max(256).optional(),
1469
- properties: propertiesSchema.optional(),
1470
- Children: z.array(uiNodeSchema).max(1e3).optional()
1471
- }));
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
+ );
1472
2318
  function registerStudioTools(server, bridge) {
1473
- server.registerTool("studio_get_tree", {
1474
- title: "Inspect Studio Tree",
1475
- description: "Read a deterministic Studio hierarchy and receive stable instance refs. Use refs from this result in later calls.",
1476
- inputSchema: {
1477
- root: instanceRefSchema.optional(),
1478
- maxDepth: z.number().int().min(0).max(12).optional().default(3),
1479
- maxNodes: z.number().int().min(1).max(5e3).optional().default(1e3)
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
+ }
1480
2336
  },
1481
- outputSchema: toolOutputSchema,
1482
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1483
- }, ({ root, maxDepth, maxNodes }) => callStudio(bridge, CliMsg.V2_GET_TREE, {
1484
- root: root ? ref(root) : void 0,
1485
- maxDepth,
1486
- maxNodes
1487
- }));
1488
- server.registerTool("studio_inspect", {
1489
- title: "Inspect Studio Instances",
1490
- description: "Read typed property values and immediate children for up to 20 instance refs. Returns writable metadata from live ReflectionService.",
1491
- inputSchema: {
1492
- targets: z.array(instanceRefSchema).min(1).max(20),
1493
- compact: z.boolean().optional().default(true)
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
+ }
2411
+ },
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
+ }
1494
2427
  },
1495
- outputSchema: toolOutputSchema,
1496
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1497
- }, ({ targets, compact }) => callStudio(bridge, CliMsg.V2_INSPECT, { targets, compact }));
1498
- server.registerTool("studio_get_selection", {
1499
- title: "Get Studio Selection",
1500
- description: "Return the user current Roblox Studio selection as stable instance refs.",
1501
- inputSchema: {},
1502
- outputSchema: toolOutputSchema,
1503
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1504
- }, () => callStudio(bridge, CliMsg.V2_GET_SELECTION, {}));
1505
- server.registerTool("studio_read_script", {
1506
- title: "Read Studio Script",
1507
- description: "Read a Script, LocalScript, or ModuleScript and receive its source plus a revision required for conflict-safe updates.",
1508
- inputSchema: { target: instanceRefSchema },
1509
- outputSchema: toolOutputSchema,
1510
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1511
- }, ({ target }) => callStudio(bridge, CliMsg.V2_READ_SCRIPT, { target }));
1512
- server.registerTool("studio_update_script", {
1513
- title: "Update Studio Script",
1514
- description: "Replace script source only if expectedRevision still matches the editor. Read the script again after updating to verify it.",
1515
- inputSchema: {
1516
- target: instanceRefSchema,
1517
- source: z.string().max(75e4),
1518
- expectedRevision: z.string().regex(/^\d+:[0-9a-f]{8}$/)
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
+ }
1519
2446
  },
1520
- outputSchema: toolOutputSchema,
1521
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1522
- }, ({ target, source, expectedRevision }) => callStudio(
1523
- bridge,
1524
- CliMsg.V2_UPDATE_SCRIPT,
1525
- { target, source, expectedRevision },
1526
- 6e4
1527
- ));
1528
- server.registerTool("studio_apply", {
1529
- title: "Apply Atomic Studio Mutations",
1530
- description: "Atomically set properties, create instances, and move instances. Any failed operation cancels and undoes the entire batch.",
1531
- inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
1532
- outputSchema: toolOutputSchema,
1533
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1534
- }, ({ operations }) => callStudio(bridge, CliMsg.V2_APPLY, { operations }, 6e4));
1535
- server.registerTool("studio_delete_instances", {
1536
- title: "Delete Studio Instances",
1537
- description: "Destructively delete up to 50 instances in one undoable recording. Only call when the user explicitly requested deletion or cleanup.",
1538
- inputSchema: {
1539
- targets: z.array(instanceRefSchema).min(1).max(50),
1540
- confirm: z.literal(true)
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
+ }
1541
2468
  },
1542
- outputSchema: toolOutputSchema,
1543
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
1544
- }, ({ targets, confirm }) => callStudio(bridge, CliMsg.V2_DELETE, { targets, confirm }, 6e4));
1545
- server.registerTool("studio_build_ui", {
1546
- title: "Build Atomic Roblox UI",
1547
- 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.",
1548
- inputSchema: {
1549
- parent: instanceRefSchema.optional(),
1550
- tree: uiNodeSchema,
1551
- replaceExisting: z.boolean().optional().default(false),
1552
- maxDepth: z.number().int().min(1).max(50).optional().default(30),
1553
- maxNodes: z.number().int().min(1).max(3e3).optional().default(1e3)
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
+ }
1554
2492
  },
1555
- outputSchema: toolOutputSchema,
1556
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
1557
- }, ({ parent, tree, replaceExisting, maxDepth, maxNodes }) => callStudio(
1558
- bridge,
1559
- CliMsg.V2_BUILD_UI,
1560
- { parent, tree, replaceExisting, maxDepth, maxNodes },
1561
- 12e4
1562
- ));
1563
- server.registerTool("studio_snapshot_ui", {
1564
- title: "Snapshot Roblox UI",
1565
- description: "Serialize a live UI subtree into typed, studio_build_ui-compatible data for fidelity checks and read-back verification.",
1566
- inputSchema: {
1567
- target: instanceRefSchema,
1568
- maxDepth: z.number().int().min(0).max(50).optional().default(30)
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
+ }
1569
2513
  },
1570
- outputSchema: toolOutputSchema,
1571
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1572
- }, ({ target, maxDepth }) => callStudio(bridge, CliMsg.V2_SNAPSHOT_UI, { target, maxDepth }, 12e4));
1573
- server.registerTool("studio_run_test", {
1574
- title: "Run Roblox Studio Test",
1575
- description: "Run a bounded StudioTestService run, play, or multiplayer scenario. The game test harness must call StudioTestService:EndTest with its result.",
1576
- inputSchema: {
1577
- mode: z.enum(["run", "play", "multiplayer"]).optional().default("run"),
1578
- args: z.unknown().optional(),
1579
- players: z.number().int().min(1).max(8).optional().default(1),
1580
- timeoutMs: z.number().int().min(5e3).max(6e5).optional().default(12e4)
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
+ }
1581
2537
  },
1582
- outputSchema: toolOutputSchema,
1583
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1584
- }, ({ mode, args, players, timeoutMs }) => callStudio(
1585
- bridge,
1586
- CliMsg.V2_RUN_TEST,
1587
- { mode, args, players, timeoutMs },
1588
- timeoutMs + 15e3
1589
- ));
1590
- server.registerTool("studio_get_output", {
1591
- title: "Read Studio Output",
1592
- description: "Read the bounded Studio output buffer, optionally filtered by severity. Dominus transport messages are excluded.",
1593
- inputSchema: {
1594
- limit: z.number().int().min(1).max(500).optional().default(100),
1595
- level: z.enum(["info", "warn", "error"]).optional()
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
+ }
1596
2553
  },
1597
- outputSchema: toolOutputSchema,
1598
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1599
- }, ({ limit, level }) => callStudio(bridge, CliMsg.V2_GET_OUTPUT, { limit, level }));
1600
- server.registerTool("studio_get_reflection", {
1601
- title: "Read Live Roblox Reflection",
1602
- description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata.",
1603
- inputSchema: { className: z.string().min(1).max(100) },
1604
- outputSchema: toolOutputSchema,
1605
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1606
- }, ({ className }) => callStudio(bridge, CliMsg.V2_GET_REFLECTION, { className }));
2554
+ ({ className }) => callStudio(bridge, CliMsg.V2_GET_REFLECTION, { className })
2555
+ );
1607
2556
  }
1608
2557
 
1609
2558
  // src/mcp/server.ts
1610
- var VERSION = "2.1.1";
2559
+ var VERSION = "2.2.0";
1611
2560
  function createDominusMcpServer(bridge) {
1612
- const server = new McpServer({
1613
- name: "dominus-2",
1614
- version: VERSION
1615
- }, {
1616
- instructions: DOMINUS_MCP_INSTRUCTIONS
1617
- });
2561
+ const server = new McpServer(
2562
+ {
2563
+ name: "dominus-2",
2564
+ version: VERSION
2565
+ },
2566
+ {
2567
+ instructions: DOMINUS_MCP_INSTRUCTIONS
2568
+ }
2569
+ );
1618
2570
  registerConnectionTools(server, bridge);
1619
2571
  registerStudioTools(server, bridge);
2572
+ registerParallelTaskTool(server, bridge);
1620
2573
  registerDocsTools(server);
1621
2574
  registerDominusResources(server, bridge);
1622
2575
  return server;