---
draft: false
title: agentsmesh lessons
description: Query and capture lessons from .agentsmesh/lessons/lessons.json — the universal recall/capture graph that any harness can read through one shell primitive.
---

import { Aside } from '@astrojs/starlight/components';

Query and capture lessons from `.agentsmesh/lessons/lessons.json` — the single source of truth for the recall/capture subsystem. Every harness (Claude, Cursor, Codex, plain shell) calls the same two commands.

The contract is delivered in two tiers. A minimal always-on **trigger** is injected into `.agentsmesh/rules/_root.md`, so it reaches every target through canonical rule generation (rules are native everywhere). The full operating manual — every subcommand below, the topic workflow, trigger flags, and the MCP fallback — is seeded as a `lessons` skill (`.agentsmesh/skills/lessons/SKILL.md`) and surfaced on demand on skill-capable targets. Targets without skills still get the trigger, so the binding contract is universal while the manual stays out of always-on context.

## Getting started

From an empty project to a working recall + capture loop:

```bash
# 1. Scaffold the subsystem (graph, config, skill, ritual, recall hook).
agentsmesh init --lessons
agentsmesh generate              # project the ritual into every target

# 2. Capture your first lesson after a failure (creates the topic on the fly).
agentsmesh lessons add "Run tsc --noEmit before committing type changes." \
  --topic build --new-topic --topic-summary "Build & typecheck rules." \
  --trigger-file "src/**/*.ts" --evidence commit:abc1234

# 3. Recall it before the next edit / command.
agentsmesh lessons query --file src/index.ts --cmd "git commit -m wip"

# 4. List what's there.
agentsmesh lessons topics
```

Run lessons commands from the **project root** (the directory holding `.agentsmesh`). Run from a subdirectory and the CLI warns it found no graph there — `cd` to the root. Mistyped a flag? The command errors and names the unknown flag rather than silently ignoring it (a typoed `--trigger-flie` would otherwise drop a trigger).

If you initialized agentsmesh **without** `--lessons`, the lessons commands still run but tell you the subsystem isn't wired: reads (`query`, `topics`, `journal`) print a one-line hint to run `agentsmesh init --lessons`, and `lessons add` still captures to the graph but warns that **recall isn't wired into your AI tools yet** (no hook, ritual, or skill) until you run `init --lessons` + `generate`. Activate the full subsystem and the hints disappear.

## Usage

```bash
agentsmesh lessons <subcommand> [args] [flags]
```

## Subcommands

| Subcommand | Purpose |
|------------|---------|
| `query` | Recall primitive — return active lessons whose triggers match the supplied file / command / keyword predicates. |
| `add` | Capture primitive — atomically add a new lesson, deduplicating triggers against the graph. |
| `topics` | List every topic with its summary. |
| `show <topic\|lesson-id>` | Render a topic's lessons, OR a single lesson by id — its rule, status, topics, and every trigger resolved to its pattern (the diagnosis view for an irrelevant recall). Read-only. |
| `deprecate <id>` | Mark a lesson `deprecated`. With `--superseded-by <id>`, mark it `superseded`. |
| `merge <loser-id> <keeper-id>` | Fold a duplicate lesson into its canonical twin: union the loser's triggers, topics, and evidence onto the keeper, then mark the loser `superseded`. Preserves recall reachability across topics. |
| `untrigger <lesson-id> <trigger-id>` | Detach one trigger from a lesson in place (e.g. drop a dead `LOW_SIGNAL_KEYWORD` keyword, then re-`add` a short one). Garbage-collects the trigger node when no lesson references it anymore. Refuses to remove the only trigger of an `active` lesson. |
| `strip-markers` | Remove dead legacy provenance markers (`See L123`, `(L69)`, `[L3]`, "(also relevant …)") from rule prose. `--dry-run` reports without writing. |
| `journal` | Render lessons chronologically (sorted by `createdAt` then id). |
| `validate` | Schema + integrity + trigger-liveness checks (dead `file_glob`s that match no file in the working tree, runner-anchored `command_pattern`s). Non-zero exit on errors; warnings do not affect exit code. |
| `stats` | Summarize the opt-in recall and capture telemetry logs: no-match rate, returned-token percentiles, cumulative recall cost vs. the whole-active-set preload baseline (break-even), keyword-only reachability gap, and a Capture block (total captures, blocked count, new vs. upsert split, trigger-kind breakdown, recall:capture ratio). `--json` for raw output. Requires `AGENTSMESH_LESSONS_TELEMETRY=1`. |
| `prune` | Curate the graph — trim over-cap lessons (drop the least-specific triggers), detach **dead `file_glob` triggers** (matching no on-disk file) from any lesson that keeps ≥1 other trigger, remove dead triggers, and GC orphan topics. Lessons whose *every* trigger is a dead glob are reported as unreachable, not stripped (that would strand them). **Dry-run by default**; `--apply` writes, `--cap <n>` overrides the per-lesson cap (default 8). |
| `import-md` | One-shot migrator from legacy `index.yaml` + `topics/*.md` + `journal.md`. |

