@robota-sdk/agent-command 3.0.0-beta.100

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 (251) hide show
  1. package/CHANGELOG.md +1351 -0
  2. package/LICENSE +661 -0
  3. package/README.md +100 -0
  4. package/dist/node/index.cjs +121 -0
  5. package/dist/node/index.d.cts +1053 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1053 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +122 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +83 -0
  12. package/src/advisor/advisor-command-module.ts +47 -0
  13. package/src/advisor/advisor-command.test.ts +102 -0
  14. package/src/advisor/advisor-command.ts +55 -0
  15. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  16. package/src/advisor/index.ts +6 -0
  17. package/src/agent/__tests__/agent-command.test.ts +597 -0
  18. package/src/agent/agent-command-module.ts +89 -0
  19. package/src/agent/agent-command-parser.ts +180 -0
  20. package/src/agent/agent-command.ts +255 -0
  21. package/src/agent/index.ts +7 -0
  22. package/src/background/__tests__/background-command-module.test.ts +261 -0
  23. package/src/background/background-command-module.ts +52 -0
  24. package/src/background/background-command.ts +65 -0
  25. package/src/background/index.ts +6 -0
  26. package/src/command-module-utils.test.ts +37 -0
  27. package/src/command-module-utils.ts +35 -0
  28. package/src/compact/__tests__/compact-command-module.test.ts +181 -0
  29. package/src/compact/compact-command-module.ts +59 -0
  30. package/src/compact/compact-command.ts +33 -0
  31. package/src/compact/index.ts +6 -0
  32. package/src/context/__tests__/context-command-module.test.ts +502 -0
  33. package/src/context/auto-compact-format.ts +25 -0
  34. package/src/context/context-breakdown.ts +295 -0
  35. package/src/context/context-command-module.ts +82 -0
  36. package/src/context/context-command.ts +291 -0
  37. package/src/context/index.ts +6 -0
  38. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
  39. package/src/default/__tests__/default-command-modules.test.ts +192 -0
  40. package/src/default/__tests__/model-exposure.test.ts +269 -0
  41. package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
  42. package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
  43. package/src/default/default-command-modules.ts +247 -0
  44. package/src/default/index.ts +5 -0
  45. package/src/devices/__tests__/devices-command-module.test.ts +299 -0
  46. package/src/devices/devices-command-module.ts +308 -0
  47. package/src/devices/devices-command-port.ts +143 -0
  48. package/src/devices/index.ts +14 -0
  49. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  50. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  51. package/src/doctor/__tests__/doctor-fixture.ts +146 -0
  52. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  53. package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
  54. package/src/doctor/doctor-command-module.ts +132 -0
  55. package/src/doctor/doctor-extensions-probe.ts +323 -0
  56. package/src/doctor/doctor-node-deps.ts +88 -0
  57. package/src/doctor/doctor-redaction.ts +99 -0
  58. package/src/doctor/doctor-render.ts +70 -0
  59. package/src/doctor/doctor-repair.ts +136 -0
  60. package/src/doctor/doctor-runner.ts +122 -0
  61. package/src/doctor/doctor-settings-probe.ts +239 -0
  62. package/src/doctor/doctor-storage-probe.ts +187 -0
  63. package/src/doctor/doctor-types.ts +109 -0
  64. package/src/doctor/index.ts +32 -0
  65. package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
  66. package/src/editor/editor-command-module.ts +53 -0
  67. package/src/editor/editor-command.ts +58 -0
  68. package/src/editor/index.ts +7 -0
  69. package/src/editor/resolve-editor.ts +21 -0
  70. package/src/effort/effort-command-module.ts +51 -0
  71. package/src/effort/effort-command.test.ts +203 -0
  72. package/src/effort/effort-command.ts +136 -0
  73. package/src/effort/index.ts +6 -0
  74. package/src/events/__tests__/events-command.test.ts +73 -0
  75. package/src/events/events-command-module.ts +49 -0
  76. package/src/events/events-command.ts +69 -0
  77. package/src/events/index.ts +2 -0
  78. package/src/exit/__tests__/exit-command-module.test.ts +61 -0
  79. package/src/exit/exit-command-module.ts +47 -0
  80. package/src/exit/exit-command.ts +26 -0
  81. package/src/exit/index.ts +6 -0
  82. package/src/fork/__tests__/fork-command.test.ts +184 -0
  83. package/src/fork/fork-command-module.ts +61 -0
  84. package/src/fork/fork-command.ts +162 -0
  85. package/src/fork/index.ts +8 -0
  86. package/src/git/__tests__/fake-git-port.ts +35 -0
  87. package/src/git/__tests__/git-command-module.test.ts +111 -0
  88. package/src/git/__tests__/git-commit.test.ts +185 -0
  89. package/src/git/git-command-module.ts +138 -0
  90. package/src/git/git-commit.ts +254 -0
  91. package/src/git/index.ts +20 -0
  92. package/src/goal/__tests__/goal-command.test.ts +80 -0
  93. package/src/goal/goal-command-module.ts +49 -0
  94. package/src/goal/goal-command.ts +69 -0
  95. package/src/goal/index.ts +6 -0
  96. package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
  97. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  98. package/src/handoff/handoff-command-module.ts +49 -0
  99. package/src/handoff/handoff-command.ts +187 -0
  100. package/src/handoff/index.ts +6 -0
  101. package/src/help/__tests__/help-command-module.test.ts +134 -0
  102. package/src/help/__tests__/help-command.test.ts +130 -0
  103. package/src/help/help-command-module.ts +47 -0
  104. package/src/help/help-command.ts +11 -0
  105. package/src/help/index.ts +6 -0
  106. package/src/index.ts +163 -0
  107. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  108. package/src/keybindings/index.ts +6 -0
  109. package/src/keybindings/keybindings-command-module.ts +88 -0
  110. package/src/language/__tests__/language-command-module.test.ts +126 -0
  111. package/src/language/index.ts +6 -0
  112. package/src/language/language-command-module.ts +55 -0
  113. package/src/language/language-command.ts +52 -0
  114. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
  115. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
  116. package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
  117. package/src/mcp-activation/index.ts +12 -0
  118. package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
  119. package/src/mcp-activation/mcp-activation-command.ts +662 -0
  120. package/src/mcp-activation/mcp-model-notice.ts +99 -0
  121. package/src/mcp-activation/mcp-model-status.ts +104 -0
  122. package/src/memory/__tests__/memory-command-module.test.ts +476 -0
  123. package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
  124. package/src/memory/index.ts +6 -0
  125. package/src/memory/memory-command-module.ts +77 -0
  126. package/src/memory/memory-command.ts +326 -0
  127. package/src/mode/__tests__/mode-command-module.test.ts +230 -0
  128. package/src/mode/index.ts +6 -0
  129. package/src/mode/mode-command-module.ts +55 -0
  130. package/src/mode/mode-command.ts +67 -0
  131. package/src/model/__tests__/model-command-module.test.ts +80 -0
  132. package/src/model/__tests__/model-command.test.ts +265 -0
  133. package/src/model/index.ts +6 -0
  134. package/src/model/model-command-module.ts +56 -0
  135. package/src/model/model-command.ts +249 -0
  136. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  137. package/src/output-style/index.ts +6 -0
  138. package/src/output-style/output-style-command-module.ts +143 -0
  139. package/src/peers/__tests__/peers-command-module.test.ts +24 -0
  140. package/src/peers/__tests__/peers-command.test.ts +399 -0
  141. package/src/peers/index.ts +9 -0
  142. package/src/peers/peers-command-module.ts +49 -0
  143. package/src/peers/peers-command.ts +220 -0
  144. package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
  145. package/src/permissions/index.ts +6 -0
  146. package/src/permissions/permissions-command-module.ts +55 -0
  147. package/src/permissions/permissions-command.ts +89 -0
  148. package/src/plan/__tests__/plan-command.test.ts +129 -0
  149. package/src/plan/index.ts +9 -0
  150. package/src/plan/plan-command-module.ts +49 -0
  151. package/src/plan/plan-command.ts +92 -0
  152. package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
  153. package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
  154. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  155. package/src/plugin/index.ts +7 -0
  156. package/src/plugin/plugin-command-module.ts +85 -0
  157. package/src/plugin/plugin-command.ts +271 -0
  158. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  159. package/src/preset/__tests__/preset-command-module.test.ts +220 -0
  160. package/src/preset/index.ts +6 -0
  161. package/src/preset/preset-command-module.ts +80 -0
  162. package/src/preset/preset-command.ts +148 -0
  163. package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
  164. package/src/provider/__tests__/org-policy.test.ts +174 -0
  165. package/src/provider/__tests__/provider-command-module.test.ts +884 -0
  166. package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
  167. package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
  168. package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
  169. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  170. package/src/provider/__tests__/scripted-interaction.ts +40 -0
  171. package/src/provider/index.ts +32 -0
  172. package/src/provider/provider-command-execution.ts +235 -0
  173. package/src/provider/provider-command-module.ts +82 -0
  174. package/src/provider/provider-command-profile-lifecycle.ts +239 -0
  175. package/src/provider/provider-command-profile-operations.ts +218 -0
  176. package/src/provider/provider-command-profile.ts +116 -0
  177. package/src/provider/provider-command-setup.ts +79 -0
  178. package/src/provider/provider-connection-setup.ts +287 -0
  179. package/src/provider/provider-connection-validation.ts +38 -0
  180. package/src/provider/provider-onboarding.ts +73 -0
  181. package/src/provider/provider-prompt-interaction.ts +28 -0
  182. package/src/provider/provider-setup-ask.ts +67 -0
  183. package/src/provider/provider-setup-flow.ts +326 -0
  184. package/src/provider/provider-startup.ts +225 -0
  185. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  186. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  187. package/src/remote-control/index.ts +8 -0
  188. package/src/remote-control/remote-control-command-module.ts +61 -0
  189. package/src/remote-control/remote-control-command.ts +138 -0
  190. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  191. package/src/reset/index.ts +2 -0
  192. package/src/reset/reset-command-module.ts +47 -0
  193. package/src/reset/reset-command.ts +10 -0
  194. package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
  195. package/src/rewind/index.ts +2 -0
  196. package/src/rewind/rewind-command-module.ts +56 -0
  197. package/src/rewind/rewind-command.ts +279 -0
  198. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  199. package/src/sandbox/index.ts +2 -0
  200. package/src/sandbox/sandbox-command-module.ts +56 -0
  201. package/src/sandbox/sandbox-command.ts +94 -0
  202. package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
  203. package/src/schedule/__tests__/loop-command.test.ts +594 -0
  204. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  205. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  206. package/src/schedule/__tests__/schedule-command.test.ts +247 -0
  207. package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
  208. package/src/schedule/index.ts +12 -0
  209. package/src/schedule/loop-command.ts +427 -0
  210. package/src/schedule/loop-jitter.ts +41 -0
  211. package/src/schedule/schedule-command-module.ts +184 -0
  212. package/src/schedule/schedule-command.ts +161 -0
  213. package/src/schedule/schedule-spec-parser.ts +70 -0
  214. package/src/session/__tests__/model-pricing.test.ts +66 -0
  215. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  216. package/src/session/__tests__/session-command-module.test.ts +589 -0
  217. package/src/session/index.ts +17 -0
  218. package/src/session/model-pricing.ts +26 -0
  219. package/src/session/session-command-module.ts +197 -0
  220. package/src/session/session-command.ts +272 -0
  221. package/src/settings/index.ts +7 -0
  222. package/src/settings/settings-command-module.ts +47 -0
  223. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  224. package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
  225. package/src/shell/index.ts +8 -0
  226. package/src/shell/resolve-shell.ts +25 -0
  227. package/src/shell/shell-command-module.ts +53 -0
  228. package/src/shell/shell-command.ts +48 -0
  229. package/src/shell/spawn-inherited.ts +32 -0
  230. package/src/skills/__tests__/skill-read-command.test.ts +64 -0
  231. package/src/skills/__tests__/skills-command-module.test.ts +200 -0
  232. package/src/skills/index.ts +7 -0
  233. package/src/skills/skill-read-command.ts +60 -0
  234. package/src/skills/skills-command-module.ts +76 -0
  235. package/src/skills/skills-command.ts +108 -0
  236. package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
  237. package/src/statusline/index.ts +6 -0
  238. package/src/statusline/statusline-command-module.ts +55 -0
  239. package/src/statusline/statusline-command.ts +80 -0
  240. package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
  241. package/src/terminal-client/index.ts +5 -0
  242. package/src/terminal-client/terminal-client-commands.ts +109 -0
  243. package/src/theme/__tests__/theme-command.test.ts +204 -0
  244. package/src/theme/index.ts +12 -0
  245. package/src/theme/theme-command-module.ts +221 -0
  246. package/src/user-local/__tests__/user-local-command.test.ts +180 -0
  247. package/src/user-local/index.ts +13 -0
  248. package/src/user-local/user-local-command-constants.ts +5 -0
  249. package/src/user-local/user-local-command-module.ts +65 -0
  250. package/src/user-local/user-local-command.ts +214 -0
  251. package/src/user-local/user-local-memory-command.ts +160 -0
