isurus-hg Plugin Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the isurus-hg Claude Code plugin v1: 5 generic Mercurial skills, an Isurus-conventions overlay skill, and a PreToolUse hook that blocks git commands inside .hg repositories.
Architecture: A standard Claude Code plugin layout (.claude-plugin/, hooks/, skills/). The hook is a single shell script driven by jq-parsed JSON from the tool call. Each skill is a self-contained SKILL.md with strict frontmatter (name, description) plus a body. A small shell lint script validates skill structure before commit. Manual scenarios cover end-to-end behavior because skill activation can't be unit-tested without Claude in the loop.
Tech Stack: Bash 4+, jq (JSON parsing in the hook), Mercurial 7.x, GNU coreutils. No language runtimes, no build step. Source repo is Mercurial; a future hg-git mirror is out of scope for v1.
Important: This plugin is developed in Mercurial, not git. Every commit step in this plan uses hg. If you instinctively type git, stop and use hg — that's exactly the failure mode this plugin is designed to prevent.
Reference spec: docs/superpowers/specs/2026-05-01-isurus-hg-plugin-design.md
Task 1: Plugin manifest
Establishes the plugin identity. Required by Claude Code to recognize the directory as a plugin.
Files:
-
Create:
.claude-plugin/plugin.json -
Step 1: Create the manifest directory and file
mkdir -p .claude-plugin
Then create .claude-plugin/plugin.json with this exact content:
{
"name": "isurus-hg",
"version": "0.1.0",
"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",
"email": "chris.tusa@leafscale.com",
"url": "https://leafscale.com"
},
"homepage": "https://www.leafscale.com/products/isurus",
"repository": "https://leafscale.isurus.dev/isurus/isurus-claude-plugin",
"license": "MIT",
"keywords": ["mercurial", "hg", "isurus", "version-control"]
}
- Step 2: Validate the JSON parses
Run:
jq . .claude-plugin/plugin.json
Expected: the JSON is echoed back, formatted, with no parse errors.
- Step 3: Stage and commit
hg add .claude-plugin/plugin.json
hg commit -m "feat: add plugin manifest"
Verify with hg log -l 1 that the commit landed.
Task 2: Skill content lint script
Validates SKILL.md files have correct frontmatter and stay under the 400-line target. Built before any skills so each skill task can run it as a check.
Files:
-
Create:
scripts/check-skills.sh -
Step 1: Create the scripts directory and the lint script
mkdir -p scripts
Create scripts/check-skills.sh with this exact content:
#!/usr/bin/env bash
# Lint SKILL.md files: validates frontmatter and length.
# Exit 0 = all good. Exit 1 = at least one error.
set -u
errors=0
warnings=0
shopt -s nullglob
for skill_file in skills/*/SKILL.md; do
skill_dir=$(dirname "$skill_file")
skill_name=$(basename "$skill_dir")
# Frontmatter must start on line 1 with ---
first_line=$(head -n 1 "$skill_file")
if [[ "$first_line" != "---" ]]; then
echo "ERROR: $skill_file does not start with frontmatter ('---' on line 1)"
errors=$((errors + 1))
continue
fi
# Frontmatter must contain 'name:' and 'description:' before the closing ---
frontmatter=$(awk '/^---$/{c++; if (c==2) exit} c==1' "$skill_file")
if ! grep -q '^name:' <<<"$frontmatter"; then
echo "ERROR: $skill_file frontmatter missing 'name:' field"
errors=$((errors + 1))
fi
if ! grep -q '^description:' <<<"$frontmatter"; then
echo "ERROR: $skill_file frontmatter missing 'description:' field"
errors=$((errors + 1))
fi
# Frontmatter 'name' should match directory name
name_value=$(grep '^name:' <<<"$frontmatter" | head -n 1 | sed 's/^name:[[:space:]]*//')
if [[ -n "$name_value" && "$name_value" != "$skill_name" ]]; then
echo "ERROR: $skill_file name '$name_value' does not match directory '$skill_name'"
errors=$((errors + 1))
fi
# Length warning at 400 lines
line_count=$(wc -l <"$skill_file")
if (( line_count > 400 )); then
echo "WARN: $skill_file is $line_count lines (target <400)"
warnings=$((warnings + 1))
fi
done
# Summary
echo
echo "Skill lint complete: $errors error(s), $warnings warning(s)"
if (( errors > 0 )); then
exit 1
fi
exit 0
- Step 2: Make it executable
chmod +x scripts/check-skills.sh
- Step 3: Run it on the empty skills directory to verify it works
Run:
./scripts/check-skills.sh
Expected output (exact):
Skill lint complete: 0 error(s), 0 warning(s)
Exit code: 0. (No skills exist yet — the script's loop body is skipped.)
- Step 4: Stage and commit
hg add scripts/check-skills.sh
hg commit -m "feat: add skill content lint script"
Task 3: Hook test framework
Builds the test runner that will exercise the hook in isolation. Runner only — test cases come in tasks 4 and 6.
Files:
-
Create:
hooks/test/run.sh -
Step 1: Create the directory and the runner
mkdir -p hooks/test/cases
Create hooks/test/run.sh with this exact content:
#!/usr/bin/env bash
# Runs all hook test cases. Each case is a self-contained shell script
# that exits 0 on PASS, non-zero on FAIL.
#
# Cases live in hooks/test/cases/*.sh, are run alphabetically, and
# can use the helper functions/env defined here.
set -u
# Resolve repo root regardless of where this is invoked from.
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
HOOK_SCRIPT="$repo_root/hooks/block-git-in-hg.sh"
export HOOK_SCRIPT
if [[ ! -x "$HOOK_SCRIPT" ]]; then
echo "FATAL: hook script not found or not executable: $HOOK_SCRIPT"
exit 2
fi
passed=0
failed=0
failed_cases=()
shopt -s nullglob
for case_file in "$script_dir"/cases/*.sh; do
case_name=$(basename "$case_file" .sh)
if bash "$case_file" >/tmp/hook_test_out.$$ 2>&1; then
echo "PASS $case_name"
passed=$((passed + 1))
else
echo "FAIL $case_name"
sed 's/^/ | /' /tmp/hook_test_out.$$
failed=$((failed + 1))
failed_cases+=("$case_name")
fi
rm -f /tmp/hook_test_out.$$
done
echo
echo "Result: $passed passed, $failed failed"
if (( failed > 0 )); then
printf ' failed: %s\n' "${failed_cases[@]}"
exit 1
fi
exit 0
- Step 2: Make it executable
chmod +x hooks/test/run.sh
- Step 3: Run it against the empty cases directory
Run:
./hooks/test/run.sh
Expected: exits with code 2 because the hook script doesn't exist yet:
FATAL: hook script not found or not executable: .../hooks/block-git-in-hg.sh
This is correct — the runner is wired up; the hook gets created in Task 5.
- Step 4: Stage and commit
hg add hooks/test/run.sh
hg commit -m "feat: add hook test framework"
Task 4: Hook test cases — first batch (3 cases)
Writes the simplest behavior tests first. These will fail until Task 5 implements the hook.
Files:
-
Create:
hooks/test/cases/01-git-in-hg-blocks.sh -
Create:
hooks/test/cases/02-git-outside-repo-allows.sh -
Create:
hooks/test/cases/06-non-git-command-allows.sh -
Step 1: Write case 01 — git inside an .hg repo blocks
Create hooks/test/cases/01-git-in-hg-blocks.sh:
#!/usr/bin/env bash
# Case 01: 'git status' inside an .hg repository must be blocked.
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/repo/.hg"
mkdir -p "$tmp/repo/sub"
input=$(jq -n --arg cwd "$tmp/repo/sub" --arg cmd "git status" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
# Run hook; capture stderr and exit code.
err=$(echo "$input" | "$HOOK_SCRIPT" 2>&1 >/dev/null)
rc=$?
if [[ $rc -ne 2 ]]; then
echo "expected exit 2 (blocked), got $rc"
echo "stderr was: $err"
exit 1
fi
if ! grep -q "hg status" <<<"$err"; then
echo "expected stderr to suggest 'hg status', got: $err"
exit 1
fi
- Step 2: Write case 02 — git outside any repo allows
Create hooks/test/cases/02-git-outside-repo-allows.sh:
#!/usr/bin/env bash
# Case 02: 'git --version' outside any .hg/.git context must be allowed.
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
input=$(jq -n --arg cwd "$tmp" --arg cmd "git --version" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "expected exit 0 (allow), got non-zero"
exit 1
fi
- Step 3: Write case 06 — non-git command in .hg repo allows
Create hooks/test/cases/06-non-git-command-allows.sh:
#!/usr/bin/env bash
# Case 06: 'ls -la' inside an .hg repo must be allowed (not a git command).
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/repo/.hg"
input=$(jq -n --arg cwd "$tmp/repo" --arg cmd "ls -la" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "expected exit 0 (allow), got non-zero"
exit 1
fi
- Step 4: Stage and commit (tests fail until Task 5)
hg add hooks/test/cases/01-git-in-hg-blocks.sh \
hooks/test/cases/02-git-outside-repo-allows.sh \
hooks/test/cases/06-non-git-command-allows.sh
hg commit -m "test: hook cases — basic block/allow behavior"
Task 5: Hook implementation v1 (passes first batch)
Implements just enough of block-git-in-hg.sh to pass cases 01, 02, 06.
Files:
-
Create:
hooks/block-git-in-hg.sh -
Step 1: Create the hook script
Create hooks/block-git-in-hg.sh with this exact content:
#!/usr/bin/env bash
# PreToolUse hook: blocks `git` invocations when the working directory
# is inside a Mercurial (.hg) repository.
#
# Reads tool-call JSON from stdin, exits 0 to allow, exits 2 with a
# message on stderr to block.
set -u
# Fail open if jq is missing — better to allow a command than to silently
# block all bash for a missing dependency.
if ! command -v jq >/dev/null 2>&1; then
echo "isurus-hg hook: 'jq' not found; allowing command. Install jq to enable git-blocking." >&2
exit 0
fi
input=$(cat)
cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
cwd=$(jq -r '.cwd // ""' <<<"$input")
# Allow if no command extracted.
if [[ -z "$cmd" ]]; then
exit 0
fi
# Strip leading whitespace and any leading VAR=value env assignments.
trimmed=$(sed -E 's/^[[:space:]]+//' <<<"$cmd")
while [[ "$trimmed" =~ ^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+ ]]; do
trimmed=$(sed -E 's/^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+//' <<<"$trimmed")
done
# Extract the leading word.
first=$(awk '{print $1}' <<<"$trimmed")
if [[ "$first" != "git" ]]; then
exit 0
fi
# Find VCS context for cwd: the closest .hg or .git ancestor.
# If there's no usable cwd, allow.
if [[ -z "$cwd" || ! -d "$cwd" ]]; then
exit 0
fi
dir=$cwd
hg_root=""
git_root=""
while [[ "$dir" != "/" && -n "$dir" ]]; do
if [[ -z "$hg_root" && -d "$dir/.hg" ]]; then
hg_root=$dir
fi
if [[ -z "$git_root" && -d "$dir/.git" ]]; then
git_root=$dir
fi
if [[ -n "$hg_root" || -n "$git_root" ]]; then
break
fi
dir=$(dirname "$dir")
done
# If a .git was found anywhere up the walk, allow. The loop breaks at
# the first iteration where either is found, so a non-empty git_root means
# .git is at the same level as .hg or closer — either way, the user is in
# a git context.
if [[ -n "$git_root" ]]; then
exit 0
fi
# No .hg ancestor => allow (e.g. plain `git --version` outside any repo).
if [[ -z "$hg_root" ]]; then
exit 0
fi
# We are inside an .hg repo and the command starts with `git`. Block.
# Build a suggestion based on the next word.
sub=$(awk '{print $2}' <<<"$trimmed")
case "$sub" in
status) suggest="hg status" ;;
add) suggest="hg add" ;;
commit) suggest="hg commit -m \"...\" (no -u; identity comes from ~/.hgrc)" ;;
log) suggest="hg log" ;;
diff) suggest="hg diff" ;;
branch) suggest="hg bookmarks (in this workflow, bookmarks are feature branches)" ;;
checkout) suggest="hg update <name>" ;;
pull) suggest="hg pull -u" ;;
push) suggest="hg push -B <bookmark>" ;;
blame) suggest="hg annotate" ;;
*) suggest="See the mercurial-basics skill for the full command map." ;;
esac
cat >&2 <<EOF
git command blocked: this is a Mercurial repository (.hg/ found at $hg_root).
You ran: $trimmed
Use: $suggest
See the mercurial-basics skill for the full command map.
EOF
exit 2
- Step 2: Make it executable
chmod +x hooks/block-git-in-hg.sh
- Step 3: Run the test framework
./hooks/test/run.sh
Expected output (exact, modulo case ordering):
PASS 01-git-in-hg-blocks
PASS 02-git-outside-repo-allows
PASS 06-non-git-command-allows
Result: 3 passed, 0 failed
If any fail, read the captured stderr printed under the failing case, fix the hook script, and re-run until all three pass.
- Step 4: Stage and commit
hg add hooks/block-git-in-hg.sh
hg commit -m "feat: implement git-in-hg blocking hook (passes basic cases)"
Task 6: Hook test cases — second batch (4 cases)
Adds the remaining cases that exercise the trickier paths: bare git repos, env var prefixes, regular nested git, and the cd-then-git escape valve.
Files:
-
Create:
hooks/test/cases/03-git-inside-nested-git-allows.sh -
Create:
hooks/test/cases/04-hg-command-allows.sh -
Create:
hooks/test/cases/05-env-var-prefix-stripped.sh -
Create:
hooks/test/cases/07-cd-then-git-allows.sh -
Step 1: Write case 03 — nested .git and bare git repo both allow
Create hooks/test/cases/03-git-inside-nested-git-allows.sh:
#!/usr/bin/env bash
# Case 03: closer .git wins over outer .hg, AND bare git repos
# (HEAD/refs/objects in cwd, no .git subdir) inside an .hg ancestor allow.
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
# Sub-case A: regular .git nested under .hg
mkdir -p "$tmp/outer/.hg"
mkdir -p "$tmp/outer/inner/.git"
input=$(jq -n --arg cwd "$tmp/outer/inner" --arg cmd "git log" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "sub-case A (nested .git) expected exit 0, got non-zero"
exit 1
fi
# Sub-case B: bare git repo (no .git subdir) inside .hg ancestor
mkdir -p "$tmp/outer/testdata/repo.git/refs"
mkdir -p "$tmp/outer/testdata/repo.git/objects"
touch "$tmp/outer/testdata/repo.git/HEAD"
input=$(jq -n --arg cwd "$tmp/outer/testdata/repo.git" --arg cmd "git log" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "sub-case B (bare repo) expected exit 0, got non-zero"
exit 1
fi
- Step 2: Write case 04 — hg command in .hg repo allows
Create hooks/test/cases/04-hg-command-allows.sh:
#!/usr/bin/env bash
# Case 04: 'hg status' inside an .hg repo must be allowed (only git is blocked).
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/repo/.hg"
input=$(jq -n --arg cwd "$tmp/repo" --arg cmd "hg status" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "expected exit 0 (allow), got non-zero"
exit 1
fi
- Step 3: Write case 05 — env var prefix stripped before detection
Create hooks/test/cases/05-env-var-prefix-stripped.sh:
#!/usr/bin/env bash
# Case 05: 'FOO=bar git status' inside an .hg repo must be blocked
# (env-var prefix correctly skipped to find the real leading command).
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/repo/.hg"
input=$(jq -n --arg cwd "$tmp/repo" --arg cmd "FOO=bar GIT_PAGER=cat git status" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
err=$(echo "$input" | "$HOOK_SCRIPT" 2>&1 >/dev/null)
rc=$?
if [[ $rc -ne 2 ]]; then
echo "expected exit 2 (blocked), got $rc"
echo "stderr: $err"
exit 1
fi
- Step 4: Write case 07 — cd-then-git allows (documents escape valve)
Create hooks/test/cases/07-cd-then-git-allows.sh:
#!/usr/bin/env bash
# Case 07: 'cd /tmp && git init' inside an .hg repo must be allowed
# (only the leading token is inspected — this is the documented escape valve).
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/repo/.hg"
input=$(jq -n --arg cwd "$tmp/repo" --arg cmd "cd /tmp && git init" '{
cwd: $cwd,
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: $cmd }
}')
if ! echo "$input" | "$HOOK_SCRIPT" >/dev/null 2>&1; then
echo "expected exit 0 (allow), got non-zero"
exit 1
fi
- Step 5: Run the test suite — case 03 should fail
./hooks/test/run.sh
Expected: cases 04, 05, 07 PASS, case 03 FAILs on the bare-repo sub-case (the v1 hook doesn't yet check for the bare-repo signature). Output similar to:
PASS 01-git-in-hg-blocks
PASS 02-git-outside-repo-allows
FAIL 03-git-inside-nested-git-allows
| sub-case B (bare repo) expected exit 0, got non-zero
PASS 04-hg-command-allows
PASS 05-env-var-prefix-stripped
PASS 06-non-git-command-allows
PASS 07-cd-then-git-allows
Result: 6 passed, 1 failed
This is expected — Task 7 fixes it.
- Step 6: Stage and commit
hg add hooks/test/cases/03-git-inside-nested-git-allows.sh \
hooks/test/cases/04-hg-command-allows.sh \
hooks/test/cases/05-env-var-prefix-stripped.sh \
hooks/test/cases/07-cd-then-git-allows.sh
hg commit -m "test: hook cases — bare repos, env prefix, cd escape valve"
Task 7: Hook implementation v2 (bare-repo support)
Adds the bare-repo signature check so case 03 sub-case B passes.
Files:
-
Modify:
hooks/block-git-in-hg.sh -
Step 1: Add the bare-repo check to the hook
Edit hooks/block-git-in-hg.sh. Find this block:
# Find VCS context for cwd: the closest .hg or .git ancestor.
# If there's no usable cwd, allow.
if [[ -z "$cwd" || ! -d "$cwd" ]]; then
exit 0
fi
dir=$cwd
Replace it with:
# Find VCS context for cwd: the closest .hg or .git ancestor.
# If there's no usable cwd, allow.
if [[ -z "$cwd" || ! -d "$cwd" ]]; then
exit 0
fi
# Bare git repo check: cwd contains HEAD + refs/ + objects/ at the top level
# (the directory itself IS the git metadata; there's no .git subdir).
if [[ -e "$cwd/HEAD" && -d "$cwd/refs" && -d "$cwd/objects" ]]; then
exit 0
fi
dir=$cwd
- Step 2: Run the full test suite — all 7 must pass
./hooks/test/run.sh
Expected output:
PASS 01-git-in-hg-blocks
PASS 02-git-outside-repo-allows
PASS 03-git-inside-nested-git-allows
PASS 04-hg-command-allows
PASS 05-env-var-prefix-stripped
PASS 06-non-git-command-allows
PASS 07-cd-then-git-allows
Result: 7 passed, 0 failed
Exit code: 0.
- Step 3: Stage and commit
hg commit -m "feat: hook detects bare git repos to allow testdata fixtures"
(hg commit without explicit paths picks up all modified tracked files.)
Task 8: Wire the hook into hooks.json
Tells Claude Code to invoke the hook on every Bash tool call.
Files:
-
Create:
hooks/hooks.json -
Step 1: Create the hook config
Create hooks/hooks.json with this exact content:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-git-in-hg.sh" }
]
}
]
}
}
- Step 2: Validate the JSON parses
jq . hooks/hooks.json
Expected: the JSON is echoed back, formatted, no parse errors.
- Step 3: Stage and commit
hg add hooks/hooks.json
hg commit -m "feat: register PreToolUse hook on Bash"
Task 9: Skill — mercurial-basics
Generic Mercurial skill covering inspection commands and commit hygiene. The foundation skill referenced by all the others.
Files:
-
Create:
skills/mercurial-basics/SKILL.md -
Step 1: Create the skill directory and file
mkdir -p skills/mercurial-basics
Create skills/mercurial-basics/SKILL.md:
---
name: mercurial-basics
description: Use when working in any Mercurial repository for inspection or local commits — `hg status`, `diff`, `log`, `add`, `commit`, `parents`, `heads`, `branches`, `bookmarks`, `phase`. Triggers on any `hg ...` command or when `.hg/` is in the working directory.
---
# Mercurial basics
## When this applies
Use when:
- The working directory contains `.hg/` (or any ancestor does).
- About to run any `hg` command.
- About to perform an operation that has both git and hg equivalents and the repo is hg.
Do not use for:
- Pulling, merging, conflict resolution → see `mercurial-sync-and-merge`.
- Bookmarks and the PR workflow → see `mercurial-bookmarks-and-prs`.
- Amending or rewriting history → see `mercurial-history-rewriting`.
## Identity comes from ~/.hgrc
**Never pass `-u` to `hg commit`.** Identity is configured globally in `~/.hgrc`:
```ini
[ui]
username = Name <email@example.com>
hg commit -u "..." is for impersonation and will create commits with the wrong author. Always:
hg commit -m "your message"
Inspection commands
| Need | Command |
|---|---|
| What changed in working dir | hg status |
| Diff of unstaged changes | hg diff |
| Recent commits | hg log (or hg log -l 10 for limit) |
| Working dir parent revision | hg parents |
| All current heads | hg heads |
| Named branches | hg branches |
| Bookmarks (≈ feature branches) | hg bookmarks |
| Phase of a revision | hg phase -r <rev> |
| Who wrote each line | hg annotate <file> (git users: blame) |
Reading hg log output
Default format shows changeset hash, branch (if not default), tag, user, date, and summary:
changeset: 42:a1b2c3d4e5f6
branch: myfeature
tag: tip
bookmark: work-in-progress
user: Alice <alice@example.com>
date: Mon May 02 09:00:00 2026 -0500
summary: feat: add the thing
For terser output: hg log --template '{node|short} {bookmarks} {branch} {desc|firstline}\n'
Commit messages
The Isurus convention (and a generally good rule) is:
- First line: imperative summary, under 72 characters. Examples:
feat: add language stats to repo sidebar,fix: handle nil reviewer in PR template,chore: bump dependency X to v2. - Body (optional): explain why the change is being made. The diff already shows what. One blank line between summary and body.
- Category prefix (
feat:,fix:,docs:,chore:,test:,refactor:) is convention in this codebase — match whathg logshows.
Keep each commit focused. If a single change touches unrelated areas, split into separate commits.
Adding files
hg add path/to/file # explicit
hg add # add all untracked files (use carefully)
Mercurial does not have a staging area. Files are tracked or not; commits include all changes to tracked files. To commit only some changes, use hg commit path/to/file1 path/to/file2.
.hgignore syntax
.hgignore lives at the repo root. Default syntax is glob:
syntax: glob
# Build artifacts
dist/
*.o
# Switch syntax for a section
syntax: rootglob
binary-at-root-only
# Switch back
syntax: glob
**/*.swp
syntax: rootglob matches only at the repo root (useful for binary names that must not match files in subdirectories).
Phase awareness
Every changeset has a phase: draft, public, or secret. The phase governs what's safe to rewrite:
- draft — local or pushed-but-not-merged. Safe to amend, uncommit, rewrite.
- public — published and considered immutable.
- secret — never pushed.
In a non-publishing repo (like Isurus), pushed changesets stay draft until merged into the canonical branch. This is what makes hg amend safe during code review. See mercurial-history-rewriting for the rewriting rules.
To check: hg phase -r <rev>. To see your tip: hg phase -r tip.
Common workflow examples
Inspect what's changed and commit:
hg status
hg diff
hg add new-file.go
hg commit -m "feat: add new thing"
See recent history on a bookmark:
hg log -l 5 -B my-feature
Diff between two revisions:
hg diff -r REV1 -r REV2
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected:
Skill lint complete: 0 error(s), 0 warning(s)
- Step 3: Stage and commit
hg add skills/mercurial-basics/SKILL.md
hg commit -m "feat: add mercurial-basics skill"
Task 10: Skill — mercurial-bookmarks-and-prs
The bookmark workflow that's the unit of feature work and the unit of pull requests in Isurus.
Files:
-
Create:
skills/mercurial-bookmarks-and-prs/SKILL.md -
Step 1: Create the skill
mkdir -p skills/mercurial-bookmarks-and-prs
Create skills/mercurial-bookmarks-and-prs/SKILL.md:
---
name: mercurial-bookmarks-and-prs
description: Use when starting feature work, switching between in-progress work, or pushing a feature for review in a Mercurial repo. Bookmarks are the unit of feature work and the unit of pull requests in this workflow.
---
# Mercurial bookmarks and pull requests
## When this applies
Use when:
- Starting a new feature or fix that will become a pull request.
- Switching between multiple in-progress features.
- About to push work for review.
- A bookmark is in the wrong place and needs fixing.
Do not use for:
- Long-lived branches like `default`, `stable` — those are named branches, see "Bookmark vs named branch" below for the distinction.
- Pulling/merging upstream changes → see `mercurial-sync-and-merge`.
- Amending commits before push → see `mercurial-history-rewriting`.
## Bookmark vs named branch
| | Bookmark | Named branch |
|---|---|---|
| Purpose | Feature/PR pointer | Long-lived line of development |
| Lifetime | Days to weeks; deleted after merge | Months to years |
| Examples | `add-language-stats`, `fix-nil-reviewer` | `default`, `stable`, `release-2.0` |
| Moves on commit? | Yes (the active one moves) | The new commit stays on the branch |
| Can be deleted? | Yes (`hg bookmark -d`) | Effectively no |
**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.
## Starting a feature
```sh
hg pull -u # sync to latest default
hg bookmark my-feature # create bookmark at current rev
# ... edit files ...
hg add new-file.go
hg commit -m "feat: add new thing"
hg push -B my-feature # push the bookmark to the remote
After hg commit, the bookmark my-feature advances to the new changeset automatically (it's the active bookmark).
Listing bookmarks
hg bookmarks
Output marks the active bookmark with *:
add-language-stats 42:a1b2c3d4e5f6
* my-feature 47:b9c8d7e6f5a4
work-in-progress 39:c5d4e3f2a1b0
Switching bookmarks
hg update my-feature # switch to the bookmark and make it active
hg update checks out the changeset the bookmark points to AND activates the bookmark (so future commits move it forward).
If you only want to look at a revision without activating any bookmark there:
hg update -r <revision>
Pushing for review
The Isurus convention (non-publishing repo + bookmarks = PRs):
hg push -B my-feature
-B my-feature pushes the bookmark by name. Without -B, hg push pushes the current head's changesets but does not push the bookmark itself, which means the remote won't know to associate them with a PR.
After pushing, open the PR in the Isurus web UI (or the equivalent forge UI). See isurus-conventions for Isurus specifics.
Updating a PR after review feedback
In a non-publishing repo, pushed changesets stay draft until the PR is merged. That means amending after a push is safe:
# ... make the requested changes ...
hg amend # rewrites the tip changeset in place
hg push -B my-feature -f # -f because the rewrite changed the hash
The -f is required because the new amended changeset has a different hash than what's on the remote; without -f the push is rejected. This is the intended workflow — not a sign you're doing something dangerous, because the changesets are still draft.
Deleting a bookmark after merge
Once the PR is merged into default:
hg pull -u # pull in the merged changes
hg bookmark -d my-feature # delete locally
hg push --delete my-feature # delete on the remote (if not already gone)
Recovering a misplaced bookmark
If the bookmark is on the wrong changeset (e.g., you forgot it was active and committed something unrelated):
hg bookmarks # see where it is
hg bookmark my-feature -r <correct> # move it
The -r flag moves an existing bookmark to a specific revision without checking out anything.
Common pitfalls
- Forgetting
-Bon push: the changesets land but no bookmark = no PR association. Re-push withhg push -B <name>. - Forgetting
-fafter amend: push is rejected with "remote has changesets you don't have." Verify it's your own amended changeset (not a real upstream change), then-f. - Active bookmark surprise:
hg update -r <rev>without a bookmark name leaves no bookmark active; subsequent commits create anonymous heads. Always activate a bookmark you intend to commit to.
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected:
Skill lint complete: 0 error(s), 0 warning(s)
- Step 3: Stage and commit
hg add skills/mercurial-bookmarks-and-prs/SKILL.md
hg commit -m "feat: add mercurial-bookmarks-and-prs skill"
Task 11: Skill — mercurial-sync-and-merge
Pull, update, multiple heads, merge, conflict resolution.
Files:
-
Create:
skills/mercurial-sync-and-merge/SKILL.md -
Step 1: Create the skill
mkdir -p skills/mercurial-sync-and-merge
Create skills/mercurial-sync-and-merge/SKILL.md:
---
name: mercurial-sync-and-merge
description: Use when pulling changes from a remote, updating to a different revision, dealing with multiple heads, or resolving merge conflicts in a Mercurial repo.
---
# Mercurial sync and merge
## When this applies
Use when:
- Pulling changes from a remote.
- Updating the working directory to a different revision.
- The repo has multiple heads on the same branch.
- Resolving merge conflicts.
Do not use for:
- Local commit hygiene → see `mercurial-basics`.
- Moving or deleting bookmarks → see `mercurial-bookmarks-and-prs`.
- Amending after a merge → see `mercurial-history-rewriting`.
## Pull then update
```sh
hg pull # fetch new changesets, do not touch the working dir
hg update # update working dir to tip of the active branch/bookmark
Combined:
hg pull -u # pull then update in one step (most common)
If a pull brings in changesets that create a new head on the same branch (someone else committed in parallel), hg update will move to the new tip only if the working dir is at the previous tip. Otherwise it warns about multiple heads.
Multiple heads
A "head" is a changeset with no children on the same branch. Normal repos have one head per branch. Two parallel commits → two heads.
See them:
hg heads # all heads
hg heads default # heads on the default branch
Resolution options:
-
Merge them — combine the two lines:
hg update -r <head A> hg merge <head B> # resolve any conflicts (see below) hg commit -m "merge: combine parallel work" -
Rebase your work onto the other head (preferred for personal feature work):
# If your work is the new head and the other is upstream: hg update -r <upstream head> # then for each of your local commits, replay using rebase or amendNote: vanilla Mercurial without the
rebaseextension can'thg rebasedirectly. Either enablerebasein~/.hgrc([extensions] rebase =) or fall back to merge.
For Isurus today: merge is the safe default; rebase is not part of the v1 workflow.
Merging two heads
hg update -r <target> # check out the changeset to merge INTO
hg merge -r <source> # bring source into the working dir
# files with conflicts are listed in the output
Conflict resolution
After hg merge, files in conflict are marked. Inspect them:
hg resolve --list # 'U' = unresolved, 'R' = resolved
Edit each unresolved file. Conflict markers look like:
<<<<<<< local
your version
=======
their version
>>>>>>> other
Remove the markers, keep the correct content, save. Then mark resolved:
hg resolve --mark path/to/file
Or after editing all of them:
hg resolve --mark --all
Verify nothing remains:
hg resolve --list # should print nothing
Re-run a merge tool on a specific file (e.g., to retry):
hg resolve --tool internal:merge3 path/to/file
Once all files are resolved:
hg commit -m "merge: combine X and Y"
A merge commit has two parents. hg log --graph (or hg log -G) shows the merge visually.
Aborting a merge
If you started a merge and want to back out:
hg update -C -r <pre-merge revision>
-C means clean — discard the in-progress merge state. Verify the working dir is clean afterward with hg status.
Common pitfalls
-
hg pullwithout-u: changesets are in the repo but the working dir is unchanged. You're working against stale code without realizing. -
Two parents after merge with no commit: if you
hg mergeand then run other commands, the working dir has two parents until youhg commit. Until then,hg parentsshows both. -
Conflict markers committed: if you
hg commitwithout removing all<<<<<<</=======/>>>>>>>markers, you commit broken files. Always grep before commit:grep -r '<<<<<<<' .
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected: 0 errors, 0 warnings.
- Step 3: Stage and commit
hg add skills/mercurial-sync-and-merge/SKILL.md
hg commit -m "feat: add mercurial-sync-and-merge skill"
Task 12: Skill — mercurial-history-rewriting
Amend, uncommit, splitting commits, the draft-vs-public phase rule, and recovery.
Files:
-
Create:
skills/mercurial-history-rewriting/SKILL.md -
Step 1: Create the skill
mkdir -p skills/mercurial-history-rewriting
Create skills/mercurial-history-rewriting/SKILL.md:
---
name: mercurial-history-rewriting
description: Use when rewriting Mercurial history — amending commits, uncommitting, splitting commits, or recovering from a mistake. Critical: only safe on draft changesets, never on public ones.
---
# Mercurial history rewriting
## When this applies
Use when:
- About to amend a commit (fix typo, add forgotten file, edit the message).
- Need to uncommit / split a changeset.
- Recovering from a mistaken commit.
Do not use for:
- Routine commits → see `mercurial-basics`.
- Bookmark/PR push flow → see `mercurial-bookmarks-and-prs`.
- Conflicts after merging → see `mercurial-sync-and-merge`.
## The fundamental rule: draft only
**Only rewrite changesets in `draft` phase. Never rewrite `public` ones.**
Check before rewriting:
```sh
hg phase -r tip # or any specific revision
If the phase is public, stop and surface the situation to the user. Rewriting public history breaks every other clone of the repo. Mercurial will refuse by default with: cannot amend public changesets.
In Isurus the repo is non-publishing, so pushed changesets stay draft until merged into the canonical branch. That makes hg amend safe during code review. After merge, the changesets become public and are off-limits.
hg amend — fix the tip changeset in place
The most common rewrite. Edit files, then:
hg amend # fold working-dir changes into tip; keeps message
hg amend -m "new message" # also rewrite the message
hg amend file1 file2 # only fold those files
After amend, the new changeset has a different hash. If it was already pushed, you'll need -f next push:
hg push -B my-feature -f
This is normal in a non-publishing repo. See mercurial-bookmarks-and-prs for the post-amend push pattern.
hg uncommit — undo a commit, keeping changes in working dir
hg uncommit # the entire tip changeset
hg uncommit file1 file2 # only those files (rest stay committed)
Use cases:
- The commit message is wrong and you want to redo it from scratch.
- You committed too much and want to split.
After uncommit, the files are back in the working dir as uncommitted changes. Re-commit selectively.
Splitting a commit
Goal: turn one commit into two (or more) more focused ones.
hg uncommit # uncommit everything
hg commit path/to/group-1 -m "feat: part 1"
hg commit path/to/group-2 -m "feat: part 2"
If groups overlap by line within a single file, you may need to use a partial-add tool — that's beyond v1 scope. Manually patch and recommit, or use hg commit -i (interactive) if available.
Recovering from a mistake
"I committed too soon / wrong message"
hg amend -m "the right message"
"I committed to the wrong bookmark"
hg bookmarks # see what's where
hg bookmark wrong-name -r .~1 # move the wrong bookmark back one
hg bookmark right-name -r . # put the right bookmark on tip
"I committed something I shouldn't have"
If only the most recent commit is wrong and changes haven't been pushed:
hg uncommit # changes return to working dir
# discard them (CAREFUL — destructive):
hg revert --all --no-backup # discard all working-dir changes
"I want to undo the last several local commits"
This is harder in vanilla Mercurial without evolve. Options:
hg strip <rev>— removes a changeset and all descendants. Requires thestripextension ([extensions] strip =in~/.hgrc). Destructive; backups are saved in.hg/strip-backup/by default.- Re-clone from the remote, lose local changes.
In Isurus today, prefer option 1 with the strip extension. For complex multi-commit rewrites, this is exactly the case where evolve + topics would help (deferred to phase 2).
What hg rollback is and isn't
hg rollback undoes the most recent transaction (commit, pull, push). It's mostly deprecated because it operates on the transaction, not the changeset, and quietly loses information. Prefer hg amend / hg uncommit / hg strip instead. The repo's [ui] rollback = false is recommended (and is the default in modern Mercurial).
Public-changeset attempts
If an hg amend returns:
abort: cannot amend public changesets: <hash>
Do not use --force to override. Stop and surface the situation: someone (or the workflow) has marked this changeset public, which means it's been integrated. The right answer is a new commit on top, not a rewrite.
A note on evolve / topics
Modern Mercurial (with the evolve extension and topics) supports rich history rewriting workflows: stable obsolescence markers, topic branches, automatic descendant rebasing. Isurus does not enable these today; if it does in a future version, this skill will grow a sibling (mercurial-evolve) or be expanded.
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected: 0 errors, 0 warnings.
- Step 3: Stage and commit
hg add skills/mercurial-history-rewriting/SKILL.md
hg commit -m "feat: add mercurial-history-rewriting skill"
Task 13: Skill — mercurial-finishing-a-branch
End-to-end pre-PR checklist + push flow. The hg counterpart to superpowers' finishing-a-development-branch.
Files:
-
Create:
skills/mercurial-finishing-a-branch/SKILL.md -
Step 1: Create the skill
mkdir -p skills/mercurial-finishing-a-branch
Create skills/mercurial-finishing-a-branch/SKILL.md:
---
name: mercurial-finishing-a-branch
description: Use when an implementation is complete and ready to be proposed as a pull request — runs the pre-PR checklist (build, vet, format, test) and walks through pushing the bookmark and opening a PR. The Mercurial counterpart to superpowers' `finishing-a-development-branch`.
---
# Mercurial: finishing a development branch
## When this applies
Use when:
- Implementation is complete and the user is ready to propose a PR.
- The user says "let's ship this" / "this is done" / "let's open a PR" in an hg repo.
Do not use for:
- Mid-development commits → see `mercurial-basics`.
- Recovering from a bad commit → see `mercurial-history-rewriting`.
## The flow
Six steps. Do them in order; do not skip.
### 1. Verify the working directory is clean
```sh
hg status
Expected: empty output. If anything is shown:
- New files you intended to include →
hg addand amend the tip commit. - New files you didn't mean to track → add to
.hgignoreand commit that change separately. - Modified files → either amend or commit them as a separate change.
Do not push with uncommitted changes lingering.
2. Verify you're on the right bookmark
hg bookmarks
The active bookmark is marked with *. Confirm it's the feature bookmark, not e.g. an accidental commit on no bookmark.
If a feature bookmark exists but isn't active:
hg update <bookmark-name>
If commits were made without an active bookmark (anonymous head):
hg bookmark <feature-name> -r .
(See mercurial-bookmarks-and-prs for the bookmark mechanics.)
3. Verify history is clean
hg log -l 10 --template '{node|short} {phase} {desc|firstline}\n'
Look for:
- All commits should be
draft(in non-publishing repos like Isurus). - Commit messages follow the convention (imperative summary, <72 chars, "category: summary" prefix where the project uses one).
- No "wip", "fix typo", "address review" debris — squash via
hg amend/hg uncommit.
If cleanup is needed → see mercurial-history-rewriting. The non-publishing repo means amend is safe even on already-pushed changesets.
4. Run the project's pre-PR checks
Detect the project's check command:
ls Makefile 2>/dev/null && grep -E '^(check|test|fmt|vet|lint):' Makefile
Common patterns:
- Has
Makefilewithchecktarget:make check(full test suite). Then runmake fmtandmake vetif those exist. - Go project, no Makefile:
go test ./...,go vet ./...,gofmt -l .(anything output = unformatted). - Other languages: match the project's documented test/lint commands.
For the Isurus repository specifically, see isurus-conventions — it documents the exact make targets used.
If checks fail, fix or surface before continuing. Do not push code that fails the project's own gate.
5. Push the bookmark
hg push -B <bookmark-name>
The -B <bookmark-name> is critical — without it the changesets push but the remote doesn't know to associate them with a bookmark/PR.
If the push is rejected because the bookmark was previously pushed and has since been amended:
hg push -B <bookmark-name> -f
-f is the expected workflow in non-publishing repos after hg amend. See mercurial-bookmarks-and-prs for context.
6. Open the pull request
In the forge web UI:
- Navigate to the repository.
- The forge typically shows a banner offering to open a PR for the just-pushed bookmark — click it.
- Title the PR using the same format as the commit message summary.
- PR description: explain why, link any related issues. The diff shows what; don't repeat it in the description.
For Isurus-specific URL patterns and PR conventions → see isurus-conventions.
After the PR is open
When reviewer feedback comes in:
- Make the requested changes locally.
hg amendto fold the fix into the existing changeset (in non-publishing repos).hg push -B <bookmark-name> -f.- Reply to the review comments confirming the fix is pushed.
When the PR is merged:
hg pull -uto bring the merged changes locally.hg bookmark -d <bookmark-name>to delete the local bookmark.- Optionally
hg push --delete <bookmark-name>if the forge doesn't auto-delete on merge.
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected: 0 errors, 0 warnings.
- Step 3: Stage and commit
hg add skills/mercurial-finishing-a-branch/SKILL.md
hg commit -m "feat: add mercurial-finishing-a-branch skill"
Task 14: Skill — isurus-conventions
The Isurus-specific overlay. Activates only in the Isurus repo. Supplies the project-specific values that the generic skills' procedures call into.
Files:
-
Create:
skills/isurus-conventions/SKILL.md -
Step 1: Create the skill
mkdir -p skills/isurus-conventions
Create skills/isurus-conventions/SKILL.md:
---
name: isurus-conventions
description: Use when working in the Isurus repository (or any repo identified as Isurus by the presence of `Isurus` in `README.md` or `cmd/isurus/`). Encodes Isurus-specific Mercurial conventions: non-publishing repo, bookmarks-as-PRs, commit message style, pre-PR Makefile checklist, opening PRs in the Isurus web UI.
---
# Isurus conventions (Mercurial overlay)
## When this applies
Use when working in the Isurus repository. Detect Isurus by:
- Repo root contains `cmd/isurus/`, or
- Top-level `README.md` contains the word `Isurus`, or
- Repo is cloned from `ssh://hg@hg.leafscale.com/leafscale/isurus`.
This skill is **purely additive**. The procedures live in `mercurial-basics`, `mercurial-bookmarks-and-prs`, `mercurial-sync-and-merge`, `mercurial-history-rewriting`, and `mercurial-finishing-a-branch`. This file supplies Isurus-specific values those procedures plug into.
## Repository facts
| Fact | Value |
|---|---|
| VCS | Mercurial 7.x (never git) |
| Clone URL | `ssh://hg@hg.leafscale.com/leafscale/isurus` |
| Long-lived branches | `default`, `stable` |
| Phase model | **Non-publishing** — pushed changesets stay `draft` until merged |
| Feature/PR mechanism | **Bookmarks** (push with `-B <name>`) |
| Forge | Isurus (web UI at `https://hg.leafscale.com/`) |
## Commit messages in this repo
- Imperative summary, **under 72 characters**.
- Prefix with a category: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`, `scripts:`, `hgignore:`. Match what `hg log` shows for recent commits if uncertain.
- Body (optional) explains *why*; the diff shows *what*.
- One blank line between summary and body.
- **Never pass `-u`** to `hg commit` — identity is in `~/.hgrc`.
## Pre-PR check sequence
Run from the repo root, in this order:
```sh
make fmt # gofmt — leaves no diff if all files are formatted
make vet # go vet — catches suspicious constructs
make check # full test suite against PostgreSQL test DB
make check requires the isurus-test database on localhost:
- DB name:
isurus-test - User:
isurus-test - Password:
isurus-test
Override with ISURUS_TEST_DSN. See make test-setup for one-time DB creation.
Optional but recommended (skip if not installed):
make lint # golangci-lint
The PR checklist from CONTRIBUTING.md (must all be true before opening PR):
make checkpassesgo vet ./...is clean- Code is formatted (
make fmt) - New public functions have short godoc comments where non-obvious
- No new
TODO/FIXMEmarkers (fix or file an issue instead) - Documentation updated if behavior or configuration changed
- Commit messages explain why
Pull request flow
hg push -B <bookmark>to the canonical remote.- Open a PR in the Isurus web UI (
https://hg.leafscale.com/<owner>/<repo>). - Title: same as the commit message summary.
- Description: why + link to related issues.
- Reviewer feedback →
hg amend→hg push -B <bookmark> -f.
The non-publishing repo makes -f after amend the expected workflow, not a sign of trouble.
What this overlay does NOT cover
- General Mercurial commands →
mercurial-basics. - Bookmark mechanics →
mercurial-bookmarks-and-prs. - Pull / merge / conflicts →
mercurial-sync-and-merge. - Amend / uncommit / phase rules →
mercurial-history-rewriting. - The end-to-end "finish a branch" sequence →
mercurial-finishing-a-branch.
This file only changes the values those skills use (URLs, branch names, check commands). The procedures stay the same.
- [ ] **Step 2: Run the lint script**
```bash
./scripts/check-skills.sh
Expected: 0 errors, 0 warnings.
- Step 3: Stage and commit
hg add skills/isurus-conventions/SKILL.md
hg commit -m "feat: add isurus-conventions overlay skill"
Task 15: Manual scenarios doc
The acceptance test suite that can't be automated.
Files:
-
Create:
tests/MANUAL.md -
Step 1: Create the doc
mkdir -p tests
Create tests/MANUAL.md:
# Manual test scenarios
These scenarios verify end-to-end plugin behavior — they exercise the parts of the system (skill activation, hook firing in real Claude Code sessions) that can't be unit-tested without Claude in the loop. Walk through them after meaningful changes to any skill or the hook.
For each scenario: PASS if the listed outcome matches; FAIL otherwise. Note failures and revisit.
---
## Scenario 1: Fresh empty hg repo, basic commit
**Setup:**
```sh
cd $(mktemp -d) && hg init && echo "hello" > README.md
Open Claude Code in that directory.
Prompt: "Commit the README.md file with an appropriate message."
Expected:
- Claude loads the
mercurial-basicsskill. hg add README.mdfollowed byhg commit -m "..."(no-uflag).- Commit message follows the convention: imperative summary <72 chars, category prefix optional but reasonable.
FAIL signals:
gitcommands appear (the hook should block them, but Claude shouldn't try in the first place).-u "..."appears onhg commit.- Multi-line commit message via
-mwithout proper quoting.
Scenario 2: Isurus repo, start a feature
Setup: open Claude Code in ~/repos/isurus-project/isurus.
Prompt: "Start a new feature branch called add-language-stats and make a small no-op change."
Expected:
- Both
mercurial-bookmarks-and-prsandisurus-conventionsactivate. hg bookmark add-language-statsis created.- A small change is made and committed (no
-u, message follows the Isurus convention with a prefix likefeat:). - No mention of
gitanywhere. - Claude does NOT try to use named branches for the feature.
Scenario 3: Mid-stream conflict
Setup: in a scratch hg repo, create two heads:
cd $(mktemp -d) && hg init
echo "A" > file.txt && hg add file.txt && hg commit -m "init"
echo "A1" > file.txt && hg commit -m "branch 1"
hg update -r 0
echo "A2" > file.txt && hg commit -m "branch 2"
hg heads should show two heads.
Open Claude Code.
Prompt: "Merge the two heads."
Expected:
mercurial-sync-and-mergeactivates.- Claude runs
hg merge, resolves the conflict infile.txt(asks user how to resolve OR resolves obviously),hg resolve --mark file.txt, and finallyhg commit -m "merge: ...". - No conflict markers (
<<<<<<<) remain infile.txtafter commit.
Scenario 4: History rewrite request on a public changeset
Setup: in any hg repo, find a public-phase changeset:
hg log --template '{node|short} {phase} {desc|firstline}\n' | grep ' public '
Note its hash.
Open Claude Code.
Prompt: "Amend changeset <hash> to fix a typo."
Expected:
mercurial-history-rewritingactivates.- Claude refuses to amend a public changeset, surfaces the phase rule, and offers an alternative (a new commit on top, or coordinating with the team to demote the phase).
FAIL signals:
- Claude tries
hg amend --forceor any other override. - Claude doesn't notice the phase and just runs
hg amend.
Scenario 5: git inside Isurus is blocked
Setup: cd ~/repos/isurus-project/isurus (or any .hg repo).
In Claude Code:
Prompt: "Run git status to see what's changed."
Expected:
- The hook blocks the
git statuscall. - The blocked-call message tells Claude to use
hg statusinstead. - Claude reads the message and re-runs as
hg status.
Scenario 6: git inside testdata bare repo is allowed
Setup: cd ~/repos/isurus-project/isurus/internal/import/testdata/<any>/repo.git (these are bare git repos used as import-flow test fixtures).
In Claude Code:
Prompt: "Run git log --all here."
Expected:
- The hook does NOT block (bare-repo signature wins over the outer
.hg/). git log --allruns successfully.
If no testdata fixture is available, simulate by hand:
cd $(mktemp -d)
mkdir -p outer/.hg
cd outer
mkdir -p repo.git/refs repo.git/objects
touch repo.git/HEAD
cd repo.git
# now ask Claude to run `git log` from this directory
Acceptance criteria reminder
Per the spec, v1 is done when:
scripts/check-skills.shpasses (0 errors).hooks/test/run.shpasses (7/7 cases).- All 6 scenarios above PASS against the real Isurus repo.
- A second hg repo (any non-Isurus hg repo) demonstrates that
isurus-conventionsdoes NOT activate but the generic skills do.
- [ ] **Step 2: Stage and commit**
```bash
hg add tests/MANUAL.md
hg commit -m "docs: add manual scenario test checklist"
Task 16: README
User-facing documentation for the plugin: what it does, how to install, what the hook does.
Files:
-
Create:
README.md -
Step 1: Create the README
Create README.md:
# isurus-hg
A Claude Code plugin that teaches Claude to work fluently in **Mercurial 7.x**, with an **Isurus-conventions overlay** for the Isurus forge codebase, and a **PreToolUse hook** that prevents accidental `git` commands inside `.hg` repositories.
Built because Claude's general competence is uneven on Mercurial — bookmarks, named branches, phases, and Isurus's house conventions are easy to get wrong without explicit guidance — and because "never use `git`" is more reliable as a hook than as a memorized instruction.
## What it includes
**Skills** (auto-loaded by Claude when relevant):
- `mercurial-basics` — `status`, `diff`, `log`, `add`, `commit`, message format, phase awareness.
- `mercurial-bookmarks-and-prs` — bookmark workflow as the unit of feature work and PRs.
- `mercurial-sync-and-merge` — pull, update, multiple heads, merge, conflict resolution.
- `mercurial-history-rewriting` — `amend`, `uncommit`, the draft-vs-public phase rule, recovery patterns.
- `mercurial-finishing-a-branch` — pre-PR checklist + push flow (the hg counterpart to superpowers' `finishing-a-development-branch`).
- `isurus-conventions` — overlay skill that activates in the Isurus repo; supplies project-specific values (URLs, branches, `make` targets) that the generic skills' procedures plug into.
**Hook:**
- `block-git-in-hg.sh` — fires on every `Bash` tool call. If the command starts with `git` and the working directory is inside an `.hg` repository (and not inside a closer `.git` repo or a bare git repo), the call is blocked with a message suggesting the `hg` equivalent.
## Install
Until the plugin is published to a marketplace, install via a path entry in `~/.claude/settings.json`:
```json
{
"plugins": {
"isurus-hg": {
"path": "/absolute/path/to/isurus-claude-plugin"
}
}
}
Then restart Claude Code. The skills become available; the hook is registered automatically via hooks/hooks.json.
Requirements:
- Mercurial 7.x (the skills assume modern command behavior).
jqonPATH(the hook uses it to parse tool-call JSON; if missing, the hook fails open with a warning).- Bash 4+.
Hook behavior
The hook intercepts Bash tool calls. Decision tree:
- Command doesn't start with
git→ allow. - Working directory is a bare git repository (contains
HEAD,refs/,objects/at top level) → allow. - Closest VCS ancestor is
.git/→ allow. - Closest VCS ancestor is
.hg/→ block with exit code 2 and a stderr message suggesting thehgequivalent. - No VCS context → allow (e.g.,
git --versionoutside any repo).
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.
Development
This plugin is developed in Mercurial (eating its own dogfood). To work on it:
hg clone <repo-url>
cd isurus-claude-plugin
# Validate skills
./scripts/check-skills.sh
# Run hook tests
./hooks/test/run.sh
# Walk manual scenarios after meaningful changes
cat tests/MANUAL.md
Design spec: docs/superpowers/specs/2026-05-01-isurus-hg-plugin-design.md
Implementation plan: docs/superpowers/plans/2026-05-02-isurus-hg-plugin.md
License
MIT — see LICENSE.
- [ ] **Step 2: Stage and commit**
```bash
hg add README.md
hg commit -m "docs: add README"
Task 17: LICENSE file
MIT text. Required for the manifest's license: MIT to actually be true.
Files:
-
Create:
LICENSE -
Step 1: Create the LICENSE file
Create LICENSE with the standard MIT text:
MIT License
Copyright (c) 2026 Leafscale, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- Step 2: Stage and commit
hg add LICENSE
hg commit -m "docs: add MIT LICENSE"
Task 18: .gitignore for the future git mirror
Even though v1 is hg-only, a .gitignore makes the future hg-git mirror clean. Excludes hg metadata and Claude local state.
Files:
-
Create:
.gitignore -
Step 1: Create the file
Create .gitignore:
# Mercurial metadata (the canonical source — git is downstream mirror)
.hg/
.hgignore
.hgtags
# Claude Code local state
.claude/
.remember/
.superpowers/
# OS
.DS_Store
Thumbs.db
# Editor
*.swp
*.swo
.idea/
.vscode/
- Step 2: Stage and commit
hg add .gitignore
hg commit -m "chore: add .gitignore for future git mirror"
Task 19: Final v1 acceptance check
Verifies all the acceptance criteria from the spec are met before tagging.
Files: none (this is a verification task)
- Step 1: Run skill content lint — must be clean
./scripts/check-skills.sh
Expected:
Skill lint complete: 0 error(s), 0 warning(s)
If any warnings appear (e.g., a skill exceeds 400 lines), decide whether to split or accept; document the decision in a follow-up commit.
- Step 2: Run hook test suite — all 7 must pass
./hooks/test/run.sh
Expected:
PASS 01-git-in-hg-blocks
PASS 02-git-outside-repo-allows
PASS 03-git-inside-nested-git-allows
PASS 04-hg-command-allows
PASS 05-env-var-prefix-stripped
PASS 06-non-git-command-allows
PASS 07-cd-then-git-allows
Result: 7 passed, 0 failed
- Step 3: Verify the plugin manifest validates
jq . .claude-plugin/plugin.json && jq . hooks/hooks.json
Both files must parse cleanly.
- Step 4: Verify the file inventory matches the spec
find .claude-plugin hooks skills scripts tests -type f | sort
Expected (one per line, in this order):
.claude-plugin/plugin.json
hooks/block-git-in-hg.sh
hooks/hooks.json
hooks/test/cases/01-git-in-hg-blocks.sh
hooks/test/cases/02-git-outside-repo-allows.sh
hooks/test/cases/03-git-inside-nested-git-allows.sh
hooks/test/cases/04-hg-command-allows.sh
hooks/test/cases/05-env-var-prefix-stripped.sh
hooks/test/cases/06-non-git-command-allows.sh
hooks/test/cases/07-cd-then-git-allows.sh
hooks/test/run.sh
scripts/check-skills.sh
skills/isurus-conventions/SKILL.md
skills/mercurial-basics/SKILL.md
skills/mercurial-bookmarks-and-prs/SKILL.md
skills/mercurial-finishing-a-branch/SKILL.md
skills/mercurial-history-rewriting/SKILL.md
skills/mercurial-sync-and-merge/SKILL.md
tests/MANUAL.md
If anything is missing or extra, resolve before continuing.
- Step 5: Walk the manual scenarios
Open tests/MANUAL.md and walk all 6 scenarios. Note any failures.
If any scenario FAILs, do not tag v0.1.0 — return to the relevant skill or hook, fix, and re-walk.
- Step 6: Tag v0.1.0
hg tag v0.1.0
hg log -l 2 --template '{node|short} {tags} {desc|firstline}\n'
Expected output:
<hash> tip Added tag v0.1.0 for changeset <prev-hash>
<prev-hash> v0.1.0 <last feature commit>
The tag commit is automatic; that's hg's tag mechanism.
- Step 7: Install locally and smoke-test
Add to ~/.claude/settings.json:
{
"plugins": {
"isurus-hg": {
"path": "/home/ctusa/repos/isurus-project/isurus-claude-plugin"
}
}
}
Restart Claude Code, then in a Claude session:
cdinto~/repos/isurus-project/isurus.- Ask: "Run
git statushere." → confirm the hook blocks. - Ask: "What's the working-copy status?" → confirm
mercurial-basicsactivates andhg statusruns.
If both work, v0.1.0 is ready for the dogfooding period described in the spec's rollout section.
- Step 8: Final commit-log review
hg log --template '{node|short} {desc|firstline}\n'
Read through the full commit log. Each commit message should clearly describe its change. If any look unclear, that's information for future-you about commit hygiene — note for next time. Don't rewrite existing public-ish commits at this stage.
Self-review notes
This plan was self-reviewed against the spec. Coverage check:
| Spec section | Implementing tasks |
|---|---|
| Plugin manifest | Task 1 |
| File layout | Tasks 1-18 (each file in the layout has a creating task) |
| 5 generic skills | Tasks 9-13 |
| Isurus overlay skill | Task 14 |
| Hook config | Task 8 |
| Hook script | Tasks 5, 7 |
| Hook detection logic incl. bare-repo handling | Task 7 |
| Hook test cases (7) | Tasks 4, 6 |
| Skill content lint | Task 2 |
| Manual scenarios doc | Task 15 |
| README | Task 16 |
| LICENSE | Task 17 |
| .gitignore for future git mirror | Task 18 |
| Acceptance criteria | Task 19 |
Deferred items from the spec (forge MCP, slash commands, evolve/topics, subagents, formal evals, CI, CHANGELOG/CONTRIBUTING/SECURITY) intentionally have no tasks — they are phase 2 or beyond.
.hgignore and the spec/plan documents themselves are already committed (pre-plan setup), so no task creates them.