The go-unit-tests hook caps every package at 30s and re-runs the whole module
once per file batch, so the git-fixture tests in cmd/agentws timed out as soon
as they grew.
- Replace go-unit-tests with a module-level `go test ./...` local hook
`agentws new --branch` always forked from the base branch, so a worktree for a
branch that already existed on origin started at the base's HEAD with none of
the branch's commits, and callers had to reset --hard afterwards to recover.
- Check out an existing origin/<branch> and set the branch to track it
- Fast-forward a stale local branch onto origin, keeping unpushed commits
- Fork from --from or the remote default only when origin lacks the branch
- Name which of the two paths was taken in the command's output
- Ignore --from, with a note, when the branch is already on origin
--include-unmanaged lifts only the location gate, and nothing proved the keep
gate still held behind it.
- assert a dirty hand-made worktree survives --include-unmanaged alone
Any git error on a directory under the worktree root was classified orphan,
and orphan deletes the directory outright, so a transient failure reading the
source root became data loss on a plain `agentws prune --yes`.
- prove a backing repo gone by stat before calling a directory an orphan
- classify an unexplained git failure as keep, naming the error
- refuse to remove a worktree whose git state is unknown, even with --include-keep
- spell out that --include-keep discards uncommitted and in-progress work
- discover worktrees from `git worktree list` on each source checkout, not only
the worktree root, so hand-made ones, stale registrations and orphaned
directories are classified too
- normalise each candidate to its main checkout, so a linked worktree in the
source root cannot offer up the repo's real checkout
- keep locked, mid-rebase and detached-with-unique-commits worktrees, whose
removal would destroy state nothing else holds
- name the retained branch in every unproven verdict
- add --no-fetch, --json, --include-unmanaged and --include-keep
A stale remote-tracking ref survives a failed fetch and the next successful
--prune deletes it, so it cannot prove a branch's commits survive upstream.
Record whether the pruning fetch succeeded and gate the origin/<branch>
existence and containment proofs on it; a failed fetch removes the worktree and
keeps the branch. Local-object proofs and the merged head SHA are unaffected.
A merged or closed PR no longer authorises a delete on its own: HEAD must
be contained in the PR's head commit or in origin/<branch>, otherwise the
worktree goes and the branch stays. Branch deletion runs `git branch -d`
first and falls back to -D only for a proven branch.
Reword the cherry check to say patches reached the default branch's
history, print the verdict --keep-branches will actually perform, and warn
when a PR listing hits the pagination cap instead of reading it as "no PR".
Agents leave their managed worktrees behind, and `agentws rm` takes one path at
a time with no idea whether a branch's work is safely upstream, so clearing an
accumulation by hand risks destroying unmerged commits.
- classify every managed worktree: dirty, PR open, upstream, or unproven
- remove only what is safe; delete the local branch only when work is upstream
- prove "upstream" with merge-base and git cherry, so squash merges count
- match a PR by head.label, which survives the branch deletion a merge does
- dry run by default; --yes applies, --keep-branches spares every branch
- read the Gitea path from origin's URL rather than assuming the owner
Only a 404 from GetPR means the PR is gone. A 404 from any other call
can be a proxy or ingress blip, so it now warns and counts against the
consecutive-failure cap instead of killing the watch on first sight.
- treat a mid-run 404 on a tracked PR as terminal
- cap consecutive transient poll failures at 20 per PR
- reset the failure count on a successful poll
- export IsNotFound for callers to classify the abort
- add agent.ParseDurationFlag: bare integer means seconds, duration strings still parse
- take --interval as a string and parse it in the command
- reject unparseable and non-positive values with an error naming the flag
Vault-minted Gitea tokens last ~1h, far less than a watch, and every poll
past expiry 401'd into a warning while watchpr looked healthy.
- retry a rejected request once with a freshly minted token
- abort the watch when the fresh token is rejected too
- poll anonymously when no token can be minted, mint only on a real 401/403
Seeding an oauth2-proxy secret by hand means an agent shell-plumbing a
client secret and a cookie secret, which the classifier blocks. seed-oauth
does it in one self-contained invocation: it reads the KV path, fills in
only the keys that are missing, preserves everything else and prints key
names and the new version, never a value.
- Add SeedOAuth in internal/agent: read-modify-write of the client_id,
client_secret and cookie_secret keys with per-key created/kept/rotated
actions and a no-op when nothing changed.
- Generate secrets from 32 crypto/rand bytes; cookie_secret is base64url so
it decodes to exactly the 32 bytes oauth2-proxy requires.
- Add ReadKVOptional (missing secret = empty) and WriteKVAny (non-string
fields survive a round trip) to the KV-v2 client.
- Wire the seed-oauth subcommand and document it in README and AGENTS.md.
- Cover fresh create, patch-preserves-client_secret, other-key
preservation, --rotate, idempotence, denial errors and secret leakage.
agentpr always read gitea/creds/unkin-agent from a bare const, so a service
like repospawner could not run it as its own Gitea identity.
- Replace the GiteaCredsPath const with a function: GITEA_CREDS_PATH when set,
otherwise gitea/creds/<AGENT_LOGIN>. Unset env still resolves to
gitea/creds/unkin-agent, so existing callers are unchanged.
- Thread the creds path through fetchGiteaToken/readGiteaCreds instead of
reading a package-level const, and report it in the error messages.
- Make agentpr's help text login-agnostic and document both variables.
Interactive agents are classifier-blocked from plumbing credentials through
a shell, so seeding an Authentik outpost token into Vault KV needs to happen
inside one binary invocation that never exposes the secret.
- Add cmd/agentvault, a fourth CLI sharing the agentpr Vault AppRole login
(role_id only, VAULT_ADDR/AGENT_APPROLE_ROLE_ID defaults unchanged).
- Add `agentvault seed-outpost`: read the Authentik API token from
kv/service/authentik/agent-api-token (field `token`, falling back to
`api_token`), exact-match the outpost by name via the instances search,
fetch its key from /api/v3/core/tokens/<identifier>/view_key/ and write it
to --dest-path under --dest-key.
- Print only the outpost name, token identifier, dest path and new KV
version; keep secret material out of results, errors and logs.
- Distinguish the failure stages (login, KV read denied, outpost missing,
view_key, KV write denied) with ErrVaultDenied/ErrVaultNotFound/
ErrOutpostNotFound sentinels and actionable messages.
- Add internal/agent vaultkv.go (AppRole-authenticated KV-v2 client) and
authentik.go (outpost search + view_key) for reuse by future flows.
- Cover the happy path, idempotent re-run, field fallback and every failure
mode with httptest servers, including a leak check on error strings.
- Wire agentvault into the Makefile, build-rpm.sh, nfpm contents, release
cross-builds/assets, README and AGENTS.md.
FetchState fetched the PR (merged=true) but then failed the whole state
fetch when CommitStatus 404'd for a head commit that no longer existed
(the branch was deleted after a squash/rebase merge). The merge signal was
discarded, so the watch loop treated every post-merge poll as a transient
error and never exited -- the 37-minute hang seen in production.
- Add a typed APIError carrying the HTTP status so callers can detect a 404
without parsing error strings.
- FetchState now tolerates a 404 from CommitStatus (commit gone => no CI
status) and returns the authoritative merged/closed PR state.
- Regression tests: FetchState survives a 404 status; the full watch loop,
driven through a real client, detects a merge whose head commit is gone
(both fail/hang before the fix).
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.
watchpr only reported meaningful changes as transitions from the poll
baseline. A PR already merged or closed when the watch started was
captured as the baseline and never produced a transition, so the loop
polled the dead PR forever (process alive, never exiting) -- the
single-PR --interval case observed in production.
Add a terminal-state check applied to the baseline snapshot: a PR that
is already merged or closed the moment watchpr starts is reported and
exits immediately, since it can never change again. Extract the
baseline+poll loop into agent.Watch behind a StateFetcher interface so
the loop, its open->merged/close detection, and its poll-error
resilience are unit-testable with a fake client.
agentpr/watchpr already propagated command errors to a non-zero exit, but
that behaviour had no regression coverage and the root command was not
constructible outside main(). watchpr also fired a spurious conflict alert
because Gitea computes mergeability asynchronously and can briefly report
mergeable=false right after a push. The docs additionally printed the
AppRole role_id literal UUID.
- Extract newRootCmd() in both cmd/agentpr and cmd/watchpr so main() only
runs Execute and exits non-zero on error; add tests asserting Execute
returns an error for a bad PR ref / malformed --repo / no args.
- Debounce mergeability loss in MeaningfulChange: only alert when
mergeable=false persists across two consecutive polls (both prev and cur
false, still open); update the table test for one-poll-false (benign),
false-persisting (alert), and recovered false->true (benign).
- Refer to AGENT_APPROLE_ROLE_ID by env var in README.md/AGENTS.md without
printing the literal role_id; keep the code default and env override.
golangci-lint v2 (errcheck) flagged three unchecked resp.Body.Close()
returns in gitea.go and vault.go. Wrap each deferred Close in a closure
discarding the return, the idiomatic form for a deferred Close whose
error is intentionally ignored.
- go.mod: go 1.26.5 -> 1.25.7 to match the golang:1.25 CI image (node-lookup convention)
- .pre-commit-config: dnephin go-vet runs at repo root (no .go files here since both
tools live under cmd/); replace with a local 'go vet ./...' hook (CI pre-commit
image has go)
agentpr manages PRs/comments/whoami as unkin-agent (Vault AppRole -> gitea creds
-> Gitea API), fixing tea's post-as-Ben default. watchpr polls PRs and alerts
only on merge/close, human comment, CI failure, or lost mergeability.
- cobra multi-binary layout mirroring node-lookup (cmd/ + internal/)
- Makefile (build, patch|minor|major, completions, rpm), nfpm RPM with both
binaries + bash/zsh/fish completions, woodpecker CI publishing to rpm-internal
- unit tests for parsing, meaningful-change detection, and the Vault+Gitea client