deepflow 0.1.92 → 0.1.95
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/bin/install.js +32 -2
- package/hooks/df-subagent-registry.js +34 -0
- package/hooks/df-subagent-registry.test.js +357 -0
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -243,6 +243,7 @@ async function configureHooks(claudeDir) {
|
|
|
243
243
|
const worktreeGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-worktree-guard.js')}"`;
|
|
244
244
|
const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
|
|
245
245
|
const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
|
|
246
|
+
const subagentRegistryCmd = `node "${path.join(claudeDir, 'hooks', 'df-subagent-registry.js')}"`;
|
|
246
247
|
|
|
247
248
|
let settings = {};
|
|
248
249
|
|
|
@@ -395,6 +396,26 @@ async function configureHooks(claudeDir) {
|
|
|
395
396
|
});
|
|
396
397
|
log('PostToolUse hook configured');
|
|
397
398
|
|
|
399
|
+
// Configure SubagentStop hook for subagent registry
|
|
400
|
+
if (!settings.hooks.SubagentStop) {
|
|
401
|
+
settings.hooks.SubagentStop = [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Remove any existing subagent registry hooks
|
|
405
|
+
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
|
|
406
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
407
|
+
return !cmd.includes('df-subagent-registry');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Add subagent registry hook
|
|
411
|
+
settings.hooks.SubagentStop.push({
|
|
412
|
+
hooks: [{
|
|
413
|
+
type: 'command',
|
|
414
|
+
command: subagentRegistryCmd
|
|
415
|
+
}]
|
|
416
|
+
});
|
|
417
|
+
log('SubagentStop hook configured');
|
|
418
|
+
|
|
398
419
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
399
420
|
}
|
|
400
421
|
|
|
@@ -575,7 +596,7 @@ async function uninstall() {
|
|
|
575
596
|
];
|
|
576
597
|
|
|
577
598
|
if (level === 'global') {
|
|
578
|
-
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js');
|
|
599
|
+
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js', 'hooks/df-subagent-registry.js');
|
|
579
600
|
}
|
|
580
601
|
|
|
581
602
|
for (const item of toRemove) {
|
|
@@ -628,11 +649,20 @@ async function uninstall() {
|
|
|
628
649
|
delete settings.hooks.PostToolUse;
|
|
629
650
|
}
|
|
630
651
|
}
|
|
652
|
+
if (settings.hooks?.SubagentStop) {
|
|
653
|
+
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(hook => {
|
|
654
|
+
const cmd = hook.hooks?.[0]?.command || '';
|
|
655
|
+
return !cmd.includes('df-subagent-registry');
|
|
656
|
+
});
|
|
657
|
+
if (settings.hooks.SubagentStop.length === 0) {
|
|
658
|
+
delete settings.hooks.SubagentStop;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
631
661
|
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
632
662
|
delete settings.hooks;
|
|
633
663
|
}
|
|
634
664
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
635
|
-
console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PostToolUse hooks`);
|
|
665
|
+
console.log(` ${c.green}✓${c.reset} Removed SessionStart/SessionEnd/PostToolUse/SubagentStop hooks`);
|
|
636
666
|
} catch (e) {
|
|
637
667
|
// Fail silently
|
|
638
668
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
let raw = '';
|
|
8
|
+
process.stdin.on('data', d => raw += d);
|
|
9
|
+
process.stdin.on('end', () => {
|
|
10
|
+
try {
|
|
11
|
+
const event = JSON.parse(raw);
|
|
12
|
+
|
|
13
|
+
// Extract required fields from SubagentStop event
|
|
14
|
+
const { session_id, agent_type, agent_id } = event;
|
|
15
|
+
|
|
16
|
+
// Generate timestamp
|
|
17
|
+
const timestamp = new Date().toISOString();
|
|
18
|
+
|
|
19
|
+
// Build registry entry
|
|
20
|
+
const entry = {
|
|
21
|
+
session_id,
|
|
22
|
+
agent_type,
|
|
23
|
+
agent_id,
|
|
24
|
+
timestamp
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Append to registry file (fire-and-forget)
|
|
28
|
+
const registryPath = path.join(os.homedir(), '.claude', 'subagent-sessions.jsonl');
|
|
29
|
+
fs.appendFileSync(registryPath, JSON.stringify(entry) + '\n');
|
|
30
|
+
} catch {
|
|
31
|
+
// Exit 0 on any error (fail-open)
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for hooks/df-subagent-registry.js
|
|
3
|
+
*
|
|
4
|
+
* Tests the SubagentStop hook that reads event JSON from stdin,
|
|
5
|
+
* extracts session_id/agent_type/agent_id, generates a timestamp,
|
|
6
|
+
* and appends a JSON line to ~/.claude/subagent-sessions.jsonl.
|
|
7
|
+
* Fire-and-forget, fail-open (exit 0 on error).
|
|
8
|
+
*
|
|
9
|
+
* Uses Node.js built-in node:test to avoid adding dependencies.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
15
|
+
const assert = require('node:assert/strict');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const { execFileSync } = require('node:child_process');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const HOOK_PATH = path.resolve(__dirname, 'df-subagent-registry.js');
|
|
26
|
+
|
|
27
|
+
function makeTmpDir() {
|
|
28
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'df-subagent-registry-test-'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rmrf(dir) {
|
|
32
|
+
if (fs.existsSync(dir)) {
|
|
33
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run the subagent registry hook as a child process with JSON piped to stdin.
|
|
39
|
+
* Overrides HOME so the registry file lands in our tmp dir.
|
|
40
|
+
* Returns { stdout, stderr, code }.
|
|
41
|
+
*/
|
|
42
|
+
function runHook(input, { home } = {}) {
|
|
43
|
+
const json = typeof input === 'string' ? input : JSON.stringify(input);
|
|
44
|
+
const env = { ...process.env };
|
|
45
|
+
if (home) {
|
|
46
|
+
env.HOME = home;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const stdout = execFileSync(
|
|
50
|
+
process.execPath,
|
|
51
|
+
[HOOK_PATH],
|
|
52
|
+
{
|
|
53
|
+
input: json,
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
env,
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
return { stdout, stderr: '', code: 0 };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
stdout: err.stdout || '',
|
|
63
|
+
stderr: err.stderr || '',
|
|
64
|
+
code: err.status ?? 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read the registry file and return parsed JSON lines.
|
|
71
|
+
*/
|
|
72
|
+
function readRegistry(home) {
|
|
73
|
+
const registryPath = path.join(home, '.claude', 'subagent-sessions.jsonl');
|
|
74
|
+
if (!fs.existsSync(registryPath)) return [];
|
|
75
|
+
const content = fs.readFileSync(registryPath, 'utf8').trim();
|
|
76
|
+
if (!content) return [];
|
|
77
|
+
return content.split('\n').map(line => JSON.parse(line));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// 1. Valid SubagentStop event — appends correct JSON line
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
describe('df-subagent-registry — valid SubagentStop event', () => {
|
|
85
|
+
let tmpHome;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
tmpHome = makeTmpDir();
|
|
89
|
+
// Create ~/.claude directory so appendFileSync works
|
|
90
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
rmrf(tmpHome);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('appends JSON line with session_id, agent_type, agent_id, timestamp', () => {
|
|
98
|
+
const event = {
|
|
99
|
+
session_id: 'sess-abc-123',
|
|
100
|
+
agent_type: 'reasoner',
|
|
101
|
+
agent_id: 'agent-xyz-789',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = runHook(event, { home: tmpHome });
|
|
105
|
+
assert.equal(result.code, 0);
|
|
106
|
+
|
|
107
|
+
const entries = readRegistry(tmpHome);
|
|
108
|
+
assert.equal(entries.length, 1);
|
|
109
|
+
assert.equal(entries[0].session_id, 'sess-abc-123');
|
|
110
|
+
assert.equal(entries[0].agent_type, 'reasoner');
|
|
111
|
+
assert.equal(entries[0].agent_id, 'agent-xyz-789');
|
|
112
|
+
assert.ok(entries[0].timestamp, 'timestamp field should be present');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('timestamp is ISO-8601 format', () => {
|
|
116
|
+
const event = {
|
|
117
|
+
session_id: 'sess-ts-check',
|
|
118
|
+
agent_type: 'worker',
|
|
119
|
+
agent_id: 'agent-ts-001',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const result = runHook(event, { home: tmpHome });
|
|
123
|
+
assert.equal(result.code, 0);
|
|
124
|
+
|
|
125
|
+
const entries = readRegistry(tmpHome);
|
|
126
|
+
assert.equal(entries.length, 1);
|
|
127
|
+
|
|
128
|
+
// ISO-8601 format: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
129
|
+
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
130
|
+
assert.match(entries[0].timestamp, isoRegex, 'timestamp should be ISO-8601');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('multiple invocations append multiple lines', () => {
|
|
134
|
+
const event1 = { session_id: 'sess-1', agent_type: 'reasoner', agent_id: 'a1' };
|
|
135
|
+
const event2 = { session_id: 'sess-2', agent_type: 'worker', agent_id: 'a2' };
|
|
136
|
+
|
|
137
|
+
runHook(event1, { home: tmpHome });
|
|
138
|
+
runHook(event2, { home: tmpHome });
|
|
139
|
+
|
|
140
|
+
const entries = readRegistry(tmpHome);
|
|
141
|
+
assert.equal(entries.length, 2);
|
|
142
|
+
assert.equal(entries[0].session_id, 'sess-1');
|
|
143
|
+
assert.equal(entries[1].session_id, 'sess-2');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('entry only contains session_id, agent_type, agent_id, and timestamp', () => {
|
|
147
|
+
const event = {
|
|
148
|
+
session_id: 'sess-fields',
|
|
149
|
+
agent_type: 'qa',
|
|
150
|
+
agent_id: 'agent-f',
|
|
151
|
+
extra_field: 'should-not-appear',
|
|
152
|
+
nested: { foo: 'bar' },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = runHook(event, { home: tmpHome });
|
|
156
|
+
assert.equal(result.code, 0);
|
|
157
|
+
|
|
158
|
+
const entries = readRegistry(tmpHome);
|
|
159
|
+
assert.equal(entries.length, 1);
|
|
160
|
+
const keys = Object.keys(entries[0]).sort();
|
|
161
|
+
assert.deepEqual(keys, ['agent_id', 'agent_type', 'session_id', 'timestamp']);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// 2. Registry file creation
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe('df-subagent-registry — file creation', () => {
|
|
170
|
+
let tmpHome;
|
|
171
|
+
|
|
172
|
+
beforeEach(() => {
|
|
173
|
+
tmpHome = makeTmpDir();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
rmrf(tmpHome);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('creates registry file if ~/.claude directory exists but file does not', () => {
|
|
181
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
182
|
+
const event = { session_id: 's1', agent_type: 'r', agent_id: 'a1' };
|
|
183
|
+
|
|
184
|
+
const result = runHook(event, { home: tmpHome });
|
|
185
|
+
assert.equal(result.code, 0);
|
|
186
|
+
|
|
187
|
+
const registryPath = path.join(tmpHome, '.claude', 'subagent-sessions.jsonl');
|
|
188
|
+
assert.ok(fs.existsSync(registryPath), 'registry file should be created');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('exits 0 when ~/.claude directory does not exist (fail-open)', () => {
|
|
192
|
+
// No .claude directory — appendFileSync will throw ENOENT
|
|
193
|
+
const event = { session_id: 's1', agent_type: 'r', agent_id: 'a1' };
|
|
194
|
+
|
|
195
|
+
const result = runHook(event, { home: tmpHome });
|
|
196
|
+
assert.equal(result.code, 0, 'should exit 0 even when directory is missing');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// 3. Missing fields — fail-open (exit 0)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
describe('df-subagent-registry — missing fields (fail-open)', () => {
|
|
205
|
+
let tmpHome;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
tmpHome = makeTmpDir();
|
|
209
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
rmrf(tmpHome);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('exits 0 when session_id is missing', () => {
|
|
217
|
+
const event = { agent_type: 'reasoner', agent_id: 'a1' };
|
|
218
|
+
const result = runHook(event, { home: tmpHome });
|
|
219
|
+
assert.equal(result.code, 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('exits 0 when agent_type is missing', () => {
|
|
223
|
+
const event = { session_id: 's1', agent_id: 'a1' };
|
|
224
|
+
const result = runHook(event, { home: tmpHome });
|
|
225
|
+
assert.equal(result.code, 0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('exits 0 when agent_id is missing', () => {
|
|
229
|
+
const event = { session_id: 's1', agent_type: 'reasoner' };
|
|
230
|
+
const result = runHook(event, { home: tmpHome });
|
|
231
|
+
assert.equal(result.code, 0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('exits 0 with empty object', () => {
|
|
235
|
+
const result = runHook({}, { home: tmpHome });
|
|
236
|
+
assert.equal(result.code, 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('still writes entry with undefined fields when fields are missing', () => {
|
|
240
|
+
const event = { session_id: 's1' };
|
|
241
|
+
runHook(event, { home: tmpHome });
|
|
242
|
+
|
|
243
|
+
const entries = readRegistry(tmpHome);
|
|
244
|
+
// The hook destructures and writes whatever it gets — undefined becomes null/omitted in JSON
|
|
245
|
+
assert.equal(entries.length, 1);
|
|
246
|
+
assert.equal(entries[0].session_id, 's1');
|
|
247
|
+
assert.ok(entries[0].timestamp, 'timestamp should still be present');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// 4. Invalid JSON stdin — fail-open (exit 0)
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
describe('df-subagent-registry — invalid JSON stdin', () => {
|
|
256
|
+
test('exits 0 on completely invalid JSON', () => {
|
|
257
|
+
const result = runHook('not valid json{{{');
|
|
258
|
+
assert.equal(result.code, 0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('exits 0 on empty stdin', () => {
|
|
262
|
+
const result = runHook('');
|
|
263
|
+
assert.equal(result.code, 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('exits 0 on truncated JSON', () => {
|
|
267
|
+
const result = runHook('{"session_id": "s1", "agent_type":');
|
|
268
|
+
assert.equal(result.code, 0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('exits 0 on non-object JSON (array)', () => {
|
|
272
|
+
// JSON.parse succeeds but destructuring an array yields undefined fields
|
|
273
|
+
// appendFileSync may still work — either way, exit 0
|
|
274
|
+
let tmpHome = makeTmpDir();
|
|
275
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
276
|
+
const result = runHook('[1, 2, 3]', { home: tmpHome });
|
|
277
|
+
assert.equal(result.code, 0);
|
|
278
|
+
rmrf(tmpHome);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('exits 0 on JSON null', () => {
|
|
282
|
+
const result = runHook('null');
|
|
283
|
+
assert.equal(result.code, 0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// 5. Edge cases
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe('df-subagent-registry — edge cases', () => {
|
|
292
|
+
let tmpHome;
|
|
293
|
+
|
|
294
|
+
beforeEach(() => {
|
|
295
|
+
tmpHome = makeTmpDir();
|
|
296
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
afterEach(() => {
|
|
300
|
+
rmrf(tmpHome);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('appended line is valid JSONL (ends with newline)', () => {
|
|
304
|
+
const event = { session_id: 's1', agent_type: 'r', agent_id: 'a1' };
|
|
305
|
+
runHook(event, { home: tmpHome });
|
|
306
|
+
|
|
307
|
+
const registryPath = path.join(tmpHome, '.claude', 'subagent-sessions.jsonl');
|
|
308
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
309
|
+
assert.ok(raw.endsWith('\n'), 'registry entry should end with newline');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('handles special characters in field values', () => {
|
|
313
|
+
const event = {
|
|
314
|
+
session_id: 'sess-with-"quotes"-and-\\backslash',
|
|
315
|
+
agent_type: 'type/with/slashes',
|
|
316
|
+
agent_id: 'id with spaces & symbols!@#',
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const result = runHook(event, { home: tmpHome });
|
|
320
|
+
assert.equal(result.code, 0);
|
|
321
|
+
|
|
322
|
+
const entries = readRegistry(tmpHome);
|
|
323
|
+
assert.equal(entries.length, 1);
|
|
324
|
+
assert.equal(entries[0].session_id, 'sess-with-"quotes"-and-\\backslash');
|
|
325
|
+
assert.equal(entries[0].agent_type, 'type/with/slashes');
|
|
326
|
+
assert.equal(entries[0].agent_id, 'id with spaces & symbols!@#');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('writes to correct path: ~/.claude/subagent-sessions.jsonl', () => {
|
|
330
|
+
const event = { session_id: 's1', agent_type: 'r', agent_id: 'a1' };
|
|
331
|
+
runHook(event, { home: tmpHome });
|
|
332
|
+
|
|
333
|
+
const expectedPath = path.join(tmpHome, '.claude', 'subagent-sessions.jsonl');
|
|
334
|
+
assert.ok(fs.existsSync(expectedPath), 'file should exist at expected path');
|
|
335
|
+
|
|
336
|
+
// Verify no other jsonl files were created
|
|
337
|
+
const claudeDir = path.join(tmpHome, '.claude');
|
|
338
|
+
const files = fs.readdirSync(claudeDir).filter(f => f.endsWith('.jsonl'));
|
|
339
|
+
assert.equal(files.length, 1);
|
|
340
|
+
assert.equal(files[0], 'subagent-sessions.jsonl');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('each appended line is independently parseable JSON', () => {
|
|
344
|
+
for (let i = 0; i < 3; i++) {
|
|
345
|
+
runHook({ session_id: `s${i}`, agent_type: 'r', agent_id: `a${i}` }, { home: tmpHome });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const registryPath = path.join(tmpHome, '.claude', 'subagent-sessions.jsonl');
|
|
349
|
+
const lines = fs.readFileSync(registryPath, 'utf8').trim().split('\n');
|
|
350
|
+
assert.equal(lines.length, 3);
|
|
351
|
+
|
|
352
|
+
lines.forEach((line, i) => {
|
|
353
|
+
const parsed = JSON.parse(line);
|
|
354
|
+
assert.equal(parsed.session_id, `s${i}`);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|