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 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:
PreToolUse: - matcher: Edit|Write|Bash type: command command: agentsmesh lessons hookPostToolUse: - matcher: Edit|Write|Bash type: command command: agentsmesh lessons hookUserPromptSubmit: - matcher: "*" type: command command: agentsmesh lessons hookPostToolUseFailure: - matcher: "*" type: command command: agentsmesh lessons hookSessionStart: - matcher: "*" type: command command: agentsmesh lessons hookPreToolUseguards the first touch — recall injects before the edit. With the outcome log enabled it also runs the recurrence gate: when the exact action about to run has already failed twice or more and a captured lesson covers it, the covering rule is re-injected above the regular bullets with the failure count (RECURRENT FAILURE: this exact action has failed N× before …) — and it cuts through session dedup, since a rule the agent saw but did not apply must be shown again. Advisory context only, once per action per session.PostToolUsecovers later actions and harnesses that support only post-call injection.UserPromptSubmitrecalls against the task text itself. The tool-call events only ever see a file path or command, so akeyword(conceptual / general) lesson can otherwise fire only when its concept happens to appear as a path/command token.UserPromptSubmitis 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.PostToolUseFailurefires when a tool call fails (PostToolUseis success-only) and injects an advisory capture-decision nudge, pre-filled with a ready-to-paste--trigger-file/--trigger-cmdfor the failed file/command — once per session.SessionStartwithsource: compact/clearresets 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 (namespaced per project), so a lesson is injected at most once per session — and stamps the same id on recall/outcome telemetry rows, so stats groups by real harness sessions and a failure only impeaches deliveries from its own session (no AGENTSMESH_SESSION_ID export needed on hook-driven recalls).
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:
agentsmesh lessons add "Write comments per the repo style." --topic style --new-topic --topic-summary "Style." --scope alwaysAn 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:
$ 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, repairTriggers) 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. |
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/--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. 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:
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.
agentsmesh init --lessonsalready commits the.gitattributesbinding for you:.agentsmesh/lessons/lessons.json merge=agentsmesh-lessonsso 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 --lessonsprints exactly these commands:Terminal window 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 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.