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

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 (201) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +77 -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 +77 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +39 -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-module.test.ts +24 -0
  87. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  88. package/src/handoff/handoff-command-module.ts +49 -0
  89. package/src/handoff/handoff-command.ts +187 -0
  90. package/src/handoff/index.ts +6 -0
  91. package/src/help/__tests__/help-command-module.test.ts +63 -40
  92. package/src/help/__tests__/help-command.test.ts +46 -37
  93. package/src/help/help-command-module.ts +2 -1
  94. package/src/help/help-command.ts +3 -3
  95. package/src/index.ts +124 -2
  96. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  97. package/src/keybindings/index.ts +6 -0
  98. package/src/keybindings/keybindings-command-module.ts +74 -0
  99. package/src/language/__tests__/language-command-module.test.ts +33 -28
  100. package/src/language/language-command-module.ts +2 -1
  101. package/src/language/language-command.ts +8 -6
  102. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  103. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  104. package/src/mcp-activation/index.ts +12 -0
  105. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  106. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  107. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  108. package/src/mcp-activation/mcp-model-status.ts +99 -0
  109. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  110. package/src/memory/memory-command-module.ts +18 -1
  111. package/src/memory/memory-command.ts +38 -38
  112. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  113. package/src/mode/mode-command-module.ts +2 -1
  114. package/src/mode/mode-command.ts +15 -7
  115. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  116. package/src/output-style/index.ts +6 -0
  117. package/src/output-style/output-style-command-module.ts +143 -0
  118. package/src/peers/__tests__/peers-command.test.ts +321 -0
  119. package/src/peers/index.ts +9 -0
  120. package/src/peers/peers-command-module.ts +49 -0
  121. package/src/peers/peers-command.ts +191 -0
  122. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  123. package/src/permissions/permissions-command-module.ts +2 -1
  124. package/src/permissions/permissions-command.ts +46 -4
  125. package/src/plan/__tests__/plan-command.test.ts +129 -0
  126. package/src/plan/index.ts +9 -0
  127. package/src/plan/plan-command-module.ts +49 -0
  128. package/src/plan/plan-command.ts +92 -0
  129. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  130. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  131. package/src/plugin/plugin-command-module.ts +3 -1
  132. package/src/plugin/plugin-command.ts +17 -16
  133. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  134. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  135. package/src/preset/preset-command-module.ts +26 -8
  136. package/src/preset/preset-command.ts +85 -27
  137. package/src/provider/__tests__/org-policy.test.ts +4 -5
  138. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  139. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  140. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  141. package/src/provider/provider-command-execution.ts +4 -4
  142. package/src/provider/provider-command-module.ts +2 -1
  143. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  144. package/src/provider/provider-command-profile-operations.ts +3 -3
  145. package/src/provider/provider-command-profile.ts +1 -1
  146. package/src/provider/provider-command-setup.ts +4 -4
  147. package/src/provider/provider-startup.ts +62 -35
  148. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  149. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  150. package/src/remote-control/index.ts +8 -0
  151. package/src/remote-control/remote-control-command-module.ts +61 -0
  152. package/src/remote-control/remote-control-command.ts +138 -0
  153. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  154. package/src/reset/reset-command-module.ts +2 -1
  155. package/src/reset/reset-command.ts +3 -4
  156. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  157. package/src/rewind/rewind-command-module.ts +2 -1
  158. package/src/rewind/rewind-command.ts +73 -6
  159. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  160. package/src/sandbox/index.ts +2 -0
  161. package/src/sandbox/sandbox-command-module.ts +56 -0
  162. package/src/sandbox/sandbox-command.ts +94 -0
  163. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  164. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  165. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  166. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  167. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  168. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  169. package/src/schedule/index.ts +3 -0
  170. package/src/schedule/loop-command.ts +333 -0
  171. package/src/schedule/loop-jitter.ts +41 -0
  172. package/src/schedule/schedule-command-module.ts +92 -14
  173. package/src/schedule/schedule-command.ts +98 -9
  174. package/src/schedule/schedule-spec-parser.ts +5 -1
  175. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  176. package/src/session/__tests__/session-command-module.test.ts +210 -45
  177. package/src/session/session-command-module.ts +68 -37
  178. package/src/session/session-command.ts +147 -80
  179. package/src/settings/settings-command-module.ts +3 -2
  180. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  181. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  182. package/src/shell/resolve-shell.ts +2 -2
  183. package/src/shell/shell-command-module.ts +6 -5
  184. package/src/shell/shell-command.ts +9 -5
  185. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  186. package/src/skills/index.ts +1 -0
  187. package/src/skills/skills-command-module.ts +22 -5
  188. package/src/skills/skills-command.ts +14 -13
  189. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  190. package/src/statusline/statusline-command-module.ts +2 -1
  191. package/src/statusline/statusline-command.ts +4 -4
  192. package/src/theme/__tests__/theme-command.test.ts +204 -0
  193. package/src/theme/index.ts +12 -0
  194. package/src/theme/theme-command-module.ts +211 -0
  195. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  196. package/src/user-local/user-local-command-constants.ts +1 -1
  197. package/src/user-local/user-local-command-module.ts +6 -5
  198. package/src/user-local/user-local-command.ts +12 -6
  199. package/src/user-local/user-local-memory-command.ts +17 -7
  200. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  201. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -6,24 +6,28 @@
