@iseoane/agent-depot 0.2.0 → 0.3.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 (86) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +117 -3
  3. package/dist/src/app-cli.js +172 -0
  4. package/dist/src/app-flow.js +457 -0
  5. package/dist/src/app-latest.js +63 -0
  6. package/dist/src/app-owned-skills.js +42 -0
  7. package/dist/src/app-recipes.js +116 -0
  8. package/dist/src/app-schema.js +45 -0
  9. package/dist/src/app-version.js +46 -0
  10. package/dist/src/atomic-file.js +26 -0
  11. package/dist/src/cli.js +1256 -0
  12. package/dist/src/git-source.js +409 -0
  13. package/dist/src/path-safety.js +31 -0
  14. package/dist/src/process-runner.js +41 -0
  15. package/dist/src/project-installation.js +1411 -0
  16. package/dist/src/project-manifest.js +511 -0
  17. package/dist/src/skill-adoption.js +77 -0
  18. package/dist/src/skill-discovery.js +467 -0
  19. package/dist/src/skill-hosts.js +103 -0
  20. package/dist/src/skill-install.js +374 -0
  21. package/dist/src/skill-removal.js +119 -0
  22. package/dist/src/skill-update.js +244 -0
  23. package/dist/src/source-state.js +240 -0
  24. package/dist/src/sources.js +391 -0
  25. package/dist/src/tui/adoption-actions.js +144 -0
  26. package/dist/src/tui/app-actions.js +157 -0
  27. package/dist/src/tui/app.js +63 -0
  28. package/dist/src/tui/batch.js +17 -0
  29. package/dist/src/tui/bulk-actions.js +243 -0
  30. package/dist/src/tui/catalog-actions.js +9 -0
  31. package/dist/src/tui/catalog-browse.js +59 -0
  32. package/dist/src/tui/catalog-filter.js +20 -0
  33. package/dist/src/tui/catalog-flow.js +107 -0
  34. package/dist/src/tui/catalog-installs.js +193 -0
  35. package/dist/src/tui/catalog-panel.js +47 -0
  36. package/dist/src/tui/catalog-state.js +92 -0
  37. package/dist/src/tui/catalog-tree.js +35 -0
  38. package/dist/src/tui/catalog-view.js +89 -0
  39. package/dist/src/tui/environment.js +1 -0
  40. package/dist/src/tui/installations-browse.js +136 -0
  41. package/dist/src/tui/installations-mode.js +1 -0
  42. package/dist/src/tui/installations-panel.js +27 -0
  43. package/dist/src/tui/installations-rows.js +11 -0
  44. package/dist/src/tui/installations-tree.js +80 -0
  45. package/dist/src/tui/installations-view.js +154 -0
  46. package/dist/src/tui/installations.js +172 -0
  47. package/dist/src/tui/keys.js +31 -0
  48. package/dist/src/tui/manage-actions.js +174 -0
  49. package/dist/src/tui/manage-panel.js +65 -0
  50. package/dist/src/tui/mode-keys.js +61 -0
  51. package/dist/src/tui/panel-parts.js +23 -0
  52. package/dist/src/tui/render.js +8 -0
  53. package/dist/src/tui/sources-mode.js +17 -0
  54. package/dist/src/tui/sources-view.js +276 -0
  55. package/dist/src/tui/theme.js +29 -0
  56. package/dist/src/tui/tree-navigation.js +68 -0
  57. package/dist/src/tui/tree.js +53 -0
  58. package/dist/src/tui/unmanaged-actions.js +178 -0
  59. package/dist/src/tui/updates-flow.js +89 -0
  60. package/dist/src/tui/updates-keys.js +104 -0
  61. package/dist/src/tui/updates-mode.js +1 -0
  62. package/dist/src/tui/updates-model.js +48 -0
  63. package/dist/src/tui/updates-panel.js +44 -0
  64. package/dist/src/tui/updates-view.js +101 -0
  65. package/dist/src/tui/updates.js +230 -0
  66. package/dist/src/tui/view-state.js +42 -0
  67. package/dist/src/tui/window.js +31 -0
  68. package/dist/src/unmanaged-removal.js +60 -0
  69. package/dist/src/update-batch.js +376 -0
  70. package/dist/src/update-flow.js +135 -0
  71. package/dist/src/usage-error.js +7 -0
  72. package/dist/src/user-global-skill-inventory.js +565 -0
  73. package/dist/src/user-method.js +14 -0
  74. package/package.json +7 -4
  75. package/skills/agent-depot-apprecipe/SKILL.md +44 -0
  76. package/skills/agent-depot-apprecipe/assets/example.md +25 -0
  77. package/skills/doctor-md-agents/scripts/test_collect_sessions.py +0 -780
  78. package/skills/doctor-md-agents/scripts/test_extract_rules.py +0 -530
  79. package/skills/doctor-md-agents/scripts/test_render_report.py +0 -383
  80. package/skills/doctor-md-agents/scripts/test_scan_corrections.py +0 -343
  81. package/skills/doctor-md-agents/scripts/test_verify_citations.py +0 -236
  82. package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +0 -507
  83. package/skills/doctor-md-skill/scripts/test_collect_sessions.py +0 -593
  84. package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +0 -181
  85. package/skills/doctor-md-skill/scripts/test_render_report.py +0 -241
  86. package/skills/doctor-md-skill/scripts/test_verify_citations.py +0 -196
