Mental model curator
Review the agent's OWN Hindsight memory bank with the most capable model and PROPOSE well-formed mental models ("knowledge models") to the operator via the approve/deny proposal card, instead of blindly creating them. Use to curate, seed, or audit the standing models the agent maintains over its specialty. Triggers on phrasings like: "curate mental models", "propose knowledge models", "review my memory bank", "what standing models should I have", "bootstrap mental models", "suggest mental models from my bank", "audit my mental models", "what recurring questions do I keep re-deriving", and typos like "propose knowlege models". ALSO owns the directive TRIAGE pass (Memory v2 M2) — triggers like "audit my directives", "triage my directives", "merge my directives", "retire stale directives", "my directives are overlapping", "too many active directives", "clean up my guardrails", and when switchroom doctor FLAGs a bank over the directive WARN/FAIL threshold. Do NOT use for storing a single fact/preference/decision — that is retain, not a mental model. Do NOT use for identity or "who is the user" — that lives in profile banks, so never propose an identity model here (the operator's review of the card, not any code guard, is the only gate — so it's on you to honor). Even when the operator directly asks you to CREATE one specific named model whose shape you know, PROPOSE it through the approve/deny card — never call create_mental_model directly (it is not pre-approved; the propose card is the only sanctioned write path, Fix 1.2 / #2903).From its SKILL.md
npx -y skills add switchroom/switchroom --skill mental-model-curatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
21.3 KB, as published. Nobody here has run it
mental-model-curator — Propose standing knowledge models from your own bank
A mental model in Hindsight is a pre-computed semantic summary backed by reflection over the memory bank — a standing answer to a recurring DOMAIN question the agent keeps re-deriving (e.g. a coach's "current training plan, recent sessions, and open adjustments"). This skill's job is to use your most capable model to survey your OWN bank and PROPOSE a few well-earned models to the operator through the approve/deny card — never to bulk-create noise.
You cannot self-approve. Every proposal renders a card the operator taps. Your job is to make each proposal so obviously right that a glance is enough.
Why propose instead of create
You do NOT have direct create_mental_model access — it is deliberately not
pre-approved (Fix 1.2 / #2903). Every mental-model write routes through
mcp__switchroom-telegram__mental_model_propose, because a
standing model is a persistent, always-in-context artifact with real cost
(recall/reflect spend, and — if refresh_after_consolidation is on — invisible
post-consolidation spend). A human should decide which of those the agent runs.
Bulk self-creation is how banks fill with overlapping, stale, or wrong-fact
models. Propose the few that are earned; let the operator ratify.
Modes
- Propose mode (default): run the full workflow and fire proposal cards, one at a time, up to the rate limit.
- Dry-run / preview mode: when the user says "preview", "dry run", "just
show me the candidates", "what would you propose", or you're testing against an
unfamiliar bank — run steps 1–4, then OUTPUT the ranked candidate list as text
(name + source_query + one-line reason) and STOP. Do NOT call
mental_model_proposein this mode. This is the safe way to run against any bank. - Directive triage pass: a separate job — see "Directive triage pass (Memory v2 M2)" below. It renders one consolidated card as text, then requires an explicit operator go-ahead before ANY deactivation executes — never enacts on the same turn the card is shown — and runs independently of the mental-model modes above.
Workflow
1. Identify the bank and check it's worth analyzing
Resolve the session's bank (mcp__hindsight__list_banks, or the bank bound to
this agent's session). Then pull mcp__hindsight__get_bank_stats.
If the bank is empty or thin, STOP and report "not enough content to synthesize models yet — the bank needs more accumulated memories before standing models are meaningful." Synthesizing over an empty or thin bank produces confident noise: models with no grounding that read as authoritative. A handful of memories is not a domain. Do not propose against a thin bank, even in dry-run.
2. Read what already exists (dedupe)
Call mcp__hindsight__list_mental_models, and get_mental_model on each to read
its source_query. Build a dedupe set of the questions already covered.
The propose flow hard-rejects an EXACT-name duplicate before a card posts, but that is not enough — a near-duplicate under a fresh name is still waste. You must semantically dedupe: if a candidate's source_query asks substantially the same thing as an existing model (even reworded), drop it. Only propose genuinely new coverage.
3. Survey recurring themes
Use mcp__hindsight__reflect and mcp__hindsight__recall to find the DOMAIN
questions this agent keeps re-deriving — the standing state of its specialty.
Good prompts: "What questions do I repeatedly answer from scratch?", "What are
the recurring themes in this bank?", "What standing state would I want summarized
before every relevant turn?" Cluster the answers into candidate standing
questions. Each cluster must be backed by memories the bank ACTUALLY holds — if
you can't point to the underlying content, it's not a candidate.
4. Frame each candidate correctly
source_querymust be a DOMAIN question, never an identity / "who is the user" question. Identity lives in dedicated profile banks; never propose an identity-based model — the operator, not the platform, is the only gate, so this is on you to honor (one previously caused a wrong-fact contradiction bug). If a candidate is really "facts about the user", discard it.refresh_after_consolidationdefaults OFF. Only set it true when the model genuinely tracks fast-moving state that must be current the moment memory consolidates. It adds invisible post-consolidation spend and timeout risk, so the default answer is off.- Keep models tight. recall/reflect tiers cap around ~1024 tokens; a model
that tries to summarize everything summarizes nothing. Set a modest
max_tokensif the default would overrun. Narrow, answerable source_queries beat broad ones. - Few models per bank. Each must be earned by content the bank holds. Quality over quantity — a bank with three sharp models beats one with ten vague ones.
5. Propose within limits
Rank the surviving candidates by how much re-derivation they save and how well the bank supports them. Propose only the top few.
HARD LIMIT: 5 proposal cards per hour per agent (sliding window). Batch and prioritize — never fire one card per cluster. If you have more than 5 worthwhile candidates, propose the best 5 and mention the rest in text for a later pass.
Propose one at a time:
mcp__switchroom-telegram__mental_model_propose(
chat_id=<the current chat_id>,
name=<slug>,
source_query=<the domain question>,
reason=<CRISP one line — the operator sees this on the card and denies vague ones>,
... # refresh_after_consolidation / max_tokens only when justified above
)
The reason is load-bearing: it's the one line the operator reads before
tapping. "Tracks the athlete's current plan + open adjustments so I stop
rebuilding it each session" earns a tap; "useful model" gets denied.
After firing a card, END THE TURN CLEANLY. The flow resumes on its own via a
synthetic inbound (mental_model_proposal_applied / mental_model_proposal_denied)
when the operator taps. Do not loop, do not poll, do not fire the next card in the
same turn. You cannot self-approve — that's by design.
6. Scheduled-sweep note (future extension)
Unattended cron proposing is NOT supported today. mental_model_propose
needs a live turn with an operator present to tap the card — firing cards into an
empty topic at 3am is useless and noisy. If invoked from a scheduled sweep,
STAGE the ranked candidates (retain them, or hold them) and surface them on
the next interactive turn instead of firing cards unattended. Treat live-turn,
operator-present proposing as the only supported path for now.
Directive triage pass (Memory v2 M2)
A second, independent job of this skill: sort the bank's active directives
into E-45's five categories and clear the four heavy banks off the WARN line.
Directives are hard rules applied on every reflect — but the bank caps active
directives at MAX_DIRECTIVES=30. Past the cap, the lowest-priority
directives are truncated from the <active_directives> recall block and
never reach the agent (recorded as directives_omitted on the recall_log row
— the recall hook's stderr warning is swallowed by Claude Code, so don't look
there). Fleet doctor WARNs at >24 active and FAILs at >30 (doctor-memory.ts,
DIRECTIVE_WARN_THRESHOLD), and its fix text points here.
Honest framing (do not oversell): as of the M2 measurement pass, NOTHING is
currently being silently dropped fleet-wide — zero directives have ever been
retired, and directives_omitted has fired zero times. This is a budget +
hygiene pass, not a rescue from silent drops. Say it that way; overstating the
urgency is its own failure.
The five categories every active directive sorts into (E-45):
| Category | Meaning | What happens in THIS pass |
|---|---|---|
reflect-directive | A genuine compliance guardrail (§4.2's designed role) | KEEP — no change |
rules-block | A standing preference that belongs in the CLAUDE.md rules block (M1) | STAGE ONLY — stays ACTIVE. See the hard rule below. |
disposition | Better expressed as disposition_* bank config (E-40) | Deactivate; note the config change separately for the operator — do NOT auto-apply it |
retire | Superseded or mechanized — dead weight | Deactivate (record superseded_by when a winner exists) |
retain-as-memory | A fact filed as a directive by mistake (category error) | Deactivate — verify the fact already exists as a memory in the bank BEFORE proposing this; don't assert it, check it |
Retirement signals are deterministic and bounded (E-45). A directive is
ONLY a retirement candidate when you can point to one of: (a) it is tagged
superseded-by:<name> (or you would tag it that way, naming the winner), or
(b) a mechanization reference (a named commit/PR/scaffold rule that now does
this automatically), or (c) a documented category error you can verify against
the bank's own memories/config. Citation counts are NEVER a signal (a rule
that works quietly gets zero citations). Age alone is NEVER sufficient. A
directive with none of these signals is reflect-directive — KEEP — by
default. Never propose retiring a directive because it "seems old" or "looks
unused."
HARD RULE — never deactivate a rules-block-category directive in this pass
If a directive's behaviour is destined for the CLAUDE.md rules block (M1), its
migration happens at M3 flip time, when that surface goes live for this
agent — not now. M1 shipped dark (memory.rules_block off on every agent
today); deactivating a rules-block-category directive before that flip would
leave the agent with neither the directive (deactivated) nor the rule
(block not live) — a silent guardrail gap for however long the M2→M3 interval
runs. So: for every directive you categorise rules-block, call
list_directives to confirm it, but do NOT call deactivate_directive on it
in this pass, ever — only stage its text (mention it in the card as staged,
count it, and leave it exactly as-is) for the M3 flip to pick up later
(rule add, once memory.rules_block is on for this agent).
This is not just a prose rule you have to remember — it is now also code-
enforced, but ONLY once you complete the mandatory step below.
DirectiveAdmin (src/memory/hindsight-directive-admin.ts —
deactivate/deactivateById/deactivateByIdWithTag) refuses to deactivate
ANY directive carrying the persisted triage-category:rules-block marker
tag, on every call path, including this skill's own deactivate_directive
calls. But that refusal only ever fires for a directive that actually
CARRIES the tag — and nothing stamps the tag automatically just because you,
in your own reasoning, decided a directive is rules-block. You must
stamp it yourself, immediately, as part of classification — not as an
afterthought before presenting the card:
For every directive you classify
rules-blockin step 2 below, before you move on to the next directive, run:switchroom memory directive mark-rules-block <agent> <id>(
src/cli/memory-directive.ts— this calls the exact sameDirectiveAdmin.markRulesBlockwrite the batch triage executor uses, so the chokepoint above keys off it identically). Idempotent — re-running it on an already-marked directive is a no-op, so it is always safe to call even if you are not sure whether a prior pass already marked this one.
Do this for EVERY rules-block row before step 3's card renders, not just the ones you personally plan to leave alone — the whole point is that the code guard, not your own discipline in this pass or a future one, is what refuses a later deactivation attempt (including a mistaken one, or one made by a different pass that never saw this reasoning).
When to run it
- The operator asks (see the directive triggers in the description), OR
- switchroom doctor flags a bank at/over the directive WARN/FAIL threshold, OR
- you're already curating the bank and notice the directive set is bloated.
Workflow
- List every directive, active and inactive context.
mcp__hindsight__list_directives(active only is fine for triage — inactive ones are already off). If the active count is comfortably under the WARN threshold (≤24) AND nothing reads stale/overlapping, STOP and report "directive set is healthy (N active) — nothing to triage." Don't manufacture churn. - Classify every directive — no drops. For each one, assign exactly one of
the five categories above, with the deterministic signal that justifies it
(or "no signal — KEEP" for the default case). Do this for the FULL set —
the card is worthless if a directive silently doesn't appear on it.
The moment you classify a directive
rules-block, runswitchroom memory directive mark-rules-block <agent> <id>for it right then (see the HARD RULE above) — do not defer this to a later step or batch it at the end; every rules-block row must be stamped before you move to step 3. - Render ONE consolidated card as text. Two sections, KEEP/staged first,
retirement candidates second — never interleaved, so a skim of "the bottom
half" cannot land on a live guardrail:
- Keep / staged (N): every
reflect-directiveandrules-blockrow, each showing priority + category + signal (or "no signal — default KEEP"). - Retirement candidates (N): every
disposition/retire/retain-as-memoryrow, each showing priority + category + the deterministic signal — never a bare category with no rationale. - The resulting active count vs
MAX_DIRECTIVESif the retirements land.
- Keep / staged (N): every
- STOP. Get an explicit operator go-ahead before touching anything. Do not deactivate on the same turn the card is shown. The card is the operator's one look before a batch of guardrails changes state.
- On go-ahead, retire ONE AT A TIME with a human tap PER retirement — never
a single blanket go-ahead for the whole batch.
deactivate_directiveis pre-approved fleet-wide (scaffold.ts's 2026-07-29 amendment,HINDSIGHT_MCP_TOOLS) — unlikedelete_directive, calling it does NOT raise a Telegram permission card by itself. That is exactly why this skill must supply the per-call gatedelete_directive's card would otherwise have given: present the NEXT retirement candidate (name, priority, signal, proposedsuperseded_by) on its own, then STOP and end the turn — do not proceed to it, and do not queue the next one, until the operator's reply for THIS ONE arrives. Only after an explicit per-row confirmation do you callmcp__hindsight__deactivate_directivefor that single directive. Then present the next candidate the same way. A single "yes, go ahead" that covers the whole retirement list is NOT sufficient — that collapses back to the batch-level prose go-ahead this per-row loop exists to replace, and silently trades away the human tapdelete_directive's approval card used to guarantee for every one of these retirements (see PR #4760 review B2). Passsuperseded_bywhen you named a winner. After the LAST retirement in the plan lands,list_directivesagain and report the new active count againstMAX_DIRECTIVES, plus which directives moved where. - Disposition rows are a note, not an auto-edit. When you deactivate a
disposition-category directive, separately flag to the operator whichdisposition_*bank config change it corresponds to — do NOT edit bank config yourself in this pass (E-40: audit before leaning on it).
windows-boxes-class fixes (a drifted directive with a peer's superset text) — special-cased, rare
Do NOT attempt this as an ordinary retirement. If a directive on this bank has
drifted from a peer agent's superset/reconciled version of the "same" rule
(content differs but the two should say the same thing), that is NOT
expressible as a content-PATCH — the shim deliberately refuses to PATCH
directive content (hindsight-directive-admin.ts — a directive body is the
guardrail itself; there is no history table to recover a bad rewrite from).
It is also NOT safely expressible as plain create_directive +
deactivate_directive calls from this skill: once the new copy is created
carrying the SAME name, deactivate_directive's name-resolution sees two
directives sharing that name and refuses as ambiguous. Use
reconcileDirectiveSuperset (src/memory/directive-triage-executor.ts) for
this instead — it resolves the old copy's id BEFORE creating the new one and
retires it by id, so the name collision never becomes an ambiguity error. This
is NOT a bare MCP tool call from this skill (no MCP tool exposes an
id-targeted deactivate) — it runs via
switchroom memory directive reconcile <agent> <name> <content...>
(src/cli/memory-directive.ts), an operator or repo-authorized CLI/script
invocation, not something this skill invokes autonomously. Flag any drift you
notice to the operator rather than improvising a fix.
Directive-pass anti-patterns
- ❌ Deactivating a
rules-block-category directive before M3 flips this agent — opens a guardrail gap; see the hard rule above. - ❌ Classifying a directive
rules-blockwithout immediately callingswitchroom memory directive mark-rules-block <agent> <id>for it — an unmarked directive gets NO code-level protection from theDirectiveAdminchokepoint; the classification in your head is not enough. - ❌ Retiring on age or citation count alone — E-45 refuses both as signals.
- ❌ Calling
deactivate_directivein parallel / without awaiting each one — sequential only (tag-write race). - ❌ Executing on the same turn the card is shown — the card review is the gate; always stop for an explicit go-ahead first.
- ❌ Treating one blanket "yes, go ahead" as clearance for the whole
retirement list —
deactivate_directiveis pre-approved (no Telegram card per call), so this skill's own per-retirement stop-and-confirm loop IS the human tap; batching it away silently weakens the guardrail (review B2). - ❌ Auto-editing bank config for a
dispositionrow — flag it, don't apply it. - ❌ Treating a drifted/superset-text directive as an ordinary retirement — see the windows-boxes-class section above.
- ❌ Manufacturing retirements on a healthy small set just to "tidy" — run the pass when it's earned (bloat, overlap, staleness, or a doctor flag), not reflexively.
- ❌ Leaving a live contradictory pair in place — surface it as the top
retirement candidate; it's what makes
reflectnondeterministic.
Anti-patterns
- ❌ Proposing against an empty or thin bank — synthesizes to noise.
- ❌ An identity / "who is the user" source_query — forbidden; profile banks own that.
- ❌ Ten near-duplicate models under different names — semantically dedupe first.
- ❌ Firing a card per cluster — respect the 5/hour limit; propose the top few.
- ❌
refresh_after_consolidation: trueby default — it's an invisible cost; leave it off unless the model tracks fast-moving state. - ❌ A vague
reason— the operator denies it and you've burned a slot. - ❌ Looping/polling after a proposal — end the turn; the resume inbound wakes you.
Output
In propose mode: a short line naming what you proposed and that the operator's tap will resume the flow. In dry-run mode: the ranked candidate list (name + source_query + reason) and nothing fired.