6
6
  import { resolveShell } from './resolve-shell.js';
7
7
  import { spawnInherited } from './spawn-inherited.js';
8
8
 
9
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
10
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
9
+ import type {
10
+ ICommandHostTerminalHandoff,
11
+ ICommandHostWorkspace,
12
+ } from '@robota-sdk/agent-framework';
13
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
11
14
 
12
15
  export const SHELL_COMMAND_DESCRIPTION =
13
16
  'Drop to an interactive shell (or run `/shell <command>` interactively), then return to the agent.';
14
17
 
15
18
  export async function executeShellCommand(
16
- context: ICommandHostContext,
19
+ context: ICommandHostTerminalHandoff & ICommandHostWorkspace,
17
20
  args: string,
21
+ executable?: string,
18
22
  ): Promise<ICommandResult> {
19
- if (context.canHandoffTerminal?.() !== true || context.runWithTerminal === undefined) {
23
+ if (!context.canHandoffTerminal()) {
20
24
  return {
21
25
  message: 'An interactive shell is unavailable here (no interactive terminal).',
22
26
  success: false,
23
27
  };
24
28
  }
25
29
 
26
- const shell = resolveShell();
30
+ const shell = resolveShell(executable);
27
31
  const cwd = context.getCwd();
28
32
  const command = args.trim();
29
33
 
@@ -1,10 +1,38 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { InteractiveSession, SystemCommandExecutor } from '@robota-sdk/agent-framework';
6
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
5
+ import {
6
+ createNodeHostContributionSource,
7
+ InteractiveSession,
8
+ SystemCommandExecutor,
9
+ WorkspaceTrustService,
10
+ } from '@robota-sdk/agent-framework';
7
11
  import { createSkillsCommandModule } from '../skills-command-module.js';
12
+ import {
13
+ createTestCommandHost,
14
+ type ICreateTestCommandHostOptions,
15
+ } from '@robota-sdk/agent-framework/testing';
16
+
17
+ import type { IWorkspaceIdentity, IWorkspaceTrustStore } from '@robota-sdk/agent-framework';
18
+
19
+ async function createTrustedProjectAccess(cwd: string) {
20
+ const root = realpathSync(cwd);
21
+ const identity: IWorkspaceIdentity = {
22
+ repositoryKey: `skills-command-test:${root}`,
23
+ displayPath: root,
24
+ worktreeRoot: root,
25
+ };
26
+ const store: IWorkspaceTrustStore = {
27
+ inspect: () => Promise.resolve({ state: 'trusted', generation: 1 }),
28
+ grant: () => Promise.resolve({ state: 'trusted', generation: 1 }),
29
+ revoke: () => Promise.resolve({ state: 'revoked', generation: 2 }),
30
+ };
31
+ return new WorkspaceTrustService({
32
+ identityResolver: { resolve: () => identity },
33
+ store,
34
+ }).inspect(root);
35
+ }
8
36
 
9
37
  function createTempSkill(cwd: string): void {
10
38
  const skillDir = join(cwd, '.agents', 'skills', 'audit');
@@ -28,34 +56,39 @@ function makeParentSession() {
28
56
  }),
29
57
  injectMessage: vi.fn(),
30
58
  getSessionId: vi.fn().mockReturnValue('parent-session-id'),
59
+ // SELFHOST-004 P6: the span collector subscribes to the session bus each turn.
60
+ getEventService: vi.fn().mockReturnValue({ subscribe: vi.fn(), unsubscribe: vi.fn() }),
31
61
  getSystemMessage: vi.fn().mockReturnValue('# system'),
32
62
  getToolSchemas: vi.fn().mockReturnValue([]),
33
63
  };
34
64
  }