## Recall ritual

Before any edit or shell command, agents call:

```bash
agentsmesh lessons query --file <path-about-to-edit> --cmd <command-about-to-run>
```

Omit a flag when not applicable. Add `--keyword <text>` for task-description matches. Default `--format plain` prints one rule per line — the cheapest shape for agents to paste back into context.

```bash
# Examples
agentsmesh lessons query --file src/cli/lessons.ts
agentsmesh lessons query --cmd "pnpm test:e2e"
agentsmesh lessons query --keyword "windows path normalization"
agentsmesh lessons query --file src/x.ts --format md
agentsmesh lessons query --file src/x.ts --format json
```

:::note[Recall precision]
Triggers may be shared across lessons in a topic, so a match produces a broad **candidate** set. A query therefore **ranks** candidates (BM25 over rule text fused with trigger specificity) and returns the **top 10 by default, bounded by a ~400-token budget** so mandatory recall stays lean — matching is "this lesson is a candidate", not "you must read all of these". A stderr notice reports how many matched; use `--top <n>`/`--max-tokens <n>` to widen, or `--all` to bypass both caps. `validate` emits a `HIGH_FANOUT_TRIGGERS` warning when triggers are over-shared; per-lesson trigger refinement is the remaining precision lever.
:::

## Capture ritual

Immediately after any failure, agents call:

```bash
agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <commit-sha|lesson-id>
```

Each `--trigger-*` value is **opaque** — pass the flag multiple times for multiple triggers; commas are kept verbatim (so regex/globs like `^foo{1,3}$` or `src/{a,b}/**` are safe). `--evidence` is comma-separable. The CLI dedupes triggers against the graph and assigns a stable lesson id.

```bash
# Examples
agentsmesh lessons add "Always normalize CLI display paths to forward slashes." \
  --topic windows-paths \
  --trigger-file "src/cli/**/*.ts" \
  --evidence commit:abc1234

agentsmesh lessons add "Treat the cache as advisory." \
  --topic perf \
  --new-topic --topic-summary "Performance-related rules." \
  --trigger-kw "cache,latency"
```

<Aside type="tip">
The graph at `.agentsmesh/lessons/lessons.json` is the single source of truth. Never edit it by hand. The CLI handles deduping, locking, atomic writes, and id assignment.
</Aside>

## Hook mode (deterministic recall)

The recall ritual asks the agent to **run** recall before each mutating action — an extra model turn every time, and only as reliable as the agent's compliance. On harnesses that support context-injecting tool-call hooks, recall is **deterministic** instead: a `PreToolUse` hook runs recall automatically and injects the matching lessons into the model's context **before** the action — **zero extra model turn, zero compliance dependence**.

