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 any folder of the project. Like git, the CLI walks up to the nearest folder that holds .agentsmesh/lessons/ (a lessons.json or config.json) and works on that project, the same way the recall hook and the MCP server find it (see Hook mode and the MCP server). With no such folder up the tree, the current folder is the project. Paths you pass to --file and --trigger-file stay project-relative, even from a subfolder. lessons add and lessons import-md refuse (exit 2) in your home folder, because ~/.agentsmesh holds the global agentsmesh config, not a lessons project.

Mistyped a flag? The command errors (exit 2) and names the unknown flag rather than silently ignoring it (a typoed --trigger-flie would otherwise drop a trigger). A single-value flag given twice, or an extra positional argument (for example, a multi-word rule without quotes), is an exit-2 error too. Only --trigger-file, --trigger-cmd, --trigger-kw and --evidence can repeat. A flag that takes a value but gets none (deprecate old-rule --superseded-by, query --session) is an exit-2 error too, so nothing runs with the value missing.

A value that starts with -- would be read as a new flag. Join it to its flag with = instead: --trigger-cmd=--force, or --rule="--no-verify is forbidden" for a rule that starts with --. agentsmesh lessons help prints the overview, and agentsmesh lessons help <subcommand> prints the same help as lessons <subcommand> --help.

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, rationale, 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). A retired lesson is marked [deprecated] or [superseded by <id>]; --json carries status and supersededBy on each entry.
validateSchema + integrity + trigger-liveness checks (dead file_globs whose path git history deleted or renamed, runner-anchored command_patterns, unsafe globs). An unreadable graph is reported as MERGE_CONFLICT, CORRUPT_GRAPH, SCHEMA_INVALID, or NEWER_GRAPH_VERSION. Non-zero exit on errors, with a summary line such as Lessons graph has 2 errors (UNKNOWN_TOPIC, UNKNOWN_TRIGGER). (the --json envelope error); warnings do not affect exit code.
resolveFix a lessons.json left in a git merge conflict: combine the lessons from both branches, then tell you to git add the file and finish the merge. See resolve.
statsSummarize the recall and capture telemetry logs (opt-in) and the outcome log (on by default): no-match rate, returned-token percentiles, cumulative recall cost vs. the whole-active-set preload baseline (break-even), keyword-only reachability gap, a Capture block (total captures, blocked count, new vs. upsert split, trigger-kind breakdown, recall:capture ratio), and an Effectiveness block. --json for raw output. The recall and capture blocks need telemetry turned on ("telemetry": true or 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, with git history showing the path was deleted or renamed) 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.

Adding a rule that an active lesson already has updates that lesson instead of creating a new one. The CLI prints Updated lesson: <id> — <changes>, where the changes are any of scope set to always, topic added: …, evidence added: …, rationale added, and trigger attached: … (or triggers attached: …). It prints Existing lesson: <id> (no change) only when nothing changed. With --json, the result has a changes array.

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 --trigger-kw latency

Each --trigger-kw is one keyword. --trigger-kw "cache,latency" would be a single keyword that needs both words.

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 four events in .agentsmesh/hooks.yaml, and generate projects them to each hook-capable target. A target keeps a recall entry only on the events where its hook output reaches the model (the descriptor’s hookContextEvents, taken from the tool’s hook docs); a target whose hooks can never inject context gets no recall entry at all. Targets without such an event fall back to the always-on lessons paragraph, whose ritual drives the same recall via the CLI / MCP lessons_query. Your own hooks are never filtered. The injected entries:

PreToolUse:
- matcher: Edit|Write|NotebookEdit|Bash|PowerShell
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

