Ivan Tarasov

Ivan Tarasov · Senior Frontend Engineer

The iceberg of AI usage · Part 6 of 6

Agent memory & lessons

July 8, 2026 · 8 min read

  • ai
  • claude-code
  • multi-agent
  • llm-wiki

For the system to be complete, we need to somehow get the result of the agents' work (our tailored resumes, cover letters, recommendations) and give the agents the ability to learn from the results of their own work. Let's implement the long-term memory mechanism from the Theory of building multi-agent systems article, the "Contracts, memory, verification" section.

What does LLM Wiki have to do with it?

LLM Wiki is a pattern of keeping a personal knowledge base in which the LLM itself incrementally builds and maintains a structured, interconnected wiki of markdown files.

I recommend reading the concept in full.

Since the concept doesn't impose strict rules, we can interpret it in a custom way to fit our needs:

  • The agent must study the sources (our resume and the cover letter template).
  • The agent must study our growing candidate profile (skills and bullets from every job) to tailor the resume successfully.
  • The agent must put the tailored result in a specific place.
  • The agent must learn from its own mistakes and accumulate the user's specific preferences.

In short, we just need to write out our experience in full and gather all the keywords, so the agent can shuffle them for a specific vacancy to successfully pass the ATS filters.

Organization and structure

We need to work on the file structure again — let's extend it with new contracts.

hr-department/
├── .claude/
│    ├── agents/
│    │    └── ...
│    └── skills/
│        └── ...
├── Storage/
│    └── hr-vault/
│        ├── Sources/
│        │    ├── cover-letter.md
│        │    ├── cv-en.tex
│        │    ├── cv-ru.tex
│        │    └── profile.md
│        ├── Vacancies/
│        │   └── <dd.mm.yyyy>/
│        │       └── <slug>/
│        │           ├── <slug>.md
│        │           ├── resume.tex
│        │           ├── cover-letter.md
│        │           └── recommendations.md
│        ├── CLAUDE.md
│        ├── root.md
│        └── seen.json
├── CLAUDE.md
├── config.yaml
├── PIPELINE.md
└── VAULT.md

We've got a lot of new files — let's go through them one by one:

  • The internal CLAUDE.md — the internal contract, which explains how the storage itself is organized from the inside.
  • VAULT.md — the external contract for interacting with the storage.
  • Sources/ — the primary sources the agent studies and later adapts (the cover letter, the resume, the candidate profile).
  • Vacancies/ — the accumulated artifacts tailored to a specific vacancy (the reworked resume and cover letter), grouped by dates. (Maintained and filled in by the agent.)
  • root.md — the central node that keeps the list of all collected vacancies and builds links to them. (Maintained and filled in by the agent.)
  • seen.json — the list of already processed vacancies, so we don't waste tokens and time on duplicates. (Maintained and filled in by the agent.)

Why split into two CLAUDE.md files? The external CLAUDE.md describes the system's behavior (who does what and in what order), while the internal CLAUDE.md describes the storage's layout (how the data is arranged). When an agent moves on to working with the storage, the information about its detailed layout automatically lands in its context.

The internal CLAUDE.md
# HR Vault — Schema
 
This Obsidian vault stores **only** finalized application artifacts produced by the `hr-specialist`, the index files maintained by the team lead, and the user-owned ground-truth sources.
 
Pipeline scratch (scout reports, shortlist, verification notes, reviews) lives outside the vault in `DEPARTMENT_ROOT/.runtime/<run-id>/`.
 
## Layout
 
```
hr-vault/
├── CLAUDE.md                      # This file
├── root.md                        # Index of all vacancies (team-lead-owned)
├── seen.json                      # Cross-run dedup ledger (team-lead-owned)
├── Sources/                       # USER-OWNED ground truth — agents read, never write
│   ├── cv-ru.tex                  # Master resume (Russian), frozen LaTeX layout, 1 page
│   ├── cv-en.tex                  # Master resume (English), frozen LaTeX layout, 1 page
│   ├── cover-letter.md            # Voice/sample reference for cover letters
│   └── profile.md                 # Ground-truth facts: skills, dates, projects
├── Vacancies/
│   └── <dd.mm.yyyy>/              # Harvest date of a run
│       └── <dep>-<company>-<key-tech>/    # One vacancy (the slug dir)
│           ├── <slug>.md          # Entry node — metadata hub (hr-specialist)
│           ├── resume.md          # Tailored resume, LaTeX in a fenced block (hr-specialist)
│           ├── resume.tex         # Same content raw, compile-ready (hr-specialist)
│           ├── cover-letter.md    # Tailored cover letter (hr-specialist)
│           └── recommendations.md # Screening psychology guide (hr-specialist)
└── Lessons/
    ├── lessons.md                 # Curated lesson index (team-lead-owned)
    └── L<N>-<slug>.md             # One file per lesson (team-lead-owned)
```
 