**`agentsmesh init --lessons` wires this automatically.** It injects the recall hook under five events in `.agentsmesh/hooks.yaml`, so `generate` projects each to every hook-capable target that supports it (Claude Code, Cursor, Copilot, …); targets that can't represent an event drop it silently, and targets with no hook support fall back to the always-on lessons paragraph, whose ritual drives the same recall via the CLI / MCP `lessons_query`. The injected entries:

```yaml
PreToolUse:
  - matcher: Edit|Write|Bash
    type: command
    command: agentsmesh lessons hook
PostToolUse:
  - matcher: Edit|Write|Bash
    type: command
    command: agentsmesh lessons hook
UserPromptSubmit:
  - matcher: "*"
    type: command
    command: agentsmesh lessons hook
PostToolUseFailure:
  - matcher: "*"
    type: command
    command: agentsmesh lessons hook
SessionStart:
  - matcher: "*"
    type: command
    command: agentsmesh lessons hook
```

- **`PreToolUse`** guards the **first touch** — recall injects before the edit.
- **`PostToolUse`** covers later actions and harnesses that support only post-call injection.
- **`UserPromptSubmit`** recalls against the **task text itself**. The tool-call events only ever see a file path or command, so a `keyword` (conceptual / general) lesson can otherwise fire only when its concept happens to appear as a path/command token. `UserPromptSubmit` is the only event that carries the prompt, so it is where keyword lessons recall against actual **intent** — and where the universal always-on lessons ride onto every task.
- **`PostToolUseFailure`** fires when a tool call **fails** (`PostToolUse` is success-only) and injects an advisory **capture-decision nudge**, pre-filled with a ready-to-paste `--trigger-file`/`--trigger-cmd` for the failed file/command — once per session.
- **`SessionStart`** with `source: compact`/`clear` **resets recall dedup**. Dedup suppresses a lesson already delivered this session — but a long chat gets **summarized** and can drop an earlier injection from context, while dedup keeps suppressing it. Resetting on compaction re-injects those lessons on the next recall, so dedup tracks the *context* lifecycle, not the wall clock.

The last three are agentsmesh recall refinements; a hook-capable target that can't represent one simply misses that refinement (no warning — recall falls back to the other events).

(Scaffolded lessons before this was automatic? Re-run `agentsmesh init --lessons` — it adds the hooks idempotently — or paste the block above into your `hooks.yaml`.)

`agentsmesh lessons hook` reads the harness's hook payload from stdin, **branches on `hook_event_name`** (recall for tool-call / prompt events; the capture nudge for `PostToolUseFailure`), recalls lessons for the touched `file_path` / `notebook_path` / `command` or the submitted prompt, and emits the harness context-injection JSON (`hookSpecificOutput.additionalContext`). It uses the harness `session_id` for [dedup](#query) (namespaced per project), so a lesson is injected at most once per session.

It is **safe everywhere**: the command is harness-adaptive and a silent no-op (exit 0, no output) on any payload it doesn't recognize, so projecting the hook to a target whose hooks can't inject context (or to an unrecognized event) does nothing rather than breaking the run.

**Capture stays model-driven.** A hook can't author a rule or judge "this was a failure worth a lesson" — it can only run, not reason — so the agent still issues `lessons add` itself. The `PostToolUseFailure` nudge is **advisory only**: it removes the "I forgot to consider capturing" failure mode by reminding at the exact teachable moment, without ever authoring the lesson (single-party authorship stays the agent's).

### Non-hook targets — conceptual recall via CLI / MCP

Hook-capable tools recall `keyword`/conceptual lessons dynamically (via `UserPromptSubmit`). Targets with **no hook** rely on the always-on paragraph, which drives recall the agent runs itself: `--file`/`--cmd` before each mutation, **plus a task-start `--keyword "<the task's terms>" --always`** to surface conceptual rules no path or command names *and* the universal always-on lessons (no shell → the `lessons_query` MCP tool, `keyword` + `always` fields). That queries the whole graph, filtered to the task — complete and always live — rather than baking a static, cap-limited digest into the instruction file.

