Skip to content

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:

Terminal window
# 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

Terminal window
agentsmesh lessons <subcommand> [args] [flags]

Subcommands

SubcommandPurpose
queryRecall primitive — return active lessons whose triggers match the supplied file / command / keyword predicates.
addCapture primitive — atomically add a new lesson, deduplicating triggers against the graph.
topicsList 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-markersRemove dead legacy provenance markers (See L123, (L69), [L3], “(also relevant …)”) from rule prose. --dry-run reports without writing.
journalRender lessons chronologically (sorted by createdAt then id).
validateSchema + 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.
statsSummarize 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.
pruneCurate 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-mdOne-shot migrator from legacy index.yaml + topics/*.md + journal.md.

Recall ritual

Before any edit or shell command, agents call:

Terminal window
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.

Terminal window
# 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

Capture ritual

Immediately after any failure, agents call:

Terminal window
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.

Terminal window
# 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"

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:

Terminal window
$ 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

FlagDescription
--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|jsonOutput shape. Default plain (one rule per line).
--top <n>Keep only the top n relevance-ranked matches. Default 10.
--allReturn 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-dedupForce dedup off for this call even when a session id is set — return the full ranked set including already-seen lessons.
--idsPrefix 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

FlagDescription
--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-topicAllow 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 requiredadd 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.

deprecate

FlagDescription
--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

FlagDescription
--dry-runReport which lessons would change without writing.

prune

FlagDescription
--applyWrite 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 --capvalidate 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

FlagDescription
--jsonEmit 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

FlagDescription
--mergeFold 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.
--forceOverwrite 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. 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 .gitattributes entry so the driver is used for the graph:

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

    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