## Page types
 
| Type | Owner | Path |
|------|-------|------|
| `vault-root` | Team lead | `root.md` |
| `vacancy` | hr-specialist | `Vacancies/<date>/<slug>/<slug>.md` |
| (resume / cover / recommendations) | hr-specialist | siblings of the entry node |
| `lessons-index` | Team lead | `Lessons/lessons.md` |
| `lesson` | Team lead | `Lessons/L<N>-<slug>.md` |
| (ground truth) | **User** | `Sources/*` — agents treat as read-only |
 
Full frontmatter contracts live in `DEPARTMENT_ROOT/VAULT.md`. Read that file (top through the writing rules) before writing anything to this vault.
 
## Wikilinks
 
- Inside a vacancy folder, siblings link short-form: `[[resume]]`, `[[cover-letter]]`, `[[recommendations]]`.
- From `root.md` to an entry node: `[[Vacancies/<dd.mm.yyyy>/<slug>/<slug>|<slug>]]`.
- From the lesson index to a lesson: `[[Lessons/L<N>-<slug>|L<N>]]`.
 
## Language
 
Entry-node prose and `recommendations.md`: **Russian**. `resume.md`/`resume.tex` and `cover-letter.md`: the vacancy's language. Frontmatter keys and enum values: English. URLs, emails, handles, brand names: verbatim.
 
## Ownership invariants
 
- The `hr-specialist` writes vacancy folders — nothing else.
- The team lead writes `root.md`, `seen.json`, `Lessons/`, and `status:` updates in entry nodes — nothing else.
- The user owns `Sources/` and advances vacancy `status` (directly or by asking the team lead).
- Nobody deletes a vacancy folder; an irrelevant vacancy is `status: withdrawn`.
VAULT.md
# Vault — HR Department Contract
 
HR-specific vault schema. Self-contained — the hr-vault is independent of any other department's vault.
 
> [!info] Readers
> **Agents** read from the top through the writing rules and stop at `## Team Lead Only`. That's the full writing contract.
> **Team lead** reads the whole file — adds index-file formats, the dedup ledger, and ownership rules on top.
 
## Vault scope
 
The vault stores **only** finalized, user-facing application artifacts. Internal pipeline scratch (scout reports, shortlist, verification notes, reviews) lives in `DEPARTMENT_ROOT/.runtime/<run-id>/` and never enters the vault.
 
## Your artifact
 
| Agent | Type | Path pattern |
|-------|------|--------------|
| `hr-specialist` | `vacancy` (entry node) | `Vacancies/<dd.mm.yyyy>/<slug>/<slug>.md` |
| `hr-specialist` | tailored resume | `Vacancies/<dd.mm.yyyy>/<slug>/resume.md` (+ raw `resume.tex`) |
| `hr-specialist` | cover letter | `Vacancies/<dd.mm.yyyy>/<slug>/cover-letter.md` |
| `hr-specialist` | screening guide | `Vacancies/<dd.mm.yyyy>/<slug>/recommendations.md` |
 
Other agents (`scout`, `curator`, `verifier`, `reviewer`) **do not write to the vault.** They write to `.runtime/<run-id>/` paths declared in their spawn prompt. The reviewer reads vacancy folders but never modifies them.
 
The exact `VAC_DIR` comes from the spawn prompt. Do not derive it. `mkdir -p` it before writing.
 
## Slug — the vacancy identity
 
`<dep>-<company>-<key-tech>`, kebab-case, ASCII-lowercased.
 
