a-gent

module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: MIT

README

a-gent

        ▄▄██▄▄        
      ▄████████▄      
      ██████████      
      ▀████████▀      
       ▀▀████▀▀       
  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄  
▄████████████████████▄
██████████████████████
██████████████████████
████▀████████████▀████
████  ██████████  ████
████  ██████████  ████
████  ██████████  ████
▀██▀  ██████████  ▀██▀
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ████  ████      
      ▀██▀  ▀██▀      

ci Go Reference Go Report Card

Task state for coding agents, stored as git objects under refs/a-gent/.

Task data travels with the repo, syncs over any git remote, works offline, and never appears in the working tree — no .gitignore entry, no stray directory, nothing for an agent to stumble into.

Status: in development. The storage layer is the current focus.

Install

Requires git. macOS and Linux, amd64 and arm64.

go install github.com/samsambutdifferent/a-gent/cmd/gent@latest

That drops the binary in $(go env GOPATH)/bin, which is not on PATH by default. If gent comes back as command not found, add it:

echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc

Or download a release archive from Releases. The Linux binaries are statically linked, so they run on any distribution regardless of libc — including musl-based ones like Alpine.

Use

gent init                              # prepare the repo
gent create "Add rate limiting"        # -> E5MVA1
gent set E5MV dispatched "to claude"   # any unambiguous prefix will do
gent list
gent show E5MV
gent archive E5MV                      # off the list, still there
gent list --archived                   # or --all
gent unarchive E5MV
gent push                              # send task refs to origin
gent pull                              # fetch and reconcile
gent doctor                            # check the config

Task IDs are random, not sequential. Two agents working offline would both reach for T-001, and because reconciliation keys events by ULID it would fuse the two chains into one task holding two created events — silently. Random ids make that unreachable, and a guard in reconciliation refuses the merge if it ever happens anyway.

Listings and messages show the first six characters; commands accept any unambiguous prefix, the same bargain git makes with commit SHAs. --json always carries the full id.

Every read command takes --json, which is the query layer — pipe it wherever you like:

gent list --json | jq -r '.[] | select(.state=="blocked") | .id'

Working on a task in a container

gent gives a task its own container: a clone of the repository, the task's spec mounted, no access to your home directory, your credentials, your object store, or your other tasks. What gets committed comes back as a branch. Your working tree is never touched.

There are two ways in, and which one you want depends on who starts the work.

An agent — gent shell
git config --global a-gent.engine podman      # docker, podman, or nerdctl
git config a-gent.image ghcr.io/acme/dev:1    # your toolchain

gent shell E5MV
#   branch  gent/E5MV/01J8XK  (based on 4f2a1c9e8b3d)
#   spec    /gent/spec.md
#   $ claude          # you start it, at your own prompt
#   $ exit
#   -> 3 commits on gent/E5MV/01J8XK

You start the session, which matters for two reasons. An agent's permission system is built to escalate to a human — auto mode ends a session rather than guessing — so with you there it works as designed, and nothing has to be pre-approved or skipped. And starting an agent session from a script is automating access to a service, which a consumer subscription does not cover; opening a terminal isn't.

You are not tied to the terminal you started it in. ctrl-\ detaches and the session keeps running; gent attach E5MV's run id brings you back, printing what happened while you were gone before it reconnects — usually the part you need in order to answer whatever it is waiting for.

gent attach 01J8XK4M       # replay, then reconnect
gent logs 01J8XK4M -f      # or just watch, without a keyboard attached

Which raises the question of when to come back. The container installs Claude Code hooks that report a permission prompt or an idle prompt, and gent reads them:

gent runs
#  RUN       TASK    STATUS   ENGINE          NEEDS YOU
#  01J8XK4M  E5MV    running  docker (shell)  permission: Bash(git push --force)
#  01J8XN2P  A71Q    running  docker (shell)  —

gent runs --watch                      # redraws, and pings you
git config a-gent.notify \
  'osascript -e "display notification \"{{reason}}\" with title \"gent {{run}}\""'

a-gent.notify runs whatever you like — {{run}}, {{task}}, {{reason}} and {{branch}} expand per argument, never through a shell. It fires once per distinct request, not once per poll. Unset by default: the column is still the signal.

The detach key is ctrl-\ rather than the engine's own ctrl-p ctrl-q, because ctrl-p is previous-history in readline and in Claude Code's input — you would fight it every time you recalled a command. Change it with git config a-gent.detach-keys.

Deterministic work — gent run
gent run E5MV -- make test        # dispatched; returns immediately
gent runs                        # live and finished
gent logs 01J8XK4M -f            # tail the output
gent stop 01J8XK4M               # one clean note, no orphans
gent clean                       # remove containers and clones, keep branches

