Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Feature request
description: Suggest an improvement or new capability for writ
labels: ["enhancement"]
body:
- type: textarea
id: use-case
attributes:
label: What are you trying to do?
description: The problem in your propose -> approve -> implement -> attest -> merge flow, not the solution. A workflow that needs a workaround today is a great answer.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: What should writ do?
description: The behavior you want, ideally as commands and output. If you have a sketch of a writ TOML change, include it.
placeholder: |
$ writ <something>
<expected output>
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: What do you do today instead?
description: Existing writ features, raw git, scripts, or other tools you use to get by without this.
validations:
required: false
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: Gofmt
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-clean:"
echo "$unformatted"
exit 1
fi

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test ./... -race -count=1

test-macos:
name: Test (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version-file: go.mod
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- The writ lifecycle is `propose` (agent drafts a complete writ, unapproved) → `approve` (human tightens it in `$EDITOR`, `--yes` to skip) → implement → `attest`/`unattest <criterion-id>` (claim or clear a criterion as met, `--human` for a human claim) → `status`/`merge`, with `discard` throwing away an unwanted open writ (rejected proposal, abandoned work, corrupt state file) so `propose` is unblocked. There is no `writ open`: a hand-authored writ from a blank file was the failure mode this replaced. `writ.Writ.Approved` (nil = proposed) and `Criterion.Attestation` gate `gate.Decide`'s auto-merge path; `render.Status` keeps agent claims visually distinct from human confirmation and from machine-verified evidence — never blur the three. Intake (`propose` and `approve`) validates via `writ.Writ.ValidateProposal`, which additionally refuses criteria arriving with `met`/`attestation` set — claims exist only after approval, so pre-assessed drafts cannot self-bless through `approve --yes`; plain `Validate` stays the validator for mid-flight state. Author-written TOML (stdin/`--file` drafts, `$EDITOR` saves) parses through exported `writ.Parse`, which refuses keys naming no Writ field so typos are reported instead of silently dropped; `writ.Load` deliberately stays lenient for Save-written `.writ/current.toml` reads so version skew cannot strand an open writ.
- `.writ/current.toml` is runtime output, never committed. Package-level constant `writPath` in `internal/writ/writ.go` is the source of truth for its location. Two defenses keep it that way: `propose` seeds `.writ/` into the repo-local `.git/info/exclude` (best-effort, skipped when an ignore rule already covers it; resolved via `--git-common-dir` because git reads info/exclude only from the common dir — seeding a linked worktree's own gitdir silently does nothing) so blanket `git add -A` cannot track the state file, and `decide()` (status/merge) refuses a git-indexed state file (`cmd/writ.writStateTracked`, staged counts too) with `git rm --cached` guidance — a tracked copy leaves stale state on base and blocks checkout with raw git errors.
- `cmd/writ` commands signal a specific process exit code (0 auto-mergeable, 1 needs human, 2 no writ open) by returning the unexported `exitCodeErr{code}` from `RunE`; `main()` unwraps it via `errors.As` before falling back to the default exit-1 path. Command tests that need a real repo `os.Chdir` into a `t.TempDir()` git repo, since `repoRoot()` shells out to `git rev-parse --show-toplevel` against the process cwd rather than taking a directory argument.
- The README's FAQ section states verifiable facts about the codebase ("no HTTP client anywhere in it or its two dependencies", "reads no API keys — only `EDITOR`/`VISUAL`/`NO_COLOR`/`WRIT_VERIFY_TIMEOUT`", "only child processes are `git` and POSIX `sh`"), and its `$ writ status` fence embeds byte-real output for the same writ the propose example creates. These rot silently when reality changes: adding a dependency, an env read, a subprocess, or changing `render.Status` output means re-verifying/updating them (`rg "net/http|Getenv|LookupEnv|exec.Command" --type go` covers the FAQ trio). Capture doc examples by running the real binary in a throwaway repo, never by hand-typing output.
- This repo is on the captain's **personal** GitHub account (`Laaaaksh`). The default `gh` login has changed between the personal and work profiles over time (as of 2026-08-22 the personal account is the default and `~/.config/gh-personal` no longer exists), so never assume either way: verify with `gh api user --jq .login` (must print `Laaaaksh`) before any GitHub CLI operation, and if it prints a work account, export `GH_CONFIG_DIR="$HOME/.config/gh-personal"` first — recreating that dir via `gh auth login` if needed. The default branch is `master`, not `main`.
- A repo commit-identity guard hook requires the git author email to match the personal class (`laksh.sadhwani07@gmail.com`) for this remote — set via local (not global) `git config user.email` if a commit is rejected.
- Release packaging: `.github/workflows/ci.yml` (build/vet/test on PR and push to `master`), `.github/workflows/release.yml` (goreleaser on `v*` tag push), and `.goreleaser.yml` (darwin/linux amd64/arm64, publishing a Homebrew formula to `Laaaaksh/homebrew-writ` via `brews:`) mirror the sibling `Laaaaksh/vessel` repo's setup. Releasing requires a `HOMEBREW_TAP_TOKEN` repo secret — without it goreleaser falls back to `GITHUB_TOKEN`, which cannot write to the separate tap repo and the release fails (this is what broke vessel's first release). The tap repo's README.md is hand-maintained (goreleaser writes only `Formula/`), so when writ's README install instructions change, mirror them there too — it kept serving a stale broken one-liner for releases after the main README was fixed (corrected 2026-08-22). `.github/dependabot.yml` runs weekly `gomod` + `github-actions` version-update PRs on top of the GitHub-side Dependabot alerts/security-fixes toggles (those live in repo settings, not files); expect its PRs against `master`. `.github/workflows/codeql.yml` runs CodeQL code scanning (languages go,actions) on PRs, pushes to `master`, and a weekly cron — its status check is named `CodeQL`; activity is proven by `/code-scanning/analyses` entries, not default-setup state.
- Release packaging: `.github/workflows/ci.yml` (ubuntu build/vet/gofmt/test plus a separate `Test (macOS)` job for the darwin release target; the macOS job must stay its own job, not a matrix on `test`, because branch protection requires the literal check name `Test` and matrix suffixes like `Test (ubuntu-latest)` would never satisfy it — the gofmt gate lives inside `Test` so unformatted code cannot land even though only that one check is required), `.github/workflows/release.yml` (goreleaser on `v*` tag push), and `.goreleaser.yml` (darwin/linux amd64/arm64, publishing a Homebrew formula to `Laaaaksh/homebrew-writ` via `brews:`) mirror the sibling `Laaaaksh/vessel` repo's setup. Releasing requires a `HOMEBREW_TAP_TOKEN` repo secret — without it goreleaser falls back to `GITHUB_TOKEN`, which cannot write to the separate tap repo and the release fails (this is what broke vessel's first release). The tap repo's README.md is hand-maintained (goreleaser writes only `Formula/`), so when writ's README install instructions change, mirror them there too — it kept serving a stale broken one-liner for releases after the main README was fixed (corrected 2026-08-22). `.github/dependabot.yml` runs weekly `gomod` + `github-actions` version-update PRs on top of the GitHub-side Dependabot alerts/security-fixes toggles (those live in repo settings, not files); expect its PRs against `master`. `.github/workflows/codeql.yml` runs CodeQL code scanning (languages go,actions) on PRs, pushes to `master`, and a weekly cron — its status check is named `CodeQL`; activity is proven by `/code-scanning/analyses` entries, not default-setup state. `.github/ISSUE_TEMPLATE/` holds GitHub issue-form YAML (`bug_report.yml`, `feature_request.yml`) plus `config.yml`; keep new templates in that schema rather than markdown bodies. `CHANGELOG.md` is the human-readable release record in Keep a Changelog format — GitHub release bodies are just goreleaser commit dumps — so land user-facing changes with an `[Unreleased]` entry as part of the change itself, and when cutting a tag move `[Unreleased]` into a dated section with compare links (backfilled 0.1.0–0.2.4 from per-tag `git log --no-merges <prev>..<tag>` ranges, since goreleaser's release notes misattribute commits across the tightly-spaced v0.2.x tags).
- `go.mod`'s `go` directive is the single source of the Go toolchain for CI and releases: both workflows pass `go-version-file: go.mod` to actions/setup-go, so that line decides which compiler (and which stdlib security patches) ship inside released binaries — v0.2.x shipped go1.22.12, EOL since Feb 2025, before the directive was bumped to a supported line. Audit it against the two-current-versions support policy whenever cutting a release.
- `master` is branch-protected: required `Test` status check (strict), enforced for admins, conversation resolution required, no direct pushes — land every change through a PR and wait for its `Test` check (`gh pr merge` honors the rules); `CONTRIBUTING.md` documents the contributor-facing flow. Repo settings keep `delete_branch_on_merge` on: after any PR merges (including this run's own gnhf branch PRs), GitHub deletes that head branch from origin — re-push the local branch to recreate it; don't misread the vanished origin ref as data loss. Wiki/Projects tabs are deliberately disabled; don't re-enable them when auditing settings.
- `cmd/writ/main.go`'s `version`, `commit`, and `date` must stay package-level `var`s (with `default*` const placeholders), never `const` — goreleaser's `-X main.version=...` ldflags silently no-op against a `const` string, so every released binary would misreport itself. `versionString` is the single place that renders them; it omits the `(commit ..., built ...)` suffix when both are still at their defaults.
Expand Down
116 changes: 116 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Changelog

All notable changes to writ are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- FAQ answering what AI-coding-agent users actually ask: writ needs no AI subscription or API key, works with any agent that can pipe TOML and run shell commands, allows one open writ per repo (parallel agents need separate git worktrees), and each worktree should use its own provider key or subscription because sharing one invites rate-limit retry storms.
- Real rendered `$ writ status` example output and a pasteable agent-discipline snippet for your repo's `AGENTS.md` / `CLAUDE.md` driving the propose → implement → attest loop.
- Feature-request issue template alongside the bug template.
- CI runs a gofmt gate inside the required `Test` check and a dedicated macOS job covering the darwin/arm64 release target.
- This changelog.

## [0.2.4] - 2026-08-22

### Security

- Released binaries now build with a supported Go toolchain (`go.mod`'s `go` directive bumped off EOL Go 1.22); both workflows resolve their compiler from that directive.
- CodeQL code scanning added as an advanced-setup workflow covering Go and GitHub Actions.

### Fixed

- Generated Homebrew formula now passes `brew style` and `brew audit --strict`.

### Changed

- Documented the fork-and-PR contribution flow under branch protection in `CONTRIBUTING.md`.
- Pinned `goreleaser-action` to `~> v2` instead of the deprecated floating latest.

## [0.2.3] - 2026-08-22

### Fixed

- `writ merge` from a linked git worktree printed an Auto-mergeable verdict and then died on git's raw "'main' is already used by worktree" fatal; it now refuses up front with detach-or-remove instructions.

### Security

- Added `SECURITY.md` with a private vulnerability-disclosure channel; enabled Dependabot vulnerability alerts, automated security fixes, and private vulnerability reporting.
- Weekly dependency-update PRs via `.github/dependabot.yml` (`gomod` + `github-actions`).

### Changed

- Bumped `goreleaser-action` from v6 to v7.

## [0.2.2] - 2026-08-22

### Fixed

- Clear failure message when git itself is missing from PATH (previously misreported as "not inside a git repository").
- README corrected: writ requires git and POSIX sh and ships for macOS/Linux only.

## [0.2.1] - 2026-08-22

### Security

- CI workflow hardened to least-privilege `permissions: contents: read`.

### Fixed

- Install docs cover the explicit trust step current Homebrew requires before using an untapped third-party formula.

## [0.2.0] - 2026-08-22

Public-release hardening of the core shipped in 0.1.x.

### Added

- `writ discard` to throw away an unwanted open writ (rejected proposal, abandoned work, corrupt state file) so `propose` is unblocked without hand-deleting state.
- `--version` / `-v` root flag.
- Strict TOML intake: author-written drafts (stdin, `--file`, `$EDITOR`) reject keys naming no writ field, so typos are reported instead of silently dropped.
- Shell-completion install instructions for bash/zsh/fish using an idiom that works on macOS's stock bash 3.2, plus documentation of color behavior when output is scripted.
- The exit-code contract (0 auto-mergeable, 1 needs human, 2 no writ open) documented across all six deciding commands.
- `WRIT_VERIFY_TIMEOUT` knob documented (10-minute default) and guarded against a zero-value footgun.

### Fixed

- `.writ/current.toml` state file can no longer be accidentally committed: seeded into the repo-local git exclude at propose time (working inside linked worktrees too), with a refusal and untrack guidance if a tracked copy appears at decide time.
- Corrupt state files now point every reading command at `writ discard` instead of surfacing raw errors.
- Commands outside a git repository fail loudly instead of silently treating the current directory as the repo.
- status/merge handle brand-new repos with zero commits (unborn HEAD).
- `approve` works when `EDITOR` contains arguments (e.g. `code -w`).
- Bare interactive `writ propose` refuses up front with actionable remedies instead of hanging silently.
- Dirty-tree merge guard no longer weakened by git's `status.showUntrackedFiles=no`.
- Vacuous verdicts closed: deleted criteria no longer count as "all met"; criteria with empty ids or text are rejected; whole-repo-scoped tampered writs are refused; unresolvable declared bases are refused; driving a writ from its own base branch is refused.
- Agents cannot self-bless acceptance claims: intake rejects criteria arriving pre-assessed (`met`/`attestation` set).
- README documents creating a feature branch off base during implement, matching writ's own refusals.

## [0.1.1] - 2026-08-21

### Fixed

- Generated Homebrew formula writes into the tap's `Formula/` directory so it cannot be shadowed by a root-level formula later.

## [0.1.0] - 2026-08-21

### Added

- Initial public release of `writ`, a git-native contract between you and your coding agent.
- Lifecycle commands: `propose`, `approve`, `attest`/`unattest <criterion-id>`, `status`, `merge`, `version`.
- Drift detection diffing committed work against the writ's declared scope.
- Evidence-based verification running each criterion's verify command and gating the merge decision on results.
- Human attestation kept visually distinct from machine claims and from agent attestations.
- Cross-platform releases (darwin/linux × amd64/arm64) with Homebrew tap publishing.

[Unreleased]: https://github.com/Laaaaksh/writ/compare/v0.2.4...HEAD
[0.2.4]: https://github.com/Laaaaksh/writ/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/Laaaaksh/writ/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/Laaaaksh/writ/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/Laaaaksh/writ/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/Laaaaksh/writ/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/Laaaaksh/writ/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/Laaaaksh/writ/releases/tag/v0.1.0
Loading