- `dep` — sphere/track: `fe`, `be`, `fs` (fullstack), `ai`, `ml`, `bc` (blockchain), `mob`, `devops`, `qa`, `data`, … The canonical list lives in the `slug-conventions` skill; extend there, not ad hoc.
- `company` — company short name, transliterated, no spaces (`sber`, `yandex`, `tinkoff`, `revolut`).
- `key-tech` — the single root technology of the role (`react`, `vue`, `node`, `go`, `solidity`, `python`).
- Example: `fe-sber-react`.
- **Collision guard:** if the slug dir already exists in today's date folder, append `-2`, `-3`, …
 
The vacancy lives at `Vacancies/<dd.mm.yyyy>/<slug>/` where the date is the run's harvest date in `dd.mm.yyyy`.
 
## The four artifacts per vacancy
 
| File | Producer | Purpose |
|------|----------|---------|
| `<slug>.md` | hr-specialist | **Entry node** — metadata hub + semantic summary + wikilinks to the three siblings |
| `resume.md` | hr-specialist | Tailored resume: the **LaTeX source**, layout frozen, content adapted to the vacancy |
| `cover-letter.md` | hr-specialist | Tailored cover letter in the vacancy's language |
| `recommendations.md` | hr-specialist | Screening psychology / vibe guide |
 
One vacancy = one slug dir = these four files (plus the optional raw `resume.tex`). The hr-specialist is the **only** agent that writes vacancy artifacts. The team lead is the **only** writer of `root.md`, `seen.json`, the lesson index — and may update `status:` in entry nodes on user request.
 
## Entry node — `<slug>.md`
 
### Frontmatter
 
```yaml
---
type: vacancy
slug: <dep>-<company>-<key-tech>
run: <run-id>
date: dd.mm.yyyy                 # harvest date
posted: dd.mm.yyyy               # when the vacancy was published (null if undeterminable)
last_verified: dd.mm.yyyy        # verifier confirmed the listing live + open on this date
dep: fe
company: <Company>
key_tech: react
direction: Frontend
track: ai | web3 | platform-dx | design-system | fintech | realtime | perf | testing | ui | null
                                 # the role's angle within dep — detected by the vacancy-semantics skill;
                                 # drives track adaptation in resume-tailoring. null = generic role
stack: [React, TypeScript, Redux Toolkit, Vite]
seniority: middle | senior
employment: remote | hybrid | onsite
location: <city / "remote">
comp: "250–350k ₽"              # or null
currency: RUB | USD | EUR | null
link: <canonical vacancy URL>
source: <source name from config, e.g. getmatch>
hr_contact:                      # whatever was discoverable; null fields allowed
  name: <name | null>
  email: <email | null>
  telegram: <@handle | null>
  phone: <phone | null>
vibe: formal | corporate | casual | startup | mission-driven | hardcore-eng
fit_score: 0-100                 # from the curator
status: created                  # enum below — the user advances this over time
lang: ru | en                    # language the resume/cover were written in
related: ["[[resume]]", "[[cover-letter]]", "[[recommendations]]"]
---
```
 
Every metadata value comes from the verified vacancy record in the spawn prompt. Unknown fields are `null` — never guessed.
 
### Status enum
 
A lightweight application CRM the user advances by hand (the team lead may set it on request):
 
```
created → applied → screening → test-task → interview → offer
```
 
Terminal branches at any point: `rejected`, `on-hold`, `withdrawn`, `dropped`.
 
- `withdrawn` — never pursued (curator/verifier dropped it: dead, stale, off-target); used in `seen.json` for non-delivered listings.
- `dropped` — the user deliberately abandons a vacancy they *were* pursuing (e.g. lost interest after applying, comp/terms turned out wrong).
 
### Body (Russian prose)
 
