errlore 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {errlore-0.2.0 → errlore-0.2.2}/CHANGELOG.md +27 -0
- {errlore-0.2.0 → errlore-0.2.2}/PKG-INFO +5 -5
- {errlore-0.2.0 → errlore-0.2.2}/README.md +4 -4
- {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/README.md +11 -7
- errlore-0.2.2/examples/claude-code/errlore_posttoolusefailure.py +40 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/settings.json.example +11 -0
- {errlore-0.2.0 → errlore-0.2.2}/pyproject.toml +1 -1
- {errlore-0.2.0 → errlore-0.2.2}/site/index.html +3 -3
- errlore-0.2.2/src/errlore/__init__.py +24 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/cli.py +18 -1
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/integrations/claude_code.py +51 -2
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_cli.py +52 -2
- errlore-0.2.0/src/errlore/__init__.py +0 -16
- {errlore-0.2.0 → errlore-0.2.2}/.github/workflows/ci.yml +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/.github/workflows/release.yml +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/.gitignore +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/LICENSE +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/SECURITY.md +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/bench_error_reduction.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/bench_retrieval.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/REPRODUCIBILITY_2026-07-11.md +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/error_reduction/report.md +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/gemma_2026-07-11_report.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_report.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed11.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed22.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed33.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_newfamilies_2026-07-11_report.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/anthropic_agent.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/errlore_posttooluse.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/errlore_sessionstart.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/langchain_agent.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/examples/openai_agent.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/README.md +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/errlore_feedback_action.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/errlore_memory_filter.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/demo.gif +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/demo_script.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/llms.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/og.png +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/robots.txt +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/site/sitemap.xml +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/classifier.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/injector.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/patterns.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/tracker.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/facade.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/integrations/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/jsonl_index.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/jsonl_writer.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/repair.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/models.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/store.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/py.typed +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/backend.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/index.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/sanitize.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/trust/__init__.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/src/errlore/trust/engine.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/conftest.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_errmem.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_facade.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_io.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_lessons.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_openwebui_integration.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_regressions.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_retrieval.py +0 -0
- {errlore-0.2.0 → errlore-0.2.2}/tests/test_trust.py +0 -0
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] - 2026-07-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `__version__` (and `errlore --version`) now reads the installed package
|
|
12
|
+
metadata instead of a hardcoded string — 0.2.0/0.2.1 wheels reported
|
|
13
|
+
themselves as 0.1.4.
|
|
14
|
+
|
|
15
|
+
## [0.2.1] - 2026-07-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Claude Code integration: failed Bash commands are now actually captured.**
|
|
19
|
+
Current Claude Code routes tool failures to the dedicated
|
|
20
|
+
`PostToolUseFailure` event (`PostToolUse` fires only on success), so the
|
|
21
|
+
previous hook never saw a failure — the loop was silently inert. New
|
|
22
|
+
`post_tool_use_failure()` handler parses the real payload (top-level `error`
|
|
23
|
+
string, `is_interrupt` guard); `errlore init claude-code` now installs and
|
|
24
|
+
registers all three hooks. The legacy PostToolUse handler stays for older
|
|
25
|
+
Claude Code versions. Verified against the official hooks reference; example
|
|
26
|
+
shims and `settings.json.example` updated to match.
|
|
27
|
+
- `errlore stats` / `errlore lessons` now print the resolved data dir (to
|
|
28
|
+
stderr) — running them next to a quickstart `./agent_memory` while the
|
|
29
|
+
default points at `~/.errlore/claude-code` silently showed zeros.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Docs/site model placeholder refreshed `gpt-5.5` → `gpt-5.6` (released
|
|
33
|
+
2026-07-09); site JSON-LD `softwareVersion` unstuck from 0.1.4.
|
|
34
|
+
|
|
8
35
|
## [0.2.0] - 2026-07-11 — "The Proof Release"
|
|
9
36
|
|
|
10
37
|
The headline benchmark is now proven on all three generality axes:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: errlore
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server.
|
|
5
5
|
Project-URL: Homepage, https://errlore.com
|
|
6
6
|
Project-URL: Repository, https://github.com/Ma4etaSS/errlore
|
|
@@ -50,7 +50,7 @@ Your agent keeps making the same mistakes. errlore fixes that:
|
|
|
50
50
|
|
|
51
51
|
- **Lessons** -- every resolved failure becomes a lesson; relevant lessons are injected
|
|
52
52
|
into the prompt for similar future tasks.
|
|
53
|
-
- **Known issues** -- per-model weakness tracking ("gpt-5.
|
|
53
|
+
- **Known issues** -- per-model weakness tracking ("gpt-5.6 keeps hallucinating dates in
|
|
54
54
|
extraction tasks") injected as warnings.
|
|
55
55
|
- **Trust** *(experimental)* -- Bayesian per-model, per-domain trust weights: a starting
|
|
56
56
|
point for which model to pick per job, based on observed outcomes. Needs a spread of
|
|
@@ -88,14 +88,14 @@ from errlore import AgentMemory
|
|
|
88
88
|
mem = AgentMemory("./agent_memory")
|
|
89
89
|
|
|
90
90
|
# 1. Agent failed -- record it
|
|
91
|
-
err_id = mem.log_error("gpt-5.
|
|
91
|
+
err_id = mem.log_error("gpt-5.6", "extraction", error="hallucinated dates")
|
|
92
92
|
|
|
93
93
|
# 2. You fixed it -- extract a lesson
|
|
94
94
|
mem.resolve(err_id, "Added date format validation",
|
|
95
95
|
lesson="For date extraction, demand ISO-8601 and verify against source")
|
|
96
96
|
|
|
97
97
|
# 3. Next similar task -- lessons + known issues injected automatically
|
|
98
|
-
inj = mem.inject_for("extract dates from contract", model="gpt-5.
|
|
98
|
+
inj = mem.inject_for("extract dates from contract", model="gpt-5.6",
|
|
99
99
|
task_type="extraction")
|
|
100
100
|
prompt = f"Your task: extract dates\n{inj.text}"
|
|
101
101
|
print(prompt)
|
|
@@ -107,7 +107,7 @@ mem.report_outcome(inj, success=True)
|
|
|
107
107
|
print(mem.stats())
|
|
108
108
|
# {'errors_total': 1, 'errors_resolved': 1, 'errors_unresolved': 0,
|
|
109
109
|
# 'lessons_total': 1, 'lessons_applied': 1, 'pending_injections': 0,
|
|
110
|
-
# 'trust': {'gpt-5.
|
|
110
|
+
# 'trust': {'gpt-5.6': 0.5522...}}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
No API keys needed. errlore itself never calls any LLM -- it manages local
|
|
@@ -16,7 +16,7 @@ Your agent keeps making the same mistakes. errlore fixes that:
|
|
|
16
16
|
|
|
17
17
|
- **Lessons** -- every resolved failure becomes a lesson; relevant lessons are injected
|
|
18
18
|
into the prompt for similar future tasks.
|
|
19
|
-
- **Known issues** -- per-model weakness tracking ("gpt-5.
|
|
19
|
+
- **Known issues** -- per-model weakness tracking ("gpt-5.6 keeps hallucinating dates in
|
|
20
20
|
extraction tasks") injected as warnings.
|
|
21
21
|
- **Trust** *(experimental)* -- Bayesian per-model, per-domain trust weights: a starting
|
|
22
22
|
point for which model to pick per job, based on observed outcomes. Needs a spread of
|
|
@@ -54,14 +54,14 @@ from errlore import AgentMemory
|
|
|
54
54
|
mem = AgentMemory("./agent_memory")
|
|
55
55
|
|
|
56
56
|
# 1. Agent failed -- record it
|
|
57
|
-
err_id = mem.log_error("gpt-5.
|
|
57
|
+
err_id = mem.log_error("gpt-5.6", "extraction", error="hallucinated dates")
|
|
58
58
|
|
|
59
59
|
# 2. You fixed it -- extract a lesson
|
|
60
60
|
mem.resolve(err_id, "Added date format validation",
|
|
61
61
|
lesson="For date extraction, demand ISO-8601 and verify against source")
|
|
62
62
|
|
|
63
63
|
# 3. Next similar task -- lessons + known issues injected automatically
|
|
64
|
-
inj = mem.inject_for("extract dates from contract", model="gpt-5.
|
|
64
|
+
inj = mem.inject_for("extract dates from contract", model="gpt-5.6",
|
|
65
65
|
task_type="extraction")
|
|
66
66
|
prompt = f"Your task: extract dates\n{inj.text}"
|
|
67
67
|
print(prompt)
|
|
@@ -73,7 +73,7 @@ mem.report_outcome(inj, success=True)
|
|
|
73
73
|
print(mem.stats())
|
|
74
74
|
# {'errors_total': 1, 'errors_resolved': 1, 'errors_unresolved': 0,
|
|
75
75
|
# 'lessons_total': 1, 'lessons_applied': 1, 'pending_injections': 0,
|
|
76
|
-
# 'trust': {'gpt-5.
|
|
76
|
+
# 'trust': {'gpt-5.6': 0.5522...}}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
No API keys needed. errlore itself never calls any LLM -- it manages local
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Give your coding agent a memory of its own failures across sessions:
|
|
4
4
|
|
|
5
|
-
- **
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- **PostToolUseFailure hook** — every failed Bash command is logged into
|
|
6
|
+
errlore. Current Claude Code reports tool failures on this dedicated event
|
|
7
|
+
(PostToolUse fires only on success); a legacy PostToolUse hook is installed
|
|
8
|
+
too for older versions. Resolve the ones you fixed
|
|
9
|
+
(`mem.resolve(err_id, ..., lesson=...)`) or use `mem.add_lesson()` to
|
|
10
|
+
capture takeaways directly.
|
|
8
11
|
- **SessionStart hook** — each new session begins with a briefing block of
|
|
9
12
|
relevant lessons and per-tool KNOWN ISSUES, printed into the context.
|
|
10
13
|
|
|
@@ -16,7 +19,7 @@ errlore init claude-code # global (~/.claude/settings.json)
|
|
|
16
19
|
errlore init claude-code --project # or this repo only (./.claude/settings.json)
|
|
17
20
|
```
|
|
18
21
|
|
|
19
|
-
That writes the
|
|
22
|
+
That writes the three hook scripts (to `~/.errlore/hooks/`) and merges them into
|
|
20
23
|
your `settings.json` — idempotently, preserving any hooks you already have.
|
|
21
24
|
Restart Claude Code (or open a new session) to pick them up. Options:
|
|
22
25
|
`--data-dir` (where the memory lives, default `~/.errlore/claude-code`) and
|
|
@@ -26,12 +29,13 @@ Handy afterwards: `errlore stats` and `errlore lessons` to see what it learned.
|
|
|
26
29
|
|
|
27
30
|
### Manual setup (if you'd rather wire it yourself)
|
|
28
31
|
|
|
29
|
-
The scripts in this folder (`
|
|
30
|
-
|
|
32
|
+
The scripts in this folder (`errlore_posttoolusefailure.py`,
|
|
33
|
+
`errlore_posttooluse.py`, `errlore_sessionstart.py`) plus
|
|
34
|
+
`settings.json.example` show the shape: copy them somewhere stable, fix the
|
|
31
35
|
paths, and merge into `.claude/settings.json`. `export ERRLORE_DATA=...` picks
|
|
32
36
|
the memory dir.
|
|
33
37
|
|
|
34
38
|
Notes: hook event field names can differ between Claude Code versions —
|
|
35
|
-
|
|
39
|
+
both failure scripts read them defensively and never break the agent
|
|
36
40
|
loop (exit 0 on anything unexpected). Check `claude --help` / the hooks
|
|
37
41
|
docs for your version if events don't arrive.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Code PostToolUseFailure hook: failed Bash commands become errlore errors.
|
|
3
|
+
|
|
4
|
+
Current Claude Code routes tool FAILURES to this dedicated event (PostToolUse
|
|
5
|
+
fires only on success). The payload carries `tool_name`, `tool_input` and a
|
|
6
|
+
top-level `error` string — no `tool_response`, no structured exit code.
|
|
7
|
+
Pair with errlore_sessionstart.py, which briefs the next session on them.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from errlore import AgentMemory
|
|
15
|
+
|
|
16
|
+
DATA_DIR = os.environ.get("ERRLORE_DATA", os.path.expanduser("~/.errlore/claude-code"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
try:
|
|
21
|
+
event = json.load(sys.stdin)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
return 0 # never break the agent loop
|
|
24
|
+
|
|
25
|
+
if event.get("tool_name", "") != "Bash":
|
|
26
|
+
return 0
|
|
27
|
+
if bool(event.get("is_interrupt")):
|
|
28
|
+
return 0 # user pressed Esc — not a failure worth learning from
|
|
29
|
+
|
|
30
|
+
command = str((event.get("tool_input") or {}).get("command", ""))[:160]
|
|
31
|
+
error = str(event.get("error") or "")[:200]
|
|
32
|
+
if not command and not error:
|
|
33
|
+
return 0
|
|
34
|
+
mem = AgentMemory(DATA_DIR)
|
|
35
|
+
mem.log_error("claude-code", "bash", f"CommandFailed: {command} :: {error}")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "errlore"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"url": "https://errlore.com/",
|
|
33
33
|
"downloadUrl": "https://pypi.org/project/errlore/",
|
|
34
34
|
"installUrl": "https://pypi.org/project/errlore/",
|
|
35
|
-
"softwareVersion": "0.1
|
|
35
|
+
"softwareVersion": "0.2.1",
|
|
36
36
|
"license": "https://opensource.org/licenses/MIT",
|
|
37
37
|
"programmingLanguage": "Python",
|
|
38
38
|
"codeRepository": "https://github.com/Ma4etaSS/errlore",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
mem = AgentMemory(<span class="c-str">"./agent_memory"</span>)
|
|
228
228
|
|
|
229
229
|
<span class="c-com"># 1. agent failed — record it</span>
|
|
230
|
-
err = mem.log_error(<span class="c-str">"gpt-5.
|
|
230
|
+
err = mem.log_error(<span class="c-str">"gpt-5.6"</span>, <span class="c-str">"extraction"</span>, error=<span class="c-str">"hallucinated dates"</span>)
|
|
231
231
|
|
|
232
232
|
<span class="c-com"># 2. you fixed it — extract the lesson</span>
|
|
233
233
|
mem.resolve(err, <span class="c-str">"added validation"</span>,
|
|
@@ -235,7 +235,7 @@ mem.resolve(err, <span class="c-str">"added validation"</span>,
|
|
|
235
235
|
|
|
236
236
|
<span class="c-com"># 3. next similar task — memory speaks up</span>
|
|
237
237
|
task = <span class="c-str">"extract dates from contract"</span>
|
|
238
|
-
inj = mem.inject_for(task, model=<span class="c-str">"gpt-5.
|
|
238
|
+
inj = mem.inject_for(task, model=<span class="c-str">"gpt-5.6"</span>, task_type=<span class="c-str">"extraction"</span>)
|
|
239
239
|
prompt = task + <span class="c-str">"\n\n"</span> + inj.text
|
|
240
240
|
<span class="c-out"># [LESSONS FROM PAST FAILURES]
|
|
241
241
|
# - UnclassifiedError: hallucinated dates -> For date extraction,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""errlore -- memory for AI agents that learns from failures."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _pkg_version
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
# Single source of truth: installed package metadata (pyproject version).
|
|
8
|
+
# A hardcoded string here shipped 0.2.x wheels reporting themselves 0.1.4.
|
|
9
|
+
__version__ = _pkg_version("errlore")
|
|
10
|
+
except PackageNotFoundError: # source tree without an install
|
|
11
|
+
__version__ = "0.0.0.dev0"
|
|
12
|
+
|
|
13
|
+
from errlore.facade import AgentMemory, Injection
|
|
14
|
+
from errlore.lessons.store import LessonStore
|
|
15
|
+
from errlore.trust import FeedbackSignal, TrustEngine
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AgentMemory",
|
|
19
|
+
"FeedbackSignal",
|
|
20
|
+
"Injection",
|
|
21
|
+
"LessonStore",
|
|
22
|
+
"TrustEngine",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -76,13 +76,16 @@ def _cmd_init_claude_code(args: argparse.Namespace) -> int:
|
|
|
76
76
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
77
77
|
|
|
78
78
|
post = hooks_dir / "errlore_posttooluse.py"
|
|
79
|
+
failure = hooks_dir / "errlore_posttoolusefailure.py"
|
|
79
80
|
session = hooks_dir / "errlore_sessionstart.py"
|
|
80
81
|
_write_hook(post, "post_tool_use", data_dir)
|
|
82
|
+
_write_hook(failure, "post_tool_use_failure", data_dir)
|
|
81
83
|
_write_hook(session, "session_start", data_dir)
|
|
82
84
|
|
|
83
85
|
# Same interpreter that runs the CLI -> the one where errlore is installed.
|
|
84
86
|
py = sys.executable or "python3"
|
|
85
87
|
cmd_post = f"{py} {post}"
|
|
88
|
+
cmd_failure = f"{py} {failure}"
|
|
86
89
|
cmd_session = f"{py} {session}"
|
|
87
90
|
|
|
88
91
|
settings_path = (
|
|
@@ -110,7 +113,11 @@ def _cmd_init_claude_code(args: argparse.Namespace) -> int:
|
|
|
110
113
|
print(f"error: {settings_path} 'hooks' is not an object; not touching it", file=sys.stderr)
|
|
111
114
|
return 1
|
|
112
115
|
|
|
116
|
+
# PostToolUseFailure is where current Claude Code reports failed tool
|
|
117
|
+
# calls (PostToolUse fires only on success). The PostToolUse hook stays
|
|
118
|
+
# registered for older Claude Code versions.
|
|
113
119
|
added_post = _ensure_hook(hooks, "PostToolUse", cmd_post, matcher="Bash")
|
|
120
|
+
added_failure = _ensure_hook(hooks, "PostToolUseFailure", cmd_failure, matcher="Bash")
|
|
114
121
|
added_session = _ensure_hook(hooks, "SessionStart", cmd_session)
|
|
115
122
|
|
|
116
123
|
settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
@@ -119,7 +126,7 @@ def _cmd_init_claude_code(args: argparse.Namespace) -> int:
|
|
|
119
126
|
print(f" hooks written to : {hooks_dir}")
|
|
120
127
|
print(f" memory data dir : {data_dir}")
|
|
121
128
|
print(f" settings updated : {settings_path}")
|
|
122
|
-
if not (added_post and added_session):
|
|
129
|
+
if not (added_post and added_failure and added_session):
|
|
123
130
|
print(" (hooks were already present — nothing duplicated)")
|
|
124
131
|
print("\nRestart Claude Code (or start a new session) to pick them up.")
|
|
125
132
|
print("Failed Bash commands now become lessons; each new session is briefed on past pitfalls.")
|
|
@@ -132,7 +139,16 @@ def _open_memory(data_dir: str): # type: ignore[no-untyped-def]
|
|
|
132
139
|
return AgentMemory(os.path.expanduser(data_dir))
|
|
133
140
|
|
|
134
141
|
|
|
142
|
+
def _print_data_dir(data_dir: str) -> None:
|
|
143
|
+
"""Always show WHERE we read from — the #1 confusion is running `errlore
|
|
144
|
+
stats` next to a quickstart ./agent_memory dir while the CLI default
|
|
145
|
+
points at ~/.errlore/claude-code and silently prints zeros."""
|
|
146
|
+
resolved = os.path.abspath(os.path.expanduser(data_dir))
|
|
147
|
+
print(f"data dir: {resolved} (change with --data-dir PATH)", file=sys.stderr)
|
|
148
|
+
|
|
149
|
+
|
|
135
150
|
def _cmd_stats(args: argparse.Namespace) -> int:
|
|
151
|
+
_print_data_dir(args.data_dir)
|
|
136
152
|
stats = _open_memory(args.data_dir).stats()
|
|
137
153
|
if args.json:
|
|
138
154
|
print(json.dumps(stats, indent=2))
|
|
@@ -150,6 +166,7 @@ def _cmd_stats(args: argparse.Namespace) -> int:
|
|
|
150
166
|
|
|
151
167
|
|
|
152
168
|
def _cmd_lessons(args: argparse.Namespace) -> int:
|
|
169
|
+
_print_data_dir(args.data_dir)
|
|
153
170
|
lessons = _open_memory(args.data_dir).lessons(limit=args.limit)
|
|
154
171
|
if not lessons:
|
|
155
172
|
print("(no lessons yet)")
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
functions here, so the real (tested) logic lives in the package instead of in
|
|
5
5
|
copy-pasted example files:
|
|
6
6
|
|
|
7
|
-
* :func:`
|
|
8
|
-
errlore error.
|
|
7
|
+
* :func:`post_tool_use_failure` -- a PostToolUseFailure hook: a failed Bash
|
|
8
|
+
command becomes an errlore error. Current Claude Code routes tool failures
|
|
9
|
+
to this dedicated event (PostToolUse fires only on SUCCESS) with a top-level
|
|
10
|
+
``error`` string and no ``tool_response``.
|
|
11
|
+
* :func:`post_tool_use` -- legacy PostToolUse handler, kept for older Claude
|
|
12
|
+
Code versions whose PostToolUse payload carried ``exit_code``/``is_error``.
|
|
13
|
+
On current versions a failed command never reaches PostToolUse, so this
|
|
14
|
+
stays a harmless no-op.
|
|
9
15
|
* :func:`session_start` -- a SessionStart hook: print the lessons + KNOWN ISSUES
|
|
10
16
|
briefing to stdout (Claude Code adds hook stdout to the session context).
|
|
11
17
|
|
|
@@ -73,6 +79,49 @@ def post_tool_use(event_json: str | None = None) -> int:
|
|
|
73
79
|
return 0
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
def post_tool_use_failure(event_json: str | None = None) -> int:
|
|
83
|
+
"""Log a failed Bash command from a PostToolUseFailure event.
|
|
84
|
+
|
|
85
|
+
Current Claude Code fires this dedicated event when a tool call fails
|
|
86
|
+
(PostToolUse only fires on success). The payload carries ``tool_name``,
|
|
87
|
+
``tool_input`` and a top-level ``error`` string — there is no
|
|
88
|
+
``tool_response`` and no structured exit code.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
event_json: The raw hook-event JSON. ``None`` reads ``sys.stdin``
|
|
92
|
+
(the real hook path); tests pass a string.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Always ``0`` -- a hook must never break the agent loop.
|
|
96
|
+
"""
|
|
97
|
+
raw = event_json if event_json is not None else sys.stdin.read()
|
|
98
|
+
try:
|
|
99
|
+
event: Any = json.loads(raw)
|
|
100
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
101
|
+
return 0
|
|
102
|
+
if not isinstance(event, dict):
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
if event.get("tool_name", "") != "Bash":
|
|
106
|
+
return 0
|
|
107
|
+
# A user hitting Esc is not a failure worth learning from.
|
|
108
|
+
if bool(event.get("is_interrupt")):
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
tool_input = event.get("tool_input") or {}
|
|
112
|
+
command = str(tool_input.get("command", "") if isinstance(tool_input, dict) else "")[:160]
|
|
113
|
+
error = str(event.get("error") or "")[:200]
|
|
114
|
+
if not command and not error:
|
|
115
|
+
return 0
|
|
116
|
+
try:
|
|
117
|
+
AgentMemory(data_dir()).log_error(
|
|
118
|
+
"claude-code", "bash", f"CommandFailed: {command} :: {error}",
|
|
119
|
+
)
|
|
120
|
+
except Exception: # never break the agent loop
|
|
121
|
+
return 0
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
76
125
|
def session_start() -> int:
|
|
77
126
|
"""Print the lessons + KNOWN ISSUES briefing for the new session.
|
|
78
127
|
|
|
@@ -9,7 +9,11 @@ import pytest
|
|
|
9
9
|
|
|
10
10
|
from errlore import AgentMemory
|
|
11
11
|
from errlore.cli import main
|
|
12
|
-
from errlore.integrations.claude_code import
|
|
12
|
+
from errlore.integrations.claude_code import (
|
|
13
|
+
post_tool_use,
|
|
14
|
+
post_tool_use_failure,
|
|
15
|
+
session_start,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
# --------------------------------------------------------------------------
|
|
15
19
|
# Claude Code hook logic
|
|
@@ -42,6 +46,45 @@ def test_post_tool_use_ignores_success_nonbash_and_junk(
|
|
|
42
46
|
assert AgentMemory(tmp_path).stats()["errors_total"] == 0
|
|
43
47
|
|
|
44
48
|
|
|
49
|
+
def test_post_tool_use_failure_logs_failed_bash(
|
|
50
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""The payload shape current Claude Code actually sends for a failed
|
|
53
|
+
command — PostToolUseFailure with a top-level `error` string and no
|
|
54
|
+
tool_response (verbatim from the hooks reference docs)."""
|
|
55
|
+
monkeypatch.setenv("ERRLORE_DATA", str(tmp_path))
|
|
56
|
+
event = json.dumps({
|
|
57
|
+
"hook_event_name": "PostToolUseFailure",
|
|
58
|
+
"tool_name": "Bash",
|
|
59
|
+
"tool_input": {"command": "npm test", "description": "Run test suite"},
|
|
60
|
+
"error": "Command exited with non-zero status code 1",
|
|
61
|
+
"is_interrupt": False,
|
|
62
|
+
"duration_ms": 4187,
|
|
63
|
+
})
|
|
64
|
+
assert post_tool_use_failure(event) == 0
|
|
65
|
+
mem = AgentMemory(tmp_path)
|
|
66
|
+
assert mem.stats()["errors_total"] == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_post_tool_use_failure_ignores_interrupt_nonbash_and_junk(
|
|
70
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
|
71
|
+
) -> None:
|
|
72
|
+
monkeypatch.setenv("ERRLORE_DATA", str(tmp_path))
|
|
73
|
+
# user pressed Esc — not a failure worth learning from
|
|
74
|
+
interrupted = json.dumps({
|
|
75
|
+
"tool_name": "Bash", "tool_input": {"command": "sleep 100"},
|
|
76
|
+
"error": "interrupted", "is_interrupt": True,
|
|
77
|
+
})
|
|
78
|
+
assert post_tool_use_failure(interrupted) == 0
|
|
79
|
+
# non-Bash tool failure
|
|
80
|
+
nonbash = json.dumps({"tool_name": "Read", "error": "file not found"})
|
|
81
|
+
assert post_tool_use_failure(nonbash) == 0
|
|
82
|
+
# junk — must never raise
|
|
83
|
+
assert post_tool_use_failure("not json") == 0
|
|
84
|
+
assert post_tool_use_failure("[]") == 0
|
|
85
|
+
assert AgentMemory(tmp_path).stats()["errors_total"] == 0
|
|
86
|
+
|
|
87
|
+
|
|
45
88
|
def test_session_start_briefs_and_is_silent_when_empty(
|
|
46
89
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
|
47
90
|
) -> None:
|
|
@@ -79,20 +122,26 @@ def test_init_claude_code_writes_hooks_and_merges_settings(
|
|
|
79
122
|
|
|
80
123
|
# hook shims written + executable
|
|
81
124
|
post = hooks_dir / "errlore_posttooluse.py"
|
|
125
|
+
failure = hooks_dir / "errlore_posttoolusefailure.py"
|
|
82
126
|
session = hooks_dir / "errlore_sessionstart.py"
|
|
83
|
-
assert post.exists() and session.exists()
|
|
127
|
+
assert post.exists() and failure.exists() and session.exists()
|
|
84
128
|
assert "post_tool_use" in post.read_text()
|
|
129
|
+
assert "post_tool_use_failure" in failure.read_text()
|
|
85
130
|
assert str(data_dir) in post.read_text() # data dir pinned into the shim
|
|
86
131
|
|
|
87
132
|
# settings.json wired
|
|
88
133
|
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
|
|
89
134
|
ptu = settings["hooks"]["PostToolUse"]
|
|
135
|
+
ptuf = settings["hooks"]["PostToolUseFailure"]
|
|
90
136
|
ss = settings["hooks"]["SessionStart"]
|
|
91
137
|
assert any("errlore_posttooluse.py" in h["command"]
|
|
92
138
|
for g in ptu for h in g["hooks"])
|
|
139
|
+
assert any("errlore_posttoolusefailure.py" in h["command"]
|
|
140
|
+
for g in ptuf for h in g["hooks"])
|
|
93
141
|
assert any("errlore_sessionstart.py" in h["command"]
|
|
94
142
|
for g in ss for h in g["hooks"])
|
|
95
143
|
assert ptu[0]["matcher"] == "Bash"
|
|
144
|
+
assert ptuf[0]["matcher"] == "Bash"
|
|
96
145
|
|
|
97
146
|
|
|
98
147
|
def test_init_claude_code_is_idempotent(
|
|
@@ -106,6 +155,7 @@ def test_init_claude_code_is_idempotent(
|
|
|
106
155
|
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
|
|
107
156
|
# no duplicated hook entries
|
|
108
157
|
assert len(settings["hooks"]["PostToolUse"]) == 1
|
|
158
|
+
assert len(settings["hooks"]["PostToolUseFailure"]) == 1
|
|
109
159
|
assert len(settings["hooks"]["SessionStart"]) == 1
|
|
110
160
|
|
|
111
161
|
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"""errlore -- memory for AI agents that learns from failures."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.1.4"
|
|
4
|
-
|
|
5
|
-
from errlore.facade import AgentMemory, Injection
|
|
6
|
-
from errlore.lessons.store import LessonStore
|
|
7
|
-
from errlore.trust import FeedbackSignal, TrustEngine
|
|
8
|
-
|
|
9
|
-
__all__ = [
|
|
10
|
-
"AgentMemory",
|
|
11
|
-
"FeedbackSignal",
|
|
12
|
-
"Injection",
|
|
13
|
-
"LessonStore",
|
|
14
|
-
"TrustEngine",
|
|
15
|
-
"__version__",
|
|
16
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|