@@ -1,593 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Tests for session collection."""
3
-
4
- import json
5
- import os
6
- import tempfile
7
- import unittest
8
- from datetime import datetime, timedelta, timezone
9
- from pathlib import Path
10
-
11
- from collect_sessions import (
12
- detect_skills_from_entries,
13
- discover_skills,
14
- find_claude_session_files,
15
- find_grok_session_files,
16
- find_pi_session_files,
17
- parse_claude_session,
18
- parse_codex_session,
19
- parse_grok_session,
20
- parse_pi_session,
21
- parse_zcode_session,
22
- session_matches_repos,
23
- )
24
-
25
- PREVIOUS_FILE_LIMIT = 8 * 1024 * 1024
26
-
27
-
28
- def write_jsonl(path, records):
29
- path.parent.mkdir(parents=True, exist_ok=True)
30
- path.write_text("\n".join(json.dumps(record) for record in records) + "\n")
31
-
32
-
33
- def write_jsonl_past_previous_limit(path, first_record, last_records):
34
- path.parent.mkdir(parents=True, exist_ok=True)
35
- filler = json.dumps({"type": "ignored", "padding": "x" * 1024}) + "\n"
36
- with path.open("w", encoding="utf-8") as stream:
37
- stream.write(json.dumps(first_record) + "\n")
38
- while stream.tell() <= PREVIOUS_FILE_LIMIT:
39
- stream.write(filler)
40
- for record in last_records:
41
- stream.write(json.dumps(record) + "\n")
42
-
43
-
44
- class ClaudeSessionTests(unittest.TestCase):
45
- def test_discovers_skills_and_matches_sessions_across_projects(self):
46
- with tempfile.TemporaryDirectory() as tmp:
47
- root = Path(tmp)
48
- first = root / "first"
49
- second = root / "second"
50
- first_skill = first / ".agents" / "skills" / "alpha" / "SKILL.md"
51
- second_skill = second / ".claude" / "skills" / "beta" / "SKILL.md"
52
- first_skill.parent.mkdir(parents=True)
53
- second_skill.parent.mkdir(parents=True)
54
- first_skill.write_text("---\ndescription: Alpha\n---\n")
55
- second_skill.write_text("---\ndescription: Beta\n---\n")
56
-
57
- skills = discover_skills(
58
- [first, second],
59
- root / "codex-home",
60
- [],
61
- False,
62
- )
63
-
64
- self.assertEqual(set(skills), {"alpha", "beta"})
65
- self.assertTrue(
66
- session_matches_repos(second / "src", [first, second])
67
- )
68
- self.assertFalse(
69
- session_matches_repos(root / "elsewhere", [first, second])
70
- )
71
-
72
- def test_discovers_global_skills_without_repositories(self):
73
- with tempfile.TemporaryDirectory() as tmp:
74
- root = Path(tmp)
75
- homes = {
76
- name: root / f"{name}-home"
77
- for name in ("codex", "pi", "grok", "zcode")
78
- }
79
- for name, home in homes.items():
80
- skill = home / "skills" / f"{name}-skill" / "SKILL.md"
81
- skill.parent.mkdir(parents=True)
82
- skill.write_text(f"---\ndescription: {name.title()} skill\n---\n")
83
-
84
- skills = discover_skills(
85
- [],
86
- homes["codex"],
87
- [],
88
- True,
89
- pi_home=homes["pi"],
90
- grok_home=homes["grok"],
91
- zcode_home=homes["zcode"],
92
- )
93
-
94
- self.assertTrue(
95
- {
96
- "codex-skill",
97
- "pi-skill",
98
- "grok-skill",
99
- "zcode-skill",
100
- }.issubset(skills)
101
- )
102
-
103
- def test_detects_skills_from_deferred_tool_entries(self):
104
- entries = [
105
- ("tool:Skill", '{"skill": "alpha"}'),
106
- ("tool:read", '{"path": "/repo/.agents/skills/beta/SKILL.md"}'),
107
- ("assistant", "Mentioning gamma here does not count."),
108
- ]
109
-
110
- self.assertEqual(
111
- detect_skills_from_entries(entries, {"alpha", "beta", "gamma"}),
112
- {"alpha", "beta"},
113
- )
114
-
115
- def test_discovers_parent_sessions_and_optional_subagents(self):
116
- with tempfile.TemporaryDirectory() as tmp:
117
- claude_home = Path(tmp)
118
- parent = claude_home / "projects" / "-repo" / "parent.jsonl"
119
- subagent = (
120
- claude_home
121
- / "projects"
122
- / "-repo"
123
- / "parent"
124
- / "subagents"
125
- / "agent-child.jsonl"
126
- )
127
- old = claude_home / "projects" / "-repo" / "old.jsonl"
128
- for path in (parent, subagent, old):
129
- write_jsonl(path, [{"type": "user"}])
130
- old_time = (datetime.now(timezone.utc) - timedelta(days=10)).timestamp()
131
- os.utime(old, (old_time, old_time))
132
- cutoff = datetime.now(timezone.utc) - timedelta(days=1)
133
-
134
- parents = find_claude_session_files(claude_home, cutoff, False)
135
- with_subagents = find_claude_session_files(claude_home, cutoff, True)
136
-
137
- self.assertEqual([path for _, path in parents], [parent])
138
- self.assertEqual(
139
- {path for _, path in with_subagents},
140
- {parent, subagent},
141
- )
142
-
143
- def test_parses_messages_tools_skills_and_stats(self):
144
- with tempfile.TemporaryDirectory() as tmp:
145
- path = Path(tmp) / "session.jsonl"
146
- common = {
147
- "sessionId": "session-1",
148
- "cwd": "/tmp/repo",
149
- "timestamp": "2026-08-20T10:00:00Z",
150
- "version": "1.0.0",
151
- }
152
- write_jsonl(path, [
153
- {
154
- **common,
155
- "type": "user",
156
- "uuid": "user-1",
157
- "message": {"role": "user", "content": "Improve my skill"},
158
- },
159
- {
160
- **common,
161
- "type": "assistant",
162
- "uuid": "assistant-1",
163
- "message": {
164
- "id": "message-1",
165
- "role": "assistant",
166
- "content": [
167
- {"type": "text", "text": "I will inspect it."},
168
- {
169
- "type": "tool_use",
170
- "name": "Skill",
171
- "input": {"skill": "update-skill"},
172
- },
173
- ],
174
- },
175
- },
176
- {
177
- **common,
178
- "type": "assistant",
179
- "uuid": "assistant-2",
180
- "message": {
181
- "id": "message-1",
182
- "role": "assistant",
183
- "content": [
184
- {
185
- "type": "tool_use",
186
- "name": "Edit",
187
- "input": {"file_path": "/tmp/repo/SKILL.md"},
188
- }
189
- ],
190
- },
191
- },
192
- {
193
- **common,
194
- "type": "user",
195
- "uuid": "result-1",
196
- "message": {
197
- "role": "user",
198
- "content": [
199
- {
200
- "type": "tool_result",
201
- "is_error": True,
202
- "content": "permission denied",
203
- }
204
- ],
205
- },
206
- },
207
- ])
208
-
209
- meta, stats, entries, skills = parse_claude_session(
210
- path,
211
- {"update-skill"},
212
- False,
213
- )
214
-
215
- self.assertEqual(meta["id"], "session-1")
216
- self.assertEqual(meta["cwd"], "/tmp/repo")
217
- self.assertEqual(stats["user_turns"], 1)
218
- self.assertEqual(stats["assistant_turns"], 1)
219
- self.assertEqual(stats["tool_calls"], 2)
220
- self.assertEqual(stats["error_outputs"], 1)
221
- self.assertTrue(stats["has_code_edits"])
222
- self.assertEqual(skills, ["update-skill"])
223
- self.assertIn(("user", "Improve my skill"), entries)
224
- self.assertIn(("assistant", "I will inspect it."), entries)
225
-
226
- def test_excludes_sidechains_by_default(self):
227
- with tempfile.TemporaryDirectory() as tmp:
228
- path = Path(tmp) / "agent-child.jsonl"
229
- write_jsonl(path, [{
230
- "type": "user",
231
- "sessionId": "session-1",
232
- "agentId": "child-1",
233
- "isSidechain": True,
234
- "cwd": "/tmp/repo",
235
- "timestamp": "2026-08-20T10:00:00Z",
236
- "message": {"role": "user", "content": "Investigate"},
237
- }])
238
-
239
- self.assertIsNone(parse_claude_session(path, set(), False))
240
- parsed = parse_claude_session(path, set(), True)
241
- self.assertEqual(parsed[0]["id"], "session-1-child-1")
242
- self.assertEqual(parsed[0]["thread_source"], "subagent")
243
-
244
-
245
- class PiSessionTests(unittest.TestCase):
246
- def test_parses_messages_tools_skills_and_stats(self):
247
- with tempfile.TemporaryDirectory() as tmp:
248
- path = Path(tmp) / "2026-08-20T10-00-00-000Z_session-1.jsonl"
249
- write_jsonl(path, [
250
- {
251
- "type": "session",
252
- "id": "session-1",
253
- "cwd": "/tmp/repo",
254
- "timestamp": "2026-08-20T10:00:00.000Z",
255
- },
256
- {"type": "model_change", "provider": "zai", "modelId": "glm"},
257
- {
258
- "type": "message",
259
- "timestamp": "2026-08-20T10:00:01.000Z",
260
- "message": {
261
- "role": "user",
262
- "content": [{"type": "text", "text": "Improve my skill"}],
263
- },
264
- },
265
- {
266
- "type": "message",
267
- "timestamp": "2026-08-20T10:00:02.000Z",
268
- "message": {
269
- "role": "assistant",
270
- "content": [
271
- {"type": "thinking", "thinking": "plan the edit"},
272
- {"type": "text", "text": "I will inspect it."},
273
- {
274
- "type": "toolCall",
275
- "name": "read",
276
- "arguments": {
277
- "path": "/repo/.agents/skills/update-skill/SKILL.md"
278
- },
279
- },
280
- ],
281
- },
282
- },
283
- {
284
- "type": "message",
285
- "timestamp": "2026-08-20T10:00:03.000Z",
286
- "message": {
287
- "role": "toolResult",
288
- "toolName": "read",
289
- "content": [{"type": "text", "text": "--- description: alpha"}],
290
- },
291
- },
292
- {
293
- "type": "message",
294
- "timestamp": "2026-08-20T10:00:04.000Z",
295
- "message": {
296
- "role": "assistant",
297
- "content": [
298
- {
299
- "type": "toolCall",
300
- "name": "edit",
301
- "arguments": {
302
- "path": "/tmp/repo/SKILL.md",
303
- "oldText": "a",
304
- "newText": "b",
305
- },
306
- },
307
- ],
308
- },
309
- },
310
- {
311
- "type": "message",
312
- "timestamp": "2026-08-20T10:00:05.000Z",
313
- "message": {
314
- "role": "toolResult",
315
- "toolName": "edit",
316
- "content": [
317
- {"type": "text", "text": "oldText not found", "isError": True}
318
- ],
319
- },
320
- },
321
- ])
322
-
323
- meta, stats, entries, skills = parse_pi_session(path, {"update-skill"}, False)
324
-
325
- self.assertEqual(meta["id"], "session-1")
326
- self.assertEqual(meta["cwd"], "/tmp/repo")
327
- self.assertEqual(meta["originator"], "pi")
328
- self.assertEqual(stats["user_turns"], 1)
329
- self.assertEqual(stats["assistant_turns"], 2)
330
- self.assertEqual(stats["tool_calls"], 2)
331
- self.assertEqual(stats["repeated_tool_calls"], 0)
332
- self.assertEqual(stats["error_outputs"], 1)
333
- self.assertTrue(stats["has_code_edits"])
334
- self.assertEqual(stats["first_ts"], "2026-08-20T10:00:00.000Z")
335
- self.assertEqual(stats["last_ts"], "2026-08-20T10:00:05.000Z")
336
- self.assertEqual(skills, ["update-skill"])
337
- self.assertIn(("user", "Improve my skill"), entries)
338
- self.assertIn(("assistant", "I will inspect it."), entries)
339
- self.assertIn(("output", "--- description: alpha"), entries)
340
- self.assertFalse(any(role == "thinking" for role, _ in entries))
341
-
342
- def test_accepts_include_subagents_without_changing_result(self):
343
- with tempfile.TemporaryDirectory() as tmp:
344
- path = Path(tmp) / "session-2.jsonl"
345
- write_jsonl(path, [
346
- {"type": "session", "id": "session-2", "cwd": "/tmp/repo",
347
- "timestamp": "2026-08-20T10:00:00.000Z"},
348
- {"type": "message", "timestamp": "2026-08-20T10:00:01.000Z",
349
- "message": {"role": "user", "content": "Hello there"}},
350
- ])
351
-
352
- without_flag = parse_pi_session(path, set(), False)
353
- with_flag = parse_pi_session(path, set(), True)
354
-
355
- self.assertEqual(without_flag, with_flag)
356
- self.assertEqual(with_flag[0]["originator"], "pi")
357
-
358
- def test_finds_recent_session_files_only(self):
359
- with tempfile.TemporaryDirectory() as tmp:
360
- pi_home = Path(tmp)
361
- recent = pi_home / "sessions" / "--tmp-repo" / "recent.jsonl"
362
- old = pi_home / "sessions" / "--tmp-repo" / "old.jsonl"
363
- for path in (recent, old):
364
- write_jsonl(path, [{"type": "session", "id": "x"}])
365
- old_time = (datetime.now(timezone.utc) - timedelta(days=10)).timestamp()
366
- os.utime(old, (old_time, old_time))
367
- cutoff = datetime.now(timezone.utc) - timedelta(days=1)
368
-
369
- files = find_pi_session_files(pi_home, cutoff)
370
-
371
- self.assertEqual([path for _, path in files], [recent])
372
-
373
-
374
- class GrokSessionTests(unittest.TestCase):
375
- def test_parses_tool_calls_and_skips_synthetic_messages(self):
376
- with tempfile.TemporaryDirectory() as tmp:
377
- grok_home = Path(tmp)
378
- session_dir = grok_home / "sessions" / "%2Ftmp%2Frepo" / "abc-123"
379
- path = session_dir / "chat_history.jsonl"
380
- write_jsonl(path, [
381
- {"type": "system", "content": "You are Grok."},
382
- {
383
- "type": "user",
384
- "synthetic_reason": "system_reminder",
385
- "content": [{"type": "text", "text": "<system-reminder>skills</system-reminder>"}],
386
- },
387
- {"type": "user", "content": "Fix the collector"},
388
- {
389
- "type": "reasoning",
390
- "id": "rs-1",
391
- "summary": "[]",
392
- "encrypted_content": "xx",
393
- "status": "completed",
394
- },
395
- {
396
- "type": "assistant",
397
- "content": "I will read the skill.",
398
- "tool_calls": [{
399
- "id": "call-1",
400
- "name": "read_file",
401
- "arguments": "{\"path\": \"/repo/.grok/skills/update-skill/SKILL.md\"}",
402
- }],
403
- },
404
- {"type": "tool_result", "tool_call_id": "call-1", "content": "file body"},
405
- {
406
- "type": "assistant",
407
- "content": "Now writing the file.",
408
- "tool_calls": [{
409
- "id": "call-2",
410
- "name": "write",
411
- "arguments": "{\"path\": \"/tmp/repo/out.txt\", \"content\": \"hi\"}",
412
- }],
413
- },
414
- ])
415
- cutoff = datetime.now(timezone.utc) - timedelta(days=1)
416
-
417
- files = find_grok_session_files(grok_home, cutoff)
418
- self.assertEqual([found for _, found in files], [path])
419
-
420
- meta, stats, entries, skills = parse_grok_session(path, {"update-skill"}, False)
421
-
422
- self.assertEqual(meta["id"], "abc-123")
423
- self.assertEqual(meta["cwd"], "/tmp/repo")
424
- self.assertEqual(meta["originator"], "grok")
425
- self.assertEqual(stats["user_turns"], 1)
426
- self.assertEqual(stats["assistant_turns"], 2)
427
- self.assertEqual(stats["tool_calls"], 2)
428
- self.assertEqual(stats["error_outputs"], 0)
429
- self.assertTrue(stats["has_code_edits"])
430
- self.assertEqual(skills, ["update-skill"])
431
- self.assertEqual(entries[0], ("user", "Fix the collector"))
432
- self.assertNotIn("system-reminder", json.dumps(entries))
433
-
434
-
435
- class ZcodeSessionTests(unittest.TestCase):
436
- def test_parses_last_request_messages_and_tool_calls(self):
437
- with tempfile.TemporaryDirectory() as tmp:
438
- path = Path(tmp) / "model-io-sess_1.jsonl"
439
- empty_request = {"request": {"body": {"messages": []}}}
440
- full_request = {
441
- "request": {
442
- "body": {
443
- "messages": [
444
- {"role": "system", "content": "You are ZCode."},
445
- {"role": "user", "content": "Update the skill"},
446
- {
447
- "role": "assistant",
448
- "content": "Checking.",
449
- "tool_calls": [{
450
- "id": "c1",
451
- "function": {
452
- "name": "read_file",
453
- "arguments": "{\"path\": \"/r/.agents/skills/update-skill/SKILL.md\"}",
454
- },
455
- }],
456
- },
457
- {"role": "tool", "content": "skill body"},
458
- ]
459
- }
460
- }
461
- }
462
- path.write_text(
463
- json.dumps(empty_request) + "\n" + json.dumps(full_request) + "\n"
464
- )
465
-
466
- meta, stats, entries, skills = parse_zcode_session(path, {"update-skill"}, False)
467
-
468
- self.assertEqual(meta["id"], "sess_1")
469
- self.assertEqual(meta["originator"], "zcode")
470
- self.assertEqual(stats["user_turns"], 1)
471
- self.assertEqual(stats["assistant_turns"], 1)
472
- self.assertEqual(stats["tool_calls"], 1)
473
- self.assertEqual(skills, ["update-skill"])
474
- self.assertEqual(entries[0], ("user", "Update the skill"))
475
-
476
- def test_returns_none_without_request_records(self):
477
- with tempfile.TemporaryDirectory() as tmp:
478
- path = Path(tmp) / "model-io-sess_2.jsonl"
479
- path.write_text("{\"unrelated\": true}\n")
480
-
481
- self.assertIsNone(parse_zcode_session(path, set(), False))
482
-
483
-
484
- class StreamingSessionReadTests(unittest.TestCase):
485
- def test_claude_and_codex_parse_records_after_previous_file_limit(self):
486
- with tempfile.TemporaryDirectory() as tmp:
487
- root = Path(tmp)
488
- claude_path = root / "claude.jsonl"
489
- codex_path = root / "codex.jsonl"
490
- write_jsonl_past_previous_limit(
491
- claude_path,
492
- {
493
- "type": "user",
494
- "sessionId": "claude-session",
495
- "cwd": "/tmp/repo",
496
- "timestamp": "2026-08-30T10:00:00Z",
497
- "message": {"role": "user", "content": "hello"},
498
- },
499
- [{
500
- "type": "assistant",
501
- "sessionId": "claude-session",
502
- "cwd": "/tmp/repo",
503
- "timestamp": "2026-08-30T10:01:00Z",
504
- "message": {
505
- "id": "late-message",
506
- "role": "assistant",
507
- "content": [
508
- {"type": "text", "text": "late claude message"},
509
- {
510
- "type": "tool_use",
511
- "name": "Read",
512
- "input": {
513
- "file_path": "/tmp/.agents/skills/late-skill/SKILL.md"
514
- },
515
- },
516
- ],
517
- },
518
- }],
519
- )
520
- write_jsonl_past_previous_limit(
521
- codex_path,
522
- {
523
- "type": "session_meta",
524
- "payload": {"id": "codex-session", "cwd": "/tmp/repo"},
525
- },
526
- [{
527
- "type": "response_item",
528
- "timestamp": "2026-08-30T10:00:30Z",
529
- "payload": {
530
- "type": "function_call",
531
- "name": "read_file",
532
- "arguments": {
533
- "path": "/tmp/.codex/skills/late-skill/SKILL.md",
534
- "operation": "apply_patch",
535
- },
536
- },
537
- }, {
538
- "type": "response_item",
539
- "timestamp": "2026-08-30T10:01:00Z",
540
- "payload": {
541
- "type": "message",
542
- "role": "assistant",
543
- "content": [{"type": "text", "text": "late codex message"}],
544
- },
545
- }],
546
- )
547
-
548
- parsed_claude = parse_claude_session(claude_path, {"late-skill"}, False)
549
- parsed_codex = parse_codex_session(codex_path, {"late-skill"}, False)
550
-
551
- self.assertEqual(parsed_claude[1]["assistant_turns"], 1)
552
- self.assertEqual(parsed_claude[1]["tool_calls"], 1)
553
- self.assertEqual(parsed_claude[3], ["late-skill"])
554
- self.assertIn(("assistant", "late claude message"), parsed_claude[2])
555
- self.assertEqual(parsed_codex[1]["tool_calls"], 1)
556
- self.assertTrue(parsed_codex[1]["has_code_edits"])
557
- self.assertEqual(parsed_codex[3], ["late-skill"])
558
- self.assertIn(("assistant", "late codex message"), parsed_codex[2])
559
-
560
- def test_codex_keeps_only_transcript_head_and_tail_while_parsing(self):
561
- with tempfile.TemporaryDirectory() as tmp:
562
- path = Path(tmp) / "codex.jsonl"
563
- records = [{
564
- "type": "session_meta",
565
- "payload": {"id": "codex-session", "cwd": "/tmp/repo"},
566
- }]
567
- for index in range(201):
568
- records.extend(({
569
- "type": "event_msg",
570
- "payload": {"type": "agent_message"},
571
- }, {
572
- "type": "response_item",
573
- "payload": {
574
- "type": "message",
575
- "role": "assistant",
576
- "content": [{"type": "text", "text": f"message-{index}"}],
577
- },
578
- }))
579
- write_jsonl(path, records)
580
-
581
- _, stats, entries, _ = parse_codex_session(path, set(), False)
582
-
583
- self.assertEqual(stats["assistant_turns"], 201)
584
- self.assertEqual(len(entries), 141)
585
- self.assertEqual(entries[0], ("assistant", "message-0"))
586
- self.assertEqual(entries[99], ("assistant", "message-99"))
587
- self.assertEqual(entries[100], ("note", "[... 61 entries omitted ...]"))
588
- self.assertEqual(entries[101], ("assistant", "message-161"))
589
- self.assertEqual(entries[-1], ("assistant", "message-200"))
590
-
591
-
592
- if __name__ == "__main__":
593
- unittest.main()