1. **Краткое резюме роли** — 2–4 предложения: что за роль, что за компания, что за продукт/домен.
2. **Требования** — явные (из текста) и неявные (выведенные семантическим анализом), маркированным списком.
3. **Почему такой fit_score** — 1–3 предложения, привязанные к профилю пользователя.
4. **Натяжки** — the stretch ledger (`truthful-tailoring`). Every keyword added to a Skills/Stack line that the ground truth does NOT directly support, one row each: `ключевое слово` · `где` (строка скиллов / стек какой роли) · `под требование вакансии` · `мост из` (реальный смежный навык) · `цвет` (🟢/🟡) · для 🟡 — `подготовиться` note. If nothing was stretched: «Нет натяжек — все ключевые слова из ground truth.» This section is mandatory whenever any stretch was made; an unlogged stretch is a truthfulness breach. Stretches live only in the resume's skills/stack lines — never in experience, metrics, or the summary.
5. **Как податься** — concrete, vacancy-specific application steps to maximize success. Built from the source's `apply:` path (config) + the HR contact + the screening read: the exact route (DM which @handle / email whom / which form), what to attach (resume + cover), how to address the recruiter (named greeting only if the contact is known), timing/urgency (e.g. expiring listing), and any first-step quirk (AI-recruiter screen, test task). When `direct_apply: true` and no direct route is discoverable, say so plainly and flag it (the team lead may drop the vacancy). Keep it actionable — steps the user can execute now.
6. **Артефакты** — wikilinks: `[[resume]]`, `[[cover-letter]]`, `[[recommendations]]`.
 
## `resume.md` — the frozen-layout LaTeX resume
 
- Body is a single fenced ` ```latex ` block containing the **full .tex**.
- **Layout is frozen.** Copy the preamble, document class, packages, spacing, section macros, and visual structure of the **matching-language master** (`Sources/cv-ru.tex` for Russian vacancies, `Sources/cv-en.tex` for English — `MASTER_RESUME` in the spawn prompt) **verbatim**. Only the *content fields* change: professional summary, skills ordering/emphasis, experience bullet wording, project selection — adapted to the vacancy's semantics. See the `latex-resume` skill.
- The specialist **must not** invent experience, technologies, employers, dates, or metrics. Ground truth = `Sources/cv-ru.tex` + `cv-en.tex` + `Sources/profile.md`. Tailoring = re-emphasize, reorder, rephrase to mirror the JD's vocabulary — never fabricate. See the `truthful-tailoring` skill.
- Also write a sibling raw `resume.tex` (same content, no fence) so the user can compile directly.
 
## `cover-letter.md`
 
- In the **vacancy's language** (or the `profile.language` override).
- Voice modeled on `Sources/cover-letter.md` — same register, comparable length.
- References the specific role, company, and stack — never a generic template.
- No fabricated claims; every assertion traces to the ground truth.
 
## `recommendations.md`
 
- **In Russian.** The psychological/vibe guide — the operationalization of "determine the SEMANTICS".
- From the detected `vibe`, advise: the register/tone to use at screening (formal vs. relaxed), what the company seems to value, what to emphasize and de-emphasize, likely screening questions, salary-conversation posture, and red/green flags.
- **Tie every recommendation to a concrete signal in the vacancy text** — a quoted phrase, a structural choice, a perk framing. No horoscopes.
- If the JD wants something the ground truth lacks, this file carries the honest gap note: «они хотят Kafka; у тебя RabbitMQ — будь готов объяснить перенос опыта».
 
## Writing rules (the agent-facing contract)
 
1. **Language.** `recommendations.md` and the entry-node prose: **Russian**. `resume.md` + `cover-letter.md`: the **vacancy's language** (`LANG_POLICY = auto`), or the `ru`/`en` override. Frontmatter keys + enum values stay English (machine-readable). URLs, emails, @handles, brand names verbatim.
2. **Tailoring integrity — two zones** (`truthful-tailoring`). The **red zone** — experience, employers, dates, titles, metrics, projects, seniority, plus the summary and cover letter — is never fabricated: a fact not in the ground truth does not go in. The **stretch zone** — the resume's Skills/Stack lines only — may add adjacent JD keywords per `STRETCH_LEVEL`, each logged in the entry node's «Натяжки» ledger and never leaking into a bullet/metric/summary. Unknown metadata fields are `null`, not guessed.
3. Write only to the paths in your spawn prompt; `mkdir -p` the parent first.
4. One vacancy = one slug dir = four files (+ optional `resume.tex`). Do not write index files, `seen.json`, lessons, or anything outside your `VAC_DIR`.
5. Wikilinks between siblings are short-form (`[[resume]]`) — they live in the same folder.
 
---
 
## Team Lead Only
 
> [!warning] Agents stop reading here.
> Below is team-lead reference. Agents must not write any of the files described in this section.
 
### Vault layout
 
```
hr-vault/                              # VAULT_ROOT
├── CLAUDE.md                          # global vault schema
├── root.md                            # index of ALL vacancies (team-lead owned)
├── seen.json                          # cross-run dedup ledger (team-lead owned)
├── Sources/
│   ├── cv-ru.tex                      # MASTER resume (Russian) — fixed LaTeX layout, 1 page
│   ├── cv-en.tex                      # MASTER resume (English) — fixed LaTeX layout, 1 page
│   ├── cover-letter.md                # voice/sample reference for cover letters
│   └── profile.md                     # GROUND TRUTH facts (skills, dates, projects)
├── Vacancies/
│   └── <dd.mm.yyyy>/<slug>/           # one vacancy — four files (+ resume.tex)
└── Lessons/
    ├── lessons.md                     # curated index (team-lead owned)
    └── L<N>-<slug>.md                 # one file per lesson
