Compare commits

..

1 Commits

Author SHA1 Message Date
a20e9721b2 Add session_reader.py - JSONL transcript parser (#6) 2026-04-14 17:30:18 +00:00
4 changed files with 268 additions and 1115 deletions

View File

@@ -1,10 +0,0 @@
{
"last_harvest": "2026-04-14T18:04:45.484759+00:00",
"harvested_sessions": [
"20260413_175935_20cb44",
"20260413_171106_62c276",
"20260413_181734_aed35b"
],
"total_sessions_processed": 3,
"total_facts_extracted": 59
}

View File

@@ -1,597 +1,6 @@
{
"version": 1,
"last_updated": "2026-04-14T18:04:45.484238+00:00",
"total_facts": 59,
"facts": [
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_z8ielhro/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477585+00:00",
"harvested_at": "2026-04-14T18:04:45.479057+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: crons.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477603+00:00",
"harvested_at": "2026-04-14T18:04:45.479059+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 300.07",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477614+00:00",
"harvested_at": "2026-04-14T18:04:45.479060+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox__3wxy21d/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477622+00:00",
"harvested_at": "2026-04-14T18:04:45.479061+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_dimnu9ba/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477633+00:00",
"harvested_at": "2026-04-14T18:04:45.479062+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: nhermes_cli/cron.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477664+00:00",
"harvested_at": "2026-04-14T18:04:45.479062+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: hermes_cli/cron.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477793+00:00",
"harvested_at": "2026-04-14T18:04:45.479063+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: config.yaml",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.477921+00:00",
"harvested_at": "2026-04-14T18:04:45.479064+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: ~/.hermes",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478092+00:00",
"harvested_at": "2026-04-14T18:04:45.479065+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: ncli.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478281+00:00",
"harvested_at": "2026-04-14T18:04:45.479065+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 300.17",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478293+00:00",
"harvested_at": "2026-04-14T18:04:45.479066+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 10.88",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478370+00:00",
"harvested_at": "2026-04-14T18:04:45.479067+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: k2.5",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478386+00:00",
"harvested_at": "2026-04-14T18:04:45.479067+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 300.92",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478410+00:00",
"harvested_at": "2026-04-14T18:04:45.479068+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Successful command pattern: python observatory.py --check ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478498+00:00",
"harvested_at": "2026-04-14T18:04:45.479069+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: devkit/health.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478571+00:00",
"harvested_at": "2026-04-14T18:04:45.479069+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: CHANGELOG.md",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478608+00:00",
"harvested_at": "2026-04-14T18:04:45.479070+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 300.06",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478635+00:00",
"harvested_at": "2026-04-14T18:04:45.479071+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: 300.03",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478658+00:00",
"harvested_at": "2026-04-14T18:04:45.479072+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: crons.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478703+00:00",
"harvested_at": "2026-04-14T18:04:45.479072+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: crons.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478757+00:00",
"harvested_at": "2026-04-14T18:04:45.479073+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_1h5nj9lg/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478778+00:00",
"harvested_at": "2026-04-14T18:04:45.479074+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: job.get",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478833+00:00",
"harvested_at": "2026-04-14T18:04:45.479074+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: CreateIssueOption.Labels",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.478975+00:00",
"harvested_at": "2026-04-14T18:04:45.479075+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Successful command pattern: git process seems to be running in this repository",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_175935_20cb44",
"extracted_at": "2026-04-14T18:04:45.479018+00:00",
"harvested_at": "2026-04-14T18:04:45.479076+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_175935_20cb44.json"
},
{
"fact": "Error encountered with file: ~/.hermes",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.479242+00:00",
"harvested_at": "2026-04-14T18:04:45.482379+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: pokayoke/hermes_constants.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.479346+00:00",
"harvested_at": "2026-04-14T18:04:45.482380+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: Path.home",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.479565+00:00",
"harvested_at": "2026-04-14T18:04:45.482380+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_5pwgex20/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.479901+00:00",
"harvested_at": "2026-04-14T18:04:45.482381+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: 300.11",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.480675+00:00",
"harvested_at": "2026-04-14T18:04:45.482382+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: AIAgent.__init__",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.480862+00:00",
"harvested_at": "2026-04-14T18:04:45.482383+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: job.ge",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481044+00:00",
"harvested_at": "2026-04-14T18:04:45.482383+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: cron/scheduler.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481254+00:00",
"harvested_at": "2026-04-14T18:04:45.482384+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: __main__.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481644+00:00",
"harvested_at": "2026-04-14T18:04:45.482385+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: tests/test_prompt_injection_defense.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481654+00:00",
"harvested_at": "2026-04-14T18:04:45.482385+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_v2umc709/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481666+00:00",
"harvested_at": "2026-04-14T18:04:45.482386+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: pytest.mark",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481733+00:00",
"harvested_at": "2026-04-14T18:04:45.482387+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: ntests/test_prompt_injection_defense.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481788+00:00",
"harvested_at": "2026-04-14T18:04:45.482388+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: result.get",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.481979+00:00",
"harvested_at": "2026-04-14T18:04:45.482388+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: concurrent.future",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.482228+00:00",
"harvested_at": "2026-04-14T18:04:45.482389+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: 0.0",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.482252+00:00",
"harvested_at": "2026-04-14T18:04:45.482390+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_mjbblg0z/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_171106_62c276",
"extracted_at": "2026-04-14T18:04:45.482315+00:00",
"harvested_at": "2026-04-14T18:04:45.482390+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_171106_62c276.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_u2ngkm60/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.482463+00:00",
"harvested_at": "2026-04-14T18:04:45.484207+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_i63vbaem/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.482569+00:00",
"harvested_at": "2026-04-14T18:04:45.484208+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: 3.12",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.482589+00:00",
"harvested_at": "2026-04-14T18:04:45.484209+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Successful command pattern: git restore --staged ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.482629+00:00",
"harvested_at": "2026-04-14T18:04:45.484209+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: forge.alexanderwhitestone",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.482645+00:00",
"harvested_at": "2026-04-14T18:04:45.484210+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Successful command pattern: git restore --staged ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483301+00:00",
"harvested_at": "2026-04-14T18:04:45.484211+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: ntests/test_repo_truth.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483472+00:00",
"harvested_at": "2026-04-14T18:04:45.484211+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Successful command pattern: git restore --staged ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483479+00:00",
"harvested_at": "2026-04-14T18:04:45.484212+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: 300.02",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483596+00:00",
"harvested_at": "2026-04-14T18:04:45.484213+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Successful command pattern: git restore --staged ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483603+00:00",
"harvested_at": "2026-04-14T18:04:45.484213+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Successful command pattern: git restore --staged ",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483697+00:00",
"harvested_at": "2026-04-14T18:04:45.484214+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: 300.37",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483785+00:00",
"harvested_at": "2026-04-14T18:04:45.484215+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_2k0n79t8/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483792+00:00",
"harvested_at": "2026-04-14T18:04:45.484216+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: 300.19",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483864+00:00",
"harvested_at": "2026-04-14T18:04:45.484216+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: /private/var/folders/9k/v07xkpp133v03yynn9nx80fr0000gn/T/hermes_sandbox_qxzsy_kv/script.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483919+00:00",
"harvested_at": "2026-04-14T18:04:45.484217+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: CreateIssueOption.Labels",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483930+00:00",
"harvested_at": "2026-04-14T18:04:45.484218+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
},
{
"fact": "Error encountered with file: verify_triage_status.py",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": "20260413_181734_aed35b",
"extracted_at": "2026-04-14T18:04:45.483963+00:00",
"harvested_at": "2026-04-14T18:04:45.484218+00:00",
"session_path": "/Users/apayne/.hermes/sessions/session_20260413_181734_aed35b.json"
}
]
"last_updated": "2026-04-13T20:00:00Z",
"total_facts": 0,
"facts": []
}

