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.
Files changed (72) hide show
  1. {errlore-0.2.0 → errlore-0.2.2}/CHANGELOG.md +27 -0
  2. {errlore-0.2.0 → errlore-0.2.2}/PKG-INFO +5 -5
  3. {errlore-0.2.0 → errlore-0.2.2}/README.md +4 -4
  4. {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/README.md +11 -7
  5. errlore-0.2.2/examples/claude-code/errlore_posttoolusefailure.py +40 -0
  6. {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/settings.json.example +11 -0
  7. {errlore-0.2.0 → errlore-0.2.2}/pyproject.toml +1 -1
  8. {errlore-0.2.0 → errlore-0.2.2}/site/index.html +3 -3
  9. errlore-0.2.2/src/errlore/__init__.py +24 -0
  10. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/cli.py +18 -1
  11. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/integrations/claude_code.py +51 -2
  12. {errlore-0.2.0 → errlore-0.2.2}/tests/test_cli.py +52 -2
  13. errlore-0.2.0/src/errlore/__init__.py +0 -16
  14. {errlore-0.2.0 → errlore-0.2.2}/.github/workflows/ci.yml +0 -0
  15. {errlore-0.2.0 → errlore-0.2.2}/.github/workflows/release.yml +0 -0
  16. {errlore-0.2.0 → errlore-0.2.2}/.gitignore +0 -0
  17. {errlore-0.2.0 → errlore-0.2.2}/LICENSE +0 -0
  18. {errlore-0.2.0 → errlore-0.2.2}/SECURITY.md +0 -0
  19. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/bench_error_reduction.py +0 -0
  20. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/bench_retrieval.py +0 -0
  21. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/REPRODUCIBILITY_2026-07-11.md +0 -0
  22. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/error_reduction/report.md +0 -0
  23. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/gemma_2026-07-11_report.txt +0 -0
  24. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_report.txt +0 -0
  25. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed11.txt +0 -0
  26. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed22.txt +0 -0
  27. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_2026-07-11_seed33.txt +0 -0
  28. {errlore-0.2.0 → errlore-0.2.2}/benchmarks/results/haiku_newfamilies_2026-07-11_report.txt +0 -0
  29. {errlore-0.2.0 → errlore-0.2.2}/examples/anthropic_agent.py +0 -0
  30. {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/errlore_posttooluse.py +0 -0
  31. {errlore-0.2.0 → errlore-0.2.2}/examples/claude-code/errlore_sessionstart.py +0 -0
  32. {errlore-0.2.0 → errlore-0.2.2}/examples/langchain_agent.py +0 -0
  33. {errlore-0.2.0 → errlore-0.2.2}/examples/openai_agent.py +0 -0
  34. {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/README.md +0 -0
  35. {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/errlore_feedback_action.py +0 -0
  36. {errlore-0.2.0 → errlore-0.2.2}/integrations/openwebui/errlore_memory_filter.py +0 -0
  37. {errlore-0.2.0 → errlore-0.2.2}/site/demo.gif +0 -0
  38. {errlore-0.2.0 → errlore-0.2.2}/site/demo_script.py +0 -0
  39. {errlore-0.2.0 → errlore-0.2.2}/site/llms.txt +0 -0
  40. {errlore-0.2.0 → errlore-0.2.2}/site/og.png +0 -0
  41. {errlore-0.2.0 → errlore-0.2.2}/site/robots.txt +0 -0
  42. {errlore-0.2.0 → errlore-0.2.2}/site/sitemap.xml +0 -0
  43. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/__init__.py +0 -0
  44. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/classifier.py +0 -0
  45. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/injector.py +0 -0
  46. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/patterns.py +0 -0
  47. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/errmem/tracker.py +0 -0
  48. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/facade.py +0 -0
  49. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/integrations/__init__.py +0 -0
  50. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/__init__.py +0 -0
  51. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/jsonl_index.py +0 -0
  52. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/jsonl_writer.py +0 -0
  53. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/io/repair.py +0 -0
  54. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/__init__.py +0 -0
  55. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/models.py +0 -0
  56. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/lessons/store.py +0 -0
  57. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/py.typed +0 -0
  58. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/__init__.py +0 -0
  59. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/backend.py +0 -0
  60. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/retrieval/index.py +0 -0
  61. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/sanitize.py +0 -0
  62. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/trust/__init__.py +0 -0
  63. {errlore-0.2.0 → errlore-0.2.2}/src/errlore/trust/engine.py +0 -0
  64. {errlore-0.2.0 → errlore-0.2.2}/tests/conftest.py +0 -0
  65. {errlore-0.2.0 → errlore-0.2.2}/tests/test_errmem.py +0 -0
  66. {errlore-0.2.0 → errlore-0.2.2}/tests/test_facade.py +0 -0
  67. {errlore-0.2.0 → errlore-0.2.2}/tests/test_io.py +0 -0
  68. {errlore-0.2.0 → errlore-0.2.2}/tests/test_lessons.py +0 -0
  69. {errlore-0.2.0 → errlore-0.2.2}/tests/test_openwebui_integration.py +0 -0
  70. {errlore-0.2.0 → errlore-0.2.2}/tests/test_regressions.py +0 -0
  71. {errlore-0.2.0 → errlore-0.2.2}/tests/test_retrieval.py +0 -0
  72. {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.0
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.5 keeps hallucinating dates in
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.5", "extraction", error="hallucinated dates")
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.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.5': 0.5522...}}
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.5 keeps hallucinating dates in
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.5", "extraction", error="hallucinated dates")
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.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.5': 0.5522...}}
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
- - **PostToolUse hook** — every failed Bash command is logged into errlore.
6
- Resolve the ones you fixed (`mem.resolve(err_id, ..., lesson=...)`) or use
7
- `mem.add_lesson()` to capture takeaways directly.
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 two hook scripts (to `~/.errlore/hooks/`) and merges them into
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 (`errlore_posttooluse.py`, `errlore_sessionstart.py`)
30
- plus `settings.json.example` show the shape: copy them somewhere stable, fix the
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
- the PostToolUse script reads them defensively and never breaks the agent
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())
@@ -11,6 +11,17 @@
11
11
  ]
12
12
  }
13
13
  ],
14
+ "PostToolUseFailure": [
15
+ {
16
+ "matcher": "Bash",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python3 /path/to/errlore_posttoolusefailure.py"
21
+ }
22
+ ]
23
+ }
24
+ ],
14
25
  "SessionStart": [
15
26
  {
16
27
  "hooks": [
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "errlore"
7
- version = "0.2.0"
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.4",
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.5"</span>, <span class="c-str">"extraction"</span>, error=<span class="c-str">"hallucinated dates"</span>)
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.5"</span>, task_type=<span class="c-str">"extraction"</span>)
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:`post_tool_use` -- a PostToolUse hook: a failed Bash command becomes an
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 post_tool_use, session_start
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