```
 
### Index ownership
 
Team lead owns and is the **only** writer for:
- `root.md`
- `seen.json`
- `Lessons/lessons.md` and every `Lessons/L<N>-*.md`
- `status:` field updates in entry nodes (on user request — touch nothing else in the file)
 
### `root.md` format
 
```markdown
---
type: vault-root
updated: dd.mm.yyyy
---
# HR Vault — вакансии
 
## Recent run
- [[Vacancies/<dd.mm.yyyy>/<slug>/<slug>|<slug>]] — <одна строка: роль @ компания, fit <score>> — <dd.mm.yyyy>
- ...
 
## By status
### applied
- [[...|<slug>]]
### created
- ...
(omit empty statuses)
 
## By dep
### fe
- [[...|<slug>]]
### be
- ...
```
 
Regenerable: every line derives from entry-node frontmatter.
 
### Cross-run dedup ledger — `seen.json`
 
A single JSON file at `VAULT_ROOT/seen.json`, committed to git. It is the department's "already processed" memory across runs, so a recurring job hunt never re-tailors the same listing.
 
```json
{
  "<canonical-vacancy-url>": { "slug": "fe-sber-react", "run": "<run-id>", "date": "dd.mm.yyyy", "status": "applied", "verdict": "live" }
}
```
 
- **Key** = canonical vacancy URL — tracking params stripped, host lowercased (the `vacancy-extraction` skill defines canonicalization). The stable identity across sources and runs.
- The **curator** reads `seen.json` and drops any candidate already present, unless `config.search.reprocess_seen: true`. A vacancy the user later marks `rejected`/`withdrawn` stays in the ledger and stays skipped.
- The **team lead** appends at Step 4 — every vacancy touched this run, including those the verifier dropped (`verdict: closed|dead|stale` — so dead listings aren't re-fetched next run). It is the only writer.
- When the user advances a vacancy's `status`, mirror it into the matching ledger record — keep the entry node and the ledger in sync, reconciling after any manual status edit.
- If the file is missing on the first run, treat it as `{}` and create it.
 
### Lessons
 
Full contract in `DEPARTMENT_ROOT/LESSONS.md`. Summary: one file per lesson under `Lessons/`, curated index `lessons.md`, team lead is the only writer, agents receive explicit `LESSONS = [...]` path lists per spawn.
A graph of the storage layout
A graph of the storage layout

The learning mechanism

Suppose we've run the pipeline, the agents performed flawlessly, all the artifacts are intact and sitting in the right places, but... it's still not it? What if I don't like the content the agent writes? What if it's just not me?

For that I developed a dedicated solution to this kind of problem, based on storing the user's remarks about the agent's work. This mechanism is called "Lessons".

Lessons is a catalog of reusable rules, extracted from mistakes and the user's preferences, which the team lead adds to an agent's prompt so the system doesn't repeat the same mistake twice.

Why is this needed? An LLM is non-deterministic, and every new run starts from a clean slate. Today you corrected the agent, and tomorrow your correction is no longer in its context. For the correction not to get lost, it needs to be stored somewhere and handed back at the right moment.

How an agent works without learning
How an agent works without learning

My systems usually combine two sources of lesson accumulation, but we'll give definitions closer to our example:

  • A critical reviewer finding — for example, it caught a made-up skill or a metric pulled out of thin air, and the team lead turns the remark into a rule. The "Evaluator-optimizer" pattern from the Theory of building multi-agent systems article.
  • A user remark — for example, you corrected the agent by hand ("don't show the salary if there isn't one", "that's not my tone"), and the team lead records the correction verbatim.

Each lesson is a separate markdown file. The metadata holds the area and the trigger condition; the lesson body contains the rule itself, the reason it appeared, and how to correctly apply it in the future.

Let's look at the modified file system:

hr-department/
├── .claude/
│    ├── agents/
│    │    └── ...
│    └── skills/
│        └── ...
├── Storage/
│    └── hr-vault/
│        ├── Lessons/
│        │    ├── L<n>-<slug>.md
│        │    └── lessons.md
│        ├── Sources/
│        │    ├── cover-letter.md
│        │    ├── cv-en.tex
│        │    ├── cv-ru.tex
│        │    └── profile.md
│        ├── Vacancies/
│        │   └── <dd.mm.yyyy>/
│        │       └── <slug>/
│        │           ├── <slug>.md
│        │           ├── resume.tex
│        │           ├── cover-letter.md
│        │           └── recommendations.md
│        ├── CLAUDE.md
│        ├── root.md
│        └── seen.json
├── CLAUDE.md
├── config.yaml
├── LESSONS.md
├── PIPELINE.md
└── VAULT.md

Here:

  • LESSONS.md — the key description of the lessons mechanism, which defines the required structure and application areas (read only by the team lead).
  • Lessons/ — the directory inside the storage that is exactly what's responsible for accumulating lessons. The lessons.md file serves as the entry point with a description of and links to all the contents (similar to root.md). (Maintained and filled in by the agent.)
LESSONS.md
# Lesson Catalog Contract
 
How lessons are stored, indexed, selected, and consumed in the HR department vault. This is the department's **single, unified memory** — real mistakes and corrections become curated, per-file lessons that are selected per-spawn and fed back into future runs. There is no separate raw-memory store; Lessons *are* the memory.
 
> [!info] Readers
> **Team lead** reads this whole file — owns lesson creation, selection, and indexing.
> **Agents** do NOT need to read this file. They read individual lesson files supplied via the `LESSONS = [...]` list in their spawn prompt.
 
## Why per-file
 
Storage is **one file per lesson**. The agents never load the whole catalog. The team lead picks the relevant subset per spawn and hands the agent an explicit list of absolute paths.
 
This keeps spawn prompts bounded (~30 lessons max) regardless of catalog size, lets us mark lessons `superseded`/`deprecated` without losing history, and makes dedup grep-able by `area`/`tags`.
 
## Filesystem layout
 
```
VAULT_ROOT/Lessons/
  lessons.md                # short curated index — by-area + by-run
  L1-<short-slug>.md        # one file per lesson (lesson_id is per-vault)
  L2-<short-slug>.md
  L42-<short-slug>.md
  ...
