agentws manages per-branch git worktrees for the unkin-agent user: it clones repos into the source root (~/src/prodenv/<repo>) so branches are visible in Ben's main checkout, and creates isolated worktrees under the worktree root (~/.cache/agentws/<repo>__<branch>). - New internal/agent/git.go: small, testable git helpers shelling out to the git binary (clone/fetch/worktree add/remove/list/prune, branch + config ops, porcelain parsing, path sanitizing). No go-git dependency. - New cmd/agentws: new / list / rm / clean / token / credential subcommands. Auth uses an ephemeral git credential helper (agentws credential get) so the ~1h Gitea token is never persisted in a remote URL or config; per-worktree config keeps the shared checkout's identity untouched. - Wire agentws into Makefile, scripts/build-rpm.sh, packaging/nfpm.yaml (binary + bash/zsh/fish completions), .woodpecker/release.yaml (cross-compile + assets) and .gitignore. - Tests: table tests for parsing/sanitizing/dir-naming, a real temp-git repo for the worktree lifecycle, and hermetic cmd tests (bad input + credential-helper host guard) that never touch the network. - Document agentws in README.md and AGENTS.md.
agent-tools
Small Gitea-automation CLIs, shipped together in one RPM (agent-tools). They
act as the unkin-agent user by minting a scoped Gitea token from Vault, so
automated PRs, comments and pushes are attributed to the agent — not to whoever
happens to run the command.
agentpr— create pull requests and post PR comments asunkin-agent.watchpr— poll one or more PRs and exit when one changes in a way worth acting on.agentws— manage per-branch git worktrees forunkin-agent, cloning into Ben's source checkout and isolating agent work under the XDG cache.
How it gets a token
On first use each tool performs a Vault AppRole login (role_id only, no
secret_id), then reads gitea/creds/unkin-agent to obtain a short-lived Gitea
token, cached in-process for the run.
Everything is configured by environment variables, all with defaults:
| Variable | Default | Purpose |
|---|---|---|
VAULT_ADDR |
https://vault.service.consul:8200 |
Vault/OpenBao address |
AGENT_APPROLE_ROLE_ID |
built-in default | AppRole role_id (overridable) |
GITEA_URL |
https://git.unkin.net |
Gitea base URL |
AGENT_LOGIN |
unkin-agent |
login whose comments watchpr ignores |
AGENTWS_SRC_ROOT |
~/src/prodenv |
source-of-truth checkout root (agentws) |
AGENTWS_ROOT |
~/.cache/agentws |
worktree root (agentws) |
AGENTWS_OWNER |
unkin |
Gitea org that owns the repos (agentws) |
agentpr
# Verify identity (should print: unkin-agent)
agentpr whoami
# Open a PR
agentpr pr create --repo unkin/argocd-apps \
--base main --head benvin/my-change \
--title "Add woodpecker SA" --body "Adds the ServiceAccount ..."
# prints: #<number> <html_url>
# Comment on a PR
agentpr pr comment --repo unkin/argocd-apps --pr 42 --body "Rebased, CI green."
agentpr --version
agentpr --help
Non-zero exit on any API error.
watchpr
Poll PRs and exit (reporting what changed) when a tracked PR merges/closes,
gets a new comment from someone other than the agent, its CI fails
(failure/error), or it loses mergeability (a conflict appears). Benign
transitions — CI pending→success, the agent's own comments — are ignored.
# Watch until something meaningful happens (default interval 60s)
watchpr unkin/argocd-apps#42
# Multiple PRs, custom interval; refs accept #N or :N
watchpr --interval 30s unkin/argocd-apps#42 unkin/terraform-vault:98
# One-shot: print current state and exit 0 (great for scripts)
watchpr --once unkin/argocd-apps#42
watchpr --once --json unkin/argocd-apps#42
On a meaningful change watchpr prints the reason and the PR's current state,
then exits 0. Use --json for machine-readable output.
agentws
agentws gives an agent an isolated git worktree per branch without disturbing
Ben's shared checkouts. Repos are cloned into the source root
(~/src/prodenv/<repo>) so branches created here are visible in the main
checkout too; the worktrees themselves live under the worktree root
(~/.cache/agentws/<repo>__<branch>).
# Clone unkin/argocd-apps into ~/src/prodenv if missing, then add a worktree for
# a new branch off the remote default branch. Prints the worktree path.
agentws new argocd-apps --branch benvin/my-change
# Branch off a specific base instead of the remote default
agentws new argocd-apps --branch benvin/hotfix --from release-1.2
# List managed worktrees (repo, branch, path)
agentws list
# Remove a worktree (by path or branch); refreshes the source repo afterwards
agentws rm benvin/my-change
agentws rm ~/.cache/agentws/argocd-apps__benvin-my-change --delete-branch
# Remove every managed worktree and prune each source repo
agentws clean
# Print a fresh unkin-agent Gitea token
agentws token
Auth / credential-helper design
Gitea tokens minted from Vault are short-lived (~1h), so agentws never
persists one in a remote URL or in git config. Instead it wires itself as an
ephemeral git credential helper:
agentws tokenprints a fresh token to stdout (handy for scripts).agentws credential getspeaks the git credential protocol on stdin and, for the configured Gitea host only, emitsusername=unkin-agent+password=<fresh token>.
agentws new sets this up per worktree without touching the shared checkout: it
enables extensions.worktreeConfig on the repo once, then writes
user.name / user.email and credential.helper = !<agentws> credential to
the per-worktree config. Clone/fetch use the same helper via a transient
-c credential.helper=...; the shared origin URL is left clean. On worktree
removal agentws fetches in ~/src/prodenv/<repo> so its default branch stays
current.
Build & package
make build # -> dist/agentpr, dist/watchpr, dist/agentws
make test # go test -race ./...
make rpm # build + package dist/agent-tools-<version>-1.x86_64.rpm
Release is tag-driven (v*) via Woodpecker: builds the RPM, PUTs it to the
artifactapi rpm-internal yum repo, and cuts a Gitea release with
cross-compiled binaries attached.
Version bump
make patch # or: make minor / make major — tags vX.Y.Z and pushes the tag