The command. When the project’s package.json lists agentsmesh in dependencies, devDependencies or optionalDependencies, each entry runs npx --no --offline agentsmesh lessons hook instead. That form prefers the project’s own copy, falls back to a global install, and never downloads, so a teammate without a global install still gets recall and the pinned version wins. Without that dependency the hook runs the bare agentsmesh lessons hook, which is faster but needs a global install. Re-running init --lessons rewrites the managed entries in place, for example after you add agentsmesh as a devDependency. When a Node project wires the hook but does not depend on agentsmesh, init and generate print a hint: teammates without a global install will not get recall, so add agentsmesh as a devDependency and re-run agentsmesh init --lessons. When the wired command no longer matches the project (you added or removed that dependency, and generate already switched the merge driver), they ask you to re-run agentsmesh init --lessons to update it.

  • PreToolUse guards the first touch — recall injects before the edit. With the outcome log (on by default) it also runs the recurrence gate: when the action about to run has already failed twice or more with the same error in the last 24 hours and a captured lesson covers it, the covering rule is re-injected above the regular bullets with the failure count (RECURRENT FAILURE: this action has failed N× in the last 24 hours with the same error and a captured lesson covers it …) — and it cuts through session dedup, since a rule the agent saw but did not apply must be shown again. A tool call gets at most one such warning: for a patch that touches several files, it names each covering rule once. The warning uses at most half of the 32,000-character output cap, and the recalled rules below it never repeat its rules. Advisory context only, once per action per session.
  • No PostToolUse recall. PreToolUse fires before every tool call, not only the first touch, so a PostToolUse recall for the same action only re-ran recall after the fact: a second process and a second context block per call, carrying advice that could no longer be applied (in field data, 63% of recalls arrived within 3s of a same-shaped one). On a target whose PreToolUse output does not reach the model, file and command recall comes from the always-on ritual instead. An entry left by an older scaffold is removed the next time init --lessons runs; your own PostToolUse hooks are untouched.
  • 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. It is also where lexical retrieval runs: the prompt is scored against the wording of every active rule, so a conceptual lesson fires even when the prompt says it in different words than its keyword trigger (see the reference). Wording matches rank below triggered ones and are labelled lexical: true in --json.
  • 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 — the failed file path (project-relative), or for commands the concrete command class (--trigger-cmd '\bgit commit\b' — the program plus the subcommand that directly follows it, escaped and word-bounded so rm cannot fire on pnpm run format; field graphs starve on command triggers when the author has to invent the regex) — plus the rule shape that makes lessons worth reading (cite the symptom; say why the obvious fix is wrong). Once per session. The class is taken from the command that really ran: cd, env assignments and known global flags are skipped, so cd app && git -C sub commit keys as git commit. The failure is also written to the outcome log with a short error class read from the failure text (Claude Code’s error field, without its Exit code N line). A user interrupt (is_interrupt), or a Cursor failure_type of permission_denied, still gets the nudge but is not recorded as a failure. The file-class trigger advice appears only for a failed file edit; a failed command gets the command-class trigger instead. A failed read-only tool (Read, Glob, Grep, LS, NotebookRead, WebFetch, WebSearch, and the same kind of tool on other hosts, such as Gemini CLI’s read_file or Copilot’s view) acted on nothing, so it gets the generic nudge with a placeholder trigger and is never recorded. When the failed path or command holds a single quote, a control or line-separator character, or an invisible character, the nudge also shows a placeholder (--trigger-file '<glob>' or --trigger-cmd '<regex matching the command>'), because the pasted line would break or hide text.
  • SessionStart resets recall dedup for every source except resume. Dedup suppresses a lesson already delivered this session — but compact/clear summarize or wipe the context and can drop an earlier injection, and startup means a whole new chat, while dedup would keep suppressing. Only resume keeps the set, because it restores the very context those lessons were delivered into; an unknown or missing source resets too, since re-showing a rule is far cheaper than hiding one from a context that never saw it. Both the harness session’s set and the CLI/MCP --session auto bucket are cleared, because they are separate stores. So dedup tracks the context lifecycle, not the wall clock. On a host whose prompt event cannot inject context, SessionStart also delivers task-level recall: the always-on lessons, plus keyword recall over the first prompt when the host sends it.

The last three are agentsmesh recall refinements; a hook-capable target that can’t represent one, or whose output for that event does not reach the model, 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). A Codex apply_patch call is recalled for the files the patch touches. The injected text is a short lead line followed by a fenced block, one line per rule:

