agentsmesh lessons
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:
# 1. Scaffold the subsystem (graph, config, skill, ritual, recall hook).agentsmesh init --lessonsagentsmesh 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 topicsRun 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
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_globs that match no file in the working tree, runner-anchored command_patterns). 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:
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.
# Examplesagentsmesh lessons query --file src/cli/lessons.tsagentsmesh lessons query --cmd "pnpm test:e2e"agentsmesh lessons query --keyword "windows path normalization"agentsmesh lessons query --file src/x.ts --format mdagentsmesh lessons query --file src/x.ts --format jsonCapture ritual
Immediately after any failure, agents call:
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.
# Examplesagentsmesh 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"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 PostToolUse hook runs recall automatically and injects the matching lessons into the model’s context for its next action — zero extra model turn, zero compliance dependence.
agentsmesh init --lessons wires this automatically. It injects a PostToolUse recall hook into .agentsmesh/hooks.yaml, so generate projects it to every hook-capable target (Claude Code, Cursor, Copilot, …). Targets with no hook support simply keep the always-on lessons paragraph in their root instruction as the universal fallback. The injected entry:
PostToolUse: - matcher: Edit|Write|Bash type: command command: agentsmesh lessons hook(Scaffolded lessons before this was automatic? Re-run agentsmesh init --lessons — it adds the hook idempotently — or paste the block above into your hooks.yaml.)
agentsmesh lessons hook reads the harness’s PostToolUse payload from stdin, recalls lessons for the touched file_path / command, and emits the harness context-injection JSON (hookSpecificOutput.additionalContext). It uses the harness session_id for dedup, so a lesson is injected at most once per session even as you re-touch the same file.
It is reactive: the first touch of a file is unguarded; every later action is covered — which fits the way agents revisit files. And 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 a non-hook tool call) does nothing rather than breaking the run.
Why PostToolUse and not PreToolUse? Pre-tool hooks can only gate an action (allow/deny/ask) — they cannot inject text into the model’s context — so a pre-action recall could only surface lessons by blocking the tool with the rules in a denial reason, which forces a permission interrupt and a retry on every action. PostToolUse is the only event that injects context (additionalContext), hence the reactive design. Capture stays model-driven for the same reason: a hook can’t judge “this was a failure worth a lesson” or author the rule — it can only run, not reason — so the agent still issues lessons add itself.
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:
$ 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. |
--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 — a query with no --file/--cmd/--keyword 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. |
At least one EFFECTIVE trigger is required — 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/--cmdrecall 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.
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.
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. The logs are telemetry, not the canonical graph; agentsmesh init --lessons adds both .agentsmesh/lessons/recall-log.jsonl and .agentsmesh/lessons/capture-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:
agentsmesh lessons query --file <path> --ids— re-run the recall with lesson ids attached (or use--format json).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.- Fix it in place —
agentsmesh lessons untrigger <lesson-id> <trigger-id>to drop an over-broad trigger, oragentsmesh 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. Set it up once per clone:
Terminal window git config merge.agentsmesh-lessons.name "agentsmesh lessons union"git config merge.agentsmesh-lessons.driver "agentsmesh lessons merge-driver %O %A %B"and commit a
.gitattributesentry so the driver is used for the graph:.agentsmesh/lessons/lessons.json merge=agentsmesh-lessonsThe 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 viaagentsmesh 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 — graph schema, validation codes, and programmatic API.
- agentsmesh init — scaffold the lessons subsystem with
--lessons.