## Always-on lessons (`--scope always`)

Some standards apply to **every** task and are named in no prompt — a comment convention, "write the test first," a house style. Keyword recall can't surface them (the concept isn't in the request), and a broad `file_glob` would dilute triggered precision. Capture them as **always-on lessons** instead:

```bash
agentsmesh lessons add "Write comments per the repo style." --topic style --new-topic --topic-summary "Style." --scope always
```

An always-on lesson needs **no trigger** and is **excluded from triggered recall**. It is delivered on every task instead: the `UserPromptSubmit` hook injects it automatically on hook-capable tools, and `agentsmesh lessons query --always` (or `lessons_query` with `always: true`) returns it for CLI/MCP recall — no predicate required. It lives in the graph like any lesson (tracked, deduped, deprecatable), so the whole agent memory stays in one place. Keep the always-on set small — it is a standing context cost on every task; recall bounds it by a token budget.

## One-shot upgrade migration

If you're coming from a previous release that used `index.yaml` +
`topics/*.md` + `journal.md`, the first `lessons` subcommand auto-migrates:

```bash
$ agentsmesh lessons query --file src/x.ts
⚠ lessons.json was auto-migrated from index.yaml on first invocation.
…
```

Auto-migration **deletes the legacy files** after a successful import so the
project lands in a clean state. Run `agentsmesh lessons import-md` explicitly
if you prefer to migrate at a specific point in time. New projects skip this
entirely — `agentsmesh init --lessons` creates the graph directly, alongside
`.agentsmesh/lessons/config.json` with every tunable at its default
(`recallLimit`, `recallMaxTokens`, `autoPrune`) so they are discoverable and
editable. An existing config is never overwritten — your edits are preserved.

## Flag reference

### `query`

| Flag | Description |
|------|-------------|
| `--file <path>` | Project-relative path of the file about to be edited. Matched against `file_glob` triggers. |
| `--cmd <command>` | Shell command about to run. Matched against `command_pattern` triggers (regex). |
| `--keyword <text>` | Free-form task description. Matched against `keyword` triggers (case-insensitive substring). `keyword` triggers also match `--file`/`--cmd` on token boundaries, so conceptual lessons surface without an explicit `--keyword`. |
| `--always` | Prepend the universal **always-on lessons** (`--scope always`) — standards delivered on every task, excluded from triggered recall. Needs no other predicate; combine with `--keyword` at task start to pull both. |
| `--format plain\|md\|json` | Output shape. Default `plain` (one rule per line). |
| `--top <n>` | Keep only the top `n` relevance-ranked matches. Default 10. |
| `--all` | Return every match (disable both the limit and the token budget). |
| `--max-tokens <n>` | Cap results by cumulative estimated rule-token cost. **Approximate** — per-rule cost is estimated as `rule.length / 4`, not a real tokenizer. Defaults to ~400 when omitted. |
| `--session <id>` | Session correlator for recall **dedup**. Lessons already delivered earlier in the same session are suppressed, so each recall carries only what is new. Defaults to the `AGENTSMESH_SESSION_ID` environment variable; opt-in — with no id, recall is fully stateless (unchanged). The per-session set of delivered lesson ids lives in the OS temp dir, never the project. |
| `--no-dedup` | Force dedup off for this call even when a session id is set — return the full ranked set including already-seen lessons. |
| `--ids` | Prefix each `plain`/`md` line with the lesson id, so an irrelevant recall can be traced to `lessons show <id>` and retired with `lessons deprecate <id>`. Off by default to keep recall output paste-clean and token-lean (`--format json` always includes ids). |

Pass **at least one predicate** (or `--always`) — a query with none of `--file`/`--cmd`/`--keyword`/`--always` is rejected (exit 2). Always anchor recall to the concrete `--file` you're about to edit (and `--cmd` you're about to run); **keyword-only recall is the anti-pattern** — most lessons are keyed to a `file_glob`/`command_pattern` and silently won't surface, so the CLI prints a warning when you query keyword-only.