For tests, builds, migrations, verifiers — anything nobody needs to watch. If you want an agent dispatched this way, authenticate it with an ANTHROPIC_API_KEY (see below) and give it --permission-mode dontAsk with an explicit allowlist, so its blast radius is declared rather than removed.

What the container stops

The agent cannot reach your home directory, your credentials, your object store, or your other tasks. Only committed content on one branch is present, and untracked files like .env never enter, because the clone is made from the gitdir rather than the working tree. It cannot push, and it cannot write to your repository. What it commits returns as a real branch via a git bundle; gent lands it and stops, because merging is your decision.

Two things worth knowing. Only commits leave — the spec handed to the agent says so explicitly, and uncommitted work is discarded when the clone is cleaned. And a container stops an agent damaging your machine while it works; it says nothing about whether the code it produces is safe to run, which is why the branch is landed rather than merged.

Keeping the credential out of the container

gent proxy holds the credential so the agent never does. The container gets a placeholder and a base URL; the proxy swaps in the real value on the way out.

export REAL_KEY=...
gent proxy --upstream https://api.anthropic.com --inject 'x-api-key=${REAL_KEY}'

It prints the exact gent run flags to point a run at it. By default it binds an address containers can reach and your network cannot — loopback under a VM-based engine, the bridge gateway on native Linux — because a proxy holding a credential must not be offered to your whole LAN.

A compromised agent can still use the API while the proxy runs. What it cannot do is walk away with a credential that still works tomorrow, so a compromise ends when you stop the proxy. The access log records every call it made.

For Claude Code specifically, the container needs CLAUDE_CODE_SIMPLE=1 and a placeholder ANTHROPIC_API_KEY: without them the CLI looks for real OAuth credentials, finds none, and refuses to start.

gent doctor reports the resolved engine, whether it is reachable, and whether it can actually see your repository — that last one matters on macOS, where the engine runs in a VM and a bind mount from an unshared path silently arrives empty.

There is no delete

Events are append-only, so nothing is ever removed. What you actually want when a task is finished with is gent archive, which moves its ref from refs/a-gent/tasks/ to refs/a-gent/archive/. Nothing is rewritten and no event is appended: the chain keeps every commit, its state is still whatever its last real event said, and gent unarchive puts it back exactly as it was. Archiving is a filing decision, not a state.

An archived task stays addressable by prefix, stays readable, stays writable, and still owns its id — so create can never hand that id to something else. It also keeps syncing: gent push sends archived chains into the remote's tasks namespace, because the wire format has no notion of an archive. That is deliberate. Archiving is about your listing on this machine, so it does not propagate to other clones, and a pull will not un-file something you filed away.

Why refs

Three properties fall out of storing task state under refs/a-gent/ rather than as files in the tree:

  • Invisible to agents. The working tree only ever reflects HEAD. Task data exists in the object database but is in no checkout.
  • No merge conflicts. Nothing is in the tree, so nothing can conflict during a merge. Concurrent writes are handled by appending events and sorting them, not by git's merge machinery.
  • Optional sharing. Solo: never push the namespace. Team: push it. Same tool, same data, one config line apart.

A caveat worth knowing up front

A plain git push sends only refs/heads/* and refs/tags/*. Task refs stay local unless you push them explicitly — that is what gent push is for. Run gent doctor if you are unsure whether a repo is configured correctly.

GitHub and GitLab accept the namespace on push but do not render it in their web UI. Your task data is there and invisible in the browser; that is expected, not a failed push.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
gent command
Command gent stores agent task state as git objects under refs/a-gent/.
Command gent stores agent task state as git objects under refs/a-gent/.
internal
cli
Package cli wires the gent command tree.
Package cli wires the gent command tree.
container
Package container builds the command line that runs a task inside a container.
Package container builds the command line that runs a task inside a container.
gitx
Package gitx is a thin wrapper over the user's own git binary.
Package gitx is a thin wrapper over the user's own git binary.
proxy
Package proxy is a credential-injecting reverse proxy.
Package proxy is a credential-injecting reverse proxy.
run
Package run holds the machine-local state of a dispatched run.
Package run holds the machine-local state of a dispatched run.
store
Package store is the whole data model: append an event, read a chain, fold it to state, reconcile divergence.
Package store is the whole data model: append an event, read a chain, fold it to state, reconcile divergence.
task
Package task defines the event model stored under refs/a-gent/.
Package task defines the event model stored under refs/a-gent/.
testrepo
Package testrepo builds throwaway git repositories for tests.
Package testrepo builds throwaway git repositories for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL