This file provides guidance to Claude Code (claude.ai/code) when working in this repository.
Memory is allowed with human approval. The authoritative policy is in
the user's global ~/.claude/CLAUDE.md — agents must propose memory
writes and suggest a destination (repo memory, global CLAUDE.md, or
plugin/skill issue) before writing. See that file for the full
workflow.
Available skills:
/vergil:memory-init— set up or update the policy header in a project'sMEMORY.md./vergil:memory-audit— structured collaborative review of memory files.
This repository supports running multiple Claude Code agents in parallel via git worktrees. The convention keeps parallel agents' working trees isolated while preserving shared project memory (which Claude Code derives from the session's starting CWD).
Canonical spec:
vergil-tooling/docs/specs/worktree-convention.md
— full rationale, trust model, failure modes, and memory-path implications.
The canonical text lives in vergil-tooling; this section is the local
on-ramp.
<project-root>/ ← sessions ALWAYS start here
.git/
CLAUDE.md, … ← main worktree (usually `develop`)
.worktrees/ ← container for parallel worktrees
issue-<N>-<short-slug>/ ← worktree on feature/<N>-<short-slug>
…
- Sessions always start at the project root.
Never start Claude from inside
.worktrees/<name>/. This keeps the memory-path slug stable and shared. - Each parallel agent is assigned exactly one worktree. The session
prompt names the worktree (see Agent prompt contract below).
- For Read / Edit / Write tools: use the worktree's absolute path.
- For Bash commands that touch files:
cdinto the worktree first, or use absolute paths.
- The main worktree is read-only. All edits flow through a worktree on a feature branch — the logical endpoint of the standing "no direct commits to develop" policy.
- One worktree per issue. Don't stack in-flight issues. When a branch lands, remove the worktree before starting the next.
- Naming:
issue-<N>-<short-slug>.<N>is the GitHub issue number;<short-slug>is 2–4 kebab-case tokens.
When launching a parallel-agent session, use this template (fill in the placeholders):
You are working on issue #<N>: <issue title>.
Your worktree is: <project-root>/.worktrees/issue-<N>-<slug>/
Your branch is: feature/<N>-<slug>
Rules for this session:
- Do all git operations from inside your worktree:
cd <absolute-worktree-path> && vrg-git <command>
- For Read / Edit / Write tools, use the absolute worktree path.
- For Bash commands that touch files, cd into the worktree first
or use absolute paths.
- Do not edit files at the project root. The main worktree is
read-only — all changes flow through your worktree on your
feature branch.
- When you need to run validation, run it from inside your worktree
(vrg-container-run mounts the current directory).
All fields are required.
Use vrg-git instead of git for all git operations. Use vrg-gh
instead of gh for all GitHub CLI operations. These wrappers enforce
subcommand allowlists, flag deny lists, and credential selection.
Raw git and gh are denied by the permission model. If a command
is not available through the wrappers, explain the situation to the
human who can run it directly via ! <command> in the prompt.
vrg-container-run -- vrg-validateThis is the only validation command. Do not run individual linters,
formatters, or other tools outside of vrg-validate. If a tool is not
invoked by vrg-validate, it is not part of the validation pipeline.
Note: The command above works as-is here, even though this repo dogfoods its own unreleased code (the cached dev image deliberately skips
uv tool install, sovrg-validateis not onPATHand must be run viauv run). This repo declares a[validation]override invergil.toml(container-command = "uv run vrg-validate"), andvrg-container-runreads it from the target repo at execution time, sovrg-container-run -- vrg-validateis transparently expanded tovrg-container-run -- uv run vrg-validate. The override lives invergil.toml(not just here) so cross-repo agents pick it up regardless of whichCLAUDE.mdtheir session loaded (issues #1430, #1433).
Identity-aware tools (vrg-git, vrg-gh, vrg-submit-pr) read
VRG_IDENTITY_MODE (human, user, or audit; see
src/vergil_tooling/lib/identity_mode.py). Agent sessions run as
user or audit.
To query the resolved role, use vrg-whoami — never infer identity
from VRG_IDENTITY_MODE alone. That env var is only the first of five
fallback steps (env var → mode file → app.pem → VRG_APP_ID →
human); an unset value means "fall through," not "default to human."
vrg-whoami --mode emits a single token for scripting, and
vrg-whoami --explain reports the resolving signal and warns when
signals disagree. vrg-whoami --platform resolves a second, orthogonal
axis — physical-host / local-vm / cloud-vm — from empirical signals
(fail-closed: an unconfirmed VM resolves to cloud-vm, never
physical-host); --explain also cross-checks the platform against the
identity and warns when the two disagree.
Agents must not run vrg-submit-pr. PR submission, merge, and
finalization are human actions. The PR handoff is:
- The agent records the PR metadata with
vrg-pr-workflow report-ready --issue <N> --title --summary --notes(optional--linkage), which writes it to.vergil/pr-workflow.json.title,summary, andnotesare required and non-empty.linkagedefaults toRef; leave it there.vrg-submit-prauto-selects the keyword at submit time — a managed task (an issue with anepic-labeled parent) links withClosesso it auto-closes on merge, and its parent epic rolls up via theon: issues.closedAction; a legacy issue (no epic parent) keepsRefand stays open for manual close.Fixes/Resolvesremain banned soClosesis the one close keyword. This is safe because a task is exactly one PR: once it is in develop it is done, and any later change is a new follow-up issue, never a reopening (epic vergil-project/.github#75). - The human runs
vrg-submit-prwith no arguments, which reads the state file, previews the PR, and submits after confirmation. - The human merges and runs post-merge cleanup (
vrg-finalize-pr).
Once you run report-ready, the branch is frozen. report-ready
records the branch as the single, finished deliverable for its issue, so
until the human submits it must not change. Enforcement is at two
chokepoints — vrg-commit refuses a further commit and the vrg-git
push path refuses a further push — both printing an actionable refusal
(and a loud DRIFT warning if HEAD has already advanced past the reported
commit, the reused-branch straggler of epic #146 / issue #1719). The rule
follows directly from "a task is exactly one PR": more work is a new
follow-up issue, never a change to this branch. Two things stay allowed:
- Correcting the PR prose — re-running
report-readyoverwrites the recorded title/summary/notes. That is metadata, not code, so it is not frozen. - Deliberately reopening the branch —
vrg-pr-workflow unfreezedrops the workflow back toimplementing(keeping the recorded metadata) so commits/pushes are allowed again. This is the only sanctioned way to lift the freeze; it is a distinct, explicit action precisely so reopening a branch is never a silent side effect. An already-submitted branch cannot be unfrozen — its PR exists, so further work is a new follow-up issue, full stop.
When the human finalizes, vrg-finalize-pr will not silently strand a
merged worktree it cannot remove (dirty tree, or a reused branch name with
unmerged commits): it surfaces every such worktree prominently after the
pipeline with the reason. For the common Mac case — a merged worktree
dirtied only by un-gitignored build/validation output — --clean-dirty is
an opt-in that clears exactly that after showing the untracked paths and
confirming; it never touches modified tracked files or a reused-branch
straggler's unmerged commits. The cleaner fix is to gitignore that output
in the first place, so the worktree is never dirtied and sweeps
automatically — consuming repos should keep validation/build artifacts out
of the tree.
The PR handoff above no longer needs a shared filesystem. report-ready
always mirrors the recorded ready-state onto a reserved git ref,
refs/vergil/pr-workflow/<branch> (the relay ref), in addition to the
local .vergil/pr-workflow.json. The push is unconditional — no config
key, no off-platform detection — so a cloud x86 VM's report-ready is
visible to the Mac even though the two never share a disk. The write is a
pure ref update built out-of-band with git plumbing; it never advances the
feature branch, so it stays freeze-neutral (the post-report-ready freeze
still holds).
Because the metadata now rides GitHub, a cloud VM can do PR-development
end-to-end — not just triage. A cloud agent implements the issue,
commits, pushes the feature branch to origin, and runs report-ready,
exactly as it would under Lima. The old "cloud x86 VMs are triage-only /
not for PR-development" boundary is retired, along with the "until the
relay lands" framing — the relay
(#1858,
Deliverable B) shipped.
Only submission and merge stay human-on-Mac. From the Mac's main
worktree, the human runs vrg-submit-pr worktree-free with an explicit
branch list:
vrg-submit-pr <branch> [<branch> …]
Each branch's ready-state is resolved from a local worktree's
pr-workflow.json when one exists, else fetched from the relay ref; the
tip of origin/<branch> is verified against the recorded head_sha, and
the PR is opened without pushing (the branch already rode GitHub, so
--head just names it). Merge and cleanup stay human actions:
vrg-finalize-pr deletes the branch's relay ref alongside the branch on
cleanup, and a swept safety net prunes any relay ref whose branch no longer
exists, so a cloud-handoff ref never outlives its work.
The relay ref is world-readable on a public repo. Anyone can read
refs/vergil/pr-workflow/<branch>, so the report-ready --title,
--summary, and --notes must carry no secrets — treat them as public
the moment they are recorded.
This does not loosen the "agents must not run vrg-submit-pr" policy: the
cloud agent stops at report-ready, exactly like a Lima agent, and the
human submits and merges on the Mac. What changed is only where the
development can happen — the relay removed the shared-disk requirement, so
a cloud VM is now a full PR-development environment, not a triage-only one.
This is a Python package providing shared development tooling for all managed repositories: CLI tools for commits, PRs, releases, and validation; bash validators and git hooks consumed via PATH from a sibling checkout (local) or CI checkout (GitHub Actions).
Project name: vergil-tooling
Status: Stable (v2.x)
Standards reference: https://github.com/wphillipmoore/standards-and-conventions
— historical reference; active standards documentation lives in this
repository under docs/.
Host-side vrg-* tools are installed via uv tool install (see
Consumption Model). For developing
vergil-tooling itself, there is also a dev-tree override using
a local .venv for testing unreleased code on the host:
# Dev-tree override (vergil-tooling development only)
uv sync --group dev
export PATH="$(pwd)/.venv/bin:$PATH"
A single .venv is safe because the dev container never touches the
host .venv — it is masked by an anonymous volume (#2486).
After host tools are available, use vrg-container-run to run all
commands inside the dev container. See Validation
above.
Testing is split across two tiers with increasing scope and cost:
Tier 1 — Local pre-commit (seconds): The single entry point
vrg-container-run -- vrg-validate runs everything
(lint, typecheck, tests, audit, common checks) inside one dev
container. (Here that transparently expands to uv run vrg-validate
via the [validation] override in vergil.toml — see
Validation.)
Tier 2 — PR CI (~5-8 min): Triggers on pull_request. Runs every
quality check across the full Python version matrix (3.12, 3.13, 3.14),
security scanners (CodeQL, Trivy, Semgrep), standards compliance, and
release gates. Workflow: .github/workflows/ci.yml. ci.yml is a thin
caller of the vergil-actions reusable workflows, passing only
language:/container-suffix:; the matrix is read from [ci].versions in
vergil.toml at run time, and branch protection requires the stable,
version-agnostic audit / evidence, quality / evidence, and
test / evidence gates rather than per-version checks — so a [ci].versions
change needs no edit to ci.yml or the ruleset (epic
vergil-project/.github#338).
Push-CI was retired once vrg-validate matched the checks push-CI ran.
Note one deliberate gap in the "parity with PR-CI" framing: local
vrg-container-run -- vrg-validate runs a single dev container on one
Python interpreter (currently 3.14), so its --cov-fail-under=100 gate
proves 100% coverage on that interpreter only. PR-CI re-runs the
test-and-coverage gate independently in a separate container per
[ci].versions entry (3.12, 3.13, 3.14). Because branch coverage
(--cov-branch) can legitimately differ across CPython versions, code at
100% locally can still fall below 100% on a 3.12 or 3.13 leg and fail CI —
the multi-version coverage matrix is a PR-CI-only gate. See
docs/site/docs/guides/ci-architecture.md for the full rationale and
vergil-project/vergil-actions#176 for the parity audit.
Docker is the only host prerequisite. The validation stack uses exactly one container per run:
- Outer layer:
vrg-container-runlaunches the dev container once and runsvrg-validateinside. - Inner layer:
vrg-validatereadsprimary_languagefromvergil.tomland runs common checks (markdownlint, shellcheck, yamllint, hadolint, actionlint), then language-specific checks (lint, typecheck, test, audit) from the built-in command registry.
Dev container images are maintained in vergil-containers.
# Build the dev image (one-time)
cd ../vergil-containers && docker/build.sh
# Run the full validation pipeline in one container
# (the [validation] override in vergil.toml expands this to `uv run vrg-validate`)
vrg-container-run -- vrg-validateCLI tools installed as vrg-* console scripts:
vrg-commit— Construct standards-compliant conventional commits with co-author resolutionvrg-reword— Reword a branch-local commit's message via a scripted, non-interactive rebase; the agent-safe path to correct a branch-local commit message (raw interactive rebase is blocked repo-wide). Bounded: refuses shared/merged history and protected branches, refuses a foreign author without--allow-foreign-author, and pushes the rewrite with--force-with-leasevrg-submit-pr— Create standards-compliant PRs (manual merge; human-run — agents hand off via.vergil/pr-workflow.json)vrg-pr-fix-body— Regenerate a PR body from corrected fields via the validated builder; the agent-safe path to fix body-level standards failures on its own PR during pr-watch (pushes an empty commit to re-trigger CI)vrg-release— Mechanized end-to-end release workflow (develop to main)vrg-resolve-tracking-issue— Extract tracking issue number from a merge commit's PR linkagevrg-finalize-pr— Merge a PR and run post-merge cleanup (branch/worktree deletion, remote pruning)vrg-validate— Unified validation driver (runs inside dev container)vrg-ensure-label— Idempotent GitHub label creationvrg-hook-guard— Claude Code PreToolUse hook guard (blocks raw git/gh)vrg-whoami— Canonical identity-mode and platform resolver (--modefor a scripting token,--explainto report the resolving signal and warn on signal disagreement,--platformfor the empirical fail-closedphysical-host/local-vm/cloud-vmtoken)vrg-container-run— Run arbitrary commands inside a dev containervrg-container-test— Run repo test suite inside a dev container
Shared libraries under src/vergil_tooling/lib/:
git.py— Git subprocess wrappersgithub.py— gh CLI subprocess wrappersconfig.py— Parsevergil.tomlrelease/— Mechanized release workflow (preflight, prepare, merge, bump, confirm, finalize, handoff, orchestrator)
Dev container images (Dockerfiles, build script, publish workflow) are maintained in vergil-containers.
The vrg-container-test entry point auto-detects the project
language (Gemfile, pyproject.toml, go.mod, pom.xml/mvnw) and runs the test
suite inside the appropriate container. Consuming repos call it directly or wrap
it in a thin scripts/dev/test.sh. Environment overrides:
DOCKER_DEV_IMAGE— override the container imageDOCKER_TEST_CMD— override the test commandDOCKER_NETWORK— join a Docker network (e.g., for integration tests)
Env-var passthrough is configured per-repo via [container].env-prefixes
in vergil.toml (see docs/specs/2026-05-25-configurable-container-env-passthrough-design.md).
The sibling [container] keys — system-packages (apt names) and
build-command/build-cache-files (a non-apt provisioning step) — are documented
in docs/site/docs/reference/container-config.md.
Raw git and gh commands are blocked by a Claude Code PreToolUse
hook. The enforcement has two layers:
- Per-repo hook wiring (
.claude/settings.json) — calls.claude/hooks/guard.sh, a thin shell shim that execsvrg-hook-guardwhen vergil-tooling is installed, or falls back to ajq-based hard deny for git/gh commands. - Per-developer plugin (
vergil-claude-plugin) — provides the same guard via the plugin's hook system.
Both layers call vrg-hook-guard, which uses regex matching to
detect raw git/gh invocations while allowing vrg-git/vrg-gh
wrappers through. Only active in repos with a vergil.toml.
vergil-tooling has two coordinated deployment targets (see
docs/specs/host-level-tool.md — historical spec, written under the
old standard-tooling/st-* naming):
| Target | Install mechanism | Who uses it |
|---|---|---|
| Developer host | uv tool install from git URL |
Host-side commands: vrg-container-run, vrg-commit, vrg-submit-pr, vrg-release, vrg-finalize-pr |
| Container runtime (all languages) | vrg-container-run cache-first install per vergil.toml |
vrg-* inside the container for all consumers |
Host install (canonical):
uv tool install --python 3.14 'vergil-tooling @ git+https://github.com/vergil-project/vergil-tooling@v2.1'Claude Code hook (any consuming repo): each repo ships a thin
shell shim at .claude/hooks/guard.sh that calls vrg-hook-guard
to block raw git/gh commands in agent sessions.
CI (GitHub Actions): All repos use the cache-first runtime path
via vrg-container-run, which reads vergil.toml for the
version tag and builds a per-branch cached image with
vergil-tooling pre-installed.
- Portability: Scripts must work on both macOS and Linux
- shellcheck clean: All bash scripts must pass shellcheck
- No repo-specific logic: Scripts must work in any consuming repository