Results are **relevance-ranked** (BM25 over rule text fused with trigger specificity) and capped by default to the top 10 **and** a ~400-token budget, so mandatory recall stays lean; the single most-relevant result is always returned even if it alone exceeds the budget. A truncation notice on stderr reports how many matched. Pass `--all` (or a larger `--top`/`--max-tokens`) to see the rest.

**Session dedup.** Recall is deterministic, so the same `--file` returns the identical rules every time — N recalls touching one area re-deliver the same rules N times. Set `--session <id>` (or export `AGENTSMESH_SESSION_ID`) and lessons already delivered earlier in that session are suppressed *before* ranking, so the caps fill with what is **new**; a stderr note reports how many repeats were hidden. Dedup happens pre-rank so a fresh lesson is never crowded out by a seen one. It is opt-in: with no session id recall is fully stateless, exactly as before. `--no-dedup` overrides it for one call.

### `add`

| Flag | Description |
|------|-------------|
| `--rule "<text>"` | **Required.** Imperative rule that prevents recurrence. Must be ≤2000 characters — a rule is one sentence, not a pasted log; a longer one is rejected (`OVERSIZED_RULE`, exit 2). Also accepted positionally: `lessons add "<rule>" …`. |
| `--topic <id>` | **Required.** Topic id. Pass `--new-topic --topic-summary "..."` to create one. |
| `--trigger-file <glob>` | `file_glob` trigger. Opaque — repeat the flag for multiple (commas kept). |
| `--trigger-cmd <regex>` | `command_pattern` trigger. Opaque — repeat for multiple. Matched by a non-backtracking linear engine, so any ReDoS-shaped pattern (`(a+)+`, `a+a+`) is safe; only **backreferences** and **lookarounds** (which the engine can't run) are rejected at capture. |
| `--trigger-kw <text>` | `keyword` trigger. Opaque — repeat the flag for multiple (commas kept). |
| `--evidence <ref>` | Evidence reference (`commit:SHA`, `lesson:id`, …). Comma-separate for multiple. |
| `--rationale <text>` | One-line "why" behind the rule. |
| `--new-topic` | Allow creating a new topic if missing. Requires `--topic-summary`. |
| `--topic-summary "<text>"` | One-line summary when `--new-topic` creates a topic. |
| `--scope always` | Capture a **universal always-on lesson** (a standard that applies to every task). Needs no trigger and is delivered on every task instead of matched — see [Always-on lessons](#always-on-lessons---scope-always). |

**At least one EFFECTIVE trigger is required** (unless `--scope always`) — `add` errors (`UNRECALLABLE_LESSON`, exit 2) when every trigger on the resulting lesson is dead on the mandatory `--file`/`--cmd` recall path. A trigger is dead when it is a `keyword` whose needle loses all tokens to stopword filtering (e.g. a stopword-only keyword cannot fire on the mandatory --file/--cmd recall path), or a `command_pattern` rejected by the write barrier as invalid or ReDoS-shaped. A lesson with a mix of live and dead triggers is NOT rejected — the dead trigger surfaces as a non-blocking warning. Prefer a precise `--trigger-file` glob: it is the most reliable trigger, since it fires on the `--file` recall before every edit.

Beyond that hard requirement, `add` prints **non-blocking guardrail warnings** to stderr (capture still succeeds):

- `OVERSIZED_LESSON_TRIGGERS` — the lesson has too many triggers (cap 8).
- `BROAD_GLOB_TRIGGER` — a glob matches large swaths of the tree; prefer a path specific to the lesson.
- `KEYWORD_ONLY_LESSON` — every trigger is a keyword. These fire on `--file`/`--cmd` recall only when the keyword appears as a path/command token — less reliable than a precise glob.
- `DEAD_GLOB` — a glob matches no file in the working tree (likely a rename or typo); re-point it or the lesson is unreachable via that glob.
- `NEAR_DUPLICATE_LESSON` — the rule closely paraphrases an existing active lesson (token-Jaccard ≥ 0.6); consider updating the existing lesson instead.

Prefer a few specific triggers. See the [guardrails reference](../../reference/lessons/#capture-guardrails).

### `deprecate`

| Flag | Description |
|------|-------------|
| `--superseded-by <id>` | Replacement lesson id. Without it, the lesson is marked `deprecated`; with it, `superseded`. |

### `merge`

`agentsmesh lessons merge <loser-id> <keeper-id>` — both ids are positional and required. The keeper must be `active`; the loser must not already be `superseded`.

### `untrigger`

`agentsmesh lessons untrigger <lesson-id> <trigger-id>` — both ids are positional and required. Detaches the trigger from the lesson and, if no remaining lesson references it, deletes the trigger node (no `ORPHAN_TRIGGER` left behind). Refuses (exit 1) to remove the only trigger of an `active` lesson, since that would make it unreachable — `add` a replacement trigger first, then `untrigger` the old one. The graph is git-tracked, so the change is reviewable and revertible. This is the clean way to replace a `LOW_SIGNAL_KEYWORD` keyword without the rename/corpse cost of deprecate→re-add.

### `strip-markers`

| Flag | Description |
|------|-------------|
| `--dry-run` | Report which lessons would change without writing. |

### `prune`

| Flag | Description |
|------|-------------|
| `--apply` | Write the curation through the transactional path. **Without it, `prune` is a dry run** that prints the plan and changes nothing. |
| `--cap <n>` | Per-lesson trigger cap (positive integer; default 8). Over-cap active lessons keep their `n` most-specific triggers; the highest-fanout (least specific) ones are dropped first. |

Dead triggers (referenced by no `active` lesson) and orphan topics (referenced by no lesson at all) are removed regardless of `--cap` — `validate` only warns about these (`ORPHAN_TRIGGER` / `ORPHAN_TOPIC`); `prune` actually removes them. It also **detaches dead `file_glob` triggers** (the ones `validate` flags `DEAD_FILE_GLOB`) from any lesson that keeps another trigger, then GCs the now-orphaned glob — automating the rename-rot cleanup. A lesson whose every trigger is a dead glob is **reported as unreachable and left intact** (stripping its last trigger would strand it); re-point a trigger or `deprecate` it by hand. The graph is git-tracked, so an applied prune is reviewable in the diff and revertible. See the [curation reference](../../reference/lessons/#curation-prune).

### `stats`

| Flag | Description |
|------|-------------|
| `--json` | Emit the raw report object instead of the human summary. |

Recall runs before each edit and each state-changing command (pure-read commands and the recall query itself are exempt), so its **frequency** — not its per-call payload — is the real token cost. Set `AGENTSMESH_LESSONS_TELEMETRY=1` to record one append-only row per recall to `.agentsmesh/lessons/recall-log.jsonl` (field-presence booleans, match counts, returned-lesson ids, a `bypassed` flag, and an optional `AGENTSMESH_SESSION_ID` — never the file / command / keyword text). The same flag enables a symmetric **capture log** at `.agentsmesh/lessons/capture-log.jsonl` — one row per `agentsmesh lessons add` / `captureLesson` call, recording `isNewLesson`, `isNewTopic`, `newTriggerCount`, `triggerKinds` counts, `blocked` (true when the capture was rejected as `UNRECALLABLE_LESSON`), `warningCodes`, and optional `session` / `lessonId` — never the rule text. Both logs are **size-capped** — they self-truncate to the most recent records once they grow past the cap, so they never accumulate unbounded in a committed `.agentsmesh/`. `stats` then reports the no-match rate, returned-token percentiles, the **per-session preload break-even** (preload costs the whole active set *once per session*, so the comparison multiplies by the session count and excludes `--all` dumps), the **intra-session redundancy** rate (repeat-delivered rule-tokens — the dedup opportunity), and the keyword-only reachability gap. It also prints a **Capture block** (included under a `capture` key in `--json` output, shown even when only a capture log exists): total captures, blocked count, new-lesson vs. upsert split, new-topics count, warned count, trigger-kind breakdown, and a **recall:capture ratio**. It also prints an **Effectiveness block** (`effectiveness` key in `--json`) from the outcome log — total deliveries, the coarse **held rate** (deliveries with no recorded repeat on the same action — a weak upper bound, never presented as proof of prevention), the ineffective-lesson count, and failures observed, with a pointer to `lessons validate` for the actionable list. The logs are telemetry, not the canonical graph; `agentsmesh init --lessons` adds `.agentsmesh/lessons/recall-log.jsonl`, `.agentsmesh/lessons/capture-log.jsonl`, and `.agentsmesh/lessons/outcome-log.jsonl` to your project's `.gitignore` automatically, so opted-in logs never dirty your worktree. (Scaffolded lessons before this was automatic? Add those two lines to `.gitignore` yourself — it is git's ignore file, not `.agentsmesh/ignore`, which only controls what AI tools see.)

### `import-md`

| Flag | Description |
|------|-------------|
| `--merge` | Fold legacy lessons INTO an existing `lessons.json` (rules dedup by text, triggers content-address and dedup, topics union). The recovery path when a legacy `index.yaml` is stranded alongside a populated graph — no data loss. |
| `--force` | Overwrite an existing `lessons.json`. Default behavior (with neither `--merge` nor `--force`) is to refuse. |
| `--migrated-at <ISO date>` | Date stamped onto every imported lesson's `createdAt`. Defaults to today. |

## Diagnosing a recall

When a recall surfaces a rule that does not belong, you do not need to open `lessons.json`:

1. `agentsmesh lessons query --file <path> --ids` — re-run the recall with lesson ids attached (or use `--format json`).
2. `agentsmesh lessons show <lesson-id>` — inspect that lesson: its rule, status, topics, and **every trigger resolved to its pattern**, so you can see exactly which trigger fired.
3. Fix it in place — `agentsmesh lessons untrigger <lesson-id> <trigger-id>` to drop an over-broad trigger, or `agentsmesh lessons deprecate <lesson-id>` to retire a rule that is wrong or no longer true (a deprecated lesson stops being recalled immediately).

## Team workflow

`lessons.json` is a single git-tracked file, serialized deterministically (stable key order, trailing newline) so lesson changes show up as readable, reviewable diffs in a PR. Two caveats for teams:

- **Parallel captures on separate branches** would otherwise conflict at merge time (both edit the same JSON tables). Wire the bundled **union merge driver** and git resolves them automatically — each branch's new lessons/topics/triggers are merged by key, with a deprecated lesson winning a divergent edit. `agentsmesh init --lessons` already **commits the `.gitattributes` binding** for you:

  ```
  .agentsmesh/lessons/lessons.json merge=agentsmesh-lessons
  ```

  so the whole team inherits it once one person commits it. Each clone then enables the per-clone half **once** (git cannot auto-run this on clone) — `init --lessons` prints exactly these commands:

  ```bash
  git config merge.agentsmesh-lessons.name "agentsmesh lessons union"
  git config merge.agentsmesh-lessons.driver "agentsmesh lessons merge-driver %O %A %B"
  ```

  The driver writes nothing and exits non-zero if a side is unparseable or the merged graph fails validation, so git falls back to ordinary conflict markers rather than persisting a bad merge. `agentsmesh lessons validate` (run in CI via `agentsmesh lint`) is the backstop.
- **Worktrees / discarded branches**: the graph lives in the working tree, so a lesson captured in a worktree or branch that is later discarded is lost with it. Commit captures you want to keep.

## See also

- [Lessons reference](../../reference/lessons/) — graph schema, validation codes, and programmatic API.
- [agentsmesh init](../init/) — scaffold the lessons subsystem with `--lessons`.
