@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.81

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 (200) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +76 -38
  3. package/dist/node/index.d.cts +946 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +564 -51
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +76 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +34 -16
  10. package/src/advisor/advisor-command-module.ts +47 -0
  11. package/src/advisor/advisor-command.test.ts +102 -0
  12. package/src/advisor/advisor-command.ts +55 -0
  13. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  14. package/src/advisor/index.ts +6 -0
  15. package/src/agent/__tests__/agent-command.test.ts +39 -8
  16. package/src/agent/agent-command-module.ts +14 -7
  17. package/src/agent/agent-command-parser.ts +1 -1
  18. package/src/agent/agent-command.ts +3 -3
  19. package/src/background/__tests__/background-command-module.test.ts +59 -52
  20. package/src/background/background-command-module.ts +2 -1
  21. package/src/background/background-command.ts +3 -3
  22. package/src/command-module-utils.ts +33 -0
  23. package/src/compact/__tests__/compact-command-module.test.ts +31 -23
  24. package/src/compact/compact-command-module.ts +10 -1
  25. package/src/compact/compact-command.ts +6 -3
  26. package/src/context/__tests__/context-command-module.test.ts +214 -63
  27. package/src/context/auto-compact-format.ts +25 -0
  28. package/src/context/context-breakdown.ts +295 -0
  29. package/src/context/context-command-module.ts +45 -14
  30. package/src/context/context-command.ts +34 -257
  31. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
  32. package/src/default/__tests__/default-command-modules.test.ts +59 -7
  33. package/src/default/__tests__/model-exposure.test.ts +241 -0
  34. package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
  35. package/src/default/default-command-modules.ts +130 -18
  36. package/src/default/index.ts +4 -1
  37. package/src/devices/__tests__/devices-command-module.test.ts +212 -0
  38. package/src/devices/devices-command-module.ts +224 -0
  39. package/src/devices/devices-command-port.ts +82 -0
  40. package/src/devices/index.ts +11 -0
  41. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  42. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  43. package/src/doctor/__tests__/doctor-fixture.ts +145 -0
  44. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  45. package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
  46. package/src/doctor/doctor-command-module.ts +132 -0
  47. package/src/doctor/doctor-extensions-probe.ts +299 -0
  48. package/src/doctor/doctor-node-deps.ts +89 -0
  49. package/src/doctor/doctor-redaction.ts +99 -0
  50. package/src/doctor/doctor-render.ts +70 -0
  51. package/src/doctor/doctor-repair.ts +136 -0
  52. package/src/doctor/doctor-runner.ts +122 -0
  53. package/src/doctor/doctor-settings-probe.ts +239 -0
  54. package/src/doctor/doctor-storage-probe.ts +187 -0
  55. package/src/doctor/doctor-types.ts +109 -0
  56. package/src/doctor/index.ts +32 -0
  57. package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
  58. package/src/editor/editor-command-module.ts +6 -5
  59. package/src/editor/editor-command.ts +10 -5
  60. package/src/effort/effort-command-module.ts +51 -0
  61. package/src/effort/effort-command.test.ts +140 -0
  62. package/src/effort/effort-command.ts +122 -0
  63. package/src/effort/index.ts +6 -0
  64. package/src/exit/__tests__/exit-command-module.test.ts +16 -9
  65. package/src/exit/exit-command-module.ts +2 -1
  66. package/src/exit/exit-command.ts +6 -6
  67. package/src/fork/__tests__/fork-command.test.ts +184 -0
  68. package/src/fork/fork-command-module.ts +61 -0
  69. package/src/fork/fork-command.ts +162 -0
  70. package/src/fork/index.ts +8 -0
  71. package/src/git/__tests__/fake-git-port.ts +28 -0
  72. package/src/git/__tests__/git-command-module.test.ts +103 -0
  73. package/src/git/__tests__/git-commit.test.ts +185 -0
  74. package/src/git/__tests__/git-diff.test.ts +125 -0
  75. package/src/git/__tests__/git-process.test.ts +221 -0
  76. package/src/git/__tests__/git-status.test.ts +117 -0
  77. package/src/git/git-command-module.ts +139 -0
  78. package/src/git/git-commit.ts +251 -0
  79. package/src/git/git-diff.ts +129 -0
  80. package/src/git/git-process.ts +169 -0
  81. package/src/git/git-status.ts +134 -0
  82. package/src/git/index.ts +17 -0
  83. package/src/goal/__tests__/goal-command.test.ts +9 -4
  84. package/src/goal/goal-command-module.ts +2 -1
  85. package/src/goal/goal-command.ts +6 -9
  86. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  87. package/src/handoff/handoff-command-module.ts +48 -0
  88. package/src/handoff/handoff-command.ts +187 -0
  89. package/src/handoff/index.ts +6 -0
  90. package/src/help/__tests__/help-command-module.test.ts +63 -40
  91. package/src/help/__tests__/help-command.test.ts +46 -37
  92. package/src/help/help-command-module.ts +2 -1
  93. package/src/help/help-command.ts +3 -3
  94. package/src/index.ts +124 -2
  95. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  96. package/src/keybindings/index.ts +6 -0
  97. package/src/keybindings/keybindings-command-module.ts +74 -0
  98. package/src/language/__tests__/language-command-module.test.ts +33 -28
  99. package/src/language/language-command-module.ts +2 -1
  100. package/src/language/language-command.ts +8 -6
  101. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  102. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  103. package/src/mcp-activation/index.ts +12 -0
  104. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  105. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  106. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  107. package/src/mcp-activation/mcp-model-status.ts +99 -0
  108. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  109. package/src/memory/memory-command-module.ts +18 -1
  110. package/src/memory/memory-command.ts +38 -38
  111. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  112. package/src/mode/mode-command-module.ts +2 -1
  113. package/src/mode/mode-command.ts +15 -7
  114. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  115. package/src/output-style/index.ts +6 -0
  116. package/src/output-style/output-style-command-module.ts +143 -0
  117. package/src/peers/__tests__/peers-command.test.ts +241 -0
  118. package/src/peers/index.ts +9 -0
  119. package/src/peers/peers-command-module.ts +47 -0
  120. package/src/peers/peers-command.ts +142 -0
  121. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  122. package/src/permissions/permissions-command-module.ts +2 -1
  123. package/src/permissions/permissions-command.ts +46 -4
  124. package/src/plan/__tests__/plan-command.test.ts +129 -0
  125. package/src/plan/index.ts +9 -0
  126. package/src/plan/plan-command-module.ts +49 -0
  127. package/src/plan/plan-command.ts +92 -0
  128. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  129. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  130. package/src/plugin/plugin-command-module.ts +3 -1
  131. package/src/plugin/plugin-command.ts +17 -16
  132. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  133. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  134. package/src/preset/preset-command-module.ts +26 -8
  135. package/src/preset/preset-command.ts +85 -27
  136. package/src/provider/__tests__/org-policy.test.ts +4 -5
  137. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  138. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  139. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  140. package/src/provider/provider-command-execution.ts +4 -4
  141. package/src/provider/provider-command-module.ts +2 -1
  142. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  143. package/src/provider/provider-command-profile-operations.ts +3 -3
  144. package/src/provider/provider-command-profile.ts +1 -1
  145. package/src/provider/provider-command-setup.ts +4 -4
  146. package/src/provider/provider-startup.ts +62 -35
  147. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  148. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  149. package/src/remote-control/index.ts +8 -0
  150. package/src/remote-control/remote-control-command-module.ts +61 -0
  151. package/src/remote-control/remote-control-command.ts +138 -0
  152. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  153. package/src/reset/reset-command-module.ts +2 -1
  154. package/src/reset/reset-command.ts +3 -4
  155. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  156. package/src/rewind/rewind-command-module.ts +2 -1
  157. package/src/rewind/rewind-command.ts +73 -6
  158. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  159. package/src/sandbox/index.ts +2 -0
  160. package/src/sandbox/sandbox-command-module.ts +56 -0
  161. package/src/sandbox/sandbox-command.ts +94 -0
  162. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  163. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  164. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  165. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  166. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  167. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  168. package/src/schedule/index.ts +3 -0
  169. package/src/schedule/loop-command.ts +333 -0
  170. package/src/schedule/loop-jitter.ts +41 -0
  171. package/src/schedule/schedule-command-module.ts +92 -14
  172. package/src/schedule/schedule-command.ts +98 -9
  173. package/src/schedule/schedule-spec-parser.ts +5 -1
  174. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  175. package/src/session/__tests__/session-command-module.test.ts +210 -45
  176. package/src/session/session-command-module.ts +68 -37
  177. package/src/session/session-command.ts +147 -80
  178. package/src/settings/settings-command-module.ts +3 -2
  179. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  180. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  181. package/src/shell/resolve-shell.ts +2 -2
  182. package/src/shell/shell-command-module.ts +6 -5
  183. package/src/shell/shell-command.ts +9 -5
  184. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  185. package/src/skills/index.ts +1 -0
  186. package/src/skills/skills-command-module.ts +22 -5
  187. package/src/skills/skills-command.ts +14 -13
  188. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  189. package/src/statusline/statusline-command-module.ts +2 -1
  190. package/src/statusline/statusline-command.ts +4 -4
  191. package/src/theme/__tests__/theme-command.test.ts +204 -0
  192. package/src/theme/index.ts +12 -0
  193. package/src/theme/theme-command-module.ts +211 -0
  194. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  195. package/src/user-local/user-local-command-constants.ts +1 -1
  196. package/src/user-local/user-local-command-module.ts +6 -5
  197. package/src/user-local/user-local-command.ts +12 -6
  198. package/src/user-local/user-local-memory-command.ts +17 -7
  199. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  200. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -1,8 +1,12 @@
