Skip to content

Repository files navigation

writ

writ — why read the whole diff when only the drift matters?

Today an agent writes code, opens a PR, and a human reads a large diff to decide whether to merge. That does not scale: the diff is too big to read and arrives stripped of the context that produced it.

writ replaces that. A writ is the agreed scope of a piece of work, approved before any code exists: an intent, a set of checkable acceptance criteria, a declared file scope, and a verification command. The agent implements against it. writ then computes what actually changed, compares it against the declared scope, runs the verification, and shows the human only what fell outside the writ - the drift. Zero drift, plus every criterion met, plus a green verification, merges with no human at all.

A writ that declares its scope as the whole repository makes drift meaningless, so writ refuses to accept one - at intake and again before any status or merge decision.

Star this repo Built for agent workflows

CI CodeQL Release License: MIT Go Platform Homebrew

InstallThe loopAgent rulesCompletionsExit codesFAQContributingLicense

Code of conductContributingLicenseSecurity

Install

brew tap laaaaksh/writ
brew trust laaaaksh/writ
brew install Laaaaksh/writ/writ

The brew trust step is required once: current Homebrew refuses to load formulae from third-party taps it has not been told to trust.

writ ships for macOS and Linux. Its only runtime requirements are git, which every command uses to compute drift and merge, and a POSIX sh, which runs verification commands and $EDITOR launches. Alternatively, with a Go toolchain:

go install github.com/Laaaaksh/writ/cmd/writ@latest

Shell completions

writ generates tab-completion scripts for bash, zsh, fish, and PowerShell; writ completion <shell> --help prints full per-shell instructions.

# zsh - current session; append the line to your .zshrc to keep it
source <(writ completion zsh)

# bash - current session; append the line to your .bashrc to keep it.
# eval is used because macOS's default bash (3.2) cannot source from
# <(...); results require the bash-completion package.
eval "$(writ completion bash)"

# fish
writ completion fish > ~/.config/fish/completions/writ.fish

The loop: propose -> approve -> implement -> attest -> merge

1. writ propose - the agent drafts a complete writ and proposes it, before writing any code:

$ writ propose <<'EOF'
id = "writ-1"
intent = "add a retry to the webhook sender"
base = "main"
created = 2026-01-01T00:00:00Z
scope = ["internal/webhook/**"]

[[criteria]]
id = "retries-on-5xx"
text = "a 5xx response is retried with backoff"

[verify]
command = "go test ./..."
EOF

--file <path> reads the writ from a file instead of stdin. writ validates it and writes it to .writ/current.toml, unapproved. Criteria must arrive unassessed: a draft carrying met or an attestation is refused, because claims are recorded with writ attest only after a human approves. Drafting a concrete proposal - not hand-authoring path globs from a blank file - is the agent's job, because a lazy scope like app/** makes drift meaningless.

propose also keeps writ's bookkeeping out of your history: when no ignore rule covers .writ/, it seeds the repo-local .git/info/exclude, so committing your work wholesale (git add -A) can never track the state file - and if it does end up tracked anyway, status and merge refuse with instructions to untrack it.

2. writ approve - a human reviews the proposal, opening it in $EDITOR to tighten scope or criteria before agreeing to them. On save and exit, writ re-validates; if invalid, it prints the problems and leaves the file in place. --yes approves the proposal as-is, skipping the editor.

3. Implement - the agent creates a branch off base (git checkout -b <branch>) and does the work described by the writ there, committing as it goes: merge refuses a dirty working tree, and commits made on base itself are invisible to drift.

4. writ attest <criterion-id> --note "<how>" - the agent (or a human, with --human) claims that a criterion is met and records how. This is a claim, not a fact: writ status renders it as "claimed by agent", visibly distinct from criteria a human confirmed or evidence that ran and passed. Attesting requires an approved writ - claiming against a contract nobody agreed to is meaningless. writ unattest <criterion-id> clears it.

5. writ status / writ merge - status computes drift, runs the verification command, and renders the decision: zero drift, a green verification, an approved writ, and every criterion attested auto-merges with no human; anything else names the reasons a human is needed. merge does the same and, if mergeable (or given --approve), merges the writ's branch into base and clears .writ/current.toml.

Run both from the writ's own feature branch, not from base: writ refuses them while HEAD is on the base branch itself, where nothing can be merged and commits on base would be invisible to drift. Linked git worktree checkouts work too, but merge additionally needs base not to be checked out anywhere else - git allows a branch in only one worktree - so writ refuses with the command to detach or remove the checkout holding it.

