@aryacode/dsh-engineering 0.4.7 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +40 -26
  3. package/cordis.patch.yml +7 -6
  4. package/entries/config.js +62 -0
  5. package/entries/ftview.js +5 -4
  6. package/entries/l5x.js +47 -15
  7. package/entries/mwi.js +26 -6
  8. package/entries/optix.js +105 -0
  9. package/lib/client.js +335 -6
  10. package/package.json +11 -10
  11. package/vendor/ftview/SKILL.md +17 -0
  12. package/vendor/l5x/SKILL.md +17 -0
  13. package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
  14. package/vendor/mwi/README.md +41 -18
  15. package/vendor/mwi/SKILL.md +39 -20
  16. package/vendor/mwi/bridge/mw_bridge.ps1 +2768 -2763
  17. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
  18. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
  19. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  21. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  22. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
  23. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
  24. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
  25. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
  26. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
  27. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
  28. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
  29. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
  30. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
  31. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +134 -130
  32. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
  33. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
  34. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
  35. package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
  36. package/vendor/mwi/lib/core/code-engine.js +18 -1
  37. package/vendor/mwi/lib/edit.d.ts +1 -1
  38. package/vendor/mwi/lib/edit.js +1 -1
  39. package/vendor/mwi/lib/read/service.js +8 -1
  40. package/vendor/mwi/lib/session/backup.d.ts +72 -0
  41. package/vendor/mwi/lib/session/backup.js +257 -0
  42. package/vendor/mwi/lib/session/project.d.ts +4 -1
  43. package/vendor/mwi/lib/session/project.js +13 -2
  44. package/vendor/mwi/lib/session/service.d.ts +13 -6
  45. package/vendor/mwi/lib/session/service.js +127 -36
  46. package/vendor/mwi/lib/session/workspace.d.ts +10 -0
  47. package/vendor/mwi/lib/session/workspace.js +53 -3
  48. package/vendor/mwi/lib/skill.d.ts +2 -2
  49. package/vendor/mwi/lib/skill.js +2 -2
  50. package/vendor/mwi/lib/tools/edit.d.ts +3 -3
  51. package/vendor/mwi/lib/tools/edit.js +14 -15
  52. package/vendor/mwi/lib/tools/read.js +1 -1
  53. package/vendor/mwi/lib/tools/session.js +13 -9
  54. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
  55. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
  56. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
  57. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
  58. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  59. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
  60. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
  61. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
  62. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
  63. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
  64. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  65. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  66. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
  67. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
  68. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  69. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
  70. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
  71. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
  72. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
  73. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
  74. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
  75. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  76. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
  77. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
  78. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  79. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
  80. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
  81. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
  82. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
  83. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
  84. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
  85. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  86. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
  87. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
  88. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
  89. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
  90. package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
  91. package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  92. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
  93. package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
  94. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
  95. package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
  96. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
  97. package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  98. package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  99. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
  100. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
  101. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  102. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
  103. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
  104. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
  105. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
  106. package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
  107. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
  108. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  109. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
  110. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
  111. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
  112. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
  113. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
  114. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
  115. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
  116. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
  117. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
  118. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  119. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
  120. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
  121. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
  122. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
  123. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
  124. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
  125. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
  126. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
  127. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
  128. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
  129. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
  130. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
  131. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
  132. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
  133. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
  134. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
  135. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
  136. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
  137. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
  138. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
  139. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
  140. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  141. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
  142. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
  143. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
  144. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  145. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  146. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  147. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  148. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  149. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  150. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  151. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  152. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  153. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  154. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  155. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  156. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  157. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  158. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  159. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  160. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  161. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  162. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
@@ -24,6 +24,7 @@ import { CodeEngine } from "../core/code-engine.js";
24
24
  import { MwiError } from "../core/errors.js";
25
25
  import { PowerShellBridgeTransport } from "../core/ps-bridge.js";
26
26
  import { StageService } from "./stage.js";
27
+ import { ensureBackup, findBackup, listBackups } from "./backup.js";
27
28
  import { compareVariables } from "../edit/compare.js";
28
29
  import { ReadService } from "../read/service.js";
29
30
  import { stageRootOf, workspaceOf, isInside } from "./workspace.js";