1
1
  import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
2
- import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-transport';
2
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
  import { createPluginCommandModule } from '../plugin-command-module.js';
5
5
  import { executePluginCommand, executeReloadPluginsCommand } from '../plugin-command.js';
6
+ import {
7
+ createTestCommandHost,
8
+ createTestSessionRuntime,
9
+ } from '@robota-sdk/agent-framework/testing';
6
10
 
7
11
  function createPluginAdapter(overrides?: Partial<ICommandPluginAdapter>): ICommandPluginAdapter {
8
12
  return {
@@ -21,8 +25,8 @@ function createPluginAdapter(overrides?: Partial<ICommandPluginAdapter>): IComma
21
25
  };
22
26
  }
23
27
 
24
- function createCommandSessionRuntime(): ICommandSessionRuntime {
25
- return {
28
+ function createCommandSessionRuntime() {
29
+ return createTestSessionRuntime({
26
30
  clearHistory: () => undefined,
27
31
  compact: async () => undefined,
28
32
  getContextState: () => ({
@@ -39,10 +43,10 @@ function createCommandSessionRuntime(): ICommandSessionRuntime {
39
43
  getAutoCompactThreshold: () => false,
40
44
  getFullHistory: () => [],
41
45
  getHistory: () => [],
42
- };
46
+ });
43
47
  }
44
48
 
45
- function createCommandHostContext(adapter?: ICommandPluginAdapter): ICommandHostContext {
49
+ function createCommandHostContext(adapter?: ICommandPluginAdapter) {
46
50
  const checkpoint = {
47
51
  id: 'checkpoint_1',
48
52
  sessionId: 'session_1',
@@ -51,39 +55,41 @@ function createCommandHostContext(adapter?: ICommandPluginAdapter): ICommandHost
51
55
  createdAt: '2026-05-03T00:00:00.000Z',
52
56
  fileCount: 0,
53
57
  };
54
- return {
55
- getSession: () => createCommandSessionRuntime(),
56
- getContextState: () => ({
57
- maxTokens: 100,
58
- usedTokens: 10,
59
- usedPercentage: 10,
60
- remainingPercentage: 90,
61
- }),
62
- getAutoCompactThreshold: () => 0.8,
63
- getCommandHostAdapters: () => (adapter === undefined ? {} : { plugin: adapter }),
64
- compactContext: async () => undefined,
65
- getCwd: () => '/workspace',
66
- listCommands: () => [],
67
- listEditCheckpoints: () => [],
68
- restoreEditCheckpoint: async () => ({
69
- target: checkpoint,
70
- restoredCheckpointCount: 0,
71
- restoredFileCount: 0,
72
- removedCheckpointCount: 0,
73
- }),
74
- rollbackEditCheckpoint: async () => ({
75
- target: checkpoint,
76
- restoredCheckpointCount: 0,
77
- restoredFileCount: 0,
78
- removedCheckpointCount: 0,
79
- }),
80
- getUsedMemoryReferences: () => [],
81
- recordMemoryEvent: () => undefined,
82
- listBackgroundTasks: () => [],
83
- readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
84
- cancelBackgroundTask: async () => undefined,
85
- closeBackgroundTask: async () => undefined,
86
- };
58
+ return createTestCommandHost({
59
+ overrides: {
60
+ getSession: () => createCommandSessionRuntime(),
61
+ getContextState: () => ({
62
+ maxTokens: 100,
63
+ usedTokens: 10,
64
+ usedPercentage: 10,
65
+ remainingPercentage: 90,
66
+ }),
67
+ getAutoCompactThreshold: () => 0.8,
68
+ getCommandHostAdapters: () => (adapter === undefined ? {} : { plugin: adapter }),
69
+ compactContext: async () => undefined,
70
+ getCwd: () => '/workspace',
71
+ listCommands: () => [],
72
+ listEditCheckpoints: () => [],
73
+ restoreEditCheckpoint: async () => ({
74
+ target: checkpoint,
75
+ restoredCheckpointCount: 0,
76
+ restoredFileCount: 0,
77
+ removedCheckpointCount: 0,
78
+ }),
79
+ rollbackEditCheckpoint: async () => ({
80
+ target: checkpoint,
81
+ restoredCheckpointCount: 0,
82
+ restoredFileCount: 0,
83
+ removedCheckpointCount: 0,
84
+ }),
85
+ getUsedMemoryReferences: () => [],
86
+ recordMemoryEvent: () => undefined,
87
+ listBackgroundTasks: () => [],
88
+ readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
89
+ cancelBackgroundTask: async () => undefined,
90
+ closeBackgroundTask: async () => undefined,
91
+ },
92
+ });
87
93
  }
88
94
 
89
95
  describe('createPluginCommandModule', () => {
@@ -119,7 +125,7 @@ describe('executePluginCommand', () => {
119
125
  await expect(executePluginCommand(createCommandHostContext(), '')).resolves.toEqual({
120
126
  success: true,
121
127
  message: 'Opening plugin manager...',
122
- effects: [{ type: 'plugin-tui-requested' }],
128
+ uiIntents: [{ type: 'show-plugin-manager' }],
123
129
  });
124
130
  });
125
131
 
@@ -193,7 +199,7 @@ describe('executeReloadPluginsCommand', () => {
193
199
  ).resolves.toEqual({
194
200
  success: true,
195
201
  message: 'Reloaded 3 plugin resources.',
196
- effects: [{ type: 'plugin-registry-reload-requested' }],
202
+ data: { pluginRegistryReloaded: true },
197
203
  });
198
204
  expect(adapter.reloadPlugins).toHaveBeenCalled();
199
205
  });
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import * as commands from '../../index.js';
4
+
5
+ describe('plugin command host boundary', () => {
6
+ it('keeps the concrete plugin host adapter out of the command package entrypoint', () => {
7
+ expect(commands).not.toHaveProperty('createDefaultPluginCommandAdapter');
8
+ expect(commands).not.toHaveProperty('reloadPluginCommandSource');
9
+ expect(commands).not.toHaveProperty('pluginScopeDirs');
10
+ });
11
+ });
@@ -8,7 +8,7 @@ import {
8
8
  import { executePluginCommand, executeReloadPluginsCommand } from './plugin-command.js';
9
9
 
10
10
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
11
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
11
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
12
12
 
13
13
  export function createPluginCommandEntry(): ICommand {
14
14
  return {
@@ -16,6 +16,7 @@ export function createPluginCommandEntry(): ICommand {
16
16
  displayName: 'Plugins',
17
17
  description: PLUGIN_COMMAND_DESCRIPTION,
18
18
  source: 'plugin-manager',
19
+ // User-only: installs and enables code; a trust decision.
19
20
  modelInvocable: false,
20
21
  argumentHint: PLUGIN_COMMAND_ARGUMENT_HINT,
21
22
  subcommands: buildPluginCommandSubcommands(),
@@ -28,6 +29,7 @@ export function createReloadPluginsCommandEntry(): ICommand {
28
29
  displayName: 'Reload Plugins',
29
30
  description: RELOAD_PLUGINS_COMMAND_DESCRIPTION,
30
31
  source: 'plugin-manager',
32
+ // User-only: reloading activates whatever plugin code is installed now; a trust decision.
31
33
  modelInvocable: false,
32
34
  };
33
35
  }
@@ -1,11 +1,10 @@
1
1
  import {
2
- createPluginRegistryReloadRequestedEffect,
3
- createPluginTuiRequestedEffect,
2
+ createShowPluginManagerIntent,
4
3
  resolvePluginCommandAdapter,
5
4
  } from '@robota-sdk/agent-framework';
6
5
 
7
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
8
- import type { ICommandPluginAdapter, ICommandResult } from '@robota-sdk/agent-interface-transport';
6
+ import type { ICommandHostAdapterAccess } from '@robota-sdk/agent-framework';
7
+ import type { ICommandPluginAdapter, ICommandResult } from '@robota-sdk/agent-interface-command';
9
8
 
10
9
  function getSubcommandParts(args: string): { subcommand: string; subArgs: string } {
11
10
  const parts = args
@@ -25,12 +24,12 @@ function usage(message: string): ICommandResult {
25
24
  };
26
25
  }
27
26
 
28
- function getPluginAdapter(context: ICommandHostContext): ICommandPluginAdapter | undefined {
27
+ function getPluginAdapter(context: ICommandHostAdapterAccess): ICommandPluginAdapter | undefined {
29
28
  return resolvePluginCommandAdapter(context);
30
29
  }
31
30
 
32
31
  async function executePluginOperation(
33
- context: ICommandHostContext,
32
+ context: ICommandHostAdapterAccess,
34
33
  operation: (adapter: ICommandPluginAdapter) => Promise<string>,
35
34
  ): Promise<ICommandResult> {
36
35
  const adapter = getPluginAdapter(context);
@@ -56,7 +55,7 @@ async function executePluginOperation(
56
55
  }
57
56
 
58
57
  async function executeMarketplaceCommand(
59
- context: ICommandHostContext,
58
+ context: ICommandHostAdapterAccess,
60
59
  subArgs: string,
61
60
  ): Promise<ICommandResult> {
62
61
  const { subcommand, subArgs: marketplaceArgs } = getSubcommandParts(subArgs);
@@ -102,7 +101,7 @@ type TPluginIdOperation = (
102
101
  ) => Promise<string> | string;
103
102
 
104
103
  function executePluginIdOperation(
105
- context: ICommandHostContext,
104
+ context: ICommandHostAdapterAccess,
106
105
  pluginId: string,
107
106
  usageMessage: string,
108
107
  operation: TPluginIdOperation,
@@ -119,12 +118,12 @@ function executePluginManager(): ICommandResult {
119
118
  return {
120
119
  success: true,
121
120
  message: 'Opening plugin manager...',
122
- effects: [createPluginTuiRequestedEffect()],
121
+ uiIntents: [createShowPluginManagerIntent()],
123
122
  };
124
123
  }
125
124
 
126
125
  function executeInstallCommand(
127
- context: ICommandHostContext,
126
+ context: ICommandHostAdapterAccess,
128
127
  pluginId: string,
129
128
  ): Promise<ICommandResult> {
130
129
  return executePluginIdOperation(
@@ -139,7 +138,7 @@ function executeInstallCommand(
139
138
  }
140
139
 
141
140
  function executeUninstallCommand(
142
- context: ICommandHostContext,
141
+ context: ICommandHostAdapterAccess,
143
142
  pluginId: string,
144
143
  ): Promise<ICommandResult> {
145
144
  return executePluginIdOperation(
@@ -154,7 +153,7 @@ function executeUninstallCommand(
154
153
  }
155
154
 
156
155
  function executeEnableCommand(
157
- context: ICommandHostContext,
156
+ context: ICommandHostAdapterAccess,
158
157
  pluginId: string,
159
158
  ): Promise<ICommandResult> {
160
159
  return executePluginIdOperation(
@@ -169,7 +168,7 @@ function executeEnableCommand(
169
168
  }
170
169
 
171
170
  function executeDisableCommand(
172
- context: ICommandHostContext,
171
+ context: ICommandHostAdapterAccess,
173
172
  pluginId: string,
174
173
  ): Promise<ICommandResult> {
175
174
  return executePluginIdOperation(
@@ -184,7 +183,7 @@ function executeDisableCommand(
184
183
  }
185
184
 
186
185
  export async function executePluginCommand(
187
- context: ICommandHostContext,
186
+ context: ICommandHostAdapterAccess,
188
187
  args: string,
189
188
  ): Promise<ICommandResult> {
190
189
  const { subcommand, subArgs } = getSubcommandParts(args);
@@ -208,7 +207,7 @@ export async function executePluginCommand(
208
207
  }
209
208
 
210
209
  export async function executeReloadPluginsCommand(
211
- context: ICommandHostContext,
210
+ context: ICommandHostAdapterAccess,
212
211
  _args: string,
213
212
  ): Promise<ICommandResult> {
214
213
  return executePluginOperation(context, async (adapter) => {
@@ -220,9 +219,11 @@ export async function executeReloadPluginsCommand(
220
219
  return `Reloaded ${suffix}.`;
221
220
  }).then((result) => {
222
221
  if (!result.success) return result;
222
+ // CMD-004 Stage E: the semantic reload already ran HOST-side above (`adapter.reloadPlugins()`);
223
+ // the requester-local registry/autocomplete refresh rides the result as a data hint.
223
224
  return {
224
225
  ...result,
225
- effects: [createPluginRegistryReloadRequestedEffect()],
226
+ data: { ...result.data, pluginRegistryReloaded: true },
226
227
  };
227
228
  });
228
229
  }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { executePresetCommand } from '../preset-command.js';
4
+
5
+ /**
6
+ * ARCH-009 — `/preset` discovers through the registry the HOST resolved with.
7
+ *
8
+ * ARCH-008 moved startup preset resolution onto a per-call instance registry, but `/preset` kept
9
+ * reading `agent-preset`'s module-global one, because a command runs with only an
10
+ * `ICommandHostContext` and had no path to the instance the shell built. That left the last mutable
11
+ * process-global on this axis load-bearing, and it made ARCH-008's R8 property — two products in one
12
+ * process, each with its own presets — only half-true: both would have shared one registry.
13
+ *
14
+ * The registry arrives in the host-ADAPTER bag, the path `/permission-mode` and `/plugin` already take
15
+ * to their host capabilities. A capability the composition root supplies is what that bag is for, and
16
+ * reusing it means this seam has no shape of its own to get wrong.
17
+ *
18
+ * The decisive case is the last one. Everything else is a property of a single host; only "two hosts,
19
+ * one process" is impossible to satisfy with a global, which is why it is the one that proves the
20
+ * seam rather than merely exercising it.
21
+ */
22
+ function presetSummary(id: string) {
23
+ return { id, title: `${id} title`, description: `${id} description` };
24
+ }
25
+
26
+ /** A host whose registry contains exactly the ids it was built with. */
27
+ function hostWithPresets(ids: string[]) {
28
+ const applied: string[] = [];
29
+ return {
30
+ applied,
31
+ context: {
32
+ getCommandHostAdapters: () => ({
33
+ presetRegistry: {
34
+ listPresets: () => ids.map(presetSummary),
35
+ getPreset: (id: string) => (ids.includes(id) ? presetSummary(id) : undefined),
36
+ resolvePreset: (id: string) => ({ id }),
37
+ },
38
+ }),
39
+ getSession: () => ({ getActivePresetId: () => 'default' }),
40
+ getUserInteraction: () => undefined,
41
+ applyPersona: vi.fn(),
42
+ applySelfVerification: vi.fn(),
43
+ applyCommandModuleSelection: () => [],
44
+ } as never,
45
+ };
46
+ }
47
+
48
+ describe('ARCH-009 — /preset reads the host-supplied registry', () => {
49
+ it('lists the host registry, not the built-ins', async () => {
50
+ const { context } = hostWithPresets(['acme-reviewer']);
51
+
52
+ const result = await executePresetCommand(context, 'list');
53
+
54
+ expect(result.message).toContain('acme-reviewer');
55
+ // `careful-reviewer` is a shipped built-in and is what the no-registry fallback would list. Its
56
+ // absence here is what shows the fallback was not taken.
57
+ expect(result.message).not.toContain('careful-reviewer');
58
+ });
59
+
60
+ it('rejects an id the HOST does not have, even though a built-in would resolve it', async () => {
61
+ const { context } = hostWithPresets(['acme-reviewer']);
62
+
63
+ const result = await executePresetCommand(context, 'careful-reviewer');
64
+
65
+ expect(result.success).toBe(false);
66
+ expect(result.message).toContain('Unknown preset');
67
+ });
68
+
69
+ it('falls back to the built-ins when the host supplies no registry', async () => {
70
+ // Deliberate, not transitional: a host that never loads external presets has nothing to hand
71
+ // over, and `/preset` should still list the built-ins rather than reporting an empty catalog.
72
+ const context = {
73
+ getSession: () => ({ getActivePresetId: () => 'default' }),
74
+ getUserInteraction: () => undefined,
75
+ applyPersona: vi.fn(),
76
+ applySelfVerification: vi.fn(),
77
+ applyCommandModuleSelection: () => [],
78
+ } as never;
79
+
80
+ const result = await executePresetCommand(context, 'list');
81
+
82
+ expect(result.success).toBe(true);
83
+ expect(result.message).toContain('Available presets:');
84
+ });
85
+
86
+ it('gives two hosts in ONE process their own presets — the property a global makes impossible', async () => {
87
+ const first = hostWithPresets(['acme-reviewer']);
88
+ const second = hostWithPresets(['globex-auditor']);
89
+
90
+ const firstList = await executePresetCommand(first.context, 'list');
91
+ const secondList = await executePresetCommand(second.context, 'list');
92
+
93
+ expect(firstList.message).toContain('acme-reviewer');
94
+ expect(firstList.message).not.toContain('globex-auditor');
95
+ expect(secondList.message).toContain('globex-auditor');
96
+ expect(secondList.message).not.toContain('acme-reviewer');
97
+ });
98
+ });
@@ -1,10 +1,19 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { listPresets } from '@robota-sdk/agent-preset';
3
- import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
2
+ import { createPresetRegistry } from '@robota-sdk/agent-preset';
3
+ import type {
4
+ ICommandHostContext,
5
+ ICommandSessionRuntime,
6
+ IUnknownCommandModuleName,
7
+ } from '@robota-sdk/agent-framework';
4
8
  import { createPresetCommandModule, executePresetCommand } from '../index.js';
5
-
6
- function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>): ICommandSessionRuntime {
7
- return {
9
+ import { createTestSessionRuntime } from '@robota-sdk/agent-framework/testing';
10
+ import {
11
+ createTestCommandHost,
12
+ type ICreateTestCommandHostOptions,
13
+ } from '@robota-sdk/agent-framework/testing';
14
+
15
+ function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>) {
16
+ return createTestSessionRuntime({
8
17
  clearHistory: () => undefined,
9
18
  compact: async () => undefined,
10
19
  getContextState: () => ({
@@ -22,59 +31,61 @@ function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>): ICom
22
31
  getFullHistory: () => [],
23
32
  getHistory: () => [],
24
33
  ...overrides,
25
- };
34
+ });
26
35
  }
27
36
 
28
37
  function createCommandHostContext(
29
38
  runtime: ICommandSessionRuntime,
30
- overrides?: Partial<ICommandHostContext>,
31
- ): ICommandHostContext {
32
- return {
33
- getSession: () => runtime,
34
- getContextState: () => ({
35
- maxTokens: 100,
36
- usedTokens: 10,
37
- usedPercentage: 10,
38
- remainingPercentage: 90,
39
- }),
40
- getAutoCompactThreshold: () => 0.8,
41
- compactContext: async () => undefined,
42
- getCwd: () => '/workspace',
43
- listEditCheckpoints: () => [],
44
- restoreEditCheckpoint: async () => ({
45
- target: {
46
- id: 'checkpoint_1',
47
- sessionId: 'session_1',
48
- sequence: 1,
49
- prompt: 'edit',
50
- createdAt: '2026-05-03T00:00:00.000Z',
51
- fileCount: 0,
52
- },
53
- restoredCheckpointCount: 1,
54
- restoredFileCount: 0,
55
- removedCheckpointCount: 0,
56
- }),
57
- rollbackEditCheckpoint: async () => ({
58
- target: {
59
- id: 'checkpoint_1',
60
- sessionId: 'session_1',
61
- sequence: 1,
62
- prompt: 'edit',
63
- createdAt: '2026-05-03T00:00:00.000Z',
64
- fileCount: 0,
65
- },
66
- restoredCheckpointCount: 1,
67
- restoredFileCount: 0,
68
- removedCheckpointCount: 0,
69
- }),
70
- getUsedMemoryReferences: () => [],
71
- recordMemoryEvent: () => undefined,
72
- listBackgroundTasks: () => [],
73
- readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
74
- cancelBackgroundTask: async () => undefined,
75
- closeBackgroundTask: async () => undefined,
76
- ...overrides,
77
- };
39
+ overrides?: ICreateTestCommandHostOptions['overrides'],
40
+ ) {
41
+ return createTestCommandHost({
42
+ overrides: {
43
+ getSession: () => runtime,
44
+ getContextState: () => ({
45
+ maxTokens: 100,
46
+ usedTokens: 10,
47
+ usedPercentage: 10,
48
+ remainingPercentage: 90,
49
+ }),
50
+ getAutoCompactThreshold: () => 0.8,
51
+ compactContext: async () => undefined,
52
+ getCwd: () => '/workspace',
53
+ listEditCheckpoints: () => [],
54
+ restoreEditCheckpoint: async () => ({
55
+ target: {
56
+ id: 'checkpoint_1',
57
+ sessionId: 'session_1',
58
+ sequence: 1,
59
+ prompt: 'edit',
60
+ createdAt: '2026-05-03T00:00:00.000Z',
61
+ fileCount: 0,
62
+ },
63
+ restoredCheckpointCount: 1,
64
+ restoredFileCount: 0,
65
+ removedCheckpointCount: 0,
66
+ }),
67
+ rollbackEditCheckpoint: async () => ({
68
+ target: {
69
+ id: 'checkpoint_1',
70
+ sessionId: 'session_1',
71
+ sequence: 1,
72
+ prompt: 'edit',
73
+ createdAt: '2026-05-03T00:00:00.000Z',
74
+ fileCount: 0,
75
+ },
76
+ restoredCheckpointCount: 1,
77
+ restoredFileCount: 0,
78
+ removedCheckpointCount: 0,
79
+ }),
80
+ getUsedMemoryReferences: () => [],
81
+ recordMemoryEvent: () => undefined,
82
+ listBackgroundTasks: () => [],
83
+ readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
84
+ cancelBackgroundTask: async () => undefined,
85
+ closeBackgroundTask: async () => undefined,
86
+ ...overrides,
87
+ },
88
+ });
78
89
  }
79
90
 
80
91
  describe('preset command module', () => {
@@ -90,7 +101,7 @@ describe('preset command module', () => {
90
101
  const result = await executePresetCommand(context, '');
91
102
 
92
103
  expect(result.success).toBe(true);
93
- for (const preset of listPresets()) {
104
+ for (const preset of createPresetRegistry().listPresets()) {
94
105
  expect(result.message).toContain(preset.id);
95
106
  }
96
107
  // The active preset is marked with the `* ` prefix.
@@ -130,7 +141,7 @@ describe('preset command module', () => {
130
141
  const result = await executePresetCommand(context, '__nope__');
131
142
 
132
143
  expect(result.success).toBe(false);
133
- for (const preset of listPresets()) {
144
+ for (const preset of createPresetRegistry().listPresets()) {
134
145
  expect(result.message).toContain(preset.id);
135
146
  }
136
147
  expect(setActivePresetId).not.toHaveBeenCalled();
@@ -171,4 +182,39 @@ describe('preset command module', () => {
171
182
  expect(result.message).toContain('Available presets:');
172
183
  expect(setActivePresetId).not.toHaveBeenCalled();
173
184
  });
185
+
186
+ describe('INFRA-032: unmatched command-module names surface in the /preset result', () => {
187
+ it('a preset disabling an unknown module name reports it (no longer silent)', async () => {
188
+ // ARCH-009: the external preset reaches `/preset` through the HOST, not through a process-wide
189
+ // registration this case used to perform and then have to undo in an afterEach. This is the
190
+ // seam the item added, exercised by a case that already needed an external preset.
191
+ const registry = createPresetRegistry([
192
+ {
193
+ id: 'ext-unknown-module',
194
+ title: 'Ext Unknown Module',
195
+ description: 'disables a short-form command module name',
196
+ disabledCommandModules: ['editor'],
197
+ },
198
+ ]);
199
+ const unknowns: readonly IUnknownCommandModuleName[] = [{ name: 'editor', kind: 'disabled' }];
200
+ const applyCommandModuleSelection = vi.fn(() => unknowns);
201
+ const runtime = createSessionRuntime({ setActivePresetId: vi.fn() });
202
+ const context = createCommandHostContext(runtime, {
203
+ applyCommandModuleSelection,
204
+ getCommandHostAdapters: () => ({ presetRegistry: registry }),
205
+ });
206
+
207
+ const result = await executePresetCommand(context, 'ext-unknown-module');
208
+
209
+ expect(applyCommandModuleSelection).toHaveBeenCalledWith(undefined, ['editor']);
210
+ // Non-fatal: the switch still succeeds…
211
+ expect(result.success).toBe(true);
212
+ expect(result.message).toContain('Switched to preset: ext-unknown-module');
213
+ // …and the unmatched name is surfaced in the command result (message + data).
214
+ expect(result.message).toContain(
215
+ 'Preset command-module "editor" (disabled) matched no module',
216
+ );
217
+ expect(result.data?.unknownCommandModules).toEqual(unknowns);
218
+ });
219
+ });
174
220
  });
@@ -1,20 +1,37 @@
1
- import { listPresets } from '@robota-sdk/agent-preset';
1
+ import { createPresetRegistry } from '@robota-sdk/agent-preset';
2
2
 
3
3
  import { executePresetCommand } from './preset-command.js';
4
4
 
5
5
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
6
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
6
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
7
7
 
8
8
  const PRESET_COMMAND_DESCRIPTION = 'List presets or switch the active preset';
9
9
  const PRESET_ARGUMENT_HINT = 'list | <preset-id>';
10
10
 
11
- /** Build one subcommand per registered preset id (mirrors the permission-mode subcommands). */
11
+ /**
12
+ * Build one subcommand per registered preset id (mirrors the permission-mode subcommands).
13
+ *
14
+ * ARCH-009 LIMIT, stated rather than left implicit: this lists the BUILT-INS, because it builds a
15
+ * STATIC catalog entry at module-construction time and has no `ICommandHostContext` to reach a
16
+ * host-supplied registry. `/preset`'s own execution — listing, lookup, resolution — goes through the
17
+ * host's registry, so an embedded host with its own presets resolves and applies correctly; only the
18
+ * pre-computed subcommand hints for tab-completion omit that host's external presets.
19
+ *
20
+ * What ARCH-009 changed here is what the limit COSTS. This used to read a process-wide mutable
21
+ * registry, so the hints a host saw depended on which other host had loaded presets first. They are
22
+ * now the built-ins and nothing else — wrong the same way for everyone, and never someone else's.
23
+ *
24
+ * Closing it needs the catalog to be built per host rather than per module, which is a change to how
25
+ * command modules are constructed and not to this file. Filed separately.
26
+ */
12
27
  function buildPresetSubcommands(source = 'preset'): ICommand[] {
13
- return listPresets().map((preset) => ({
14
- name: preset.id,
15
- description: preset.description,
16
- source,
17
- }));
28
+ return createPresetRegistry()
29
+ .listPresets()
30
+ .map((preset) => ({
31
+ name: preset.id,
32
+ description: preset.description,
33
+ source,
34
+ }));
18
35
  }
19
36
 
20
37
  export function createPresetCommandEntry(): ICommand {
@@ -25,6 +42,7 @@ export function createPresetCommandEntry(): ICommand {
25
42
  source: 'preset',
26
43
  argumentHint: PRESET_ARGUMENT_HINT,
27
44
  subcommands: buildPresetSubcommands('preset'),
45
+ // User-only: a preset swaps tools, commands and permission posture; the user chooses it.
28
46
  modelInvocable: false,
29
47
  };
30
48
  }