```
 
## Lesson file frontmatter
 
```yaml
---
type: lesson
lesson_id: L<N>                                  # per-vault, monotonic, never reused, no zero-pad
title: <short human title>                       # one line; matches body H1
area: <one of the area enum>                     # exactly one — primary axis for selection
tags: [<freeform kebab tags>]                    # secondary axis; 0–5 tags
status: active | superseded | deprecated
supersedes: ["[[L<N>-<slug>|L<N>]]"]             # optional; lessons this one replaces
source_run: <run-id>                             # run that originated the lesson (or "user" for inline feedback)
trigger: <one line: when this lesson applies>    # free text; helps the team lead filter
created: YYYY-MM-DD
updated: YYYY-MM-DD
---
```
 
### Area enum
 
Pick exactly one. The set is deliberately small so filtering is sharp.
 
| Area | Use for |
|------|---------|
| `user-pref` | Verbatim user preference / explicit instruction |
| `sourcing` | Source access patterns, scouting tactics, gated-source handling |
| `semantics` | Reading vacancy semantics — explicit/implicit requirements, vibe detection |
| `resume` | Resume tailoring content decisions (emphasis, ordering, wording) |
| `latex` | LaTeX layout preservation, compile safety, escaping |
| `cover-letter` | Cover letter voice, structure, length, register |
| `screening` | Screening-psychology advice quality, recommendations grounding |
| `truthfulness` | Fabrication incidents and their prevention |
| `slug` | Slug construction, dep/key-tech choices, transliteration |
| `fit-scoring` | Fit-score rubric calibration, filter application |
| `vault` | Vault structure, frontmatter, wikilinks, index rules |
| `process` | Workflow, agent coordination, pipeline order, review process |
| `scope` | Run scope decisions, N targets, in/out boundaries |
 
### Status semantics
 
- `active` — current rule; eligible for selection.
- `superseded` — replaced by another (the **new** lesson sets `supersedes: [[old]]`; the old lesson sets `status: superseded`, keeps its file, and is never selected).
- `deprecated` — no longer applies (source died, user's stack changed, requirement removed). Kept for history; never selected.
 
The team lead filters out non-`active` lessons during selection.
 
## Lesson file body
 
```markdown
# <Title — same as frontmatter `title`>
 
