--- description: Use when the user wants to create a team engineering rule for the Rulebook (e.g. "/memhub:create-rule", "add a rule that we never force-push", "make a rule for this mistake", "make it actually stop me"). Pins a when-X-then-Y sentence, drafts a deterministic check, and files it for review through the memhub `create_rule` tool — a rule that advises and a rule that stops the command are filed the same way, and a reviewer turns either one on. argument-hint: [--rulebook ""] [the rule, in your own words] allowed-tools: Bash, Read, Write, Task, Agent, AskUserQuestion, mcp__plugin_memhub_memhub__list_rules, mcp__plugin_memhub_memhub__create_rule, mcp__plugin_memhub_memhub__list_rulebooks, mcp__plugin_memhub_memhub__create_rulebook, mcp__plugin_memhub-staging_memhub__list_rules, mcp__plugin_memhub-staging_memhub__create_rule, mcp__plugin_memhub-staging_memhub__list_rulebooks, mcp__plugin_memhub-staging_memhub__create_rulebook --- You are creating a **Rulebook rule**: a human-authored, team-owned rule stored in MemHub, fetched once per session by the coding agent of everyone the rule's **rulebook** binds, and measured on every fire. Rules are data, not prose in a doc — and a rule with a loose check fires on innocent commands more often than not, so the check is where the care goes. There is no local rule file. The write path is the memhub **`create_rule`** MCP tool; the rule reaches the rulebook's members once a reviewer activates it (on a book that binds only its creator, that reviewer is the user themselves). Arguments: `$ARGUMENTS` - `--rulebook ""` (optional) → the rulebook to write into (`rulebook_id` on `create_rule`). Omit and step 0 resolves it. `--brain ""` is still accepted and means the same thing — a rulebook used to be a brain and people still type it — but say "rulebook" back. - Remaining text = the rule in the user's words. If absent, ask for it — one sentence, ideally already conditional ("when X, do/never Y"). ## Handed a turn by the harness MemHub's harness can block a stop with `MemHub harness: before you stop: turn N of this session was flagged as …`. Then you are here because a turn was flagged, not because the user asked for a rule, and two things differ: **This section, not the harness line, is the harness path.** The line is rendered to the person as `Stop hook feedback:` — there is no Stop channel that reaches only the model — so it stays a pointer and everything it used to carry lives here. **The invariant, before the details: nothing on this path reaches the person except one line naming a rule that was filed.** Every step of this skill that shows, asks, offers or reports — §4b.1's prompt preview, §4b's cleanup report, the revision offer when a forward test finds nothing, the conflict choices, the step-5 confirmation — is suppressed here, including ones added after this was written. Where a step says to stop and ask, stop silently instead. The enumerations below say what to DO at each such point; this says what the person sees, which is nothing until there is a rule. **One exception, and it is not about the rule: the rulebook cache.** The invariant covers disclosures about the CANDIDATE; it does not cover machine state this skill changed. Since §4b.3 arms the candidate in a private base and never writes the shared book, there is now only one such notice, and it is spoken even though nothing was filed: - **The claim did not take** — `book-path` still answers with the shared book after you wrote the redirect. Say so, name the file, and **abort before arming anything**. Carrying on would mean doctoring the book every other session on the machine reads, which is what the claim exists to prevent. (There used to be a second notice: a failed restore, or a `$BOOK.pretest-*` left by an interrupted run, either of which meant an unreviewed candidate was armed in the shared cache. Neither can happen now — nothing shared is written, so there is nothing to restore and nothing to recover. If you are reading this skill against an older plugin, that recovery still matters; against this one it is gone rather than merely unlikely.) Silence about the claim failing is a safety bug wearing the costume of quiet. - **The test.** A lesson is one that would change what an agent DOES next time, is not already a RULE, is not project state, and will still be true next month. Already written in CLAUDE.md or the docs does NOT disqualify it: if this turn tripped over it anyway, the prose was not enough — file it and cite where it is written. Skip only when nothing went wrong and you would merely be restating the docs. - **No lesson** → say nothing to the person about the turn, and stop. Nothing is recorded: how often a block produces a rule is already the `handed` rows in the moments file against `session_draft` rules on the server, and whether a rule HELPS is the fire-event fold's question, not this one's. - **A lesson** → run this flow with it as the user's words, and **ask the person nothing at all**. A `session_draft` lands `proposed` and fires for nobody until a reviewer activates it, so every confirmation this skill asks for elsewhere is already held by whoever reviews the book. Concretely, on this path: - **Step 0 (which rulebook)** does not ask, and never guesses. In order: 1. Exactly one `bound` book → file there. 2. Exactly one `bound` book with `scope: all_org` among several → file there. 3. **Otherwise — several `all_org` books bound, or no book bound at all — the repo's own book**, named `Rulebook: ` exactly as `scope_repos` spells the repo. Use the bound one with that name if it exists; if none does, `create_rulebook` it with `scope: "explicit"`. Rule 3 replaced "anything else → file nothing". That clause read as fail-closed and was not: the arithmetic is prose, a model applies it, and on an org with TWO bound `all_org` books it refused once and picked a book every other time. Silently non-deterministic about which team's rulebook gets written to is worse than either answer — a per-repo book is one the configuration can always produce, so there is nothing left to resolve. **Read `member_count` off the create reply before filing into a new book.** `create_rulebook` seeds an `explicit` book with you — *unless you are an organisation admin*, who is seeded only by naming themselves in `member_user_ids`. An admin who takes the default therefore gets a book binding NOBODY, and "a book that binds nobody serves its rules to no session": the rule files, the reply says success, and it reaches no one. So pass your own id in `member_user_ids` when you know it, and either way treat `member_count: 0` as a FAILURE — report that the book was created but binds nobody and needs a member, rather than filing into a void. A book created here is `explicit` and small on purpose. `all_org` is an admin act that binds everyone in the organisation, now and in future, and nobody can leave it — not a thing to do on a path that asks nobody anything. - **Step 4b.6** (no git checkout, no Agent tool) does not ask. Say the precondition was missing, file with the pattern proven only against the verifier's synthetic cases, and note that in the report. - **Step 5** does not ask. File, then tell the person one line naming the rule. - **A conflict that the mandatory policy says not to file, is not filed** — and on this path it is not reported either: `cross_book` (a rule in a book `supersedes_rule_id` cannot reach), or `same_matcher` on an active rule that is not this one. File nothing, say nothing, stop. - **A live verification that runs and fails is terminal.** The forward test firing on zero candidate rows is not a conflict and not a missing precondition: the pattern is unproven, so file nothing, say nothing, and stop. Do not offer to revise it — that offer is the interruption this path exists to avoid, and an unproven matcher is worse than no rule. - **Never pass `activate`.** Never put a person's name, home directory or e-mail in a rule. - **`unchanged: true` is a filing, not a blocker.** That reply means the rule is already in the book — a retry after a lost response, or identical content re-filed. The moment ended WITH a rule, so tell the person as you would for any filing. - **`scope_repos` is the harness line's, verbatim** — it is already narrowed by `proposal_scope`. When the line says the turn worked in several repositories, cut it further to the ones the lesson is about. Do NOT rebuild it from `state.touched_repos`: that re-broadens it to repos the lesson has nothing to do with, which then fire on unrelated work. - **`source_ref` is passed EXACTLY as the harness line gives it.** The generic steps append `|applies N/M|precision P` to a `source_ref`; on this path they do not. That value is half of the server's `(rulebook, source_ref, title)` re-import identity, so a retry carrying different evidence counts files a second row instead of matching the first. Put those numbers in the report to the person instead. - **The stamp** comes from the moments file (`$MEMHUB_HARNESS_DIR`, else `~/.config/memhub-plugin/harness`), `.moments.jsonl`: the last JSON object whose `source_ref` matches the harness line's. Pass its `state` verbatim in step 5 with `source="session_draft"` and that `source_ref` — MemHub refuses a `session_draft` without its `state`. `mode: "gate"` still needs the user's own words asking for a block (step 3). ## 0. Which rulebook — resolve it first A **rulebook** is a container with its own membership: whoever is a member has their agent bound by its rules. One person can be in several (an org-wide book plus their team's), and a rule is filed into exactly one. So the destination is a decision, not a default — settle it before drafting anything. Call `list_rulebooks`. Each row has `rulebook_id`, `name`, `scope` (`all_org` | `explicit`), `member_count`, `rule_count`, `bound` (does it govern me?) and `is_admin`. Then: - `--rulebook` given → match it against `rulebook_id` first, then `name` (case-insensitive). No match → show the list and ask; never file into a book the user did not name. - Omitted, exactly one book → use it, and say which one in step 6. - Omitted, several books → **ask** with AskUserQuestion, one option per book labelled with its name and who it binds ("org-wide" / "N members"). Do not guess: filing into the wrong book binds the wrong people. (This mirrors the server's own `TOOL_MANY_RULEBOOKS` — it refuses to guess too.) - **No books at all** (an empty list, or `TOOL_NO_RULEBOOK` from a write) → nothing is auto-provisioned. Offer to create one: propose `create_rulebook(name: " rules", scope: "explicit")` — a book that binds only the user, which is the only shape a non-admin may create — and ask. On yes, create it and file into it. On no, stop and report; there is nowhere to put the rule. Never call `create_rulebook` with `scope: "all_org"`, and never name another user in `member_user_ids`. Both are org-admin acts (`rulebook_scope_needs_admin` / `rulebook_members_need_admin`), and widening who a rulebook binds is a governance decision this skill does not make. Membership changes are not MCP tools at all — they are done in MemHub. **When the create is refused.** `create_rulebook` validates the creator as an active org member, so it can answer `rulebook_member_not_in_org` naming *the user themselves* — even though you named nobody. That is not a bug to retry: their org membership is inactive, and no rulebook can be created until someone fixes it in MemHub. Say that plainly and stop. (`rulebook_name_too_long` means the name exceeded 200 characters — shorten it and retry once.) **Older backend:** if `list_rulebooks` / `create_rulebook` are not present, or `create_rule` rejects `rulebook_id`, the server predates rulebook containers. File with no destination — omit `rulebook_id` and let the server put the rule where it used to — and say so in step 6; the rest of this skill is unchanged. Do NOT reach for `agent_brain_id`: `create_rule` has no such parameter any more, so passing it turns a degraded-but-working file into a failed one. ## The flow — every step is mandatory ### 1. Pin the rule sentence Get to a **when-X-then-Y** sentence with a **why**. A conditional shape is what makes a rule actionable; a bare observation is not a rule. If the user gave a war story, extract the conditional from it and confirm your reading. ### 1b. Evidence: how often would it have applied? A rule is worth the team's attention in proportion to how often the situation actually occurs. Write the candidate `create_rule` body to a file and replay it over the local transcripts (Claude Code, Codex, Cursor): ```bash python3 "${CLAUDE_PLUGIN_ROOT}/skills/rules-from-sessions/scripts/mine_sessions.py" \ --rule-file /tmp/cand.json --out /tmp/mine ``` Read the candidate's line: `applies-in N/M sessions` (by host) and 3 sample commands. For a "do X before Y" rule add `"requires_prior_rx": ""` to the body — the line then shows `precision = fired-with-no-prior-X / fired`; below ~50 % the matcher would nag people who already complied, so use the `ordering` shape (step 3) or make it `session_context`. Carry the numbers into `source_ref` in step 5 (`…|applies N/M|precision P`). If N is 0 across all hosts, say so to the user before filing — it may still be right (insurance for a new teammate) but it is not lift. For deriving many rules at once from sessions, use `/memhub:rules-from-sessions` instead. ### 2. Duplicate check — by eye now, deterministically in step 5 Call the memhub `list_rules` tool with **no `rulebook_id`** and **`include_retired=True, limit=200`** so the reply spans every rulebook the user can see in every state, and read the new rule against every title and statement. Same subject → plan to replace the existing rule instead of adding a twin: note its `rule_id` for `supersedes_rule_id` in step 5. The server does no title matching — you decide what a rule replaces. Keep the `list_rules` reply: step 5 runs the deterministic check over it. `include_retired=True` matters: a rule someone already dismissed is exactly the twin you must not re-file, and the default view hides retired rules. `limit` is 200 at most — if the reply says `has_more`, ask again with `offset` and concatenate `rules` before running the check, or the comparison silently misses whatever fell off the first page. A twin in **another** rulebook is a different problem: `supersedes_rule_id` only retires a rule inside one book, so nothing you file can absorb it, and both rules reach the same call if both books bind the user. Say so and let them decide — step 5 flags these as `cross_book`. ### 3. Draft the rule — one delivery, one engine block | the rule is… | `delivery` | engine block | |---|---|---| | a Bash command with a checkable form | `agent_hook` | `matcher: {event: "bash", command_rx, command_not_rx?, warn_once_per}` | | an edit/write to certain paths or content — by the Edit/Write tools OR by a Bash command that wrote the file (heredoc, `write_text()`, `sed -i`) | `agent_hook` | `matcher: {event: "edit", path_rx, path_not_rx?, content_rx?}` | | a failing or noteworthy tool output | `agent_hook` | `matcher: {event: "output", content_rx, command_rx?, content_not_rx?}` | | a file about to be read into the agent's context — the Read tool, OR a `cat`/`head`/`tail`/`less`/`more`/`sed` on a path in a Bash command (not piped, not redirected; `cd`-relative paths resolve) | `agent_hook` | `matcher: {event: "read", path_rx?, path_not_rx?, command_not_rx?, given: {file: {lines_gt}, agent: {main}}}` — needs `path_rx` or `given.file`, or it fires on every file | | "run X after edits, before Y" | `agent_hook` | `ordering: {required_command_rx, gated_command_rx, armed_by_events, min_edits, display_name}` | | "run X once a session, before Y" — X is not owed to an edit, it is owed to the session (`git fetch` before reading `origin/*`) | `agent_hook` | the same `ordering` block with `armed_by_events: ["session"]`: armed at session start, discharged by one green X, and re-armed for the next session | | "when the person asks about Z, do X before answering" (probe staging before answering a staging question) | `agent_hook` | the same `ordering` block with `armed_by_events: ["prompt"]` **and** `armed_by_rx` — the pattern the prompt must match. Without `armed_by_rx` the rule arms on nothing; only what a person TYPED arms it, never a slash command's body or a loop wake-up | | applies when a file / symbol / command is in play, but the form isn't checkable | `anchor_recall` | `anchors: [identifiers]` — the server decides relevance per call | | worldview with no trigger at all | `session_context` | none — at most 15 such rules per repo scope are shown at session start; prefer a checkable shape when one exists, because advice shown in-flight is acted on far more often than advice shown at session start | Plus on every rule: `title` (short, imperative; the server allows up to 200 chars but aim for under 60), `statement` (the advisory line and the nuance a reviewer needs: sanctioned forms, exemptions), `scope_repos` (`[""]` or `[]` for all — `` is the repo's name, `basename $(git remote get-url origin)` without `.git`, NEVER the directory you are in: in a worktree that is the branch name, and the hook matches `scope_repos` by exact string, so the rule would bind nobody), `scope_paths` / `scope_exclude_paths` (globs — they constrain edit rules by file path; a Bash call carries no path, so an include-scoped rule never fires on one). **A rule that needs a newer hook.** If the rule uses a key an older installed hook would not understand, pass `min_hook_version: ""`. Where the installed hook is older it runs the rule as ADVICE, never as a gate, and says so once per session naming the version it wanted — instead of ignoring the condition and firing as if it held. A key the hook does not know degrades the same way even without the field, so `min_hook_version` is how you make the message say what is actually missing. **Advise, or stop the command?** A rule advises by default: its sentence is shown and the call goes through. Pass `mode: "gate"` and the rule DENIES a matching command before it runs — the person can still run that exact command by prefixing `RULEBOOK_OVERRIDE=''`, and their reason is recorded with the fire. Advice has the same channel, one call later: an agent that reads an advisory and goes on without it says why on its next command as `RULEBOOK_OVERRIDE='[