clawvault 2.5.3 → 2.6.0

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 (95) hide show
  1. package/README.md +159 -159
  2. package/bin/clawvault.js +111 -111
  3. package/bin/command-registration.test.js +166 -166
  4. package/bin/command-runtime.js +93 -93
  5. package/bin/command-runtime.test.js +154 -154
  6. package/bin/help-contract.test.js +39 -39
  7. package/bin/register-config-commands.js +153 -153
  8. package/bin/register-config-route-commands.test.js +121 -121
  9. package/bin/register-core-commands.js +237 -237
  10. package/bin/register-kanban-commands.js +56 -56
  11. package/bin/register-kanban-commands.test.js +83 -83
  12. package/bin/register-maintenance-commands.js +282 -282
  13. package/bin/register-project-commands.js +209 -209
  14. package/bin/register-project-commands.test.js +206 -206
  15. package/bin/register-query-commands.js +317 -317
  16. package/bin/register-query-commands.test.js +65 -65
  17. package/bin/register-resilience-commands.js +182 -182
  18. package/bin/register-resilience-commands.test.js +81 -81
  19. package/bin/register-route-commands.js +114 -114
  20. package/bin/register-session-lifecycle-commands.js +206 -206
  21. package/bin/register-tailscale-commands.js +106 -106
  22. package/bin/register-task-commands.js +348 -348
  23. package/bin/register-task-commands.test.js +69 -69
  24. package/bin/register-template-commands.js +75 -72
  25. package/bin/register-template-commands.test.js +87 -0
  26. package/bin/register-vault-operations-commands.js +300 -300
  27. package/bin/test-helpers/cli-command-fixtures.js +119 -119
  28. package/dashboard/lib/graph-diff.js +104 -104
  29. package/dashboard/lib/graph-diff.test.js +75 -75
  30. package/dashboard/lib/vault-parser.js +556 -556
  31. package/dashboard/lib/vault-parser.test.js +254 -254
  32. package/dashboard/public/app.js +796 -796
  33. package/dashboard/public/index.html +52 -52
  34. package/dashboard/public/styles.css +221 -221
  35. package/dashboard/server.js +374 -374
  36. package/dist/{chunk-J5EMBUPK.js → chunk-4OXMU5S2.js} +1 -1
  37. package/dist/{chunk-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
  38. package/dist/{chunk-4IV3R2F5.js → chunk-4TE4JMLA.js} +1 -1
  39. package/dist/{chunk-5GZFTAL7.js → chunk-AZYOKJYC.js} +128 -42
  40. package/dist/{chunk-FG6RJMCN.js → chunk-HA5M6KJB.js} +4 -4
  41. package/dist/{chunk-IZEY5S74.js → chunk-IEVLHNLU.js} +1 -1
  42. package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
  43. package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
  44. package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
  45. package/dist/{chunk-OSMS7QIG.js → chunk-ME37YNW3.js} +2 -2
  46. package/dist/chunk-MFAWT5O5.js +301 -0
  47. package/dist/{chunk-TPDH3JPP.js → chunk-PBEE567J.js} +1 -1
  48. package/dist/{chunk-S2IG7VNM.js → chunk-Q2J5YTUF.js} +2 -2
  49. package/dist/{chunk-IOALNTAN.js → chunk-QWQ3TIKS.js} +103 -29
  50. package/dist/{chunk-YCVDVI5B.js → chunk-R2MIW5G7.js} +1 -1
  51. package/dist/{chunk-M25QVSJM.js → chunk-RVYA52PY.js} +1 -1
  52. package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
  53. package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
  54. package/dist/{chunk-LMEMZGUV.js → chunk-UEOUADMO.js} +3 -3
  55. package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
  56. package/dist/cli/index.js +15 -13
  57. package/dist/commands/backlog.js +3 -1
  58. package/dist/commands/blocked.js +3 -1
  59. package/dist/commands/canvas.js +3 -1
  60. package/dist/commands/context.js +3 -3
  61. package/dist/commands/doctor.js +9 -7
  62. package/dist/commands/embed.js +2 -2
  63. package/dist/commands/kanban.js +4 -2
  64. package/dist/commands/observe.js +7 -5
  65. package/dist/commands/project.js +5 -3
  66. package/dist/commands/rebuild.js +6 -4
  67. package/dist/commands/replay.js +6 -4
  68. package/dist/commands/setup.js +2 -2
  69. package/dist/commands/sleep.js +7 -5
  70. package/dist/commands/status.js +8 -6
  71. package/dist/commands/tailscale.js +3 -3
  72. package/dist/commands/task.js +4 -2
  73. package/dist/commands/template.d.ts +10 -1
  74. package/dist/commands/template.js +47 -55
  75. package/dist/commands/wake.js +2 -2
  76. package/dist/index.js +23 -22
  77. package/dist/lib/project-utils.js +4 -2
  78. package/dist/lib/tailscale.js +2 -2
  79. package/dist/lib/task-utils.d.ts +14 -13
  80. package/dist/lib/task-utils.js +3 -1
  81. package/dist/lib/template-engine.d.ts +1 -0
  82. package/dist/lib/webdav.js +1 -1
  83. package/hooks/clawvault/HOOK.md +83 -83
  84. package/hooks/clawvault/handler.js +816 -816
  85. package/hooks/clawvault/handler.test.js +263 -263
  86. package/package.json +94 -94
  87. package/templates/checkpoint.md +34 -19
  88. package/templates/daily-note.md +34 -19
  89. package/templates/daily.md +34 -19
  90. package/templates/decision.md +39 -17
  91. package/templates/handoff.md +34 -19
  92. package/templates/lesson.md +31 -16
  93. package/templates/person.md +37 -19
  94. package/templates/project.md +84 -23
  95. package/templates/task.md +81 -0
@@ -1,263 +1,263 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
-
6
- const { execFileSyncMock } = vi.hoisted(() => ({
7
- execFileSyncMock: vi.fn()
8
- }));
9
-
10
- vi.mock('child_process', () => ({
11
- execFileSync: execFileSyncMock
12
- }));
13
-
14
- function makeVaultFixture() {
15
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-hook-'));
16
- fs.writeFileSync(path.join(root, '.clawvault.json'), JSON.stringify({ name: 'test' }), 'utf-8');
17
- return root;
18
- }
19
-
20
- function makeOpenClawSessionFixture(agentId, sessionId, transcriptBytes = 0) {
21
- const stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-openclaw-'));
22
- const sessionsDir = path.join(stateRoot, 'agents', agentId, 'sessions');
23
- fs.mkdirSync(sessionsDir, { recursive: true });
24
- fs.writeFileSync(
25
- path.join(sessionsDir, 'sessions.json'),
26
- JSON.stringify({
27
- [`agent:${agentId}:main`]: {
28
- sessionId,
29
- updatedAt: Date.now()
30
- }
31
- }),
32
- 'utf-8'
33
- );
34
- const transcriptPath = path.join(sessionsDir, `${sessionId}.jsonl`);
35
- const payload = transcriptBytes > 0 ? 'x'.repeat(transcriptBytes) : '';
36
- fs.writeFileSync(transcriptPath, payload, 'utf-8');
37
- return { stateRoot, sessionsDir, transcriptPath };
38
- }
39
-
40
- async function loadHandler() {
41
- vi.resetModules();
42
- const mod = await import('./handler.js');
43
- return mod.default;
44
- }
45
-
46
- afterEach(() => {
47
- vi.clearAllMocks();
48
- delete process.env.CLAWVAULT_PATH;
49
- delete process.env.OPENCLAW_STATE_DIR;
50
- delete process.env.OPENCLAW_HOME;
51
- delete process.env.OPENCLAW_AGENT_ID;
52
- });
53
-
54
- describe('clawvault hook handler', () => {
55
- it('injects recovery warning on gateway startup when death detected', async () => {
56
- const vaultPath = makeVaultFixture();
57
- process.env.CLAWVAULT_PATH = vaultPath;
58
-
59
- execFileSyncMock.mockImplementation((_command, args) => {
60
- if (args[0] === 'recover') {
61
- return '⚠️ CONTEXT DEATH DETECTED\nWorking on: ship memory graph';
62
- }
63
- return '';
64
- });
65
-
66
- const handler = await loadHandler();
67
- const event = {
68
- type: 'gateway',
69
- action: 'startup',
70
- messages: [{ role: 'user', content: 'hello' }]
71
- };
72
-
73
- await handler(event);
74
-
75
- expect(execFileSyncMock).toHaveBeenCalledWith(
76
- 'clawvault',
77
- expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
78
- expect.objectContaining({ shell: false })
79
- );
80
- const injected = event.messages.find((message) => message.role === 'system');
81
- expect(injected?.content).toContain('Context death detected');
82
- expect(injected?.content).toContain('ship memory graph');
83
-
84
- fs.rmSync(vaultPath, { recursive: true, force: true });
85
- });
86
-
87
- it('supports alias event names for command:new', async () => {
88
- const vaultPath = makeVaultFixture();
89
- process.env.CLAWVAULT_PATH = vaultPath;
90
- execFileSyncMock.mockReturnValue('');
91
-
92
- const handler = await loadHandler();
93
- await handler({
94
- event: 'command:new',
95
- sessionKey: 'agent:clawdious:main',
96
- context: { commandSource: 'cli' }
97
- });
98
-
99
- expect(execFileSyncMock).toHaveBeenCalledWith(
100
- 'clawvault',
101
- expect.arrayContaining(['checkpoint', '--working-on']),
102
- expect.objectContaining({ shell: false })
103
- );
104
-
105
- fs.rmSync(vaultPath, { recursive: true, force: true });
106
- });
107
-
108
- it('injects recap and memory context on session start alias event', async () => {
109
- const vaultPath = makeVaultFixture();
110
- process.env.CLAWVAULT_PATH = vaultPath;
111
-
112
- execFileSyncMock.mockImplementation((_command, args) => {
113
- if (args[0] === 'session-recap') {
114
- return JSON.stringify({
115
- messages: [
116
- { role: 'user', text: 'Need a migration plan.' },
117
- { role: 'assistant', text: 'Suggested phased rollout.' }
118
- ]
119
- });
120
- }
121
- if (args[0] === 'context') {
122
- return JSON.stringify({
123
- context: [
124
- {
125
- title: 'Use Postgres',
126
- age: '1 day ago',
127
- snippet: 'Selected Postgres for durability.'
128
- }
129
- ]
130
- });
131
- }
132
- return '';
133
- });
134
-
135
- const handler = await loadHandler();
136
- const event = {
137
- eventName: 'session:start',
138
- sessionKey: 'agent:clawdious:main',
139
- context: { initialPrompt: 'Need migration plan' },
140
- messages: [{ role: 'user', content: 'Need migration plan' }]
141
- };
142
-
143
- await handler(event);
144
-
145
- const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
146
- expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
147
-
148
- const injected = event.messages.find((message) => message.role === 'system');
149
- expect(injected?.content).toContain('Session context restored');
150
- expect(injected?.content).toContain('Recent conversation');
151
- expect(injected?.content).toContain('Relevant memories');
152
- expect(injected?.content).toContain('Use Postgres');
153
-
154
- fs.rmSync(vaultPath, { recursive: true, force: true });
155
- });
156
-
157
- it('delegates profile selection to context auto mode for urgent prompts', async () => {
158
- const vaultPath = makeVaultFixture();
159
- process.env.CLAWVAULT_PATH = vaultPath;
160
-
161
- execFileSyncMock.mockImplementation((_command, args) => {
162
- if (args[0] === 'session-recap') {
163
- return JSON.stringify({ messages: [] });
164
- }
165
- if (args[0] === 'context') {
166
- return JSON.stringify({ context: [] });
167
- }
168
- return '';
169
- });
170
-
171
- const handler = await loadHandler();
172
- await handler({
173
- eventName: 'session:start',
174
- sessionKey: 'agent:clawdious:main',
175
- context: { initialPrompt: 'URGENT outage: rollback failed in production' },
176
- messages: [{ role: 'user', content: 'URGENT outage: rollback failed in production' }]
177
- });
178
-
179
- const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
180
- expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
181
-
182
- fs.rmSync(vaultPath, { recursive: true, force: true });
183
- });
184
-
185
- it('triggers active observation on heartbeat when threshold is crossed', async () => {
186
- const vaultPath = makeVaultFixture();
187
- const sessionId = 'heartbeat-session-1';
188
- const openClawFixture = makeOpenClawSessionFixture('main', sessionId, 70 * 1024);
189
- process.env.CLAWVAULT_PATH = vaultPath;
190
- process.env.OPENCLAW_STATE_DIR = openClawFixture.stateRoot;
191
-
192
- fs.mkdirSync(path.join(vaultPath, '.clawvault'), { recursive: true });
193
- fs.writeFileSync(
194
- path.join(vaultPath, '.clawvault', 'observe-cursors.json'),
195
- JSON.stringify({
196
- [sessionId]: {
197
- lastObservedOffset: 0,
198
- lastObservedAt: '2026-02-14T00:00:00.000Z',
199
- sessionKey: 'agent:main:main',
200
- lastFileSize: 0
201
- }
202
- }),
203
- 'utf-8'
204
- );
205
-
206
- execFileSyncMock.mockReturnValue('');
207
-
208
- const handler = await loadHandler();
209
- await handler({
210
- type: 'gateway',
211
- action: 'heartbeat'
212
- });
213
-
214
- expect(execFileSyncMock).toHaveBeenCalledWith(
215
- 'clawvault',
216
- expect.arrayContaining(['observe', '--cron', '--agent', 'main']),
217
- expect.objectContaining({ shell: false })
218
- );
219
-
220
- fs.rmSync(vaultPath, { recursive: true, force: true });
221
- fs.rmSync(openClawFixture.stateRoot, { recursive: true, force: true });
222
- });
223
-
224
- it('forces active observation flush on compaction events', async () => {
225
- const vaultPath = makeVaultFixture();
226
- process.env.CLAWVAULT_PATH = vaultPath;
227
- execFileSyncMock.mockReturnValue('');
228
-
229
- const handler = await loadHandler();
230
- await handler({
231
- eventName: 'compaction:memoryFlush',
232
- sessionKey: 'agent:clawdious:main'
233
- });
234
-
235
- expect(execFileSyncMock).toHaveBeenCalledWith(
236
- 'clawvault',
237
- expect.arrayContaining(['observe', '--cron', '--min-new', '1']),
238
- expect.objectContaining({ shell: false })
239
- );
240
-
241
- fs.rmSync(vaultPath, { recursive: true, force: true });
242
- });
243
-
244
- it('runs weekly reflection on cron.weekly at Sunday midnight', async () => {
245
- const vaultPath = makeVaultFixture();
246
- process.env.CLAWVAULT_PATH = vaultPath;
247
- execFileSyncMock.mockReturnValue('');
248
-
249
- const handler = await loadHandler();
250
- await handler({
251
- eventName: 'cron.weekly',
252
- timestamp: '2026-02-15T00:00:00.000Z'
253
- });
254
-
255
- expect(execFileSyncMock).toHaveBeenCalledWith(
256
- 'clawvault',
257
- expect.arrayContaining(['reflect', '-v', vaultPath]),
258
- expect.objectContaining({ shell: false })
259
- );
260
-
261
- fs.rmSync(vaultPath, { recursive: true, force: true });
262
- });
263
- });
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+
6
+ const { execFileSyncMock } = vi.hoisted(() => ({
7
+ execFileSyncMock: vi.fn()
8
+ }));
9
+
10
+ vi.mock('child_process', () => ({
11
+ execFileSync: execFileSyncMock
12
+ }));
13
+
14
+ function makeVaultFixture() {
15
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-hook-'));
16
+ fs.writeFileSync(path.join(root, '.clawvault.json'), JSON.stringify({ name: 'test' }), 'utf-8');
17
+ return root;
18
+ }
19
+
20
+ function makeOpenClawSessionFixture(agentId, sessionId, transcriptBytes = 0) {
21
+ const stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-openclaw-'));
22
+ const sessionsDir = path.join(stateRoot, 'agents', agentId, 'sessions');
23
+ fs.mkdirSync(sessionsDir, { recursive: true });
24
+ fs.writeFileSync(
25
+ path.join(sessionsDir, 'sessions.json'),
26
+ JSON.stringify({
27
+ [`agent:${agentId}:main`]: {
28
+ sessionId,
29
+ updatedAt: Date.now()
30
+ }
31
+ }),
32
+ 'utf-8'
33
+ );
34
+ const transcriptPath = path.join(sessionsDir, `${sessionId}.jsonl`);
35
+ const payload = transcriptBytes > 0 ? 'x'.repeat(transcriptBytes) : '';
36
+ fs.writeFileSync(transcriptPath, payload, 'utf-8');
37
+ return { stateRoot, sessionsDir, transcriptPath };
38
+ }
39
+
40
+ async function loadHandler() {
41
+ vi.resetModules();
42
+ const mod = await import('./handler.js');
43
+ return mod.default;
44
+ }
45
+
46
+ afterEach(() => {
47
+ vi.clearAllMocks();
48
+ delete process.env.CLAWVAULT_PATH;
49
+ delete process.env.OPENCLAW_STATE_DIR;
50
+ delete process.env.OPENCLAW_HOME;
51
+ delete process.env.OPENCLAW_AGENT_ID;
52
+ });
53
+
54
+ describe('clawvault hook handler', () => {
55
+ it('injects recovery warning on gateway startup when death detected', async () => {
56
+ const vaultPath = makeVaultFixture();
57
+ process.env.CLAWVAULT_PATH = vaultPath;
58
+
59
+ execFileSyncMock.mockImplementation((_command, args) => {
60
+ if (args[0] === 'recover') {
61
+ return '⚠️ CONTEXT DEATH DETECTED\nWorking on: ship memory graph';
62
+ }
63
+ return '';
64
+ });
65
+
66
+ const handler = await loadHandler();
67
+ const event = {
68
+ type: 'gateway',
69
+ action: 'startup',
70
+ messages: [{ role: 'user', content: 'hello' }]
71
+ };
72
+
73
+ await handler(event);
74
+
75
+ expect(execFileSyncMock).toHaveBeenCalledWith(
76
+ 'clawvault',
77
+ expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
78
+ expect.objectContaining({ shell: false })
79
+ );
80
+ const injected = event.messages.find((message) => message.role === 'system');
81
+ expect(injected?.content).toContain('Context death detected');
82
+ expect(injected?.content).toContain('ship memory graph');
83
+
84
+ fs.rmSync(vaultPath, { recursive: true, force: true });
85
+ });
86
+
87
+ it('supports alias event names for command:new', async () => {
88
+ const vaultPath = makeVaultFixture();
89
+ process.env.CLAWVAULT_PATH = vaultPath;
90
+ execFileSyncMock.mockReturnValue('');
91
+
92
+ const handler = await loadHandler();
93
+ await handler({
94
+ event: 'command:new',
95
+ sessionKey: 'agent:clawdious:main',
96
+ context: { commandSource: 'cli' }
97
+ });
98
+
99
+ expect(execFileSyncMock).toHaveBeenCalledWith(
100
+ 'clawvault',
101
+ expect.arrayContaining(['checkpoint', '--working-on']),
102
+ expect.objectContaining({ shell: false })
103
+ );
104
+
105
+ fs.rmSync(vaultPath, { recursive: true, force: true });
106
+ });
107
+
108
+ it('injects recap and memory context on session start alias event', async () => {
109
+ const vaultPath = makeVaultFixture();
110
+ process.env.CLAWVAULT_PATH = vaultPath;
111
+
112
+ execFileSyncMock.mockImplementation((_command, args) => {
113
+ if (args[0] === 'session-recap') {
114
+ return JSON.stringify({
115
+ messages: [
116
+ { role: 'user', text: 'Need a migration plan.' },
117
+ { role: 'assistant', text: 'Suggested phased rollout.' }
118
+ ]
119
+ });
120
+ }
121
+ if (args[0] === 'context') {
122
+ return JSON.stringify({
123
+ context: [
124
+ {
125
+ title: 'Use Postgres',
126
+ age: '1 day ago',
127
+ snippet: 'Selected Postgres for durability.'
128
+ }
129
+ ]
130
+ });
131
+ }
132
+ return '';
133
+ });
134
+
135
+ const handler = await loadHandler();
136
+ const event = {
137
+ eventName: 'session:start',
138
+ sessionKey: 'agent:clawdious:main',
139
+ context: { initialPrompt: 'Need migration plan' },
140
+ messages: [{ role: 'user', content: 'Need migration plan' }]
141
+ };
142
+
143
+ await handler(event);
144
+
145
+ const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
146
+ expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
147
+
148
+ const injected = event.messages.find((message) => message.role === 'system');
149
+ expect(injected?.content).toContain('Session context restored');
150
+ expect(injected?.content).toContain('Recent conversation');
151
+ expect(injected?.content).toContain('Relevant memories');
152
+ expect(injected?.content).toContain('Use Postgres');
153
+
154
+ fs.rmSync(vaultPath, { recursive: true, force: true });
155
+ });
156
+
157
+ it('delegates profile selection to context auto mode for urgent prompts', async () => {
158
+ const vaultPath = makeVaultFixture();
159
+ process.env.CLAWVAULT_PATH = vaultPath;
160
+
161
+ execFileSyncMock.mockImplementation((_command, args) => {
162
+ if (args[0] === 'session-recap') {
163
+ return JSON.stringify({ messages: [] });
164
+ }
165
+ if (args[0] === 'context') {
166
+ return JSON.stringify({ context: [] });
167
+ }
168
+ return '';
169
+ });
170
+
171
+ const handler = await loadHandler();
172
+ await handler({
173
+ eventName: 'session:start',
174
+ sessionKey: 'agent:clawdious:main',
175
+ context: { initialPrompt: 'URGENT outage: rollback failed in production' },
176
+ messages: [{ role: 'user', content: 'URGENT outage: rollback failed in production' }]
177
+ });
178
+
179
+ const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
180
+ expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
181
+
182
+ fs.rmSync(vaultPath, { recursive: true, force: true });
183
+ });
184
+
185
+ it('triggers active observation on heartbeat when threshold is crossed', async () => {
186
+ const vaultPath = makeVaultFixture();
187
+ const sessionId = 'heartbeat-session-1';
188
+ const openClawFixture = makeOpenClawSessionFixture('main', sessionId, 70 * 1024);
189
+ process.env.CLAWVAULT_PATH = vaultPath;
190
+ process.env.OPENCLAW_STATE_DIR = openClawFixture.stateRoot;
191
+
192
+ fs.mkdirSync(path.join(vaultPath, '.clawvault'), { recursive: true });
193
+ fs.writeFileSync(
194
+ path.join(vaultPath, '.clawvault', 'observe-cursors.json'),
195
+ JSON.stringify({
196
+ [sessionId]: {
197
+ lastObservedOffset: 0,
198
+ lastObservedAt: '2026-02-14T00:00:00.000Z',
199
+ sessionKey: 'agent:main:main',
200
+ lastFileSize: 0
201
+ }
202
+ }),
203
+ 'utf-8'
204
+ );
205
+
206
+ execFileSyncMock.mockReturnValue('');
207
+
208
+ const handler = await loadHandler();
209
+ await handler({
210
+ type: 'gateway',
211
+ action: 'heartbeat'
212
+ });
213
+
214
+ expect(execFileSyncMock).toHaveBeenCalledWith(
215
+ 'clawvault',
216
+ expect.arrayContaining(['observe', '--cron', '--agent', 'main']),
217
+ expect.objectContaining({ shell: false })
218
+ );
219
+
220
+ fs.rmSync(vaultPath, { recursive: true, force: true });
221
+ fs.rmSync(openClawFixture.stateRoot, { recursive: true, force: true });
222
+ });
223
+
224
+ it('forces active observation flush on compaction events', async () => {
225
+ const vaultPath = makeVaultFixture();
226
+ process.env.CLAWVAULT_PATH = vaultPath;
227
+ execFileSyncMock.mockReturnValue('');
228
+
229
+ const handler = await loadHandler();
230
+ await handler({
231
+ eventName: 'compaction:memoryFlush',
232
+ sessionKey: 'agent:clawdious:main'
233
+ });
234
+
235
+ expect(execFileSyncMock).toHaveBeenCalledWith(
236
+ 'clawvault',
237
+ expect.arrayContaining(['observe', '--cron', '--min-new', '1']),
238
+ expect.objectContaining({ shell: false })
239
+ );
240
+
241
+ fs.rmSync(vaultPath, { recursive: true, force: true });
242
+ });
243
+
244
+ it('runs weekly reflection on cron.weekly at Sunday midnight', async () => {
245
+ const vaultPath = makeVaultFixture();
246
+ process.env.CLAWVAULT_PATH = vaultPath;
247
+ execFileSyncMock.mockReturnValue('');
248
+
249
+ const handler = await loadHandler();
250
+ await handler({
251
+ eventName: 'cron.weekly',
252
+ timestamp: '2026-02-15T00:00:00.000Z'
253
+ });
254
+
255
+ expect(execFileSyncMock).toHaveBeenCalledWith(
256
+ 'clawvault',
257
+ expect.arrayContaining(['reflect', '-v', vaultPath]),
258
+ expect.objectContaining({ shell: false })
259
+ );
260
+
261
+ fs.rmSync(vaultPath, { recursive: true, force: true });
262
+ });
263
+ });