**Rule:** <one-sentence rule, actionable>
 
**Why:** <reason — past incident, reviewer finding, or user instruction>
 
**How to apply:** <when/where this kicks in; bullets allowed if multi-step>
 
> [!example] (optional)
> <quote, slug, JD phrase, or LaTeX snippet — only if it materially helps>
```
 
Keep each lesson under ~30 lines. If a lesson grows past that, split it.
 
## ID allocation
 
`lesson_id` is per-vault, monotonic, **no zero-pad**: `L1`, `L2`, `L42`, `L1337`. IDs are never reused. Deleted lessons leave a gap.
 
```bash
# next ID
ls VAULT_ROOT/Lessons/ \
  | grep -oE '^L[0-9]+' \
  | sed 's/^L//' \
  | sort -n \
  | tail -1
# → 42 → next is L43
```
 
Sort numerically (`sort -n`) — without it `L10` would beat `L9` lexically.
 
## Filename
 
`L<N>-<short-slug>.md` where `<short-slug>` is a 2–6 word kebab-case derivation of the title. The filename is stable: do NOT rename when the title changes — update only `title` in frontmatter. Wikilinks survive.
 
## Index file `lessons.md`
 
The team lead's curated, human-readable index. It is short by design — agents do NOT read it. The team lead reads it at Step 0 to pick relevant lessons per spawn.
 
```markdown
---
type: lessons-index
updated: YYYY-MM-DD
---
# Lessons — HR Department
 
> [!info] How this works
> One file per lesson in this folder. The team lead picks relevant lessons per agent spawn (lessons whose `area`/`tags` match the run and the role) and passes them as an explicit `LESSONS = [...]` list in the spawn prompt. Agents read only the lessons they receive.
 
## By area
 
### truthfulness
- [[Lessons/L3-<slug>|L3]] — short title.
- ...
 
### user-pref
- ...
 
(repeat per area that has any lessons; omit areas with zero lessons)
 
## By run
 
### <run-id>
- L1, L2, L3
 
## Superseded / Deprecated
Lessons with `status: superseded | deprecated`. Kept for history; not selected.
- [[Lessons/L10-<slug>|L10]] — superseded by L42.
```
 
## Selection algorithm
 
The team lead runs this at Step 0 (and may narrow per spawn). Output is the `LESSONS = [...]` list embedded in the spawn prompt.
 
1. **Read the index** at `VAULT_ROOT/Lessons/lessons.md`.
2. **Add area-relevant lessons.** Map the spawn's role to areas — scout: `sourcing`, `slug`, `scope`; curator: `fit-scoring`, `slug`, `scope`; verifier: `sourcing`, `process`; hr-specialist: `semantics`, `resume`, `latex`, `cover-letter`, `screening`, `truthfulness`, `vault`; reviewer: `truthfulness`, `semantics`, `latex`, `process`. **Always include every `user-pref` lesson** — they are department-wide rules.
3. **Add run-relevant lessons.** If this run repeats an earlier query, or touches the same sources/companies as a prior run — include lessons listed under that run's `## By run` block.
4. **Filter out non-active.** Skip any lesson whose frontmatter `status``active`.
5. **Cap at ~30 per spawn.** If more qualify, drop the least specific (broad `process` rules) before the specific ones (a `latex` lesson for an hr-specialist spawn).
6. **Resolve to absolute paths.** Replace each wikilink with `VAULT_ROOT/Lessons/L<N>-<slug>.md` and emit the list.
 
