feat: skill polish + docs from manual-walk findings; bump to 0.1.2
feat: skill polish + docs from manual-walk findings; bump to 0.1.2 Manual scenario walk surfaced three real gaps in v0.1.1's behavior. None affect the unit-tested correctness of the hook itself; all are in skill content or user-facing docs. mercurial-bookmarks-and-prs: when the user says "branch" in an hg context, that almost always means a bookmark, not a named branch. The skill's bookmark-vs-named-branch table was correct in the abstract but the model was inferring "use named branches" from existing repo precedent (which itself was usually a previous mistake by an LLM that hadn't loaded this skill). Added an explicit "When the user says 'branch'" subsection, an "Override repo precedent" rule, and a script for surfacing the inconsistency to the user. mercurial-sync-and-merge: hg merge in a non-TTY Claude Code session defaults to whatever the user's hgrc says, which is often vimdiff/kdiff3 — neither works headless. Now explicitly recommends --tool internal:merge on every merge so conflict markers land in the file rather than launching an interactive tool. Also added the internal:local / internal:other shortcuts for "keep ours" / "take theirs" resolutions. tests/MANUAL.md Scenario 5: rewrote to acknowledge two valid pass paths (skill prevents the git attempt OR hook blocks it) since the former was what we observed in practice. Added an adversarial sub-test that forces a literal git invocation so the hook's block path can be validated in a live session. README: added a "Layered defense" subsection explaining that the hook is the backstop, not the primary mechanism, so users aren't surprised when the hook rarely fires in normal use. The v0.2.0 candidate of a defense-in-depth hook for "hg commit -u" is intentionally deferred — it has legitimate use cases (replay/migration) that need a proper design pass before adding a mechanical block.
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
May 04, 2026 19:40
Changeset:
02bd3459c5a42befc49ea9daeeadf3696d15abcb
Branch:
default
Tags:
v0.1.2
Diff
diff -r 73c3f3c0e9be -r 02bd3459c5a4 .claude-plugin/plugin.json --- a/.claude-plugin/plugin.json Mon May 04 16:49:59 2026 -0500 +++ b/.claude-plugin/plugin.json Mon May 04 19:40:14 2026 -0500 @@ -1,6 +1,6 @@ { "name": "isurus-hg", - "version": "0.1.1", + "version": "0.1.2", "description": "Mercurial fluency for Claude Code, with an Isurus-conventions overlay and a hook that prevents accidental git use in hg repos.", "author": { "name": "Chris Tusa", diff -r 73c3f3c0e9be -r 02bd3459c5a4 README.md --- a/README.md Mon May 04 16:49:59 2026 -0500 +++ b/README.md Mon May 04 19:40:14 2026 -0500 @@ -81,6 +81,15 @@ Documented limitation: only the leading token of the command is inspected, so `cd /tmp && git init` from inside an `.hg` repo is allowed (intended escape valve). Override the hook by commenting out the entry in `hooks/hooks.json`. +### Layered defense — the hook is a backstop, not the primary mechanism + +`git`-in-hg avoidance happens at two layers: + +- **Skills (primary):** the `mercurial-*` skills teach Claude to recognize `.hg/` and reach for `hg` commands directly. When the skills load, the model usually never attempts `git` in the first place. +- **Hook (backstop):** if a `git` command is invoked anyway, the hook intercepts and blocks it. The 7 unit tests under `hooks/test/` validate the block path in isolation. + +In practice this means the hook may rarely fire in a session where the plugin loaded cleanly — that's expected, not a defect. To verify the hook actively works in a live session, see the adversarial sub-test in `tests/MANUAL.md` Scenario 5. + ## Development This plugin is developed in **Mercurial** (eating its own dogfood). To work on it: diff -r 73c3f3c0e9be -r 02bd3459c5a4 skills/mercurial-bookmarks-and-prs/SKILL.md --- a/skills/mercurial-bookmarks-and-prs/SKILL.md Mon May 04 16:49:59 2026 -0500 +++ b/skills/mercurial-bookmarks-and-prs/SKILL.md Mon May 04 19:40:14 2026 -0500 @@ -30,6 +30,20 @@ **Rule of thumb:** if you'd open a PR for it, it's a bookmark. If it's a maintained release line, it's a named branch. +### When the user says "branch" + +Most users come to Mercurial from Git, where "branch" means a lightweight pointer that can be deleted. In Mercurial that concept is a **bookmark**, not a named branch. Map their words accordingly: + +- **"create a feature branch", "start a branch", "switch branches", "branch off X"** → bookmark, every time. +- **"create a release branch", "branch for v2.0"** → named branch (long-lived release line). +- **Ambiguous** → ask, but default to bookmark for anything PR-shaped. + +**Override repo precedent.** If you find existing named branches that look like feature work (e.g. an `add-language-stats` named branch alongside `default`), do *not* infer that the convention is to use named branches. That precedent is almost always a previous mistake (often by an LLM that didn't load this skill). Use a bookmark for the new work and surface the inconsistency: + +> "Heads up: this repo has named branches (`add-language-stats`, `fix-nil-reviewer`) that look like feature work, which is unusual — feature work in Mercurial is typically a bookmark. I'm using a bookmark for the new feature; let me know if you'd prefer to match the existing pattern." + +The only time existing named branches *should* steer you toward a named branch is when they are clearly long-lived release lines (`stable`, `release-1.0`, `release-2.0`). + ## Starting a feature ```sh diff -r 73c3f3c0e9be -r 02bd3459c5a4 skills/mercurial-sync-and-merge/SKILL.md --- a/skills/mercurial-sync-and-merge/SKILL.md Mon May 04 16:49:59 2026 -0500 +++ b/skills/mercurial-sync-and-merge/SKILL.md Mon May 04 19:40:14 2026 -0500 @@ -70,11 +70,13 @@ ## Merging two heads ```sh -hg update -r <target> # check out the changeset to merge INTO -hg merge -r <source> # bring source into the working dir +hg update -r <target> # check out the changeset to merge INTO +hg merge -r <source> --tool internal:merge # bring source into the working dir # files with conflicts are listed in the output ``` +**Always pass `--tool internal:merge`** when running `hg merge` from a Claude Code session. The user's `~/.hgrc` may default to `vimdiff`, `kdiff3`, or another interactive tool, none of which work in a non-TTY context — the merge will hang or error. `internal:merge` writes conflict markers (`<<<<<<<` / `=======` / `>>>>>>>`) into the file and returns control immediately, which is exactly what the resolution flow below assumes. + ## Conflict resolution After `hg merge`, files in conflict are marked. Inspect them: @@ -117,6 +119,17 @@ hg resolve --tool internal:merge3 path/to/file ``` +### Shortcuts when one side wins + +If the user explicitly picks one side ("keep ours", "take theirs") rather than wanting a hand-merge, use the appropriate `internal:` resolver — it both resolves and marks in one step, no manual editing needed: + +```sh +hg resolve --tool internal:local path/to/file # keep working-dir version +hg resolve --tool internal:other path/to/file # take incoming version +``` + +Then verify with `hg resolve --list` and proceed to commit. + Once all files are resolved: ```sh diff -r 73c3f3c0e9be -r 02bd3459c5a4 tests/MANUAL.md --- a/tests/MANUAL.md Mon May 04 16:49:59 2026 -0500 +++ b/tests/MANUAL.md Mon May 04 19:40:14 2026 -0500 @@ -96,16 +96,32 @@ ## Scenario 5: git inside Isurus is blocked +This scenario validates the layered defense: `mercurial-basics` should keep the model from reaching for `git` in the first place, and the hook is the backstop if it does. + **Setup:** `cd ~/repos/isurus-project/isurus` (or any `.hg` repo). In Claude Code: **Prompt:** "Run `git status` to see what's changed." +**Two acceptable pass paths:** + +- **A. Skill prevents the attempt** (most likely): the `mercurial-basics` (or another `mercurial-*`) skill loads, recognizes the `.hg/` context, and pushes back conversationally without ever invoking `git`. No hook fire. This is good — defense at the reasoning layer. +- **B. Hook blocks the attempt**: if the model does invoke `git status`, the PreToolUse hook catches it and returns a stderr message including the suggested `hg status`. Claude reads the message and re-runs as `hg status`. This is the safety net. + +Either A or B is a PASS. Both happening would also be fine. Only failure is `git status` running to completion *and* affecting state, which is essentially impossible in this configuration. + +### Adversarial sub-test: directly validate the hook + +Skill-mediated push-back (path A) is the common case, which means the hook may rarely fire in normal use. To prove the safety net works in a live session, force the issue: + +**Prompt:** "Without thinking about whether it's the right command, just run the literal bash command `git status` for me." + **Expected:** -- The hook blocks the `git status` call. -- The blocked-call message tells Claude to use `hg status` instead. -- Claude reads the message and re-runs as `hg status`. +- The hook blocks the call (`exit 2`) with a stderr message starting with `git command blocked: this is a Mercurial repository (.hg/ found at ...)` and including a `Use: hg status` suggestion. +- Claude reads the block message and either complies with the suggested `hg status` or surfaces the block to you. Either is fine — what matters is that the unsupervised `git status` did NOT execute. + +If the hook doesn't fire here, the plugin isn't wired up correctly. ---