@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.
- package/README.md +11 -2
- package/dist/node/index.cjs +77 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +77 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +39 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command-module.test.ts +24 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +321 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +191 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- package/src/plugins/default-plugin-command-source-loader.ts +0 -31
|
@@ -1,15 +1,73 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import {
|
|
6
6
|
InteractiveSession,
|
|
7
7
|
SystemCommandExecutor,
|
|
8
|
-
|
|
8
|
+
WorkspaceTrustService,
|
|
9
|
+
createWorkspaceMemoryStore,
|
|
10
|
+
getWorkspaceProjectStateStorage,
|
|
9
11
|
} from '@robota-sdk/agent-framework';
|
|
10
12
|
import { MemoryCommandSource, createMemoryCommandModule, executeMemoryCommand } from '../index.js';
|
|
13
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
import type {
|
|
16
|
+
IMemoryStore,
|
|
17
|
+
IWorkspaceIdentity,
|
|
18
|
+
IWorkspaceTrustStore,
|
|
19
|
+
IWorkspaceTrustStoreSnapshot,
|
|
20
|
+
} from '@robota-sdk/agent-framework';
|
|
21
|
+
|
|
22
|
+
// SEC-003 (js/insecure-temporary-file): `mkdtempSync` yields a 0700 directory with an unpredictable
|
|
23
|
+
// name, so no other user can pre-create or read the paths this suite writes under it.
|
|
24
|
+
const TMP_BASE = realpathSync(mkdtempSync(join(tmpdir(), 'robota-command-memory-')));
|
|
25
|
+
const memoryStores = new Map<string, Promise<IMemoryStore>>();
|
|
26
|
+
|
|
27
|
+
class TrustedStore implements IWorkspaceTrustStore {
|
|
28
|
+
inspect(): Promise<IWorkspaceTrustStoreSnapshot> {
|
|
29
|
+
return Promise.resolve({ state: 'trusted', generation: 1 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
grant(): Promise<IWorkspaceTrustStoreSnapshot> {
|
|
33
|
+
return this.inspect();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
revoke(): Promise<IWorkspaceTrustStoreSnapshot> {
|
|
37
|
+
return Promise.resolve({ state: 'revoked', generation: 2 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createMemoryStore(cwd: string): Promise<IMemoryStore> {
|
|
42
|
+
const existing = memoryStores.get(cwd);
|
|
43
|
+
if (existing !== undefined) return existing;
|
|
44
|
+
const created = (async () => {
|
|
45
|
+
const root = realpathSync(cwd);
|
|
46
|
+
const identity: IWorkspaceIdentity = {
|
|
47
|
+
repositoryKey: `memory-test:${root}`,
|
|
48
|
+
displayPath: root,
|
|
49
|
+
worktreeRoot: root,
|
|
50
|
+
};
|
|
51
|
+
const service = new WorkspaceTrustService({
|
|
52
|
+
identityResolver: { resolve: () => identity },
|
|
53
|
+
store: new TrustedStore(),
|
|
54
|
+
projectStateDirectories: {
|
|
55
|
+
sessions: join('.robota', 'sessions'),
|
|
56
|
+
'session-logs': join('.robota', 'logs'),
|
|
57
|
+
memory: join('.robota', 'memory'),
|
|
58
|
+
checkpoints: join('.robota', 'checkpoints'),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const access = await service.inspect(root);
|
|
62
|
+
if (access.status !== 'trusted') throw new Error('Expected trusted project access.');
|
|
63
|
+
return createWorkspaceMemoryStore(
|
|
64
|
+
getWorkspaceProjectStateStorage(access.authority, 'memory'),
|
|
65
|
+
() => new Date('2026-05-02T00:00:00.000Z'),
|
|
66
|
+
);
|
|
67
|
+
})();
|
|
68
|
+
memoryStores.set(cwd, created);
|
|
69
|
+
return created;
|
|
70
|
+
}
|
|
13
71
|
|
|
14
72
|
function makeProject(): string {
|
|
15
73
|
const dir = join(TMP_BASE, Math.random().toString(36).slice(2));
|
|
@@ -43,20 +101,18 @@ function createMockRuntimeSession() {
|
|
|
43
101
|
};
|
|
44
102
|
}
|
|
45
103
|
|
|
46
|
-
function createInteractiveSession(cwd = makeProject()): InteractiveSession {
|
|
104
|
+
async function createInteractiveSession(cwd = makeProject()): Promise<InteractiveSession> {
|
|
47
105
|
return new InteractiveSession({
|
|
48
106
|
cwd,
|
|
49
107
|
session: createMockRuntimeSession() as never,
|
|
108
|
+
memoryStore: await createMemoryStore(cwd),
|
|
50
109
|
commandModules: [createMemoryCommandModule()],
|
|
51
110
|
});
|
|
52
111
|
}
|
|
53
112
|
|
|
54
|
-
function seedPendingMemory(cwd: string): void {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
() => new Date('2026-05-02T00:00:00.000Z'),
|
|
58
|
-
);
|
|
59
|
-
pendingStore.upsert(
|
|
113
|
+
async function seedPendingMemory(cwd: string): Promise<void> {
|
|
114
|
+
const store = await createMemoryStore(cwd);
|
|
115
|
+
await store.upsertPending(
|
|
60
116
|
{
|
|
61
117
|
id: 'mem_123',
|
|
62
118
|
type: 'project',
|
|
@@ -73,6 +129,7 @@ function seedPendingMemory(cwd: string): void {
|
|
|
73
129
|
}
|
|
74
130
|
|
|
75
131
|
afterEach(() => {
|
|
132
|
+
memoryStores.clear();
|
|
76
133
|
if (existsSync(TMP_BASE)) rmSync(TMP_BASE, { recursive: true, force: true });
|
|
77
134
|
});
|
|
78
135
|
|
|
@@ -121,46 +178,73 @@ describe('createMemoryCommandModule', () => {
|
|
|
121
178
|
kind: 'builtin-command',
|
|
122
179
|
userInvocable: true,
|
|
123
180
|
modelInvocable: true,
|
|
181
|
+
// The model is offered only what it may run: `approve`/`reject` are the user's review.
|
|
124
182
|
argumentHint:
|
|
125
|
-
'list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending |
|
|
183
|
+
'[list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending | used]',
|
|
126
184
|
safety: 'write',
|
|
127
185
|
}),
|
|
128
186
|
);
|
|
129
|
-
expect(descriptor?.description).toContain(
|
|
130
|
-
|
|
131
|
-
|
|
187
|
+
expect(descriptor?.description).toContain('look up stored conventions');
|
|
188
|
+
expect(descriptor?.description).toContain('suggest `/memory approve <id>`');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('refuses a model-issued `memory approve` before the command runs, and allows the user', async () => {
|
|
192
|
+
const executor = new SystemCommandExecutor([
|
|
193
|
+
...(createMemoryCommandModule().systemCommands ?? []),
|
|
194
|
+
]);
|
|
195
|
+
const command = executor.getCommand('memory');
|
|
196
|
+
const execute = vi.fn();
|
|
197
|
+
const host = createTestCommandHost();
|
|
198
|
+
const spied = { ...command!, execute };
|
|
199
|
+
const gated = new SystemCommandExecutor([spied]);
|
|
200
|
+
|
|
201
|
+
for (const args of ['approve cand-1', 'reject cand-1', 'APPROVE cand-1']) {
|
|
202
|
+
const result = await gated.executeModelInvocable('memory', host, args);
|
|
203
|
+
expect(result?.success).toBe(false);
|
|
204
|
+
expect(result?.message).toContain('only the user can');
|
|
205
|
+
}
|
|
206
|
+
expect(execute).not.toHaveBeenCalled();
|
|
207
|
+
|
|
208
|
+
await gated.executeModelInvocable('memory', host, 'list');
|
|
209
|
+
await gated.execute('memory', host, 'approve cand-1');
|
|
210
|
+
expect(execute).toHaveBeenCalledTimes(2);
|
|
132
211
|
});
|
|
133
212
|
});
|
|
134
213
|
|
|
135
214
|
describe('executeMemoryCommand', () => {
|
|
136
215
|
it('lists configured memory paths', async () => {
|
|
137
216
|
const cwd = makeProject();
|
|
138
|
-
const session = createInteractiveSession(cwd);
|
|
217
|
+
const session = await createInteractiveSession(cwd);
|
|
139
218
|
|
|
140
219
|
const result = await session.executeCommand('memory', 'list');
|
|
141
220
|
|
|
142
221
|
expect(result?.success).toBe(true);
|
|
143
|
-
expect(result?.message).toContain(join(
|
|
222
|
+
expect(result?.message).toContain(join('.robota', 'memory', 'MEMORY.md'));
|
|
144
223
|
});
|
|
145
224
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
225
|
+
// ARCH-047: project mutation is Linux-only (stable root-anchored host); refused elsewhere.
|
|
226
|
+
it.runIf(process.platform === 'linux')(
|
|
227
|
+
'persists index and topic entries through slash invocation',
|
|
228
|
+
async () => {
|
|
229
|
+
const cwd = makeProject();
|
|
230
|
+
const session = await createInteractiveSession(cwd);
|
|
231
|
+
|
|
232
|
+
const result = await session.executeCommand(
|
|
233
|
+
'memory',
|
|
234
|
+
'add project build Use pnpm for scripts.',
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(result?.success).toBe(true);
|
|
238
|
+
expect(readFileSync(join(cwd, '.robota', 'memory', 'MEMORY.md'), 'utf8')).toContain(
|
|
239
|
+
'(project/build) Use pnpm for scripts.',
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
);
|
|
160
243
|
|
|
161
|
-
|
|
244
|
+
// ARCH-047: project mutation is Linux-only (stable root-anchored host); refused elsewhere.
|
|
245
|
+
it.runIf(process.platform === 'linux')('uses the same handler for model invocation', async () => {
|
|
162
246
|
const cwd = makeProject();
|
|
163
|
-
const session = createInteractiveSession(cwd);
|
|
247
|
+
const session = await createInteractiveSession(cwd);
|
|
164
248
|
|
|
165
249
|
const result = await session.executeModelCommand(
|
|
166
250
|
'memory',
|
|
@@ -175,7 +259,7 @@ describe('executeMemoryCommand', () => {
|
|
|
175
259
|
|
|
176
260
|
it('rejects sensitive content before writing files', async () => {
|
|
177
261
|
const cwd = makeProject();
|
|
178
|
-
const session = createInteractiveSession(cwd);
|
|
262
|
+
const session = await createInteractiveSession(cwd);
|
|
179
263
|
|
|
180
264
|
const result = await session.executeCommand(
|
|
181
265
|
'memory',
|
|
@@ -187,10 +271,11 @@ describe('executeMemoryCommand', () => {
|
|
|
187
271
|
expect(existsSync(join(cwd, '.robota', 'memory', 'MEMORY.md'))).toBe(false);
|
|
188
272
|
});
|
|
189
273
|
|
|
190
|
-
|
|
274
|
+
// ARCH-047: project mutation is Linux-only (stable root-anchored host); refused elsewhere.
|
|
275
|
+
it.runIf(process.platform === 'linux')('lists queued automatic memory candidates', async () => {
|
|
191
276
|
const cwd = makeProject();
|
|
192
|
-
seedPendingMemory(cwd);
|
|
193
|
-
const session = createInteractiveSession(cwd);
|
|
277
|
+
await seedPendingMemory(cwd);
|
|
278
|
+
const session = await createInteractiveSession(cwd);
|
|
194
279
|
|
|
195
280
|
const result = await session.executeCommand('memory', 'pending');
|
|
196
281
|
|
|
@@ -200,47 +285,55 @@ describe('executeMemoryCommand', () => {
|
|
|
200
285
|
expect(result?.message).toContain('Use pnpm for package scripts.');
|
|
201
286
|
});
|
|
202
287
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const session = createInteractiveSession(cwd);
|
|
229
|
-
const recordMemoryEvent = vi.spyOn(session, 'recordMemoryEvent');
|
|
230
|
-
|
|
231
|
-
const result = await session.executeCommand('memory', 'reject mem_123');
|
|
288
|
+
// ARCH-047: project mutation is Linux-only (stable root-anchored host); refused elsewhere.
|
|
289
|
+
it.runIf(process.platform === 'linux')(
|
|
290
|
+
'approves and saves a pending candidate while recording audit events',
|
|
291
|
+
async () => {
|
|
292
|
+
const cwd = makeProject();
|
|
293
|
+
await seedPendingMemory(cwd);
|
|
294
|
+
const session = await createInteractiveSession(cwd);
|
|
295
|
+
const recordMemoryEvent = vi.spyOn(session, 'recordMemoryEvent');
|
|
296
|
+
|
|
297
|
+
const result = await session.executeCommand('memory', 'approve mem_123');
|
|
298
|
+
|
|
299
|
+
expect(result?.success).toBe(true);
|
|
300
|
+
expect(result?.message).toContain('Saved memory candidate mem_123');
|
|
301
|
+
expect(readFileSync(join(cwd, '.robota', 'memory', 'MEMORY.md'), 'utf8')).toContain(
|
|
302
|
+
'(project/build) Use pnpm for package scripts.',
|
|
303
|
+
);
|
|
304
|
+
expect((await (await createMemoryStore(cwd)).getPending('mem_123'))?.status).toBe('saved');
|
|
305
|
+
expect(recordMemoryEvent).toHaveBeenCalledWith(
|
|
306
|
+
expect.objectContaining({ type: 'memory_candidate_approved', candidateId: 'mem_123' }),
|
|
307
|
+
);
|
|
308
|
+
expect(recordMemoryEvent).toHaveBeenCalledWith(
|
|
309
|
+
expect.objectContaining({ type: 'memory_candidate_saved', candidateId: 'mem_123' }),
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
);
|
|
232
313
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
314
|
+
// ARCH-047: project mutation is Linux-only (stable root-anchored host); refused elsewhere.
|
|
315
|
+
it.runIf(process.platform === 'linux')(
|
|
316
|
+
'rejects a pending candidate while recording an audit event',
|
|
317
|
+
async () => {
|
|
318
|
+
const cwd = makeProject();
|
|
319
|
+
await seedPendingMemory(cwd);
|
|
320
|
+
const session = await createInteractiveSession(cwd);
|
|
321
|
+
const recordMemoryEvent = vi.spyOn(session, 'recordMemoryEvent');
|
|
322
|
+
|
|
323
|
+
const result = await session.executeCommand('memory', 'reject mem_123');
|
|
324
|
+
|
|
325
|
+
expect(result?.success).toBe(true);
|
|
326
|
+
expect(result?.message).toContain('Rejected memory candidate mem_123');
|
|
327
|
+
expect((await (await createMemoryStore(cwd)).getPending('mem_123'))?.status).toBe('rejected');
|
|
328
|
+
expect(recordMemoryEvent).toHaveBeenCalledWith(
|
|
329
|
+
expect.objectContaining({ type: 'memory_candidate_rejected', candidateId: 'mem_123' }),
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
);
|
|
240
333
|
|
|
241
334
|
it('reports references from the current turn', async () => {
|
|
242
335
|
const cwd = makeProject();
|
|
243
|
-
const session = createInteractiveSession(cwd);
|
|
336
|
+
const session = await createInteractiveSession(cwd);
|
|
244
337
|
vi.spyOn(session, 'getUsedMemoryReferences').mockReturnValue([
|
|
245
338
|
{
|
|
246
339
|
topic: 'build',
|
|
@@ -259,7 +352,7 @@ describe('executeMemoryCommand', () => {
|
|
|
259
352
|
|
|
260
353
|
it('returns usage for invalid arguments without mutating state', async () => {
|
|
261
354
|
const cwd = makeProject();
|
|
262
|
-
const session = createInteractiveSession(cwd);
|
|
355
|
+
const session = await createInteractiveSession(cwd);
|
|
263
356
|
|
|
264
357
|
const result = await executeMemoryCommand(session, 'add project missing-text');
|
|
265
358
|
const unknownResult = await executeMemoryCommand(session, 'unknown');
|
|
@@ -7,13 +7,29 @@ import {
|
|
|
7
7
|
import { executeMemoryCommand } from './memory-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-
|
|
10
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Model-invocable: remembering and recalling project conventions is the model's own work. The
|
|
14
|
+
* subcommand flags (in `buildMemoryCommandSubcommands`) keep `approve`/`reject` user-only, because
|
|
15
|
+
* pending candidates exist so the user reviews what the model proposed to remember.
|
|
16
|
+
*/
|
|
17
|
+
const MEMORY_COMMAND_MODEL_DESCRIPTION =
|
|
18
|
+
'Read and write this project’s durable memory. Use `list` or `show [topic]` to look up stored ' +
|
|
19
|
+
'conventions, preferences or references before asking the user again; `add <type> <topic> <text>` ' +
|
|
20
|
+
'to save a durable preference, project convention, feedback item or reference worth reusing ' +
|
|
21
|
+
'across sessions (never secrets, credentials or transient facts); `pending` to see candidates ' +
|
|
22
|
+
'awaiting the user’s review; `used` to report which memory items informed this turn. Bare lists ' +
|
|
23
|
+
'topics. Returns the requested topics or entries, or a confirmation of what was saved. Approving ' +
|
|
24
|
+
'or rejecting a pending candidate is the user’s review: suggest `/memory approve <id>` or ' +
|
|
25
|
+
'`/memory reject <id>`.';
|
|
11
26
|
|
|
12
27
|
export function createMemoryCommandEntry(): ICommand {
|
|
13
28
|
return {
|
|
14
29
|
name: 'memory',
|
|
15
30
|
displayName: 'Memory',
|
|
16
31
|
description: MEMORY_COMMAND_DESCRIPTION,
|
|
32
|
+
modelDescription: MEMORY_COMMAND_MODEL_DESCRIPTION,
|
|
17
33
|
source: 'memory',
|
|
18
34
|
argumentHint: MEMORY_COMMAND_ARGUMENT_HINT,
|
|
19
35
|
modelInvocable: true,
|
|
@@ -28,6 +44,7 @@ function createMemorySystemCommand(): ISystemCommand {
|
|
|
28
44
|
name: entry.name,
|
|
29
45
|
displayName: entry.displayName,
|
|
30
46
|
description: entry.description,
|
|
47
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
31
48
|
requiresPermission: false,
|
|
32
49
|
userInvocable: true,
|
|
33
50
|
modelInvocable: true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MEMORY_COMMAND_USAGE,
|
|
3
|
+
approvePendingMemoryCandidate,
|
|
3
4
|
createCommandMemoryStores,
|
|
4
5
|
hasSensitiveCommandMemoryContent,
|
|
5
6
|
isCommandMemoryType,
|
|
@@ -9,11 +10,12 @@ import {
|
|
|
9
10
|
|
|
10
11
|
import type {
|
|
11
12
|
IAppendMemoryInput,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
ICommandHostMemory,
|
|
14
|
+
ICommandHostWorkspace,
|
|
15
|
+
IMemoryStore,
|
|
15
16
|
} from '@robota-sdk/agent-framework';
|
|
16
|
-
import type { ICommandResult
|
|
17
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
18
|
+
import type { IMemoryEvent } from '@robota-sdk/agent-interface-session';
|
|
17
19
|
|
|
18
20
|
const SUBCOMMAND_INDEX = 0;
|
|
19
21
|
const TYPE_INDEX = 1;
|
|
@@ -34,8 +36,8 @@ function formatError(error: Error | string): ICommandResult {
|
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
function formatList(store:
|
|
38
|
-
const summary = store.list();
|
|
39
|
+
async function formatList(store: IMemoryStore): Promise<ICommandResult> {
|
|
40
|
+
const summary = await store.list();
|
|
39
41
|
const topics =
|
|
40
42
|
summary.topics.length > 0
|
|
41
43
|
? summary.topics.map((topic) => `- ${topic.name}: ${topic.path}`).join('\n')
|
|
@@ -57,9 +59,9 @@ function formatList(store: ICommandProjectMemoryStore): ICommandResult {
|
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
function formatShow(store:
|
|
62
|
+
async function formatShow(store: IMemoryStore, topic?: string): Promise<ICommandResult> {
|
|
61
63
|
if (!topic || topic === 'index') {
|
|
62
|
-
const memory = store.loadStartupMemory();
|
|
64
|
+
const memory = await store.loadStartupMemory();
|
|
63
65
|
return {
|
|
64
66
|
message: memory.content || '(empty memory index)',
|
|
65
67
|
success: true,
|
|
@@ -71,7 +73,7 @@ function formatShow(store: ICommandProjectMemoryStore, topic?: string): ICommand
|
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
const content = store.readTopic(topic);
|
|
76
|
+
const content = await store.readTopic(topic);
|
|
75
77
|
return {
|
|
76
78
|
message: content || `(empty memory topic: ${topic})`,
|
|
77
79
|
success: true,
|
|
@@ -88,8 +90,8 @@ function parseAdd(args: readonly string[]): IAppendMemoryInput | undefined {
|
|
|
88
90
|
return { type, topic, text };
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function formatPending(store:
|
|
92
|
-
const records = store.
|
|
93
|
+
async function formatPending(store: IMemoryStore): Promise<ICommandResult> {
|
|
94
|
+
const records = await store.listPending('pending');
|
|
93
95
|
const lines =
|
|
94
96
|
records.length > 0
|
|
95
97
|
? records.map(
|
|
@@ -105,21 +107,18 @@ function formatPending(store: ICommandPendingMemoryStore): ICommandResult {
|
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
function recordEvent(context:
|
|
110
|
+
function recordEvent(context: ICommandHostMemory, event: Omit<IMemoryEvent, 'at'>): void {
|
|
109
111
|
recordCommandMemoryEvent(context, event);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function approvePending(
|
|
113
|
-
context:
|
|
114
|
-
|
|
115
|
-
memoryStore: ICommandProjectMemoryStore,
|
|
114
|
+
async function approvePending(
|
|
115
|
+
context: ICommandHostMemory,
|
|
116
|
+
store: IMemoryStore,
|
|
116
117
|
id: string | undefined,
|
|
117
|
-
): ICommandResult {
|
|
118
|
+
): Promise<ICommandResult> {
|
|
118
119
|
if (!id) return usage();
|
|
119
120
|
try {
|
|
120
|
-
const
|
|
121
|
-
const saved = memoryStore.append(approved);
|
|
122
|
-
const record = pendingStore.mark(id, 'saved', 'approved-and-saved');
|
|
121
|
+
const { record, saved } = await approvePendingMemoryCandidate(store, id);
|
|
123
122
|
recordEvent(context, {
|
|
124
123
|
type: 'memory_candidate_approved',
|
|
125
124
|
candidateId: record.id,
|
|
@@ -150,14 +149,14 @@ function approvePending(
|
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
function rejectPending(
|
|
154
|
-
context:
|
|
155
|
-
|
|
152
|
+
async function rejectPending(
|
|
153
|
+
context: ICommandHostMemory,
|
|
154
|
+
store: IMemoryStore,
|
|
156
155
|
id: string | undefined,
|
|
157
|
-
): ICommandResult {
|
|
156
|
+
): Promise<ICommandResult> {
|
|
158
157
|
if (!id) return usage();
|
|
159
158
|
try {
|
|
160
|
-
const record =
|
|
159
|
+
const record = await store.markPending(id, 'rejected', 'rejected-by-user');
|
|
161
160
|
recordEvent(context, {
|
|
162
161
|
type: 'memory_candidate_rejected',
|
|
163
162
|
candidateId: record.id,
|
|
@@ -174,7 +173,7 @@ function rejectPending(
|
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
function formatUsed(context:
|
|
176
|
+
function formatUsed(context: ICommandHostMemory): ICommandResult {
|
|
178
177
|
const references = listCommandUsedMemoryReferences(context);
|
|
179
178
|
const lines =
|
|
180
179
|
references.length > 0
|
|
@@ -191,20 +190,21 @@ function formatUsed(context: ICommandHostContext): ICommandResult {
|
|
|
191
190
|
};
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
export function executeMemoryCommand(
|
|
195
|
-
context:
|
|
193
|
+
export async function executeMemoryCommand(
|
|
194
|
+
context: ICommandHostMemory & ICommandHostWorkspace,
|
|
196
195
|
rawArgs: string,
|
|
197
|
-
): ICommandResult {
|
|
196
|
+
): Promise<ICommandResult> {
|
|
198
197
|
const args = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
199
198
|
const subcommand = args[SUBCOMMAND_INDEX] ?? 'list';
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (subcommand === '
|
|
205
|
-
if (subcommand === '
|
|
206
|
-
|
|
207
|
-
if (subcommand === '
|
|
199
|
+
// SELFHOST-008 P1R: the single injected durable-memory port (or fs default) — authoritative for all
|
|
200
|
+
// `/memory` operations, so a surface that swaps the store is honored here too (no split-brain).
|
|
201
|
+
const store = createCommandMemoryStores(context);
|
|
202
|
+
|
|
203
|
+
if (subcommand === 'list') return formatList(store);
|
|
204
|
+
if (subcommand === 'show') return formatShow(store, args[TYPE_INDEX]);
|
|
205
|
+
if (subcommand === 'pending') return formatPending(store);
|
|
206
|
+
if (subcommand === 'approve') return approvePending(context, store, args[TYPE_INDEX]);
|
|
207
|
+
if (subcommand === 'reject') return rejectPending(context, store, args[TYPE_INDEX]);
|
|
208
208
|
if (subcommand === 'used') return formatUsed(context);
|
|
209
209
|
if (subcommand === 'add') {
|
|
210
210
|
const input = parseAdd(args);
|
|
@@ -215,7 +215,7 @@ export function executeMemoryCommand(
|
|
|
215
215
|
success: false,
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
const result =
|
|
218
|
+
const result = await store.append(input);
|
|
219
219
|
return {
|
|
220
220
|
message: result.deduplicated
|
|
221
221
|
? `${input.type} memory already exists in ${result.topicPath}`
|