If the index does not exist yet (first run), `LESSONS = []`.
 
## Lesson Quality Rules
 
- **Source.** Each lesson originates from one of: a **blocking reviewer finding**, a **re-iteration** caused by a plan/quality deviation, or an **explicit user correction** («ты придумал навык, которого у меня нет», «не тот тон для этой компании», «неправильный slug», «не показывай зарплату, если null»). Do not record nits.
- **One rule per file.** If a finding implies two unrelated rules, write two lessons.
- **Dedup before adding.** Before creating a new lesson, grep `Lessons/` for an overlapping rule (`area:` + keyword in `title`/`trigger`). If found: extend the existing lesson (bump `updated:`, append to `How to apply`) instead of duplicating. If the new rule replaces an old one outright, mark the old `status: superseded` and set `supersedes: ["[[old]]"]` on the new lesson.
- **Cite, don't paraphrase, on `user-pref`.** Quote the user's instruction verbatim in the body.
- **Density.** Each lesson is under ~30 lines. Agents read these under a spawn-prompt budget.
 
## Write-time procedures
 
### Adding a lesson from a reviewer finding (Step 4.3 of `PIPELINE.md`)
 
1. `LESSONS_INDEX = VAULT_ROOT/Lessons/lessons.md`
2. `LESSONS_DIR   = VAULT_ROOT/Lessons/`
3. Allocate the next ID per the **ID allocation** rule above.
4. Dedup. If an existing lesson covers it, extend or supersede instead.
5. Write `LESSONS_DIR/L<N>-<slug>.md` per the frontmatter and body schemas above.
6. Update `LESSONS_INDEX`:
   - Add the lesson under `## By area / <area>` (one bullet).
   - Add it under `## By run / <run-id>` (create the run block if missing).
   - Bump `updated:` in the index frontmatter.
 
### Adding a lesson from inline user feedback
 
Same as above, but `area: user-pref` (unless the correction is clearly domain-specific — e.g. a slug correction is `area: slug`), `source_run: user`. Quote the user verbatim in the body. Signal to the user: «Записал как L<N>».
 
This closes the loop: mistakes → lessons → selected into the next run's spawns → fewer repeats.
Extracting lessons
Extracting lessons

Nice in theory, but you'll devour your entire context in a second with a dump like that.

The thing is, the team lead filters lessons by application area to match them to a specific role and hand them over as a list at spawn time. This way, the hr-specialist gets lessons about resumes and truthfulness, the scout — about sources. An agent reads only what it was given and doesn't burn millions of tokens.

Lessons applied successfully
Lessons applied successfully

You have no complaints? Well, I do... (Problems with my approach)

The time has come to critique my own approach. I'll try to single out the main problem areas and talk about them in detail.

Painful portability

In the practical example we walked through a fairly large setup — now imagine having to reassemble one like it from scratch for every task. Of course, with experience this gets done much faster, but the problem still remains. The setup's aspects are too tightly coupled to each other, which gets in the way of quickly swapping out components.

The agent knows too much

The agents in this example have knowledge about working in the team baked into them — that's bad, because it's extra load on the context that could have been avoided. By the way, some multi-agent frameworks are built exactly on this, which I consider wrong by definition.

Why? Because it becomes too hard to shuffle agents between tasks. Suppose I have five different systems in which a scout appears in one way or another. Those will be five different agents, because each one has to be adjusted to its pipeline. Because of that, if I want to update the skills of one, I'll have to revisit all five.

Quality guarantees?

How exactly can I be certain the agent will do everything right? How can I guarantee quality?

The answer — you can't. Through strict contracts we can verify the structure of an agent's output, but we can't automatically verify the content. Of course, you can check an agent with another agent, but that won't give us 100% certainty.

This is more a problem of LLM non-determinism than of my approach, but it's clearly worth pointing out.

I want everything to be simple

Unfortunately, no one has yet invented a magic pill that will configure the right AI tools specifically for you and make them work exactly the way you need.

We can observe a clear imbalance between fields, where someone with a technical background can figure out the technology quickly, while someone else has never opened a text editor in their life.

I hope my practical example turns out useful for you, and that thanks to it you'll get the hang of multi-agent systems faster and be able to tackle tasks of any complexity.


Thanks for reading!