35
65
 
36
- function createMockContext(overrides?: Partial<ICommandHostContext>): ICommandHostContext {
37
- return {
38
- getSession: vi.fn(),
39
- getContextState: vi.fn(),
40
- getAutoCompactThreshold: vi.fn().mockReturnValue(0.835),
41
- compactContext: vi.fn(),
42
- getCwd: vi.fn().mockReturnValue('/workspace'),
43
- listEditCheckpoints: vi.fn().mockReturnValue([]),
44
- restoreEditCheckpoint: vi.fn(),
45
- rollbackEditCheckpoint: vi.fn(),
46
- getUsedMemoryReferences: vi.fn().mockReturnValue([]),
47
- recordMemoryEvent: vi.fn(),
48
- listBackgroundTasks: vi.fn().mockReturnValue([]),
49
- readBackgroundTaskLog: vi.fn(),
50
- cancelBackgroundTask: vi.fn(),
51
- closeBackgroundTask: vi.fn(),
52
- ...overrides,
53
- } as ICommandHostContext;
66
+ function createMockContext(overrides?: ICreateTestCommandHostOptions['overrides']) {
67
+ // ARCH-029: overrides over a conformant host rather than a 20-member literal asserting it IS one.
68
+ return createTestCommandHost({
69
+ overrides: {
70
+ getSession: vi.fn(),
71
+ getContextState: vi.fn(),
72
+ getAutoCompactThreshold: vi.fn().mockReturnValue(0.835),
73
+ compactContext: vi.fn(),
74
+ getCwd: vi.fn().mockReturnValue('/workspace'),
75
+ listEditCheckpoints: vi.fn().mockReturnValue([]),
76
+ restoreEditCheckpoint: vi.fn(),
77
+ rollbackEditCheckpoint: vi.fn(),
78
+ getUsedMemoryReferences: vi.fn().mockReturnValue([]),
79
+ recordMemoryEvent: vi.fn(),
80
+ listBackgroundTasks: vi.fn().mockReturnValue([]),
81
+ readBackgroundTaskLog: vi.fn(),
82
+ cancelBackgroundTask: vi.fn(),
83
+ closeBackgroundTask: vi.fn(),
84
+ ...overrides,
85
+ },
86
+ });
54
87
  }
55
88
 
56
89
  describe('createSkillsCommandModule', () => {
57
90
  it('exposes skills as a normal model-invocable command module', () => {
58
- const module = createSkillsCommandModule({ cwd: '/workspace' });
91
+ const module = createSkillsCommandModule({ contributionSources: [] });
59
92
  const command = module.systemCommands?.[0];
60
93
 
61
94
  expect(module.name).toBe('agent-command-skills');
@@ -64,18 +97,19 @@ describe('createSkillsCommandModule', () => {
64
97
  ]);
65
98
  expect(command).toMatchObject({
66
99
  name: 'skills',
100
+ semanticRole: 'skillActivation',
67
101
  userInvocable: true,
68
102
  modelInvocable: true,
69
103
  lifecycle: 'inline',
70
104
  argumentHint: '[list | <skill-name> [args]]',
71
105
  safety: 'read-only',
72
106
  });
73
- expect(command?.description).toContain('projected skills command tool');
107
+ expect(command?.modelDescription).toContain('projected skills command tool');
74
108
  });
75
109
 
76
110
  it('lists skill metadata from the SDK host context', async () => {
77
111
  const executor = new SystemCommandExecutor([
78
- ...(createSkillsCommandModule({ cwd: '/workspace' }).systemCommands ?? []),
112
+ ...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
79
113
  ]);
80
114
 
81
115
  const result = await executor.execute(
@@ -109,7 +143,7 @@ describe('createSkillsCommandModule', () => {
109
143
  data: { skill: 'repo-writing' },
110
144
  });
111
145
  const executor = new SystemCommandExecutor([
112
- ...(createSkillsCommandModule({ cwd: '/workspace' }).systemCommands ?? []),
146
+ ...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
113
147
  ]);
114
148
 
115
149
  const result = await executor.execute(
@@ -130,13 +164,21 @@ describe('createSkillsCommandModule', () => {
130
164
  });
131
165
 
132
166
  it('lets the SDK normalize virtual /skill-name commands into the composed skills command', async () => {
133
- const cwd = mkdtempSync(join(tmpdir(), 'robota-skills-command-module-'));
167
+ const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'robota-skills-command-module-')));
134
168
  createTempSkill(cwd);
135
169
  const parentSession = makeParentSession();
136
170
  const session = new InteractiveSession({
137
171
  session: parentSession as never,
138
172
  cwd,
139
- commandModules: [createSkillsCommandModule({ cwd })],
173
+ projectAccess: await createTrustedProjectAccess(cwd),
174
+ contributionSources: [createNodeHostContributionSource(cwd)],
175
+ skillRoots: [{ root: join('.agents', 'skills'), kind: 'skills' }],
176
+ commandModules: [
177
+ createSkillsCommandModule({
178
+ contributionSources: [createNodeHostContributionSource(cwd)],
179
+ skillRoots: [{ root: join('.agents', 'skills'), kind: 'skills' }],
180
+ }),
181
+ ],
140
182
  });
141
183
 
142
184
  const result = await session.executeCommand('audit', 'src/index.ts');
@@ -147,7 +189,6 @@ describe('createSkillsCommandModule', () => {
147
189
  skill: 'audit',
148
190
  sessionExecution: true,
149
191
  }),
150
- effects: [{ type: 'session-execution-started' }],
151
192
  });
152
193
  expect(parentSession.run).toHaveBeenCalledWith(
153
194
  expect.stringContaining('Audit src/index.ts'),
@@ -3,4 +3,5 @@ export {
3
3
  createSkillsCommandModule,
4
4
  SkillsCommandSource,
5
5
  } from './skills-command-module.js';
6
+ export type { ISkillsCommandModuleOptions } from './skills-command-module.js';
6
7
  export { executeSkillsCommand, SKILLS_COMMAND_DESCRIPTION } from './skills-command.js';
@@ -1,12 +1,22 @@
1
1
  import { SkillCommandSource } from '@robota-sdk/agent-framework';
2
2
 
3
- import { executeSkillsCommand, SKILLS_COMMAND_DESCRIPTION } from './skills-command.js';
3
+ import {
4
+ executeSkillsCommand,
5
+ SKILLS_COMMAND_DESCRIPTION,
6
+ SKILLS_COMMAND_MODEL_DESCRIPTION,
7
+ } from './skills-command.js';
4
8
 
5
- import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
6
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
9
+ import type {
10
+ ICommandModule,
11
+ IContributionSource,
12
+ ISystemCommand,
13
+ ISkillRootDescriptor,
14
+ } from '@robota-sdk/agent-framework';
15
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
7
16
 
8
17
  export interface ISkillsCommandModuleOptions {
9
- readonly cwd: string;
18
+ readonly contributionSources: readonly IContributionSource[];
19
+ readonly skillRoots?: readonly ISkillRootDescriptor[];
10
20
  }
11
21
 
12
22
  export function createSkillsCommandEntry(): ICommand {
@@ -14,6 +24,9 @@ export function createSkillsCommandEntry(): ICommand {
14
24
  name: 'skills',
15
25
  displayName: 'Skills',
16
26
  description: SKILLS_COMMAND_DESCRIPTION,
27
+ // Model-invocable: activating a skill is how the model follows one; a skill's own
28
+ // `disable-model-invocation` frontmatter still refuses it per skill.
29
+ modelDescription: SKILLS_COMMAND_MODEL_DESCRIPTION,
17
30
  source: 'skills',
18
31
  modelInvocable: true,
19
32
  userInvocable: true,
@@ -26,6 +39,8 @@ function createSkillsSystemCommand(): ISystemCommand {
26
39
  const entry = createSkillsCommandEntry();
27
40
  return {
28
41
  name: entry.name,
42
+ semanticRole: 'skillActivation',
43
+ ...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
29
44
  displayName: entry.displayName,
30
45
  description: entry.description,
31
46
  requiresPermission: false,
@@ -48,7 +63,9 @@ export class SkillsCommandSource implements ICommandSource {
48
63
 
49
64
  export function createSkillsCommandModule(options: ISkillsCommandModuleOptions): ICommandModule {
50
65
  const commandSources: ICommandSource[] = [new SkillsCommandSource()];
51
- commandSources.push(new SkillCommandSource(options.cwd));
66
+ commandSources.push(
67
+ new SkillCommandSource(options.contributionSources, options.skillRoots ?? []),
68
+ );
52
69
 
53
70
  return {
54
71
  name: 'agent-command-skills',
@@ -1,8 +1,15 @@
1
- import type { ICommandHostContext, ICommandSkillListEntry } from '@robota-sdk/agent-framework';
2
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
1
+ import type {
2
+ ICommandHostCatalog,
3
+ ICommandHostWorkspace,
4
+ ICommandSkillListEntry,
5
+ } from '@robota-sdk/agent-framework';
6
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
3
7
 
4
- export const SKILLS_COMMAND_DESCRIPTION =
5
- 'Skill command. Before following a matching registered skill from the system prompt Skills section, invoke the projected skills command tool with args "<skill-name> [args]". Without arguments, list registered skills. With a skill name, activate that skill. Slash syntax is a UI input/display concern; the SDK command identity is "skills".';
8
+ export const SKILLS_COMMAND_DESCRIPTION = 'List registered skills, or activate one by name';
9
+
10
+ /** What the model is told about `skills`; `SKILLS_COMMAND_DESCRIPTION` is the `/help` line. */
11
+ export const SKILLS_COMMAND_MODEL_DESCRIPTION =
12
+ 'Skill command. Before following a matching registered skill from the system prompt Skills section, invoke the projected skills command tool with args "<skill-name> [args]". Without arguments, list registered skills. With a skill name, activate that skill; a skill that declares it is not model-invocable is refused. Returns the skill list with each skill\'s description, or the activated skill\'s instructions to follow. Slash syntax is a UI input/display concern; the SDK command identity is "skills".';
6
13
 
7
14
  interface IParsedSkillsArgs {
8
15
  readonly action: 'list' | 'activate';
@@ -66,20 +73,14 @@ function parseSkillsArgs(args: string): IParsedSkillsArgs {
66
73
  }
67
74
 
68
75
  export async function executeSkillsCommand(
69
- context: ICommandHostContext,
76
+ context: ICommandHostCatalog & ICommandHostWorkspace,
70
77
  args = '',
71
78
  ): Promise<ICommandResult> {
72
79
  const parsed = parseSkillsArgs(args);
73
80
  if (parsed.action === 'activate' && parsed.skillName !== undefined) {
74
- if (!context.executeSkillCommandByName) {
75
- return {
76
- success: false,
77
- message: 'Skill activation is not available in this session.',
78
- };
79
- }
80
81
  const displayInput = `/${parsed.skillName}${parsed.skillArgs ? ` ${parsed.skillArgs}` : ''}`;
81
82
  const result = await context.executeSkillCommandByName(parsed.skillName, parsed.skillArgs, {
82
- invocationSource: context.getCommandInvocationSource?.() ?? 'user',
83
+ invocationSource: context.getCommandInvocationSource(),
83
84
  displayInput,
84
85
  rawInput: displayInput,
85
86
  });
@@ -91,7 +92,7 @@ export async function executeSkillsCommand(
91
92
  );
92
93
  }
93
94
 
94
- const skills = context.listSkills?.() ?? [];
95
+ const skills = context.listSkills();
95
96
  return {
96
97
  success: true,
97
98
  message: formatSkillsMessage(skills),
@@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
2
2
  import type { ICommandHostContext } from '@robota-sdk/agent-framework';
3
3
  import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
4
4
  import { createStatusLineCommandModule } from '../statusline-command-module.js';
5
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
5
6
 
6
- const COMMAND_CONTEXT = {} as ICommandHostContext;
7
+ const COMMAND_CONTEXT = createTestCommandHost();
7
8
 
8
9
  describe('createStatusLineCommandModule', () => {
9
10
  it('provides statusline metadata and user-only executable command from one module owner', () => {
@@ -76,12 +77,12 @@ describe('createStatusLineCommandModule', () => {
76
77
  expect(result).toEqual({
77
78
  success: true,
78
79
  message,
79
- effects: [{ type: 'statusline-settings-patch', patch }],
80
+ hostActions: [{ type: 'statusline-settings-patch', patch }],
80
81
  });
81
82
  },
82
83
  );
83
84
 
84
- it('reports usage for unsupported arguments without effects', async () => {
85
+ it('reports usage for unsupported arguments without host actions', async () => {
85
86
  const executor = new SystemCommandExecutor([
86
87
  ...(createStatusLineCommandModule().systemCommands ?? []),
87
88
  ]);
@@ -90,6 +91,6 @@ describe('createStatusLineCommandModule', () => {
90
91
 
91
92
  expect(result?.success).toBe(false);
92
93
  expect(result?.message).toContain('Usage: /statusline');
93
- expect(result?.effects).toBeUndefined();
94
+ expect(result?.hostActions).toBeUndefined();
94
95
  });
95
96
  });
@@ -7,7 +7,7 @@ import {
7
7
  import { executeStatusLineCommand } from './statusline-command.js';
8
8
 
9
9
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
11
11
 
12
12
  export function createStatusLineCommandEntry(): ICommand {
13
13
  return {
@@ -17,6 +17,7 @@ export function createStatusLineCommandEntry(): ICommand {
17
17
  source: 'statusline',
18
18
  argumentHint: STATUSLINE_COMMAND_ARGUMENT_HINT,
19
19
  subcommands: buildStatusLineCommandSubcommands('statusline'),
20
+ // User-only: UI preference.
20
21
  modelInvocable: false,
21
22
  };
22
23
  }
@@ -1,10 +1,10 @@
1
1
  import { DEFAULT_STATUS_LINE_COMMAND_SETTINGS } from '@robota-sdk/agent-framework';
2
2
 
3
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
3
+ import type { ICommandHostNoCapability } from '@robota-sdk/agent-framework';
4
4
  import type {
5
5
  ICommandResult,
6
6
  TStatusLineCommandSettingsPatch,
7
- } from '@robota-sdk/agent-interface-transport';
7
+ } from '@robota-sdk/agent-interface-command';
8
8
 
9
9
  interface IStatusLineCommandSuccessAction {
10
10
  success: true;
@@ -64,7 +64,7 @@ function parseStatusLineArgs(args: string): TStatusLineCommandAction {
64
64
  }
65
65
 
66
66
  export function executeStatusLineCommand(
67
- _context: ICommandHostContext,
67
+ _context: ICommandHostNoCapability,
68
68
  args: string,
69
69
  ): ICommandResult {
70
70
  const action = parseStatusLineArgs(args);
@@ -75,6 +75,6 @@ export function executeStatusLineCommand(
75
75
  return {
76
76
  success: true,
77
77
  message: action.message,
78
- effects: [{ type: 'statusline-settings-patch', patch: action.patch }],
78
+ hostActions: [{ type: 'statusline-settings-patch', patch: action.patch }],
79
79
  };
80
80
  }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * SCREEN-2002 TC-09 — `/theme`.
3
+ *
4
+ * Three properties carry this command. It emits EXACTLY ONE patch per invocation, so a submission
5
+ * cannot half-apply. An unknown id writes NOTHING — not the theme, and not the toggles submitted
6
+ * beside it — because a user who mistyped an id did not also ask for the rest. And `motion on`
7
+ * means `reducedMotion: false`: the inversion is the easy thing to read backwards, and a user
8
+ * asking for motion and getting stillness would look like the command doing nothing at all.
9
+ */
10
+ import { describe, expect, it } from 'vitest';
11
+
12
+ import { executeThemeCommand } from '../theme-command-module.js';
13
+
14
+ import type { TAppearanceSettingsPatch } from '@robota-sdk/agent-framework';
15
+ import type {
16
+ IThemeAppearanceState,
17
+ IThemeCatalogueEntry,
18
+ IThemeCataloguePort,
19
+ TReducedMotionOverride,
20
+ } from '@robota-sdk/agent-interface-command';
21
+
22
+ const THEMES: IThemeCatalogueEntry[] = [
23
+ { id: 'dark', name: 'Dark', appearance: 'dark', source: 'built-in' },
24
+ { id: 'light', name: 'Light', appearance: 'light', source: 'built-in' },
25
+ {
26
+ id: 'dark-daltonized',
27
+ name: 'Dark (daltonized)',
28
+ appearance: 'dark',
29
+ source: 'built-in',
30
+ },
31
+ ];
32
+
33
+ function catalogue(
34
+ state: Partial<IThemeAppearanceState['settings']> = {},
35
+ reducedMotionOverride?: TReducedMotionOverride,
36
+ reducedMotionForRun?: boolean,
37
+ ): IThemeCataloguePort {
38
+ const settings = {
39
+ theme: 'dark',
40
+ syntaxHighlighting: true,
41
+ reducedMotion: false,
42
+ ...state,
43
+ };
44
+ return {
45
+ listThemes: () => THEMES,
46
+ getTheme: (id) => THEMES.find((theme) => theme.id === id),
47
+ getAppearance: () => ({
48
+ settings,
49
+ ...(reducedMotionOverride === undefined
50
+ ? {}
51
+ : {
52
+ reducedMotionPin: {
53
+ tier: reducedMotionOverride,
54
+ reducedMotion: reducedMotionForRun ?? true,
55
+ },
56
+ }),
57
+ }),
58
+ };
59
+ }
60
+
61
+ function patchOf(result: { hostActions?: readonly unknown[] }): TAppearanceSettingsPatch {
62
+ expect(result.hostActions).toHaveLength(1);
63
+ const action = result.hostActions?.[0] as {
64
+ type: string;
65
+ patch: TAppearanceSettingsPatch;
66
+ };
67
+ expect(action.type).toBe('appearance-settings-patch');
68
+ return action.patch;
69
+ }
70
+
71
+ describe('/theme without a catalogue (SCREEN-2002 TC-09)', () => {
72
+ it('reports its unavailability instead of failing silently or writing anything', () => {
73
+ const result = executeThemeCommand(undefined, 'light');
74
+
75
+ expect(result.success).toBe(false);
76
+ expect(result.message).toBe('Themes are not available in this environment.');
77
+ expect(result.hostActions).toBeUndefined();
78
+ });
79
+ });
80
+
81
+ describe('/theme with no argument (SCREEN-2002 TC-09)', () => {
82
+ it('asks the surface for the picker rather than changing anything', () => {
83
+ const result = executeThemeCommand(catalogue(), ' ');
84
+
85
+ expect(result.success).toBe(true);
86
+ expect(result.uiIntents).toEqual([{ type: 'show-theme-picker' }]);
87
+ expect(result.hostActions).toBeUndefined();
88
+ });
89
+ });
90
+
91
+ describe('/theme list (SCREEN-2002 TC-09)', () => {
92
+ it('marks the active theme and names each source, and writes nothing', () => {
93
+ const result = executeThemeCommand(catalogue({ theme: 'light' }), 'list');
94
+
95
+ expect(result.success).toBe(true);
96
+ expect(result.message).toContain('* light — Light (light, built-in)');
97
+ expect(result.message).toContain(' dark — Dark (dark, built-in)');
98
+ expect(result.message).toContain('syntax highlighting: on');
99
+ expect(result.message).toContain('reduced motion: off');
100
+ expect(result.hostActions).toBeUndefined();
101
+ });
102
+
103
+ it('reports what THIS RUN does and what is saved, never one in place of the other', () => {
104
+ // SCREEN-2002 TC-13 found this at the PTY level: reporting the persisted value beside the tier
105
+ // printed `reduced motion: off (… pinned by flag)` on a `--reduced-motion` run — a line that
106
+ // contradicts itself, and that answers "is motion reduced right now" with the wrong word.
107
+ const pinnedOn = executeThemeCommand(catalogue({ reducedMotion: false }, 'flag', true), 'list');
108
+ expect(pinnedOn.message).toContain(
109
+ 'reduced motion: on for this run (pinned by flag; saved off)',
110
+ );
111
+
112
+ // `--no-reduced-motion` is an override too, and it pins the opposite value: a surface that
113
+ // renders the TIER alone cannot tell these two runs apart.
114
+ const pinnedOff = executeThemeCommand(
115
+ catalogue({ reducedMotion: true }, 'flag', false),
116
+ 'list',
117
+ );
118
+ expect(pinnedOff.message).toContain(
119
+ 'reduced motion: off for this run (pinned by flag; saved on)',
120
+ );
121
+ });
122
+ });
123
+
124
+ describe('/theme <id> (SCREEN-2002 TC-09)', () => {
125
+ it('emits exactly one patch naming the theme', () => {
126
+ const result = executeThemeCommand(catalogue(), 'dark-daltonized');
127
+
128
+ expect(result.success).toBe(true);
129
+ expect(patchOf(result)).toEqual({ theme: 'dark-daltonized' });
130
+ expect(result.message).toContain('Dark (daltonized)');
131
+ });
132
+
133
+ it('refuses an unknown id, names the catalogue, and writes NOTHING', () => {
134
+ const result = executeThemeCommand(catalogue(), 'solarized');
135
+
136
+ expect(result.success).toBe(false);
137
+ expect(result.message).toContain('Unknown theme "solarized"');
138
+ expect(result.message).toContain('dark, light, dark-daltonized');
139
+ expect(result.hostActions).toBeUndefined();
140
+ });
141
+
142
+ it('does not apply the toggles submitted beside an unknown id', () => {
143
+ const result = executeThemeCommand(catalogue(), 'solarized syntax off');
144
+
145
+ expect(result.success).toBe(false);
146
+ expect(result.hostActions).toBeUndefined();
147
+ });
148
+
149
+ it('carries the theme and its toggles in ONE patch', () => {
150
+ const result = executeThemeCommand(catalogue(), 'light syntax off motion off');
151
+
152
+ expect(patchOf(result)).toEqual({
153
+ theme: 'light',
154
+ syntaxHighlighting: false,
155
+ reducedMotion: true,
156
+ });
157
+ });
158
+ });
159
+
160
+ describe('the motion inversion (SCREEN-2002 TC-09)', () => {
161
+ it('reads `motion on` as animation, which is reducedMotion FALSE', () => {
162
+ expect(patchOf(executeThemeCommand(catalogue({ reducedMotion: true }), 'motion on'))).toEqual({
163
+ reducedMotion: false,
164
+ });
165
+ });
166
+
167
+ it('reads `motion off` as stillness, which is reducedMotion TRUE', () => {
168
+ expect(patchOf(executeThemeCommand(catalogue(), 'motion off'))).toEqual({
169
+ reducedMotion: true,
170
+ });
171
+ });
172
+
173
+ it('reports the change in the user s words, not the settings key s', () => {
174
+ expect(executeThemeCommand(catalogue(), 'motion off').message).toContain('motion off');
175
+ });
176
+
177
+ it('persists the setting but says this run keeps what pinned it', () => {
178
+ const result = executeThemeCommand(catalogue({ reducedMotion: true }, 'flag'), 'motion on');
179
+
180
+ expect(patchOf(result)).toEqual({ reducedMotion: false });
181
+ expect(result.message).toContain('Saved, but this run keeps reduced motion pinned by flag');
182
+ });
183
+ });
184
+
185
+ describe('/theme syntax (SCREEN-2002 TC-09)', () => {
186
+ it('toggles highlighting without touching the theme', () => {
187
+ expect(patchOf(executeThemeCommand(catalogue(), 'syntax off'))).toEqual({
188
+ syntaxHighlighting: false,
189
+ });
190
+ });
191
+ });
192
+
193
+ describe('a malformed argument (SCREEN-2002 TC-09)', () => {
194
+ it.each(['motion', 'syntax', 'motion yes', 'syntax enable', 'light motion', 'light nonsense on'])(
195
+ 'refuses %o with the usage line rather than guessing',
196
+ (args) => {
197
+ const result = executeThemeCommand(catalogue(), args);
198
+
199
+ expect(result.success).toBe(false);
200
+ expect(result.message).toContain('Usage: /theme');
201
+ expect(result.hostActions).toBeUndefined();
202
+ },
203
+ );
204
+ });
@@ -0,0 +1,12 @@
1
+ export {
2
+ createThemeCommandEntry,
3
+ createThemeCommandModule,
4
+ executeThemeCommand,
5
+ ThemeCommandSource,
6
+ } from './theme-command-module.js';
7
+ export type {
8
+ IThemeAppearanceState,
9
+ IThemeCatalogueEntry,
10
+ IThemeCataloguePort,
11
+ TReducedMotionOverride,
12
+ } from '@robota-sdk/agent-interface-command';