@@ -33,6 +34,25 @@ export const BRIDGE_DIR = fileURLToPath(new URL('../../bridge/', import.meta.url
33
34
  export const CODE_DIR = fileURLToPath(new URL('../../code/', import.meta.url));
34
35
  /** Vendored proven mutation helpers. */
35
36
  export const LEGACY_DIR = fileURLToPath(new URL('../../legacy/', import.meta.url));
37
+ /** A stage-shaped view of a real, backed-up project. */
38
+ function directRecord(backup) {
39
+ return {
40
+ name: backup.name,
41
+ source: backup.mwt,
42
+ source_directory: backup.directory,
43
+ workspace: backup.workspace,
44
+ staged_mwt: backup.mwt,
45
+ staged_directory: backup.directory,
46
+ bound_to: backup.directory,
47
+ previous_embedded_path: null,
48
+ staged_at: backup.created_at,
49
+ };
50
+ }
51
+ /** Whether two paths name the same project, with or without the `.mwt` suffix. */
52
+ function sameProject(left, right) {
53
+ const bare = (path) => resolve(path).toLowerCase().replace(/\.mwt$/u, '');
54
+ return bare(left) === bare(right);
55
+ }
36
56
  /** Hash every file in a native package, refusing links and oversized trees. */
37
57
  function nativePackageManifest(directory, workspace) {
38
58
  const files = [];
@@ -92,6 +112,19 @@ export class PowerShellSessionRuntime {
92
112
  async ideState() {
93
113
  return await this.bridge.run('ide_state', { include_window_state: true });
94
114
  }
115
+ /** @returns The open project path, or undefined when no IDE or no project is open. */
116
+ async activeProject() {
117
+ let status;
118
+ try {
119
+ status = await this.status();
120
+ }
121
+ catch (error) {
122
+ if (error instanceof MwiError && error.code === 'IDE_NOT_RUNNING')
123
+ return undefined;
124
+ throw error;
125
+ }
126
+ return status.is_project_open === true && typeof status.active_project === 'string' ? status.active_project : undefined;
127
+ }
95
128
  /** @returns The written stage record. */
96
129
  async stage(source, options = {}) {
97
130
  return await this.stages.stage(source, {
@@ -116,11 +149,40 @@ export class PowerShellSessionRuntime {
116
149
  },
117
150
  });
118
151
  }
152
+ /** @returns The verified pre-edit backup record. */
153
+ attach(path) {
154
+ return ensureBackup(this.workspace, path);
155
+ }
119
156
  /** @returns The bridge's open result. */
120
157
  async open(path, options) {
121
- const resolved = resolve(path);
122
- if (!isInside(stageRootOf(this.workspace), resolved) || !existsSync(resolved)) {
123
- throw new MwiError('NOT_STAGED', `${resolved} is not a staged project.`, 'Stage the workspace project with mw_project_stage, then open the staged .mwt.');
158
+ const resolved = resolve(this.workspace, path);
159
+ if (!isInside(stageRootOf(this.workspace), resolved)) {
160
+ const record = this.attach(resolved);
161
+ // An already-open project is attached, not reopened: OpenProject reloads the
162
+ // files, and a reload over unsaved editor changes is data loss the backup
163
+ // cannot undo. The verified identity is the whole job in that case.
164
+ const active = await this.activeProject();
165
+ if (active !== undefined && sameProject(active, record.mwt)) {
166
+ return {
167
+ requested: record.mwt,
168
+ is_project_open: true,
169
+ active_project: active,
170
+ matches_request: true,
171
+ already_open: true,
172
+ backup: record.backup,
173
+ backup_manifest: record.manifest,
174
+ };
175
+ }
176
+ const binding = await this.engine.run('check_mwt', { project: record.mwt });
177
+ return await this.bridge.run('open', {
178
+ path: record.mwt,
179
+ wrapper_sha256: binding.wrapper_sha256,
180
+ user_approved: options.userApproved === true,
181
+ ...(options.expectedProject === undefined ? {} : { expected_project: options.expectedProject }),
182
+ });
183
+ }
184
+ if (!existsSync(resolved)) {
185
+ throw new MwiError('NOT_STAGED', `${resolved} is not a staged project.`, 'Pass a workspace .mwt, or stage the workspace project with mw_project_stage.');
124
186
  }
125
187
  const name = resolved.replace(/\\/gu, '/').split('/').pop()?.replace(/\.mwt$/iu, '') ?? '';
126
188
  if (this.stages.identityOf(name) === undefined) {
@@ -136,11 +198,11 @@ export class PowerShellSessionRuntime {
136
198
  }
137
199
  /** @returns The bridge's start result. */
138
200
  async start(project) {
139
- const staged = project === undefined ? this.singleStage() : this.stageMwt(project);
140
- const binding = await this.engine.run('check_mwt', { project: staged });
201
+ const target = project === undefined ? this.singleStage() : this.projectMwt(project);
202
+ const binding = await this.engine.run('check_mwt', { project: target });
141
203
  return await this.bridge.run('start_ide', {
142
204
  include_trial_method: true,
143
- path: staged,
205
+ path: target,
144
206
  wrapper_sha256: binding.wrapper_sha256,
145
207
  });
146
208
  }
@@ -150,9 +212,9 @@ export class PowerShellSessionRuntime {
150
212
  if (status.is_project_open !== true || typeof status.active_project !== 'string') {
151
213
  throw new MwiError('NO_PROJECT', 'No project is open in the IDE.', 'Open a staged project first with mw_project_open.');
152
214
  }
153
- const record = this.stages.stagedAt(status.active_project);
215
+ const record = this.stagedAt(status.active_project);
154
216
  if (record === undefined) {
155
- throw new MwiError('NOT_STAGED', `The open project is not a proven stage: ${status.active_project}.`, 'Open a staged project before navigating worksheets.');
217
+ throw new MwiError('NOT_ATTACHED', `The open project is not proven: ${status.active_project}.`, 'Open the workspace project with mw_project_open first.');
156
218
  }
157
219
  const target = await this.engine.run('worksheet_target', {
158
220
  project: record.staged_directory,
@@ -175,9 +237,13 @@ export class PowerShellSessionRuntime {
175
237
  ...(options.expectedProject === undefined ? {} : { expected_project: options.expectedProject }),
176
238
  });
177
239
  }
178
- /** @returns The stage record for a path. */
240
+ /** @returns The stage record for a path, when it is staged or directly backed up. */
179
241
  stagedAt(path) {
180
- return this.stages.stagedAt(path);
242
+ const staged = this.stages.stagedAt(path);
243
+ if (staged !== undefined)
244
+ return staged;
245
+ const backup = findBackup(this.workspace, path);
246
+ return backup === undefined ? undefined : directRecord(backup);
181
247
  }
182
248
  /** @returns The bridge's compile result. */
183
249
  async compile(mode) {
@@ -466,10 +532,20 @@ export class PowerShellSessionRuntime {
466
532
  mkdirSync(evidenceDirectory, { recursive: true });
467
533
  const evidence = join(evidenceDirectory, `native-code-${randomUUID()}.json`);
468
534
  const args = { project, pou: input.pou, baseline_saved: true };
535
+ let expectedHash = input.expectedHash;
536
+ if (input.changes !== undefined && expectedHash === undefined) {
537
+ // The hash guards the exact find/replace against a stale body. The caller no
538
+ // longer has to carry it: hash the body this edit is about to patch.
539
+ const read = await this.engine.run('read_text', { project, pou: input.pou });
540
+ if (typeof read.body !== 'string' || read.body.length === 0) {
541
+ throw new MwiError('EDIT_REFUSED', `Cannot read ${input.pou} to hash it: ${String(read.body_error ?? 'empty body')}.`, 'Read the POU with mw_get, then retry the edit.');
542
+ }
543
+ expectedHash = createHash('sha256').update(read.body).digest('hex');
544
+ }
469
545
  if (input.changes !== undefined) {
470
546
  args.changes = input.changes;
471
- if (input.expectedHash !== undefined)
472
- args.expected_body_sha256 = input.expectedHash;
547
+ if (expectedHash !== undefined)
548
+ args.expected_body_sha256 = expectedHash;
473
549
  }
474
550
  else {
475
551
  args.code = input.body;
@@ -506,42 +582,57 @@ export class PowerShellSessionRuntime {
506
582
  throw new MwiError('EDIT_REFUSED', `${error.message}; evidence at ${evidence}`, 'Read the refusal, correct the request, and retry. Do not repeat an unverified edit.');
507
583
  }
508
584
  }
509
- /** The open project must be a proven stage; every compile and verify depends on it. */
585
+ /** The open project must be a proven stage or a verified direct backup; every compile and verify depends on it. */
510
586
  async openStagedRecord() {
511
- const status = await this.status();
512
- if (status.is_project_open !== true || typeof status.active_project !== 'string') {
513
- throw new MwiError('NO_PROJECT', 'No project is open in the IDE.', 'Open a staged project with mw_project_open before compiling.');
514
- }
515
- const record = this.stages.stagedAt(status.active_project);
516
- if (record === undefined) {
517
- throw new MwiError('NOT_STAGED', `The open project is not a proven stage: ${status.active_project}.`, 'Open a staged project; the verify row never compiles a workspace original.');
518
- }
519
- return record;
587
+ const active = await this.activeProject();
588
+ if (active === undefined) {
589
+ throw new MwiError('NO_PROJECT', 'No project is open in the IDE.', 'Open a project with mw_project_open before compiling.');
590
+ }
591
+ const record = this.stagedAt(active);
592
+ if (record !== undefined)
593
+ return record;
594
+ // The IDE already has a workspace project open that this bundle did not open.
595
+ // Take the verified backup now rather than refuse: the edit still needs the
596
+ // same rollback evidence, and the guarded edit itself still refuses an
597
+ // unsaved project before it writes.
598
+ if (!isInside(this.workspace, active) || isInside(join(this.workspace, '.motionworks'), active)) {
599
+ throw new MwiError('NOT_ATTACHED', `The open project has no proven backup: ${active}.`, 'Open a workspace project with mw_project_open so a verified backup is taken first.');
600
+ }
601
+ return directRecord(this.attach(active));
520
602
  }
521
- /** The only staged project, or a refusal that explains the choice. */
603
+ /** The only openable project, staged or backed up, or a refusal that explains the choice. */
522
604
  singleStage() {
523
- let names = [];
605
+ const stages = [];
524
606
  try {
525
- names = readdirSync(stageRootOf(this.workspace)).filter(name => name.toLowerCase().endsWith('.mwt'));
607
+ for (const name of readdirSync(stageRootOf(this.workspace))) {
608
+ if (name.toLowerCase().endsWith('.mwt'))
609
+ stages.push(join(stageRootOf(this.workspace), name));
610
+ }
526
611
  }
527
612
  catch {
528
- names = [];
613
+ // No stage directory.
614
+ }
615
+ const attached = listBackups(this.workspace).map(record => record.mwt);
616
+ // Direct editing is the default: a proven backup wins over a stale stage copy.
617
+ const candidates = attached.length > 0 ? attached : stages;
618
+ if (candidates.length === 0) {
619
+ throw new MwiError('NO_PROJECT', 'No project is staged or backed up in this workspace.', 'Call mw_project_find, then mw_project_open with the workspace .mwt.');
529
620
  }
530
- if (names.length === 0)
531
- throw new MwiError('NO_STAGE', 'No project is staged in this workspace.', 'Call mw_project_find, then mw_project_stage.');
532
- if (names.length > 1) {
533
- throw new MwiError('AMBIGUOUS_STAGE', `${names.length} projects are staged.`, 'Pass project: the staged .mwt to start.', names);
621
+ if (candidates.length > 1) {
622
+ throw new MwiError('AMBIGUOUS_PROJECT', `${candidates.length} projects are openable.`, 'Pass project: the exact .mwt to start.', candidates);
534
623
  }
535
- return join(stageRootOf(this.workspace), names[0]);
624
+ return candidates[0];
536
625
  }
537
- /** Resolve a requested staged project. */
538
- stageMwt(project) {
626
+ /** Resolve a requested project, taking a backup for a real workspace project. */
627
+ projectMwt(project) {
539
628
  const resolved = resolve(this.workspace, project);
540
629
  const mwt = resolved.toLowerCase().endsWith('.mwt') ? resolved : `${resolved}.mwt`;
541
- if (!isInside(stageRootOf(this.workspace), mwt) || !existsSync(mwt)) {
542
- throw new MwiError('NOT_STAGED', `${mwt} is not a staged project.`, 'Stage the workspace project first, then pass the staged .mwt.');
630
+ if (isInside(stageRootOf(this.workspace), mwt)) {
631
+ if (!existsSync(mwt))
632
+ throw new MwiError('NOT_STAGED', `${mwt} is not a staged project.`, 'Stage the workspace project first, or open the workspace .mwt directly.');
633
+ return mwt;
543
634
  }
544
- return mwt;
635
+ return ensureBackup(this.workspace, mwt).mwt;
545
636
  }
546
637
  }
547
638
  const runtimes = new Map();
@@ -6,6 +6,16 @@
6
6
  *
7
7
  * @module dsh-engineering-mwi/session/workspace
8
8
  */
9
+ /**
10
+ * Set the workspace root from the product row's config.
11
+ *
12
+ * The host does not always pass the session cwd to tool calls (it is undefined
13
+ * in the current desktop build), so the row carries the workspace explicitly.
14
+ * An empty string clears it and falls back to the marker / process directory.
15
+ *
16
+ * @param root - Absolute workspace root, or an empty string.
17
+ */
18
+ export declare function configureWorkspace(root: string | undefined): void;
9
19
  /** One MotionWorks project found in the workspace. */
10
20
  export interface FoundProject {
11
21
  /** Project name: the `.mwt` basename. */
@@ -6,10 +6,38 @@
6
6
  *
7
7
  * @module dsh-engineering-mwi/session/workspace
8
8
  */
9
- import { existsSync, readdirSync } from 'node:fs';
9
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
10
10
  import { join, resolve, sep } from 'node:path';
11
11
  /** Directories never scanned for projects. */
12
12
  const SKIP = new Set(['node_modules', '.git', 'stage', 'backups', '_backups', '__pycache__', 'chm_out', 'help_extract', '.dsh']);
13
+ /** Workspace root set from the row's config, when one is configured. */
14
+ let configured;
15
+ /**
16
+ * Set the workspace root from the product row's config.
17
+ *
18
+ * The host does not always pass the session cwd to tool calls (it is undefined
19
+ * in the current desktop build), so the row carries the workspace explicitly.
20
+ * An empty string clears it and falls back to the marker / process directory.
21
+ *
22
+ * @param root - Absolute workspace root, or an empty string.
23
+ */
24
+ export function configureWorkspace(root) {
25
+ const value = typeof root === 'string' ? root.trim() : '';
26
+ configured = value === '' ? undefined : resolve(value);
27
+ }
28
+ /** Read the documented `mwi.workspace` marker from the profile directory. */
29
+ function recordedWorkspace() {
30
+ try {
31
+ const marker = join(process.env.DSH_PROFILE_DIR ?? '', 'mwi.workspace');
32
+ if (!existsSync(marker))
33
+ return undefined;
34
+ const recorded = readFileSync(marker, 'utf8').replace(/^\uFEFF/u, '').trim();
35
+ return recorded === '' ? undefined : resolve(recorded);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
13
41
  /**
14
42
  * Resolve the caller's workspace.
15
43
  *
@@ -17,7 +45,9 @@ const SKIP = new Set(['node_modules', '.git', 'stage', 'backups', '_backups', '_
17
45
  * @returns The absolute workspace root.
18
46
  */
19
47
  export function workspaceOf(cwd) {
20
- return resolve(cwd ?? process.cwd());
48
+ if (typeof cwd === 'string' && cwd.trim() !== '')
49
+ return resolve(cwd);
50
+ return configured ?? recordedWorkspace() ?? resolve(process.cwd());
21
51
  }
22
52
  /**
23
53
  * The stage root of one workspace.
@@ -52,9 +82,20 @@ export function findProjects(workspace, options = {}) {
52
82
  const filter = options.filter?.toLowerCase();
53
83
  const stage = stageRootOf(workspace);
54
84
  const found = [];
85
+ const seen = new Set();
55
86
  const walk = (directory, depth) => {
56
87
  if (found.length >= limit || depth > 6)
57
88
  return;
89
+ let real;
90
+ try {
91
+ real = realpathSync(directory);
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ if (seen.has(real))
97
+ return;
98
+ seen.add(real);
58
99
  let entries;
59
100
  try {
60
101
  entries = readdirSync(directory, { withFileTypes: true });
@@ -66,7 +107,16 @@ export function findProjects(workspace, options = {}) {
66
107
  if (found.length >= limit)
67
108
  return;
68
109
  const absolute = join(directory, entry.name);
69
- if (entry.isDirectory()) {
110
+ let isDirectory = entry.isDirectory();
111
+ if (!isDirectory && entry.isSymbolicLink()) {
112
+ try {
113
+ isDirectory = statSync(absolute).isDirectory();
114
+ }
115
+ catch {
116
+ isDirectory = false;
117
+ }
118
+ }
119
+ if (isDirectory) {
70
120
  if (SKIP.has(entry.name) || isInside(stage, absolute))
71
121
  continue;
72
122
  walk(absolute, depth + 1);
@@ -22,8 +22,8 @@ export interface SkillContext {
22
22
  /** Registration metadata, kept here so a test can assert it without the file. */
23
23
  export declare const SKILL: {
24
24
  readonly name: "motionworks-iec";
25
- readonly description: "Operate a running Yaskawa MotionWorks IEC 3 Pro IDE through staged projects — find and stage a workspace project, read the live object model and POU code, edit declarations and code, compile, read diagnostics, and verify a staged change.";
26
- readonly whenToUse: "The user has MotionWorks IEC 3 Pro open or asks for work in it — a build or compile verdict, the live project model, reading or changing POU code, variable declarations, tasks, or the IDE error list. Also use when a MotionWorks project is mentioned: find which project is in the workspace before answering. For a question about a project outside the workspace, say it cannot be staged.";
25
+ readonly description: "Operate a running Yaskawa MotionWorks IEC 3 Pro IDE on the real project files — find and open a workspace project (one verified backup first), read the live object model and POU code, edit declarations and code, compile, read diagnostics, and verify the change.";
26
+ readonly whenToUse: "The user has MotionWorks IEC 3 Pro open or asks for work in it — a build or compile verdict, the live project model, reading or changing POU code, variable declarations, tasks, or the IDE error list. Also use when a MotionWorks project is mentioned: find which project is in the workspace before answering. For a question about a project outside the workspace, say it cannot be opened.";
27
27
  };
28
28
  /**
29
29
  * Register the MotionWorks IEC skill.
@@ -17,8 +17,8 @@ export const inject = ['skills'];
17
17
  /** Registration metadata, kept here so a test can assert it without the file. */
18
18
  export const SKILL = {
19
19
  name: 'motionworks-iec',
20
- description: 'Operate a running Yaskawa MotionWorks IEC 3 Pro IDE through staged projects — find and stage a workspace project, read the live object model and POU code, edit declarations and code, compile, read diagnostics, and verify a staged change.',
21
- whenToUse: 'The user has MotionWorks IEC 3 Pro open or asks for work in it — a build or compile verdict, the live project model, reading or changing POU code, variable declarations, tasks, or the IDE error list. Also use when a MotionWorks project is mentioned: find which project is in the workspace before answering. For a question about a project outside the workspace, say it cannot be staged.',
20
+ description: 'Operate a running Yaskawa MotionWorks IEC 3 Pro IDE on the real project files — find and open a workspace project (one verified backup first), read the live object model and POU code, edit declarations and code, compile, read diagnostics, and verify the change.',
21
+ whenToUse: 'The user has MotionWorks IEC 3 Pro open or asks for work in it — a build or compile verdict, the live project model, reading or changing POU code, variable declarations, tasks, or the IDE error list. Also use when a MotionWorks project is mentioned: find which project is in the workspace before answering. For a question about a project outside the workspace, say it cannot be opened.',
22
22
  };
23
23
  /**
24
24
  * Register the MotionWorks IEC skill.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The edit row: replace code, edit declarations, change structure and apply.
3
3
  *
4
- * Editing is off by default. Every tool writes only the staged project, and the
5
- * proven native helpers verify that unrelated saved and native state did not
6
- * change.
4
+ * Editing is on by default; a read-only profile can switch it off. Every tool writes the open project (the real files
5
+ * for a workspace project, with a verified backup already taken) and the proven
6
+ * native helpers verify that unrelated saved and native state did not change.
7
7
  *
8
8
  * @module dsh-engineering-mwi/tools/edit
9
9
  */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The edit row: replace code, edit declarations, change structure and apply.
3
3
  *
4
- * Editing is off by default. Every tool writes only the staged project, and the
5
- * proven native helpers verify that unrelated saved and native state did not
6
- * change.
4
+ * Editing is on by default; a read-only profile can switch it off. Every tool writes the open project (the real files
5
+ * for a workspace project, with a verified backup already taken) and the proven
6
+ * native helpers verify that unrelated saved and native state did not change.
7
7
  *
8
8
  * @module dsh-engineering-mwi/tools/edit
9
9
  */
@@ -32,7 +32,7 @@ export function editTools(runtimeOf = runtimeFor) {
32
32
  return [
33
33
  tool({
34
34
  name: 'mw_edit_code',
35
- description: 'Replace one ST or IL body in the open staged project through the native ChangeCodeWS path. Supply exact find/replace changes with the body hash, or a complete body with the current body. Verifies saved and native state, then leaves compilation to mw_compile.',
35
+ description: 'Replace one ST or IL body in the open project through the native ChangeCodeWS path. Supply exact find/replace changes with the body hash, or a complete body with the current body. Verifies saved and native state, then leaves compilation to mw_compile.',
36
36
  parameters: object({
37
37
  pou: string('Exact POU name.'),
38
38
  changes: list('Exact find/replace changes; each must match its expected count.', objectItem('One exact change.', {
@@ -51,17 +51,16 @@ export function editTools(runtimeOf = runtimeFor) {
51
51
  const hasChanges = changes !== undefined && changes.length > 0;
52
52
  const hasBody = typeof args.body === 'string';
53
53
  if (hasChanges === hasBody) {
54
- throw new MwiError('INVALID_ARGUMENT', 'Supply either changes with expected_body_sha256, or body with expected_body.', 'Use changes for a small exact patch, or body for a full replacement; do not mix them.');
55
- }
56
- if (hasChanges && typeof args.expected_body_sha256 !== 'string') {
57
- throw new MwiError('INVALID_ARGUMENT', 'Changes need expected_body_sha256.', 'Read the POU with mw_get and pass its text_body_sha256.');
54
+ throw new MwiError('INVALID_ARGUMENT', 'Supply either changes, or body with expected_body.', 'Use changes for a small exact patch, or body for a full replacement; do not mix them.');
58
55
  }
59
56
  if (hasBody && typeof args.expected_body !== 'string') {
60
57
  throw new MwiError('INVALID_ARGUMENT', 'A complete body needs expected_body.', 'Read the POU with mw_get and pass its current body.');
61
58
  }
62
59
  const result = await runtimeOf(workspace).editCode({
63
60
  pou,
64
- ...(hasChanges ? { changes, expectedHash: String(args.expected_body_sha256) } : { body: String(args.body), expectedBody: String(args.expected_body) }),
61
+ ...(hasChanges
62
+ ? { changes, ...(typeof args.expected_body_sha256 === 'string' ? { expectedHash: args.expected_body_sha256 } : {}) }
63
+ : { body: String(args.body), expectedBody: String(args.expected_body) }),
65
64
  });
66
65
  const verification = result.verification;
67
66
  const accepted = verification?.accepted === true;
@@ -72,7 +71,7 @@ export function editTools(runtimeOf = runtimeFor) {
72
71
  }),
73
72
  tool({
74
73
  name: 'mw_edit_variables',
75
- description: 'Add, edit or delete one declaration in the open staged project through the native IDE API. Edit replaces the complete seven-field declaration and optional boolean flags; delete needs approval and reference review.',
74
+ description: 'Add, edit or delete one declaration in the open project through the native IDE API. Edit replaces the complete seven-field declaration and optional boolean flags; delete needs approval and reference review.',
76
75
  parameters: object({
77
76
  operation: choice('Variable operation.', ['add', 'edit', 'delete']),
78
77
  pou: string('POU for a local declaration; omit for resource globals.'),
@@ -225,7 +224,7 @@ export function editTools(runtimeOf = runtimeFor) {
225
224
  }),
226
225
  tool({
227
226
  name: 'mw_transfer_pou',
228
- description: 'Export one writable POU to a private workspace package, or import that untouched package into this staged project. Import needs the export token and a dependency review; a token is consumed by one attempt.',
227
+ description: 'Export one writable POU to a private workspace package, or import that untouched package into this project. Import needs the export token and a dependency review; a token is consumed by one attempt.',
229
228
  parameters: object({
230
229
  operation: choice('Package operation.', ['export', 'import']),
231
230
  pou: string('Exact POU to export.'),
@@ -255,7 +254,7 @@ export function editTools(runtimeOf = runtimeFor) {
255
254
  }),
256
255
  tool({
257
256
  name: 'mw_validate',
258
- description: 'Validate the open staged project before applying: workflow blockers, native validation and an optional program review for one POU or proposed body. Read the findings before saving or compiling.',
257
+ description: 'Validate the open project before applying: workflow blockers, native validation and an optional program review for one POU or proposed body. Read the findings before saving or compiling.',
259
258
  parameters: object({
260
259
  pou: string('POU to review.'),
261
260
  body: string('Proposed replacement body to review without writing it.'),
@@ -268,18 +267,18 @@ export function editTools(runtimeOf = runtimeFor) {
268
267
  });
269
268
  const ready = result.ready === true;
270
269
  return success(ready ? 'Validation found no blocker.' : `Validation found ${String(result.blockers?.length ?? 0)} blocker(s) and ${String(result.validation_errors?.length ?? 0)} validation error(s).`, result, ready
271
- ? [{ tool: 'mw_apply', why: 'Save the validated staged project.' }]
270
+ ? [{ tool: 'mw_apply', why: 'Save the validated project.' }]
272
271
  : [{ tool: 'mw_review', why: 'Inspect the components named by the findings.' }], EDIT_COVERAGE);
273
272
  },
274
273
  }),
275
274
  tool({
276
275
  name: 'mw_apply',
277
- description: 'Save the open staged project through the IDE. This writes only the staged copy; promoting source back to the workspace remains a separate, explicit step.',
276
+ description: 'Save the open project through the IDE. A workspace project is written in place; its verified pre-edit backup stays under <workspace>/backups/.',
278
277
  parameters: object({}),
279
278
  async run(_args, exec) {
280
279
  const workspace = workspaceOf(exec.cwd);
281
280
  const result = await runtimeOf(workspace).apply();
282
- return success('Saved the staged project through the IDE.', result, [{ tool: 'mw_compile', why: 'Compile the saved project.' }], EDIT_COVERAGE);
281
+ return success('Saved the open project through the IDE.', result, [{ tool: 'mw_compile', why: 'Compile the saved project.' }], EDIT_COVERAGE);
283
282
  },
284
283
  }),
285
284
  ];
@@ -75,7 +75,7 @@ export function readTools(readsOf = workspace => runtimeFor(workspace).reads) {
75
75
  }),
76
76
  tool({
77
77
  name: 'mw_get',
78
- description: 'Read one POU with its declarations, one type, one task row, one block interface or the EtherNet/IP assembly map.',
78
+ description: 'Read one POU with its declarations and the text_body_sha256 that the edit preconditions take, one type, one task row, one block interface or the EtherNet/IP assembly map.',
79
79
  parameters: object({
80
80
  kind: choice('What to read.', GET_KINDS),
81
81
  name: string('Exact component name; omit only for kind eip.'),
@@ -12,6 +12,7 @@ import { success } from "../core/result.js";
12
12
  import { MwiError } from "../core/errors.js";
13
13
  import { tool } from "../core/tool.js";
14
14
  import { findProjects, stageRootOf, workspaceOf } from "../session/workspace.js";
15
+ import { listBackups } from "../session/backup.js";
15
16
  import { runtimeFor } from "../session/service.js";
16
17
  /** What every IDE answer cannot establish. */
17
18
  const IDE_COVERAGE = {
@@ -43,7 +44,7 @@ export function sessionTools(runtimeOf = runtimeFor) {
43
44
  return [
44
45
  tool({
45
46
  name: 'mw_project_find',
46
- description: 'Find MotionWorks IEC projects inside the calling workspace. Returns each .mwt, its expanded directory and whether a staged copy exists. Call this before staging or opening anything.',
47
+ description: 'Find MotionWorks IEC projects inside the calling workspace. Returns each .mwt, its expanded directory, whether a staged copy exists and whether a pre-edit backup exists. Call this before opening anything.',
47
48
  parameters: object({
48
49
  filter: string('Optional case-insensitive substring matched against the project name or path.'),
49
50
  limit: integer('Most projects to return. Defaults to 20.', { minimum: 1, maximum: 100 }),
@@ -55,11 +56,12 @@ export function sessionTools(runtimeOf = runtimeFor) {
55
56
  limit: typeof args.limit === 'number' ? args.limit : 20,
56
57
  });
57
58
  const staged = projects.filter(project => project.staged !== null).length;
59
+ const backups = listBackups(workspace);
58
60
  return success(projects.length === 0
59
61
  ? `No MotionWorks project was found inside ${workspace}.`
60
- : `Found ${projects.length} MotionWorks project(s) in ${workspace}; ${staged} already staged.`, { workspace, stage_root: stageRootOf(workspace), projects }, projects.length === 0
62
+ : `Found ${projects.length} MotionWorks project(s) in ${workspace}; ${staged} staged, ${backups.length} with a pre-edit backup.`, { workspace, stage_root: stageRootOf(workspace), backups, projects }, projects.length === 0
61
63
  ? []
62
- : [{ tool: 'mw_project_stage', why: 'Stage the intended project before opening it.', args: { source: projects[0]?.mwt } }], { complete: false, unverified: ['projects outside the workspace are never listed or staged'] });
64
+ : [{ tool: 'mw_project_open', why: 'Open the workspace project; a verified backup is taken first.', args: { path: projects[0]?.mwt } }], { complete: false, unverified: ['projects outside the workspace are never listed or staged'] });
63
65
  },
64
66
  }),
65
67
  tool({
@@ -85,7 +87,7 @@ export function sessionTools(runtimeOf = runtimeFor) {
85
87
  }),
86
88
  tool({
87
89
  name: 'mw_project_stage',
88
- description: 'Copy one workspace project into the stage so the IDE opens a copy, never the original. The .mwt and its expanded directory are copied and the wrapper is bound to the staged directory.',
90
+ description: 'Optional sandbox: copy one workspace project into .motionworks/stage so the IDE opens a copy instead of the real files. Normal editing opens the workspace project directly.',
89
91
  parameters: object({
90
92
  source: string('Workspace project folder or its .mwt. Must be inside the workspace.'),
91
93
  replace_existing: boolean('Replace an existing stage after review; defaults false. The previous stage is retained under backups/.'),
@@ -98,9 +100,9 @@ export function sessionTools(runtimeOf = runtimeFor) {
98
100
  }),
99
101
  tool({
100
102
  name: 'mw_project_open',
101
- description: 'Open one staged project in the running IDE. Refuses a path that is not a proven stage, and refuses to replace another open project without the exact approved path.',
103
+ description: 'Open a workspace project directly, or a staged copy. A direct open first writes and verifies one complete backup under <workspace>/.motionworks/attached/, and a project the IDE already has open is attached without being reloaded. Refuses to replace another open project without the exact approved path.',
102
104
  parameters: object({
103
- path: string('Staged .mwt path returned by mw_project_stage.'),
105
+ path: string('Workspace .mwt (edits the real project, backup taken first) or a staged .mwt from mw_project_stage.'),
104
106
  user_approved: boolean('True only after the user approved saving and closing another open project.'),
105
107
  expected_project: string('Exact open project path named in that approval.'),
106
108
  }, ['path']),
@@ -112,14 +114,16 @@ export function sessionTools(runtimeOf = runtimeFor) {
112
114
  });
113
115
  return success(opened.matches_request === false
114
116
  ? 'The open call returned, but the IDE has a different project open.'
115
- : `Opened ${String(opened.active_project ?? args.path)}.`, opened, [{ tool: 'mw_ide_status', why: 'Confirm the active project and compile state.' }], IDE_COVERAGE);
117
+ : opened.already_open === true
118
+ ? `Already open; verified the pre-edit backup for ${String(opened.active_project ?? args.path)}.`
119
+ : `Opened ${String(opened.active_project ?? args.path)}.`, opened, [{ tool: 'mw_ide_status', why: 'Confirm the active project and compile state.' }], IDE_COVERAGE);
116
120
  },
117
121
  }),
118
122
  tool({
119
123
  name: 'mw_ide_start',
120
- description: 'Start MotionWorks IEC with one staged project, or attach to the IDE that is already running. Refuses when several stages exist without a project argument.',
124
+ description: 'Start MotionWorks IEC with one workspace project (backup first) or a staged copy, or attach to the IDE that is already running. Refuses when several projects are openable without a project argument.',
121
125
  parameters: object({
122
- project: string('Staged .mwt to start with. Omit when exactly one project is staged.'),
126
+ project: string('Workspace or staged .mwt to start with. Omit when exactly one project is openable.'),
123
127
  }),
124
128
  async run(args, exec) {
125
129
  const workspace = workspaceOf(exec.cwd);