<recalled-lessons>
- [lesson-id] Rule text on one line
</recalled-lessons>

Each rule is forced onto one line and cut to 2000 characters, and a rule cannot close the block early, so a cloned repo’s graph cannot fake text after it (see the trust model).

The hook also reads the payloads of other hosts and answers in their own shape: Gemini CLI’s BeforeAgent (its prompt event, handled like UserPromptSubmit), Cursor’s sessionStart and postToolUseFailure (top-level additional_context), and GitHub Copilot’s camelCase sessionStart and postToolUseFailure (flat additionalContext; on a failure the hook exits with code 2, which Copilot needs to read the output). Copilot’s VS Code compatible SessionStart (snake_case, with initial_prompt) gets the same task recall, answered in the hookSpecificOutput shape. A payload that starts with a UTF-8 BOM is read normally.

It finds the project from the touched file first: the nearest directory above the file with .agentsmesh/lessons/ (a lessons.json or config.json). So in a monorepo, a package’s own lessons apply to its files, and a payload cwd outside the project does not hide the project of an absolute file path. Without a file (a prompt or a command), it starts from the payload’s cwd, else CLAUDE_PROJECT_DIR, else its own working directory, and walks up the same way, so a session opened in a package of a monorepo still gets the root lessons. In a folder with no lessons project up the tree, such as your home folder, the hook does nothing: no recall, no nudge, and no log.

It uses the harness session_id for dedup (namespaced per project), so a lesson is injected at most once per session. A Claude Code subagent (a payload with agent_id) gets its own dedup scope, because it starts with an empty context. The hook 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: generate keeps recall entries only on events whose output reaches the model, and the command is a silent no-op (exit 0, no output) on any payload it doesn’t recognize, so an event name it does not know (for example Gemini’s BeforeTool) does nothing rather than breaking the run. A payload with no event name at all is treated as a tool call. A payload over 1 MB is read to the end and ignored, so the host never sees a broken pipe. When the graph cannot be read (a merge conflict, a corrupt file, or a newer schema version), the hook says so once per session instead of staying silent, also on a session start that carries no prompt (Cursor’s sessionStart, Copilot’s sessionStart without an initialPrompt, or a UserPromptSubmit without a prompt). The logs are best-effort: a log that cannot be written, or that holds broken lines, never breaks the hook or a command.

It is also cheap on the command firehose: the hook recalls on every shell command, but most command-only recalls provably match nothing (field-measured at >80% no-match), so a temp-dir fast-path cache of the active command-reachable trigger patterns — stamped against the graph file and refreshed automatically — lets a provable no-match exit after one stat + one tiny read instead of a full graph parse. Stale, missing, or corrupt cache always falls back to the full path: the worst failure mode is “no speedup”, never a skipped match.

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 — both with --session auto, so ritual recalls dedup within the working session instead of re-delivering the matched set every call (no shell → the lessons_query MCP tool, keyword + always fields; dedup is on by default there). 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:

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

Terminal window
$ agentsmesh lessons query --file src/x.ts
⚠ lessons.json was auto-migrated from index.yaml on first invocation. journal.md, if present, was kept: add the notes you still need with `agentsmesh lessons add`.
…