View File

@@ -1,350 +0,0 @@
#!/usr/bin/env python3
"""
Session Harvester for Compounding Intelligence.
Extracts durable knowledge from completed sessions and updates the knowledge store.
"""
import json
import os
import sys
import logging
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import List, Dict, Any, Optional
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from session_reader import SessionReader
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(Path(__file__).parent.parent / 'metrics' / 'harvester.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class KnowledgeHarvester:
"""Extracts knowledge from completed sessions."""
def __init__(self, repo_root: str = None):
"""Initialize the harvester."""
if repo_root is None:
repo_root = str(Path(__file__).parent.parent)
self.repo_root = Path(repo_root)
self.knowledge_dir = self.repo_root / "knowledge"
self.index_path = self.knowledge_dir / "index.json"
self.prompt_path = self.repo_root / "templates" / "harvest-prompt.md"
# Load or create knowledge index
self.index = self._load_index()
# Initialize session reader
self.reader = SessionReader()
# Harvest state file
self.state_path = self.knowledge_dir / "harvest_state.json"
self.state = self._load_state()
def _load_index(self) -> Dict[str, Any]:
"""Load or create the knowledge index."""
if self.index_path.exists():
with open(self.index_path, 'r') as f:
return json.load(f)
else:
return {
"version": 1,
"last_updated": datetime.now(timezone.utc).isoformat(),
"total_facts": 0,
"facts": []
}
def _save_index(self):
"""Save the knowledge index."""
self.index["last_updated"] = datetime.now(timezone.utc).isoformat()
with open(self.index_path, 'w') as f:
json.dump(self.index, f, indent=2)
def _load_state(self) -> Dict[str, Any]:
"""Load harvest state."""
if self.state_path.exists():
with open(self.state_path, 'r') as f:
return json.load(f)
else:
return {
"last_harvest": None,
"harvested_sessions": [],
"total_sessions_processed": 0,
"total_facts_extracted": 0
}
def _save_state(self):
"""Save harvest state."""
with open(self.state_path, 'w') as f:
json.dump(self.state, f, indent=2)
def get_sessions_to_harvest(self, max_age_hours: float = 24) -> List[Dict[str, Any]]:
"""
Get sessions that need harvesting.
Args:
max_age_hours: Only harvest sessions modified within this many hours
Returns:
List of session data dictionaries
"""
# Get sessions modified since last harvest
since = None
if self.state["last_harvest"]:
try:
since = datetime.fromisoformat(self.state["last_harvest"].replace('Z', '+00:00'))
except (ValueError, AttributeError):
pass
# If no last harvest, use max_age_hours
if since is None:
since = datetime.now(timezone.utc) - timedelta(hours=max_age_hours)
# Get recent sessions
sessions = self.reader.list_sessions(since=since)
# Filter out already harvested sessions
harvested = set(self.state["harvested_sessions"])
to_harvest = []
for path in sessions:
session = self.reader.read_session(path)
if "error" in session:
logger.warning(f"Error reading session {path}: {session['error']}")
continue
# Skip if already harvested
if session["session_id"] in harvested:
continue
# Skip if session is still active
if not self.reader.is_session_complete(session):
continue
to_harvest.append(session)
return to_harvest
def extract_knowledge_from_session(self, session: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Extract knowledge from a single session.
This is a simplified extraction that looks for patterns in the session.
In a full implementation, this would use an LLM with the harvest prompt.
Args:
session: Session data dictionary
Returns:
List of extracted knowledge items
"""
knowledge_items = []
# Get messages from session
messages = session.get("messages", [])
# Simple pattern-based extraction
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
continue
role = msg.get("role", "")
content = msg.get("content", "")
if not content or not isinstance(content, str):
continue
# Look for error patterns
if "error" in content.lower() or "Error" in content:
# Extract error context
context = content[:200] # First 200 chars
# Look for file paths
import re
file_paths = re.findall(r'[~/.]?[\w/]+\.\w+', context)
if file_paths:
knowledge_items.append({
"fact": f"Error encountered with file: {file_paths[0]}",
"category": "pitfall",
"repo": "global",
"confidence": 0.7,
"session_id": session["session_id"],
"extracted_at": datetime.now(timezone.utc).isoformat()
})
# Look for successful patterns
if "success" in content.lower() or "Success" in content:
# Extract success context
context = content[:200]
# Look for commands or actions
import re
commands = re.findall(r'(?:git|npm|pip|python|curl|ssh)\s+[\w\s\-\.]+', context)
if commands:
knowledge_items.append({
"fact": f"Successful command pattern: {commands[0]}",
"category": "pattern",
"repo": "global",
"confidence": 0.6,
"session_id": session["session_id"],
"extracted_at": datetime.now(timezone.utc).isoformat()
})
return knowledge_items
def harvest_session(self, session: Dict[str, Any]) -> Dict[str, Any]:
"""
Harvest knowledge from a single session.
Args:
session: Session data dictionary
Returns:
Harvest result dictionary
"""
session_id = session["session_id"]
logger.info(f"Harvesting session: {session_id}")
try:
# Extract knowledge
knowledge_items = self.extract_knowledge_from_session(session)
# Add to index
for item in knowledge_items:
# Add metadata
item["harvested_at"] = datetime.now(timezone.utc).isoformat()
item["session_path"] = session.get("path", "")
# Add to facts
self.index["facts"].append(item)
# Update state
self.state["harvested_sessions"].append(session_id)
self.state["total_sessions_processed"] += 1
self.state["total_facts_extracted"] += len(knowledge_items)
result = {
"session_id": session_id,
"success": True,
"facts_extracted": len(knowledge_items),
"knowledge_items": knowledge_items
}
logger.info(f"Extracted {len(knowledge_items)} facts from session {session_id}")
except Exception as e:
logger.error(f"Error harvesting session {session_id}: {e}")
result = {
"session_id": session_id,
"success": False,
"error": str(e),
"facts_extracted": 0
}
return result
def harvest_batch(self, max_sessions: int = 10, max_age_hours: float = 24) -> Dict[str, Any]:
"""
Harvest a batch of sessions.
Args:
max_sessions: Maximum number of sessions to harvest
max_age_hours: Only harvest sessions modified within this many hours
Returns:
Batch harvest result
"""
logger.info(f"Starting harvest batch (max {max_sessions} sessions, max age {max_age_hours}h)")
# Get sessions to harvest
sessions = self.get_sessions_to_harvest(max_age_hours)
if not sessions:
logger.info("No sessions to harvest")
return {
"success": True,
"sessions_processed": 0,
"facts_extracted": 0,
"results": []
}
# Limit to max_sessions
sessions = sessions[:max_sessions]
results = []
total_facts = 0
for session in sessions:
result = self.harvest_session(session)
results.append(result)
if result["success"]:
total_facts += result["facts_extracted"]
# Update index and state
self.index["total_facts"] = len(self.index["facts"])
self._save_index()
self.state["last_harvest"] = datetime.now(timezone.utc).isoformat()
self._save_state()
batch_result = {
"success": True,
"sessions_processed": len(sessions),
"facts_extracted": total_facts,
"results": results,
"timestamp": datetime.now(timezone.utc).isoformat()
}
logger.info(f"Harvest batch complete: {len(sessions)} sessions, {total_facts} facts")
return batch_result
def main():
"""Main entry point for the harvester."""
import argparse
parser = argparse.ArgumentParser(description="Harvest knowledge from completed sessions")
parser.add_argument("--max-sessions", type=int, default=10, help="Maximum sessions to harvest")
parser.add_argument("--max-age-hours", type=float, default=24, help="Max age in hours")
parser.add_argument("--dry-run", action="store_true", help="Don't save, just report")
args = parser.parse_args()
harvester = KnowledgeHarvester()
if args.dry_run:
sessions = harvester.get_sessions_to_harvest(args.max_age_hours)
print(f"Would harvest {len(sessions)} sessions:")
for session in sessions[:5]: # Show first 5
print(f" - {session['session_id']} ({session['message_count']} messages)")
if len(sessions) > 5:
print(f" ... and {len(sessions) - 5} more")
return
result = harvester.harvest_batch(
max_sessions=args.max_sessions,
max_age_hours=args.max_age_hours
)
if result["success"]:
print(f"Harvest complete: {result['sessions_processed']} sessions, {result['facts_extracted']} facts")
else:
print(f"Harvest failed: {result.get('error', 'Unknown error')}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,193 +1,297 @@
#!/usr/bin/env python3
"""
Session reader for Compounding Intelligence.
Reads and parses Hermes session files from ~/.hermes/sessions/.
Hermes Session JSONL Transcript Parser
Parses JSONL session transcripts and extracts structured data.
Part of the compounding-intelligence harvester pipeline.
"""
import json
import re
import sys
import os
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
class SessionReader:
"""Reads and parses Hermes session files."""
@dataclass
class SessionSummary:
"""Structured summary of a Hermes session transcript."""
session_id: str
model: str
repo: str
outcome: str
message_count: int
tool_calls: int
duration_estimate: str
key_actions: List[str]
errors_encountered: List[str]
start_time: Optional[str] = None
end_time: Optional[str] = None
total_tokens_estimate: int = 0
user_messages: int = 0
assistant_messages: int = 0
tool_outputs: int = 0
def parse_jsonl_session(file_path: str) -> SessionSummary:
"""
Parse a Hermes session JSONL transcript and extract structured data.
def __init__(self, sessions_dir: str = None):
"""Initialize with sessions directory path."""
if sessions_dir is None:
sessions_dir = os.path.expanduser("~/.hermes/sessions")
self.sessions_dir = Path(sessions_dir)
self.supported_extensions = {'.json', '.jsonl'}
Args:
file_path: Path to the JSONL session file
Returns:
SessionSummary with extracted data
"""
session_id = Path(file_path).stem
messages = []
model = "unknown"
repo = "unknown"
tool_calls_count = 0
key_actions = []
errors = []
start_time = None
end_time = None
total_tokens = 0
def list_sessions(self, since: Optional[datetime] = None, limit: int = None) -> List[Path]:
"""
List session files, optionally filtered by modification time.
Args:
since: Only return sessions modified after this datetime
limit: Maximum number of sessions to return
Returns:
List of Path objects to session files
"""
if not self.sessions_dir.exists():
return []
sessions = []
for f in self.sessions_dir.iterdir():
if f.suffix in self.supported_extensions:
if since is not None:
mtime = datetime.fromtimestamp(f.stat().st_mtime, tz=timezone.utc)
if mtime <= since:
continue
sessions.append(f)
# Sort by modification time (newest first)
sessions.sort(key=lambda p: p.stat().st_mtime, reverse=True)
if limit:
sessions = sessions[:limit]
return sessions
# Common repo patterns to look for
repo_patterns = [
r"(?:the-nexus|compounding-intelligence|timmy-config|hermes-agent)",
r"(?:forge\.alexanderwhitestone\.com/([^/]+/[^/\\s]+))",
r"(?:github\.com/([^/]+/[^/\\s]+))",
r"(?:Timmy_Foundation/([^/\\s]+))",
]
def read_session(self, path: Path) -> Dict[str, Any]:
"""
Read a session file and return structured data.
Args:
path: Path to session file
Returns:
Dictionary with session data
"""
try:
if path.suffix == '.jsonl':
return self._read_jsonl_session(path)
elif path.suffix == '.json':
return self._read_json_session(path)
else:
return {"error": f"Unsupported format: {path.suffix}"}
except Exception as e:
return {"error": str(e), "path": str(path)}
def _read_json_session(self, path: Path) -> Dict[str, Any]:
"""Read a JSON format session file."""
with open(path, 'r') as f:
data = json.load(f)
return {
"session_id": data.get("session_id", path.stem),
"model": data.get("model", "unknown"),
"created_at": data.get("session_start"),
"last_updated": data.get("last_updated"),
"message_count": data.get("message_count", len(data.get("messages", []))),
"messages": data.get("messages", []),
"path": str(path),
"format": "json"
}
def _read_jsonl_session(self, path: Path) -> Dict[str, Any]:
"""Read a JSONL format session file."""
messages = []
session_meta = None
with open(path, 'r') as f:
for line in f:
# Read JSONL file
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if entry.get("role") == "session_meta":
session_meta = entry
else:
messages.append(entry)
except json.JSONDecodeError:
except json.JSONDecodeError as e:
errors.append(f"Line {line_num}: Invalid JSON - {e}")
continue
session_id = path.stem
if session_meta:
session_id = session_meta.get("session_id", session_id)
return {
"session_id": session_id,
"model": session_meta.get("model", "unknown") if session_meta else "unknown",
"created_at": session_meta.get("timestamp") if session_meta else None,
"last_updated": messages[-1].get("timestamp") if messages else None,
"message_count": len(messages),
"messages": messages,
"path": str(path),
"format": "jsonl",
"meta": session_meta
}
messages.append(entry)
# Extract model from assistant messages
if entry.get("role") == "assistant" and entry.get("model"):
model = entry["model"]
# Extract timestamps
if entry.get("timestamp"):
ts = entry["timestamp"]
if start_time is None:
start_time = ts
end_time = ts
# Count tool calls
if entry.get("tool_calls"):
tool_calls_count += len(entry["tool_calls"])
for tc in entry["tool_calls"]:
if tc.get("function", {}).get("name"):
action = f"{tc['function']['name']}"
if action not in key_actions:
key_actions.append(action)
# Estimate tokens from content length
content = entry.get("content", "")
if isinstance(content, str):
total_tokens += len(content.split())
elif isinstance(content, list):
for item in content:
if isinstance(item, dict) and "text" in item:
total_tokens += len(item["text"].split())
# Look for repo mentions in content
if entry.get("content"):
content_str = str(entry["content"])
for pattern in repo_patterns:
match = re.search(pattern, content_str, re.IGNORECASE)
if match:
if match.groups():
repo = match.group(1)
else:
repo = match.group(0)
break
# Look for error messages
if entry.get("role") == "tool" and entry.get("is_error"):
error_msg = entry.get("content", "Unknown error")
if isinstance(error_msg, str) and len(error_msg) < 200:
errors.append(error_msg[:200])
def get_session_age_hours(self, session_data: Dict[str, Any]) -> float:
"""Get session age in hours."""
last_updated = session_data.get("last_updated")
if not last_updated:
return float('inf')
except FileNotFoundError:
return SessionSummary(
session_id=session_id,
model="unknown",
repo="unknown",
outcome="failure",
message_count=0,
tool_calls=0,
duration_estimate="0m",
key_actions=[],
errors_encountered=[f"File not found: {file_path}"]
)
# Count message types
user_messages = sum(1 for m in messages if m.get("role") == "user")
assistant_messages = sum(1 for m in messages if m.get("role") == "assistant")
tool_outputs = sum(1 for m in messages if m.get("role") == "tool")
# Calculate duration estimate
duration_estimate = "unknown"
if start_time and end_time:
try:
if isinstance(last_updated, str):
# Handle various timestamp formats
for fmt in [
"%Y-%m-%dT%H:%M:%S.%fZ",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%f",
"%Y-%m-%dT%H:%M:%S"
]:
try:
dt = datetime.strptime(last_updated, fmt)
dt = dt.replace(tzinfo=timezone.utc)
break
except ValueError:
continue
else:
# Try parsing with fromisoformat
dt = datetime.fromisoformat(last_updated.replace('Z', '+00:00'))
else:
dt = last_updated
# Try to parse timestamps
start_dt = None
end_dt = None
now = datetime.now(timezone.utc)
age = now - dt
return age.total_seconds() / 3600
# Handle various timestamp formats
for fmt in ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%d %H:%M:%S"]:
try:
if start_dt is None:
start_dt = datetime.strptime(start_time, fmt)
if end_dt is None:
end_dt = datetime.strptime(end_time, fmt)
except ValueError:
continue
if start_dt and end_dt:
duration = end_dt - start_dt
minutes = duration.total_seconds() / 60
duration_estimate = f"{minutes:.0f}m"
except Exception:
return float('inf')
pass
def is_session_complete(self, session_data: Dict[str, Any]) -> bool:
"""
Check if a session appears to be complete (not actively running).
# Classify outcome
outcome = "unknown"
if errors:
# Check if any errors are fatal
fatal_errors = any("405" in e or "permission" in e.lower() or "authentication" in e.lower()
for e in errors)
if fatal_errors:
outcome = "failure"
else:
outcome = "partial"
elif messages:
# Check last message for success indicators
last_msg = messages[-1]
if last_msg.get("role") == "assistant":
content = last_msg.get("content", "")
if isinstance(content, str):
success_indicators = ["done", "completed", "success", "merged", "pushed"]
if any(indicator in content.lower() for indicator in success_indicators):
outcome = "success"
else:
outcome = "unknown"
# Deduplicate key actions (keep unique, limit to 10)
unique_actions = []
for action in key_actions:
if action not in unique_actions:
unique_actions.append(action)
if len(unique_actions) >= 10:
break
# Deduplicate errors (keep unique, limit to 5)
unique_errors = []
for error in errors:
if error not in unique_errors:
unique_errors.append(error)
if len(unique_errors) >= 5:
break
return SessionSummary(
session_id=session_id,
model=model,
repo=repo,
outcome=outcome,
message_count=len(messages),
tool_calls=tool_calls_count,
duration_estimate=duration_estimate,
key_actions=unique_actions,
errors_encountered=unique_errors,
start_time=start_time,
end_time=end_time,
total_tokens_estimate=total_tokens,
user_messages=user_messages,
assistant_messages=assistant_messages,
tool_outputs=tool_outputs
)
def process_session_directory(directory_path: str, output_file: Optional[str] = None) -> List[SessionSummary]:
"""
Process all JSONL files in a directory.
Args:
directory_path: Path to directory containing session JSONL files
output_file: Optional path to write JSON output
Heuristic: If last update was more than 5 minutes ago, consider it complete.
"""
age_hours = self.get_session_age_hours(session_data)
return age_hours > (5 / 60) # 5 minutes
Returns:
List of SessionSummary objects
"""
directory = Path(directory_path)
if not directory.exists():
print(f"Error: Directory {directory_path} does not exist", file=sys.stderr)
return []
jsonl_files = list(directory.glob("session_*.jsonl"))
if not jsonl_files:
print(f"Warning: No session_*.jsonl files found in {directory_path}", file=sys.stderr)
return []
summaries = []
for jsonl_file in sorted(jsonl_files):
print(f"Processing {jsonl_file.name}...", file=sys.stderr)
summary = parse_jsonl_session(str(jsonl_file))
summaries.append(summary)
if output_file:
with open(output_file, 'w', encoding='utf-8') as f:
json.dump([asdict(s) for s in summaries], f, indent=2)
print(f"Wrote {len(summaries)} summaries to {output_file}", file=sys.stderr)
return summaries
def main():
"""Test the session reader."""
reader = SessionReader()
"""CLI entry point."""
import argparse
# List recent sessions
sessions = reader.list_sessions(limit=5)
print(f"Found {len(sessions)} recent sessions")
parser = argparse.ArgumentParser(description="Parse Hermes session JSONL transcripts")
parser.add_argument("path", help="Path to JSONL file or directory of session files")
parser.add_argument("-o", "--output", help="Output JSON file (default: stdout)")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
for path in sessions:
session = reader.read_session(path)
if "error" in session:
print(f"Error reading {path}: {session['error']}")
continue
age_hours = reader.get_session_age_hours(session)
complete = reader.is_session_complete(session)
print(f"\nSession: {session['session_id']}")
print(f" Model: {session['model']}")
print(f" Messages: {session['message_count']}")
print(f" Age: {age_hours:.1f} hours")
print(f" Complete: {complete}")
args = parser.parse_args()
path = Path(args.path)
if path.is_file():
summary = parse_jsonl_session(str(path))
if args.output:
with open(args.output, 'w') as f:
json.dump(asdict(summary), f, indent=2)
print(f"Wrote summary to {args.output}", file=sys.stderr)
else:
print(json.dumps(asdict(summary), indent=2))
elif path.is_dir():
summaries = process_session_directory(str(path), args.output)
if not args.output:
print(json.dumps([asdict(s) for s in summaries], indent=2))
else:
print(f"Error: {args.path} is not a file or directory", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":