The verification command runs through sh -c in the repo root; its exit code alone decides pass or fail. It is killed after 10 minutes by default - set WRIT_VERIFY_TIMEOUT (a duration like 45s or 30m) to change that; invalid or non-positive values fall back to the default.

When a proposal is rejected or abandoned, writ discard removes the open writ so propose can start fresh; it touches only .writ/current.toml, never branches or commits.

A full loop, as status sees it at the end of a real run - the agent's claim on record, verification green, nothing outside the declared scope:

$ writ status
  writ    add a retry to the webhook sender

  CONTRACT     1/1 criteria
                 retries-on-5xx   claimed by agent   "injected a 500 then watched the sender retry twice before succeeding"
  EVIDENCE     go test ok
  IN SCOPE     1 files
  DRIFT        none

  Auto-mergeable: zero drift, verification passed, all criteria met.
$ writ version

Prints the writ CLI version; writ --version (or -v) prints the same line.

Drive it from your agent's rules

writ only works if the agent actually follows the loop, and agents follow what their instruction file says. Paste this into your repo's AGENTS.md or CLAUDE.md:

## Writ discipline

For any non-trivial change, drive the writ loop instead of free-form editing:

1. Propose before code. Read the relevant code first, then draft a complete
   writ - intent, checkable criteria, the narrowest honest file scope, and a
   verification command - and pipe it to `writ propose`. Never invent path
   globs from memory: a lazy scope like `app/**` makes drift meaningless.
2. Wait for approval. Never implement against an unapproved writ. The human
   runs `writ approve`; proceed only once it succeeds. `--yes` is the
   human's call, never yours.
3. Stay inside the declared scope. Work on a branch off `base`. If the work
   turns out to need files outside the writ's scope, stop: `writ discard`,
   draft a new writ that covers reality, and propose again. Quiet scope
   creep is exactly what writ exists to catch.
4. Attest only what you checked. Run each criterion's check yourself, then
   record it: `writ attest <criterion-id> --note "<what you observed>"`.
   Never pass `--human`: that records a human confirmation, and only humans
   may make one.
5. Let exit codes decide. Run `writ status` from the feature branch. Exit 0
   means it will auto-merge - say so and stop. Exit 1 means a human is
   needed: print writ's reasons verbatim and wait. Never edit
   `.writ/current.toml` by hand; use `attest`, `unattest`, or `discard`.

Exit codes

For scripting and agents, writ signals its decision on the process exit code:

  • 0 - success; for status, the writ is auto-mergeable, and for merge, it merged
  • 1 - a human is needed (always from status; from merge unless --approve was given), or any other error occurred
  • 2 - no writ is open (from any command that reads one: approve, attest, unattest, status, merge, discard)

Output stays script-friendly: ANSI color appears only when stdout is a terminal, and setting NO_COLOR (to any non-empty value) turns it off even then, so piped or captured output is always plain text.

FAQ

Does writ need an AI subscription or API key?

No. writ never talks to a model provider: there is no HTTP client anywhere in it or in its two dependencies, it reads no API keys from your environment (the only variables it touches are EDITOR, VISUAL, NO_COLOR, and WRIT_VERIFY_TIMEOUT), and its only child processes are git and a POSIX sh. All the intelligence lives in whatever coding agent you already run; writ is just the contract both of you sign and the referee that checks the work.

Which agents can drive it?

Any agent that can write TOML text and run shell commands: Claude Code, Codex CLI, opencode, Cursor, aider, a cron script you wrote yourself. There is no plugin to install - the whole interface is stdin, stdout, and exit codes.

Can several agents work in one repo?

Not on one writ. State lives in a single .writ/current.toml per repository, and a second propose refuses while one is open. Give each parallel agent its own checkout with git worktree add ../repo-agent-b; each worktree holds its own open writ.

Should my parallel agents share my API key or subscription?

No. N agents behind one key multiply your request rate N-fold, so when the provider answers HTTP 429 (rate limited), all of them hit the wall at once - and each agent's retry logic then fires together, producing a retry storm that can burn through quota or get the key throttled entirely. Put each worktree on its own key or subscription so agents fail independently instead of failing together.

Star this repo

If writ makes reviewing agent PRs tractable, leave a star - it helps other people find it.

Star History Chart

License

MIT - see LICENSE.

About

Agents work under a writ. The tool verifies they stayed inside it, and shows you only the drift.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages