Shared agent tooling for tbhb repositories. The repo provides Go command-line tools for agent harnesses, plus the shared APM package of agent primitives.
APM package
Repositories across tbhb install the shared primitives with the APM CLI:
apm install tbhb-dev/devtools/packages/agents/claude#v0.11.0
apm install tbhb-dev/devtools/packages/agents/common#v0.11.0
The package deploys these primitives from sub-packages under packages/agents/: claude contains the skills below plus the guard-markdown hook declaration, and common contains the harness-neutral worktree-wip instructions.
| Primitive |
Type |
Purpose |
commit |
skill |
Group changes into one atomic commit, draft the message in COMMIT_AGENTMSG, review it, lint it, confirm it, then commit and rebase. |
review-commit-message |
skill |
Review a drafted message against the staged diff as an independent agent, for what linting can't see. |
worktree-wip |
instructions |
Stash and work-in-progress rules for repos that run more than one agent worktree session. |
guard-markdown |
hook |
PreToolUse gate on Write and Edit that refuses Markdown whose paragraphs span more than one line. |
Pinning the bare repo (apm install tbhb-dev/devtools#v0.11.0) resolves the root workspace manifest, whose dependencies on the four sub-packages under packages/agents/ are local paths that a remote consumer's apm resolves transitively, filtering each sub-package by its own targets. For a claude-only consumer, the bare pin gives exactly what the claude and common pins deliver. The codex and agy scaffolds skip on target intersection. This repo installs its own package the way a consumer does: apm install deploys the primitives into the local harness layout, and CI rejects drift between the packages/agents/*/.apm/ sources and the deployed copies.
Checks
A check is one rule enforced everywhere it matters. Because all three callers run the same binary, the rule can't drift between them:
| Caller |
Invocation |
Claude PreToolUse |
guard-markdown --hook reads the payload on stdin and denies the Write or Edit |
| pre-commit |
guard-markdown FILES reports the offending line ranges and exits nonzero |
| verification stack |
the same command from a mise task, alongside the other linters |
.pre-commit-hooks.yaml publishes the pre-commit leg:
repos:
- repo: https://github.com/tbhb-dev/devtools
rev: v0.11.0
hooks:
- id: guard-markdown
Note that the two legs install separately. prek builds its own copy from language: golang. The Claude hook instead resolves guard-markdown on PATH, which comes from go install. A consumer wanting both adds the hook and runs the install.
Pass --fix to collapse paragraphs someone already wrapped.
Each tool under cmd/ builds as a standalone binary:
agenthooks manages shared agent hooks.
agentstore stores shared agent state.
agentcontext assembles shared agent context.
guard-markdown refuses Markdown whose paragraphs span more than one line.
Install one directly:
go install github.com/tbhb-dev/devtools/cmd/agenthooks@latest
Or build everything from a checkout:
mise run build
Development
mise pins the toolchain and runs the workflow: mise run bootstrap sets up a fresh clone, mise run lint runs the full lint suite, mise run test runs the tests, and mise run build compiles the binaries into bin/. On a machine with no mise, the committed shim at tools/mise-bootstrap runs in place of mise and installs the pinned release on first use. The pins live in mise.toml and the drop-ins under .config/mise/conf.d/, locked by digest in the committed mise.lock files. See AGENTS.md for the agent-facing contributor guide.
Language tasks
Copy the vendir manifest to vendir.yml, set both refs to a release containing these task sets, and run vendir sync. The manifest syncs the whole .devtools directory, including task files, scripts, and config templates. It defines that directory's contents. Keep local adaptations outside it. Existing consumers syncing only .devtools/tasks must expand their manifest to .devtools/** with newRootPath: .devtools. The shared vendir.toml drift gate checks that whole directory, and pins.toml updates both manifest refs without adding a pin for each task file.
Select the languages in the consumer's mise.toml:
[task_config]
includes = [
".devtools/tasks/go.toml",
".devtools/tasks/python.toml",
".devtools/tasks/vendir.toml",
".devtools/tasks/pins.toml",
]
Run mise run devtools:lint-go or mise run devtools:test-go. Omit either language file when unused. Local wrappers such as test-go can run mise run devtools:test-go. In devtools, these wrappers keep task names without a namespace. Add devtools:check-vendored and devtools:check-pins to the consumer's combined lint task. Pin vendir in the consumer before the first sync. The shared conf.d pins supply the Go executables. Install Python tools in the consumer's uv dependency groups and lockfile. Go tasks using go tool also need the corresponding tool dependencies in the consumer's go.mod.
Set overrides in [env]: DEVTOOLS_GO_PACKAGES defaults to ./..., DEVTOOLS_GO_BIN_PACKAGES to ./cmd/..., and DEVTOOLS_GO_MODULES_MODE to readonly (use vendor for a vendored module). Build metadata derives DEVTOOLS_GO_MODULE from go list -m. DEVTOOLS_GO_BUILDMETA defaults to that module's internal/buildmeta package, with string variables Version, Commit, and Date. Go ignores those linker assignments when the package is absent. Python source roots default to src: set DEVTOOLS_PYTHON_DIRS for pylint, complexipy, and vulture, DEVTOOLS_PYTHON_SOURCE_DIRS for bandit and coverage, and DEVTOOLS_PYTHON_PROPERTY_DIRS for fuzzing (default tests/property). Directory lists use shell word splitting and glob expansion, so paths must contain no whitespace. Ruff, pyrefly, pytest, and cosmic-ray read the consumer's own config for their scope and policy. Both complexipy and vulture also read local policy.
Copy the needed templates from .devtools/config to the project root, then adapt these fields. These are starting points. Syncing updates the templates without overwriting local policy.
| File |
Consumer adaptations |
go/.golangci.yml → .golangci.yml |
Replace example.com/project in the prefix for gci, the module-path for gofumpt, and the local-prefixes for goimports. Set the license and copyright template for goheader to your authors. Review dependency bans and thresholds. |
go/.go-arch-lint.yml → .go-arch-lint.yml |
Replace components' in paths and deps edges with your layers. Adjust exclude for your tooling and fixtures. |
go/.testcoverage.yml → .testcoverage.yml |
Replace local-prefix with your module path. Choose thresholds and excluded entry points. Keep profile: coverage.out for these tasks. |
go/.gremlins.yaml → .gremlins.yaml |
Adjust unleash.exclude-files for generated or third-party code. The default excludes vendor. |
python/ruff.toml → ruff.toml |
Adjust exclude and the test/script patterns under lint.per-file-ignores. Choose your rules and documentation string convention. Existing [tool.ruff] settings in pyproject.toml can stay there instead. |
python/pyrefly.toml → pyrefly.toml |
Set project-includes and project-excludes. Keep the diagnostic catalog aligned with your pinned pyrefly version. Existing [tool.pyrefly] settings can stay in pyproject.toml. |
.devtools/scripts/go-ldflags.sh |
No edits: set the module or metadata package through env if the defaults differ from your layout. tools/go-ldflags.sh in this repo forwards to it. |
Releases
cocogitto cuts vX.Y.Z tags from the Conventional Commit history. One tag serves both consumer paths, with APM installs pinning tbhb-dev/devtools#vX.Y.Z and Go installs pinning @vX.Y.Z.
Python
packages/devtools holds the Python side, which is deliberately small for now. Its gates run through mise run lint-py-all and mise run cover-py, which enforce ruff at its full ruleset, pyrefly's strict preset, and 100% branch coverage. uv sync provisions everything from uv.lock.
License
Apache-2.0. See LICENSE.
Private identifier gate
Sync .devtools/scripts/ alongside .devtools/tasks/ in your consumer's vendir.yml (or sync .devtools/ whole). Include .devtools/tasks/leaks.toml in mise's task configuration and add mise run devtools:lint-leaks to your lint task. This complements gitleaks with private network addresses, *.ts.net host names other than example.ts.net and its subdomains, password-manager references, and private-key headers.
Findings report the location and rule without printing matched text. Sensitive locations become opaque identifiers.
Add the leaks-staged, leaks-commit-msg, and leaks-pre-push hook IDs from this repository to prek. Run mise run devtools:prek-install after syncing: it installs a wrapper before prek's pre-push hook so the scan receives Git's ref lines. The pre-push hook ID verifies the wrapper installation. The wrapper scans the exact outgoing objects before prek consumes the ref lines. In CI, fetch full history and run mise run --quiet devtools:lint-leaks -- --range "$BASE..$HEAD" for the pull request's base and head commits.
Keep private literals in mise.local.toml, which Git ignores:
[env]
PROJECT_PRIVATE_STRINGS = "private-account-name, private-directory-name"
Every exported variable ending in _PRIVATE_STRINGS supplies comma-separated, case-insensitive fixed strings. The gate trims whitespace around entries and skips entries shorter than four characters with a warning. Commas and newlines inside a literal are unsupported. Run through mise so it loads the local environment. CI checks publishable patterns without these private values. Never commit the local file.
Add extra case-insensitive extended regular expressions to .leak-patterns, one per line. The gate ignores blank lines and lines starting with #. For legitimate fixtures, document anchored path expressions in .leak-allow, with a comment explaining each exception. These exceptions apply to pattern findings only: the gate checks private literals even in allowed paths. Treat changes to both files as security configuration requiring review. The gate scans working files by default, index blobs with --staged, explicit file arguments for commit messages, and complete new Git objects with --range or pre-push. Encoded or split values are outside its scope.