Auto-migration deletes index.yaml and topics/ after a successful import so the project lands in a clean state. Every list item under a ## Rules or ## Lessons heading in a topic file becomes a lesson, numbered or bullet, including lines that wrap onto it. If a topic file has a list item under any other heading, the migration stops, names the file and line, and changes nothing. journal.md is kept: its notes are not rules, so add the ones you still need with agentsmesh lessons add. The recall hook never migrates; the first lessons command you run does, and says so. 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, telemetry, outcomeLog) 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 as a contiguous whole-token run (case-insensitive; art does not fire on start, and read only needs the two words adjacent). keyword triggers also match --file/--cmd on token boundaries, so conceptual lessons surface without an explicit --keyword.
--alwaysPrepend 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. The always-on set uses the same fixed token budget as the hook and MCP (DEFAULT_ALWAYS_MAX_TOKENS, ~600); --top, --max-tokens and --all do not change it.
--format plain|md|jsonOutput shape. Default plain (one rule per line). Each plain/md rule is forced onto one line and cut to 2000 characters, and the whole output is capped at 32,000 characters (the first rule is always shown; a stderr note says how many were cut). json is never cut.
--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 recallMaxTokens from config.json (1200 unless changed) when omitted.
--session <id>|autoSession correlator for recall dedup. Lessons already delivered earlier in the same session are suppressed, so each recall carries only what is new. auto resolves to AGENTSMESH_SESSION_ID when exported, else a project-scoped day key — the right default for prose-driven (non-hook) recall, which otherwise re-delivers the whole matched set on every call. Because the CLI is never told that a chat ended, that bucket is bounded two ways: the whole session resets after 30 minutes of no deliveries (a quiet gap means the previous chat is over), and any single entry expires 1 hour after delivery. An explicit id gives exact, untimed control; with no flag and no env, recall is fully stateless (unchanged). Only rules actually printed are marked as seen, so a rule cut by the output cap comes back on the next call. The per-session set of delivered lesson ids lives in the OS temp dir, never the project; recalls that run at the same time each add to it under a short lock, so none of their deliveries is lost.
--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 (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 ~1200-token budget (recallLimit / recallMaxTokens in config.json), 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 (one field deployment measured 58.7% of all delivered rule-tokens as intra-session repeats before dedup). Set --session auto (the form the scaffolded ritual now uses: env id when exported, else a project-scoped day key), or an explicit --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. When dedup hides every match, plain/md output prints (no new matches: N already shown this session) instead of (no matches), with the usual dedup notice on stderr.

How a new chat gets its rules back. Suppressing a rule is only safe while the agent that saw it still has it in context, and the CLI is never told that a chat ended — so --session auto is bounded on three sides. A hook-capable target resets the bucket on any SessionStart except resume — startup is the exact signal that a new chat began, and compact/clear mean the context was summarized or wiped. Without a hook there is no such signal, so a 30-minute idle gap resets the whole session (a pause means the previous chat is over), and every entry expires 1 hour after delivery regardless. That leaves one residual case: a brand-new chat opened within minutes of the last one, on a target with no hook. Pass --no-dedup to re-show everything for one call — worth doing as the first recall of a new chat on those targets, and worth knowing about if recall ever looks emptier than expected.

add

FlagDescription
--rule "<text>"Required. Imperative rule that prevents recurrence. Must be ≤2000 characters (an emoji counts as one) — a rule is one sentence, not a pasted log; a longer one is rejected (OVERSIZED_RULE, exit 2), and an empty or whitespace-only rule is rejected before any write (EMPTY_RULE, exit 2). Also accepted positionally: lessons add "<rule>" ….
--topic <id>Required. Topic id, kebab-case (build, not Build; else INVALID_TOPIC_ID, exit 2). An unknown topic is exit 1, like an unknown lesson id; pass --new-topic --topic-summary "..." to create one.
--trigger-file <glob>file_glob trigger. Opaque — repeat the flag for multiple (commas kept). Stored in one project-relative form: surrounding spaces and a leading ./ are dropped, and an absolute path inside the project becomes relative, so ./src/a.ts reuses the src/a.ts trigger. Refused before any write (exit 2): a glob outside the safe glob subset (UNSAFE_GLOB_PATTERN), a path outside the project, absolute or ../ (TRIGGER_FILE_OUTSIDE_PROJECT), an existing folder (TRIGGER_FILE_IS_DIRECTORY; use src/cli/**), and the project root itself (TRIGGER_FILE_IS_PROJECT_ROOT).
--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. A pattern the engine cannot run (a backreference, a lookaround, a \u{…} escape, or one too large) or an invalid regex can never fire: next to a live trigger it is dropped (not saved) with a DEAD_COMMAND_PATTERN warning; when the lesson has no other live trigger, the add fails (UNRECALLABLE_LESSON, exit 2). A pattern that matches the empty string or nearly every command (.*, , \w, (git)?) is also rejected (BROAD_COMMAND_PATTERN, exit 2) — key it on the action, e.g. \bgit commit\b.
--trigger-kw <text>keyword trigger. Opaque — repeat the flag for multiple (commas kept).
--evidence <ref>Evidence reference (commit:SHA, lesson:id, …). Comma-separate or repeat for multiple; a repeated ref is stored once.
--rationale <text>One-line “why” behind the rule.
--new-topicAllow creating a new topic if missing. Requires a non-blank --topic-summary (else TOPIC_SUMMARY_REQUIRED, exit 2).
--topic-summary "<text>"One-line summary when --new-topic creates a topic.
--scope alwaysCapture a universal always-on lesson (a standard that applies to every task). Any other value is exit 2. 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 that is an invalid regex or that the linear engine cannot run. A lesson with a mix of live and dead triggers is NOT rejected — a dead keyword surfaces as a non-blocking warning, and a dead command pattern is dropped with a DEAD_COMMAND_PATTERN 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, or is a negated glob (!path), which matches every other path; prefer a path specific to the lesson.
  • DEAD_COMMAND_PATTERN — a --trigger-cmd is an invalid regex or one the linear engine cannot run, so it could never fire. It was dropped (not saved); the lesson keeps its other triggers.
  • 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, and git history shows its path was renamed or deleted (likely a rename); re-point it or the lesson is unreachable via that glob.
  • PENDING_GLOB — a glob matches no file yet, with no rename or delete in git history. It is kept and fires once the path exists; re-point it if the path is a typo.
  • 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 --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: git history on HEAD deleted or renamed their path) from any lesson that keeps another trigger, then GCs the now-orphaned glob — automating the rename-rot cleanup. A glob that matches nothing without that proof is pending and kept, so a path that does not exist yet is never pruned, and in a project without git history nothing is pruned for this reason. 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.

resolve

agentsmesh lessons resolve — no arguments. Use it when a git merge, rebase or cherry-pick left .agentsmesh/lessons/lessons.json conflicted. It reads the base, your side and the incoming side from git’s merge stages; when those are gone (for example, the conflict markers were committed), it rebuilds the two sides from the conflict markers in the file. It then combines them the same way the merge driver does and writes the result under the lessons lock. The markers are a weaker source: git already applied the clean parts of both branches, so the two rebuilt sides are mixtures. When combining them gives validation errors, resolve refuses (exit 1) and keeps the markers, the only record of both branches, so you fix the file by hand. Without a merge base in the markers (git’s default conflict style), a trigger or topic one branch deleted may come back; resolve warns, and git config merge.conflictStyle diff3 keeps the base in later conflicts. It prints how many lessons are only on each side (after lessons that share an id are renamed apart), warns if the combined graph has new validation errors, and tells you the next step for the operation in progress: git add .agentsmesh/lessons/lessons.json, then git commit for a merge, or git rebase --continue, git cherry-pick --continue or git revert --continue. Outside git it prints no git step. It does not stage the file for you. Exit 1 when there is nothing to resolve, or when a side cannot be read. The error names that side, for example when one branch committed invalid JSON. If lessons.json has conflict markers, fix that side between its markers and run resolve again (before git add): it then works from the markers. Otherwise fix that side by hand, keeping the lessons from both branches.

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 "telemetry": true in .agentsmesh/lessons/config.json (or AGENTSMESH_LESSONS_TELEMETRY=1 for one process) 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, which is on by default — total deliveries, misses (a failure in the same session, within 30 minutes, on an action the lesson’s own triggers match) and the distinct failing actions behind them, the coarse held rate (deliveries that were not a miss — 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. When the numbers show one of the two known field pathologies, stats appends an advice line naming the cause and the fix (advice key in --json): high redundancy with session-less recalls → “session dedup is inert; pass --session auto”, and a no-match rate dominated by command-only recalls against a starved command_pattern population → “author --trigger-cmd on command-shaped lessons”. The logs are telemetry, not the canonical graph; agentsmesh init --lessons adds .agentsmesh/lessons/recall-log.jsonl, .agentsmesh/lessons/capture-log.jsonl, .agentsmesh/lessons/outcome-log.jsonl, .agentsmesh/lessons/.lessons.lock/, and .agentsmesh/lessons/*.tmp to your project’s .gitignore automatically, so logs and other runtime files never dirty your worktree. (Scaffolded lessons before this was automatic? Re-run agentsmesh init --lessons, or add those 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, as 2026-06-05 or 2026-06-05T10:30:00Z. Must be a real date (2026-13-45 is exit 2). 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. Notes for teams:

  • Parallel captures on separate branches would otherwise conflict at merge time (both edit the same JSON tables). The bundled merge driver lets git combine them automatically. It does a three-way merge by key: new lessons, topics and triggers from each branch are kept, and a trigger or topic one branch deleted (with untrigger or prune) stays deleted when the other branch did not change it and no lesson still uses it. A lesson edited on both branches is merged field by field: triggers, topics and evidence are merged as sets, deprecated or superseded wins over active, and triggers, topics or evidence one branch added to a lesson the other branch superseded move to its successor, so no coverage is lost. agentsmesh init --lessons writes the .gitattributes binding for you (commit it):

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

    so the whole team inherits it once one person commits it. The other half is per-clone git config, which git never takes from a clone. agentsmesh sets it for you: init --lessons sets it in your clone, and agentsmesh generate (project scope) sets it in each teammate’s clone whose .gitattributes has the binding, so nobody has to re-run init --lessons. It writes local git config like this:

    Terminal window
    git config merge.agentsmesh-lessons.name "agentsmesh lessons union"
    git config merge.agentsmesh-lessons.driver "agentsmesh lessons merge-driver %O %A %B"

    When the project depends on agentsmesh, the driver command starts with npx --no --offline agentsmesh instead, as for the recall hook. agentsmesh saves the driver only when git can start it later. PATH folders from the npx cache (_npx) or from a package script (node_modules/.bin) do not count, because git runs the driver without them. The npx form also needs agentsmesh installed at the repository root (in the root package.json devDependencies) or globally, because git runs the driver from the repository root. When npx is missing (standalone pnpm or bun) but agentsmesh itself is on PATH, the bare driver is saved instead. Otherwise setup is skipped and agentsmesh says why, because a driver git cannot start is worse than none. A driver value you set yourself is kept, never replaced; init --lessons reports it, and generate stays quiet about it.

    The driver writes the combined graph whenever it can build one, because a failing driver does not make git re-merge the file with conflict markers — git leaves your side untouched and marks the path unmerged, which would discard every lesson the other branch captured. An empty ancestor (both branches created the graph) and validation errors that already existed on either side are merged cleanly. If the merge itself introduces a validation error, the combined graph is still written and the driver exits non-zero so you review it before staging. When a side cannot be read at all (corrupt, or a newer graph version), the driver writes git’s line merge with conflict markers, so both sides stay in the file for lessons resolve. agentsmesh lessons validate (run in CI via agentsmesh lint) is the backstop.

  • A conflict that got through (the driver was not set up, or could not run) leaves lessons.json with conflict markers, and no lesson can be read until it is fixed. lessons validate reports MERGE_CONFLICT, agentsmesh check and agentsmesh generate --check fail, and a normal generate warns. Run agentsmesh lessons resolve, then git add the file and finish the merge.

    A driver that git could not start leaves no markers: git keeps your side of the file and marks it unmerged, so the other branch’s lessons are missing. The same checks catch this: while git still holds lessons.json unmerged and the file lacks lessons from the other branch, lessons validate reports MERGE_CONFLICT, check and generate --check fail, and generate warns. Run agentsmesh lessons resolve before git add, or the other branch’s lessons are dropped.

  • 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