@@ -0,0 +1,287 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import type {
3
+ IEditCheckpointInspection,
4
+ IEditCheckpointRestoreResult,
5
+ IEditCheckpointSummary,
6
+ } from '@robota-sdk/agent-framework';
7
+ import { InteractiveSession } from '@robota-sdk/agent-framework';
8
+ import { RewindCommandSource, createRewindCommandModule, executeRewindCommand } from '../index.js';
9
+
10
+ function createCheckpoint(overrides: Partial<IEditCheckpointSummary> = {}): IEditCheckpointSummary {
11
+ return {
12
+ id: 'turn-0001',
13
+ sessionId: 'test-session-id',
14
+ sequence: 1,
15
+ prompt: 'change files',
16
+ createdAt: '2026-05-02T00:00:00.000Z',
17
+ fileCount: 2,
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ function createRestoreResult(
23
+ overrides: Partial<IEditCheckpointRestoreResult> = {},
24
+ ): IEditCheckpointRestoreResult {
25
+ return {
26
+ target: createCheckpoint({ fileCount: 1 }),
27
+ restoredCheckpointCount: 2,
28
+ restoredFileCount: 3,
29
+ removedCheckpointCount: 2,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function createInspection(
35
+ overrides: Partial<IEditCheckpointInspection> = {},
36
+ ): IEditCheckpointInspection {
37
+ return {
38
+ target: createCheckpoint({ fileCount: 1 }),
39
+ capturedFiles: [
40
+ {
41
+ originalPath: '/workspace/example.ts',
42
+ relativePath: 'example.ts',
43
+ existed: true,
44
+ restoreAction: 'restore-preimage',
45
+ snapshotAvailable: true,
46
+ snapshotSizeBytes: 12,
47
+ },
48
+ ],
49
+ restoreToCheckpoint: { checkpointIds: ['turn-0002'], fileCount: 1 },
50
+ rollbackThroughCheckpoint: { checkpointIds: ['turn-0001', 'turn-0002'], fileCount: 2 },
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function createMockRuntimeSession() {
56
+ return {
57
+ run: vi.fn().mockResolvedValue('mock response'),
58
+ abort: vi.fn(),
59
+ getHistory: vi.fn().mockReturnValue([]),
60
+ getContextState: vi.fn().mockReturnValue({
61
+ usedTokens: 5000,
62
+ maxTokens: 200000,
63
+ usedPercentage: 2.5,
64
+ }),
65
+ compact: vi.fn().mockResolvedValue(undefined),
66
+ injectMessage: vi.fn(),
67
+ getSessionId: vi.fn().mockReturnValue('test-session-id'),
68
+ getSystemMessage: vi.fn().mockReturnValue('mock system prompt'),
69
+ getToolSchemas: vi.fn().mockReturnValue([]),
70
+ };
71
+ }
72
+
73
+ function createInteractiveSession(commandProductVocabulary?: { cliName: string; displayName: string }): InteractiveSession {
74
+ return new InteractiveSession({
75
+ session: createMockRuntimeSession() as never,
76
+ commandModules: [createRewindCommandModule()],
77
+ commandProductVocabulary,
78
+ });
79
+ }
80
+
81
+ describe('createRewindCommandModule', () => {
82
+ it('contributes write-safe rewind metadata and executable command', () => {
83
+ const module = createRewindCommandModule();
84
+ const commands = module.commandSources?.flatMap((source) => source.getCommands()) ?? [];
85
+
86
+ expect(module.name).toBe('agent-command-rewind');
87
+ expect(commands.map((command) => command.name)).toEqual(['rewind']);
88
+ expect(commands[0]?.source).toBe('rewind');
89
+ expect(commands[0]?.modelInvocable).toBe(false);
90
+ expect(commands[0]?.safety).toBe('write');
91
+ expect(commands[0]?.subcommands?.map((command) => command.name)).toEqual([
92
+ 'list',
93
+ 'inspect',
94
+ 'restore',
95
+ 'code',
96
+ 'rollback',
97
+ 'fork',
98
+ 'switch',
99
+ 'branches',
100
+ ]);
101
+ expect(module.systemCommands?.map((command) => command.name)).toEqual(['rewind']);
102
+ expect(module.systemCommands?.[0]?.userInvocable).toBe(true);
103
+ });
104
+
105
+ it('provides a stable command source', () => {
106
+ const source = new RewindCommandSource();
107
+
108
+ expect(source.name).toBe('rewind');
109
+ expect(source.getCommands()).toHaveLength(1);
110
+ });
111
+ });
112
+
113
+ describe('executeRewindCommand', () => {
114
+ it('lists edit checkpoints', async () => {
115
+ const session = createInteractiveSession();
116
+ vi.spyOn(session, 'listEditCheckpoints').mockReturnValue([createCheckpoint()]);
117
+
118
+ const result = await session.executeCommand('rewind', 'list');
119
+
120
+ expect(result?.success).toBe(true);
121
+ expect(result?.message).toContain('turn-0001');
122
+ expect(result?.data?.count).toBe(1);
123
+ });
124
+
125
+ it('shows an empty checkpoint list', async () => {
126
+ const session = createInteractiveSession();
127
+ vi.spyOn(session, 'listEditCheckpoints').mockReturnValue([]);
128
+
129
+ const result = await session.executeCommand('rewind', '');
130
+
131
+ expect(result?.success).toBe(true);
132
+ expect(result?.message).toContain('(no edit checkpoints)');
133
+ expect(result?.data?.count).toBe(0);
134
+ });
135
+
136
+ it('inspects captured files and rollback plans', async () => {
137
+ const session = createInteractiveSession();
138
+ const inspectEditCheckpoint = vi
139
+ .spyOn(session, 'inspectEditCheckpoint')
140
+ .mockReturnValue(createInspection());
141
+
142
+ const result = await session.executeCommand('rewind', 'inspect turn-0001');
143
+
144
+ expect(result?.success).toBe(true);
145
+ expect(inspectEditCheckpoint).toHaveBeenCalledWith('turn-0001');
146
+ expect(result?.message).toContain('Captured files:');
147
+ expect(result?.message).toContain('example.ts');
148
+ expect(result?.message).toContain('Rollback through checkpoint');
149
+ expect(result?.data?.inspection).toEqual(createInspection());
150
+ });
151
+
152
+ it('delegates restore through session.executeCommand', async () => {
153
+ const session = createInteractiveSession();
154
+ const restoreEditCheckpoint = vi
155
+ .spyOn(session, 'restoreEditCheckpoint')
156
+ .mockResolvedValue(createRestoreResult());
157
+
158
+ const result = await session.executeCommand('rewind', 'restore turn-0001');
159
+
160
+ expect(result?.success).toBe(true);
161
+ expect(restoreEditCheckpoint).toHaveBeenCalledWith('turn-0001');
162
+ expect(result?.message).toContain('Restored code to turn-0001.');
163
+ expect(result?.data?.restoredFileCount).toBe(3);
164
+ });
165
+
166
+ it('treats code as a restore alias', async () => {
167
+ const session = createInteractiveSession();
168
+ const restoreEditCheckpoint = vi
169
+ .spyOn(session, 'restoreEditCheckpoint')
170
+ .mockResolvedValue(createRestoreResult());
171
+
172
+ const result = await session.executeCommand('rewind', 'code turn-0001');
173
+
174
+ expect(result?.success).toBe(true);
175
+ expect(restoreEditCheckpoint).toHaveBeenCalledWith('turn-0001');
176
+ });
177
+
178
+ it('delegates rollback through session.executeCommand', async () => {
179
+ const session = createInteractiveSession();
180
+ const rollbackEditCheckpoint = vi
181
+ .spyOn(session, 'rollbackEditCheckpoint')
182
+ .mockResolvedValue(
183
+ createRestoreResult({ restoredCheckpointCount: 1, removedCheckpointCount: 1 }),
184
+ );
185
+
186
+ const result = await session.executeCommand('rewind', 'rollback turn-0001');
187
+
188
+ expect(result?.success).toBe(true);
189
+ expect(rollbackEditCheckpoint).toHaveBeenCalledWith('turn-0001');
190
+ expect(result?.message).toContain('Rolled back code through turn-0001.');
191
+ });
192
+
193
+ // SELFHOST-007 TC-05: branching subcommands route through the host context.
194
+ it('delegates fork through session.executeCommand (non-destructive)', async () => {
195
+ const session = createInteractiveSession();
196
+ const forkCheckpointBranch = vi
197
+ .spyOn(session, 'forkCheckpointBranch')
198
+ .mockResolvedValue(createRestoreResult());
199
+
200
+ const result = await session.executeCommand('rewind', 'fork turn-0001');
201
+
202
+ expect(result?.success).toBe(true);
203
+ expect(forkCheckpointBranch).toHaveBeenCalledWith('turn-0001');
204
+ expect(result?.message).toContain('Forked a new branch from turn-0001');
205
+ });
206
+
207
+ it('delegates switch through session.executeCommand', async () => {
208
+ const session = createInteractiveSession();
209
+ const switchCheckpointBranch = vi.spyOn(session, 'switchCheckpointBranch').mockReturnValue();
210
+
211
+ const result = await session.executeCommand('rewind', 'switch turn-0002');
212
+
213
+ expect(result?.success).toBe(true);
214
+ expect(switchCheckpointBranch).toHaveBeenCalledWith('turn-0002');
215
+ expect(result?.message).toContain('Switched to checkpoint branch turn-0002.');
216
+ });
217
+
218
+ it('lists branch tips via the branches subcommand', async () => {
219
+ const session = createInteractiveSession();
220
+ vi.spyOn(session, 'listCheckpointBranches').mockReturnValue(['turn-0003', 'turn-0005']);
221
+
222
+ const result = await session.executeCommand('rewind', 'branches');
223
+
224
+ expect(result?.success).toBe(true);
225
+ expect(result?.message).toContain('turn-0003');
226
+ expect(result?.message).toContain('turn-0005');
227
+ expect(result?.data?.branches).toEqual(['turn-0003', 'turn-0005']);
228
+ });
229
+
230
+ it('returns usage for invalid arguments without mutating state', async () => {
231
+ const session = createInteractiveSession();
232
+ const restoreEditCheckpoint = vi.spyOn(session, 'restoreEditCheckpoint');
233
+ const rollbackEditCheckpoint = vi.spyOn(session, 'rollbackEditCheckpoint');
234
+
235
+ const result = await executeRewindCommand(session, 'restore');
236
+ const unknownResult = await executeRewindCommand(session, 'unknown');
237
+
238
+ expect(result.success).toBe(false);
239
+ expect(result.message).toContain('Usage: rewind');
240
+ expect(unknownResult.success).toBe(false);
241
+ expect(restoreEditCheckpoint).not.toHaveBeenCalled();
242
+ expect(rollbackEditCheckpoint).not.toHaveBeenCalled();
243
+ });
244
+
245
+ it('returns checkpoint API errors as command failures', async () => {
246
+ const session = createInteractiveSession();
247
+ vi.spyOn(session, 'restoreEditCheckpoint').mockRejectedValue(
248
+ new Error('Unknown edit checkpoint'),
249
+ );
250
+
251
+ const result = await session.executeCommand('rewind', 'restore missing');
252
+
253
+ expect(result?.success).toBe(false);
254
+ expect(result?.message).toBe('Unknown edit checkpoint');
255
+ });
256
+
257
+ describe('in a session built without a checkpoint store', () => {
258
+ afterEach(() => {
259
+ vi.restoreAllMocks();
260
+ });
261
+
262
+ it('tells a restricted workspace to trust it, for every subcommand', async () => {
263
+ vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
264
+ const session = createInteractiveSession({ cliName: 'test-agent', displayName: 'Test Agent' });
265
+
266
+ for (const args of ['', 'list', 'inspect turn-0001', 'restore turn-0001', 'branches']) {
267
+ const result = await session.executeCommand('rewind', args);
268
+ expect(result?.success).toBe(false);
269
+ expect(result?.message).toBe(
270
+ 'Edit checkpoints need a trusted workspace: run test-agent trust --yes, then restart test-agent.',
271
+ );
272
+ }
273
+ });
274
+
275
+ it('says why on a host that cannot prove a project write is safe, without naming trust', async () => {
276
+ vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
277
+ const session = createInteractiveSession();
278
+
279
+ const result = await session.executeCommand('rewind', 'list');
280
+
281
+ expect(result?.success).toBe(false);
282
+ // #3289 §3: plain words naming the feature ("Rewind"), not the internal write-containment reason.
283
+ expect(result?.message).toBe("Rewind isn't available on this computer yet.");
284
+ expect(result?.message).not.toContain('trust');
285
+ });
286
+ });
287
+ });
@@ -0,0 +1,2 @@
1
+ export { createRewindCommandModule, RewindCommandSource } from './rewind-command-module.js';
2
+ export { executeRewindCommand } from './rewind-command.js';
@@ -0,0 +1,56 @@
1
+ import {
2
+ REWIND_COMMAND_ARGUMENT_HINT,
3
+ REWIND_COMMAND_DESCRIPTION,
4
+ buildRewindCommandSubcommands,
5
+ } from '@robota-sdk/agent-framework';
6
+
7
+ import { executeRewindCommand } from './rewind-command.js';
8
+
9
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
11
+
12
+ export function createRewindCommandEntry(): ICommand {
13
+ return {
14
+ name: 'rewind',
15
+ displayName: 'Rewind History',
16
+ description: REWIND_COMMAND_DESCRIPTION,
17
+ source: 'rewind',
18
+ argumentHint: REWIND_COMMAND_ARGUMENT_HINT,
19
+ // User-only: rewrites the user's history and files; the user decides what to undo.
20
+ modelInvocable: false,
21
+ safety: 'write',
22
+ subcommands: buildRewindCommandSubcommands(),
23
+ };
24
+ }
25
+
26
+ function createRewindSystemCommand(): ISystemCommand {
27
+ const entry = createRewindCommandEntry();
28
+ return {
29
+ name: entry.name,
30
+ displayName: entry.displayName,
31
+ description: entry.description,
32
+ requiresPermission: false,
33
+ argumentHint: entry.argumentHint,
34
+ userInvocable: true,
35
+ modelInvocable: false,
36
+ safety: 'write',
37
+ subcommands: entry.subcommands,
38
+ execute: executeRewindCommand,
39
+ };
40
+ }
41
+
42
+ export class RewindCommandSource implements ICommandSource {
43
+ readonly name = 'rewind';
44
+
45
+ getCommands(): ICommand[] {
46
+ return [createRewindCommandEntry()];
47
+ }
48
+ }
49
+
50
+ export function createRewindCommandModule(): ICommandModule {
51
+ return {
52
+ name: 'agent-command-rewind',
53
+ commandSources: [new RewindCommandSource()],
54
+ systemCommands: [createRewindSystemCommand()],
55
+ };
56
+ }
@@ -0,0 +1,279 @@
1
+ import {
2
+ EditCheckpointsUnavailableError,
3
+ inspectCommandEditCheckpoint,
4
+ listCommandEditCheckpoints,
5
+ restoreCommandEditCheckpoint,
6
+ rollbackCommandEditCheckpoint,
7
+ forkCommandEditCheckpoint,
8
+ switchCommandEditCheckpointBranch,
9
+ listCommandEditCheckpointBranches,
10
+ } from '@robota-sdk/agent-framework';
11
+
12
+ import type {
13
+ ICommandHostCheckpoints,
14
+ IEditCheckpointInspection,
15
+ IEditCheckpointRestoreResult,
16
+ IEditCheckpointSummary,
17
+ } from '@robota-sdk/agent-framework';
18
+ import type { ICommandProductVocabulary } from '@robota-sdk/agent-framework';
19
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
20
+
21
+ type TRewindCommandContext = ICommandHostCheckpoints & {
22
+ getCommandProductVocabulary?(): ICommandProductVocabulary | undefined;
23
+ };
24
+
25
+ const SUBCOMMAND_INDEX = 0;
26
+ const CHECKPOINT_ID_INDEX = 1;
27
+ const PROMPT_PREVIEW_LENGTH = 120;
28
+ const ELLIPSIS_LENGTH = 3;
29
+
30
+ function usage(): ICommandResult {
31
+ return {
32
+ message:
33
+ 'Usage: rewind [list] | rewind inspect <checkpoint-id> | rewind restore <checkpoint-id> | rewind code <checkpoint-id> | rewind rollback <checkpoint-id>',
34
+ success: false,
35
+ };
36
+ }
37
+
38
+ function formatPrompt(prompt: string): string {
39
+ const compact = prompt.replace(/\s+/g, ' ').trim();
40
+ if (compact.length <= PROMPT_PREVIEW_LENGTH) return compact;
41
+ return `${compact.slice(0, PROMPT_PREVIEW_LENGTH - ELLIPSIS_LENGTH)}...`;
42
+ }
43
+
44
+ function formatList(checkpoints: readonly IEditCheckpointSummary[]): ICommandResult {
45
+ const lines =
46
+ checkpoints.length > 0
47
+ ? checkpoints.map(
48
+ (checkpoint) =>
49
+ `- ${checkpoint.id} files=${checkpoint.fileCount} ${checkpoint.createdAt} ${formatPrompt(
50
+ checkpoint.prompt,
51
+ )}`,
52
+ )
53
+ : ['(no edit checkpoints)'];
54
+
55
+ return {
56
+ message: ['Edit checkpoints:', ...lines].join('\n'),
57
+ success: true,
58
+ data: { count: checkpoints.length, checkpoints: [...checkpoints] },
59
+ };
60
+ }
61
+
62
+ function formatCheckpointIds(ids: readonly string[]): string {
63
+ return ids.length > 0 ? ids.join(', ') : '(none)';
64
+ }
65
+
66
+ function formatInspection(inspection: IEditCheckpointInspection): ICommandResult {
67
+ const fileLines =
68
+ inspection.capturedFiles.length > 0
69
+ ? inspection.capturedFiles.map((file) => {
70
+ const size =
71
+ file.snapshotSizeBytes === undefined ? '' : ` size=${file.snapshotSizeBytes}B`;
72
+ return `- ${file.relativePath} action=${file.restoreAction} snapshot=${String(
73
+ file.snapshotAvailable,
74
+ )}${size}`;
75
+ })
76
+ : ['(no files captured)'];
77
+
78
+ return {
79
+ message: [
80
+ `Checkpoint ${inspection.target.id}`,
81
+ `Prompt: ${formatPrompt(inspection.target.prompt)}`,
82
+ 'Captured files:',
83
+ ...fileLines,
84
+ `Restore later checkpoints: files=${inspection.restoreToCheckpoint.fileCount} checkpoints=${formatCheckpointIds(
85
+ inspection.restoreToCheckpoint.checkpointIds,
86
+ )}`,
87
+ `Rollback through checkpoint: files=${inspection.rollbackThroughCheckpoint.fileCount} checkpoints=${formatCheckpointIds(
88
+ inspection.rollbackThroughCheckpoint.checkpointIds,
89
+ )}`,
90
+ ].join('\n'),
91
+ success: true,
92
+ data: { inspection },
93
+ };
94
+ }
95
+
96
+ function formatRestoreResult(result: IEditCheckpointRestoreResult): ICommandResult {
97
+ return {
98
+ message: [
99
+ `Restored code to ${result.target.id}.`,
100
+ `Restored files: ${result.restoredFileCount}`,
101
+ `Rolled back checkpoints: ${result.restoredCheckpointCount}`,
102
+ ].join('\n'),
103
+ success: true,
104
+ data: {
105
+ target: result.target,
106
+ restoredCheckpointCount: result.restoredCheckpointCount,
107
+ restoredFileCount: result.restoredFileCount,
108
+ removedCheckpointCount: result.removedCheckpointCount,
109
+ },
110
+ };
111
+ }
112
+
113
+ function formatRollbackResult(result: IEditCheckpointRestoreResult): ICommandResult {
114
+ return {
115
+ message: [
116
+ `Rolled back code through ${result.target.id}.`,
117
+ `Restored files: ${result.restoredFileCount}`,
118
+ `Removed checkpoints: ${result.removedCheckpointCount}`,
119
+ ].join('\n'),
120
+ success: true,
121
+ data: {
122
+ target: result.target,
123
+ restoredCheckpointCount: result.restoredCheckpointCount,
124
+ restoredFileCount: result.restoredFileCount,
125
+ removedCheckpointCount: result.removedCheckpointCount,
126
+ },
127
+ };
128
+ }
129
+
130
+ /** A restricted workspace is the one case the user fixes with a command; the rest say why. */
131
+ function unavailableMessage(error: EditCheckpointsUnavailableError, cliName?: string): string {
132
+ if (error.reason !== 'restricted-workspace') return error.message;
133
+ return cliName === undefined
134
+ ? 'Edit checkpoints need a trusted workspace. Trust it from a terminal, then restart the session.'
135
+ : `Edit checkpoints need a trusted workspace: run ${cliName} trust --yes, then restart ${cliName}.`;
136
+ }
137
+
138
+ function formatError(error: Error | string, cliName?: string): ICommandResult {
139
+ return {
140
+ message:
141
+ error instanceof EditCheckpointsUnavailableError
142
+ ? unavailableMessage(error, cliName)
143
+ : error instanceof Error
144
+ ? error.message
145
+ : String(error),
146
+ success: false,
147
+ };
148
+ }
149
+
150
+ function list(context: TRewindCommandContext): ICommandResult {
151
+ try {
152
+ return formatList(listCommandEditCheckpoints(context));
153
+ } catch (error) {
154
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
155
+ }
156
+ }
157
+
158
+ function inspect(
159
+ context: TRewindCommandContext,
160
+ checkpointId: string | undefined,
161
+ ): ICommandResult {
162
+ if (!checkpointId) return usage();
163
+ try {
164
+ return formatInspection(inspectCommandEditCheckpoint(context, checkpointId));
165
+ } catch (error) {
166
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
167
+ }
168
+ }
169
+
170
+ async function restore(
171
+ context: TRewindCommandContext,
172
+ checkpointId: string | undefined,
173
+ ): Promise<ICommandResult> {
174
+ if (!checkpointId) return usage();
175
+ try {
176
+ return formatRestoreResult(await restoreCommandEditCheckpoint(context, checkpointId));
177
+ } catch (error) {
178
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
179
+ }
180
+ }
181
+
182
+ async function rollback(
183
+ context: TRewindCommandContext,
184
+ checkpointId: string | undefined,
185
+ ): Promise<ICommandResult> {
186
+ if (!checkpointId) return usage();
187
+ try {
188
+ return formatRollbackResult(await rollbackCommandEditCheckpoint(context, checkpointId));
189
+ } catch (error) {
190
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
191
+ }
192
+ }
193
+
194
+ // SELFHOST-007: branching time-travel handlers.
195
+ async function fork(
196
+ context: TRewindCommandContext,
197
+ checkpointId: string | undefined,
198
+ ): Promise<ICommandResult> {
199
+ if (!checkpointId) return usage();
200
+ try {
201
+ const result = await forkCommandEditCheckpoint(context, checkpointId);
202
+ return {
203
+ message: [
204
+ `Forked a new branch from ${result.target.id} (the previous future is preserved).`,
205
+ `Restored files: ${result.restoredFileCount}`,
206
+ ].join('\n'),
207
+ success: true,
208
+ data: { target: result.target, restoredFileCount: result.restoredFileCount },
209
+ };
210
+ } catch (error) {
211
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
212
+ }
213
+ }
214
+
215
+ function switchBranch(
216
+ context: TRewindCommandContext,
217
+ checkpointId: string | undefined,
218
+ ): ICommandResult {
219
+ if (!checkpointId) return usage();
220
+ try {
221
+ switchCommandEditCheckpointBranch(context, checkpointId);
222
+ return { message: `Switched to checkpoint branch ${checkpointId}.`, success: true };
223
+ } catch (error) {
224
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
225
+ }
226
+ }
227
+
228
+ function branches(context: TRewindCommandContext): ICommandResult {
229
+ try {
230
+ const tips = listCommandEditCheckpointBranches(context);
231
+ return {
232
+ message:
233
+ tips.length === 0
234
+ ? 'No checkpoint branches yet.'
235
+ : `Branch tips:\n${tips.map((id) => `- ${id}`).join('\n')}`,
236
+ success: true,
237
+ data: { branches: tips },
238
+ };
239
+ } catch (error) {
240
+ return formatError(error instanceof Error ? error : String(error), context.getCommandProductVocabulary?.()?.cliName);
241
+ }
242
+ }
243
+
244
+ export async function executeRewindCommand(
245
+ context: TRewindCommandContext,
246
+ rawArgs: string,
247
+ ): Promise<ICommandResult> {
248
+ const args = rawArgs.trim().split(/\s+/).filter(Boolean);
249
+ const subcommand = args[SUBCOMMAND_INDEX] ?? 'list';
250
+
251
+ if (subcommand === 'list') {
252
+ return list(context);
253
+ }
254
+
255
+ if (subcommand === 'inspect') {
256
+ return inspect(context, args[CHECKPOINT_ID_INDEX]);
257
+ }
258
+
259
+ if (subcommand === 'restore' || subcommand === 'code') {
260
+ return restore(context, args[CHECKPOINT_ID_INDEX]);
261
+ }
262
+
263
+ if (subcommand === 'rollback') {
264
+ return rollback(context, args[CHECKPOINT_ID_INDEX]);
265
+ }
266
+
267
+ // SELFHOST-007: branching time-travel subcommands.
268
+ if (subcommand === 'fork') {
269
+ return fork(context, args[CHECKPOINT_ID_INDEX]);
270
+ }
271
+ if (subcommand === 'switch') {
272
+ return switchBranch(context, args[CHECKPOINT_ID_INDEX]);
273
+ }
274
+ if (subcommand === 'branches') {
275
+ return branches(context);
276
+ }
277
+
278
+ return usage();
279
+ }