Data Model
All data lives in data/. The SQLite database (lessons.db) is the source of truth; JSON files are either generated artifacts or human-editable config. See also Architecture: Data Model for the system-design view of the same records, and the Schema Reference for the authoritative field-by-field JSON Schema.
| File | Edit directly? | Purpose |
|---|---|---|
lessons.db | No (use CLI) | Source of truth — all lesson and candidate records (SQLite) |
lesson-manifest.json | No | Pre-compiled runtime manifest (run lessons build) |
config.json | Yes | Injection and scanning configuration |
scan-state.json | No | Per-file byte offsets for incremental scanning |
There is no lessons.json file — this page shows lesson records as JSON for readability, but
they are rows in lessons.db, edited through the CLI (add, edit, promote) rather than by
hand.
Lesson record
{
// ULID, generated on add
"id": "01JQSEED00000000000000001",
// kebab-case summary + 4-char random suffix
"slug": "pytest-tty-hanging-k9m2",
// candidate | reviewed | active | disabled | archived
"status": "active",
// directive | guard | hint | protocol — the authoritative injection behavior
"type": "guard",
// One-line description, max 120 chars
"summary": "pytest hangs in non-interactive envs due to TTY detection",
// Root cause (required, min 20 chars)
"problem": "Running bare `pytest` in Claude Code causes the process to hang...",
// Concrete fix (required, min 20 chars)
"solution": "Use `python -m pytest --no-header -p no:faulthandler`",
// Exact tool name match — fires on any use of these tools
"toolNames": ["Bash"],
// Regex patterns matched against the Bash command string
// Invalid regex is rejected at add time
"commandPatterns": ["\\bpytest\\b(?!.*(--no-header|-p no:faulthandler))"],
// "full" (default) or "executable" — executable strips quoted strings
// before matching commandPatterns, so guards don't fire on quoted JSON values
"commandMatchTarget": "executable",
// Regex patterns AND-gated against the command or file path
"modelPatterns": [],
// Glob patterns matched against Read/Edit/Write file paths
"pathPatterns": [],
// null = global (default). A project-ID string scopes to that project only
"scope": null,
// 1-10; higher priority wins budget conflicts
"priority": 8,
// 0.0-1.0; lessons below config.minConfidence excluded from manifest
"confidence": 0.95,
// Excludes the lesson from the manifest unless the named artifact is installed
"requires": null,
// Excludes the lesson from the manifest when the named artifact IS installed
"duplicatedBy": null,
// category:value taxonomy
"tags": ["lang:python", "tool:pytest", "severity:hang"],
// structured | heuristic | manual
"source": "manual",
// Session IDs where this mistake was observed
"sourceSessionIds": [],
// How many times this pattern was seen across all scans
"occurrenceCount": 0,
"sessionCount": 0,
"projectCount": 0,
"createdAt": "2026-08-01T00:00:00Z",
"updatedAt": "2026-08-01T00:00:00Z",
// sha256 of (problem + solution + triggers) — used for dedup at scan time
"contentHash": "sha256:...",
}
For guard-type lessons, the rendered message supports a {command} placeholder substituted
with the actual command (truncated to 120 chars) at block time.
Field constraints
| Field | Constraint |
|---|---|
summary | >= 20 chars, no ... suffix, no template placeholders |
problem | >= 20 chars, no template placeholders |
solution | >= 20 chars, no template placeholders |
commandPatterns | Valid regex — invalid patterns rejected at add time, dropped (with a warning) at build time |
type = 'protocol' | Use sparingly — fires on every session startup with no per-call dedup |
confidence | Below minConfidence → excluded from the manifest at build time |
Lesson manifest
Generated by lessons build. This is what the injection hook reads at runtime.
{
"type": "lessons-learned-manifest",
"version": 1,
"generatedAt": "2026-08-01T00:00:00Z",
"config": {
"injectionBudgetBytes": 4096,
"maxLessonsPerInjection": 3,
"minConfidence": 0.5,
"minPriority": 1,
"compactionReinjectionThreshold": 7,
},
"lessons": {
"<ulid>": {
"slug": "pytest-tty-hanging-k9m2",
"type": "guard",
"priority": 8,
"toolNames": ["Bash"],
// Regex stored as {source, flags} for JSON-safe serialization
// Reconstructed with new RegExp(source, flags) at match time
"commandRegexSources": [{ "source": "\\bpytest\\b(?!...)", "flags": "" }],
"commandMatchTarget": "executable",
"pathRegexSources": [],
"modelRegexSources": [],
"tags": ["lang:python", "tool:pytest", "severity:hang"],
"scope": null,
"message": "## REQUIRED: pytest flags...",
"summary": "pytest hangs in non-interactive envs due to TTY detection",
"problem": "...",
"solution": "...",
},
},
}
Exclusion criteria — lessons are excluded from the manifest if any of:
confidence < config.minConfidencepriority < config.minPriority- a
duplicatedByartifact is detected as installed - a
requiresartifact is NOT detected as installed status === 'disabled'
Regex serialization — commandRegexSources stores { source, flags } objects rather than regex strings. RegExp is not JSON-serializable. At match time, the hook reconstructs regex objects with new RegExp(source, flags).
Config
{
// Max bytes of additionalContext per tool call
"injectionBudgetBytes": 4096,
// Max lessons injected per tool call
"maxLessonsPerInjection": 3,
// Exclude lessons below this confidence from manifest
"minConfidence": 0.5,
// Exclude lessons below this priority from manifest
"minPriority": 1,
// Lessons above this priority re-inject after context compaction
"compactionReinjectionThreshold": 7,
"scanPaths": ["~/.claude/projects/"],
"autoScanIntervalHours": 24,
"maxCandidatesPerScan": 50,
"scoring": {
"multiSessionBonus": 2,
"multiProjectBonus": 1,
"hangTimeoutBonus": 1,
"userCorrectionBonus": 1,
"singleOccurrencePenalty": -1,
},
}
Candidate records
Candidates are ordinary rows in lessons.db with status='candidate' — there is no separate
candidate file. node scripts/lessons.mjs scan aggregate reads them and prints a ranked JSON view
to stdout; /lessons:review and promote --ids <id> act on the same rows.
{
"generatedAt": "2026-08-01T00:00:00Z",
"totalCandidates": 1,
"candidates": [
{
// 1-based position in THIS output only — not stored, not a valid handle for any command
"index": 1,
// the real, stable handle — use this with `promote --ids`
"id": "01JQSEED00000000000000001",
"slug": "git-stash-untracked-5x3q",
"tool": "Bash",
"confidence": 0.75,
"priority": 6,
"occurrenceCount": 3,
// Distinct sessions where this pattern was observed
"sessionCount": 2,
// Distinct projects where this pattern was observed
"projectCount": 1,
"problem": "...",
"solution": "...",
"tags": [],
"sourceSessionIds": ["..."],
"createdAt": "...",
},
],
}
Scope inference (informal, for review purposes):
projectCount >= 2→ likely a global lesson candidateprojectCount === 1→ likely a project-specific lesson candidate (/lessons:scopecan scope an already-active lesson after the fact)
To promote a candidate, use node scripts/lessons.mjs promote --ids <id> — scan promote <index>
has been removed.
Scan state
{
"files": {
"/abs/path/to/session.jsonl": 184320,
},
"lastFullScanAt": "2026-08-01T00:00:00Z",
}
Each file maps to the last byte offset read. The scanner resumes from this point on the next incremental scan. A --full flag resets all offsets to 0 for a complete re-scan.
Tag taxonomy
Tags follow category:value format:
| Category | Examples |
|---|---|
lang | python, typescript, javascript, go |
tool | pytest, git, npm, docker |
severity | hang, data-loss, silent-failure, error |
topic | testing, auth, networking, types, agents |
candidate | node-gotchas-skill — flagged for future skill aggregation |
See the full tag reference.