From ef0b75a5f17e23384337f619634a15cca68eb306 Mon Sep 17 00:00:00 2001 From: Francisco Rodrigues Date: Thu, 28 May 2026 20:53:55 -0300 Subject: [PATCH 1/2] feat: bootstrap the codeherd release process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This project had no release path. No LICENSE, no installable binaries, no convention for drafting per-version notes, and no standard for the commit messages that feed any future changelog. Cutting 0.1.0 by hand would set no precedent and would block downstream tooling — mise registry submission, version-pinning by users — that depends on signed, named artefacts. The project adopts a single self-contained commit message per branch, drawn from six types — feat, fix, sec, revert, docs, chore — with no Conventional Commits scopes. The squashing-commits skill codifies the writing workflow (problem-first prose, producer- before-consumer ordering, no housekeeping-only paragraphs) and defers prose-level discipline to writing-clearly-and-concisely as a required sub-skill. The creating-release skill consumes commits in that shape: it inspects each one to decide what lands in CHANGELOG.md, and writes docs/release-notes/.md as the canonical per-version narrative. The build lives in three new Make targets so the same recipe runs locally, in CI smoke tests, and during release. A new ch version subcommand lets an installed binary report what release it came from — and lets the pipeline prove the version actually made it into the artefact. The PR pipeline exercises the full cross- compile matrix on every change, surfacing toolchain regressions before they reach a tag. Releases are triggered by a commit to main that touches VERSION. The new release.yml workflow reads the file, builds the linux/darwin × amd64/arm64 matrix via those targets, signs every archive and checksums.txt with sigstore cosign, then creates the v tag and the GitHub release with docs/release-notes/.md as the body. The project ships under Apache 2.0, and the README points users at the new mise + manual install paths so binary consumers are not stuck building from source. Co-Authored-By: Claude Opus 4.7 (1M context) --- .agents/skills/creating-release/SKILL.md | 230 ++++ .../references/categorization.md | 69 ++ .../references/keepachangelog.md | 78 ++ .../creating-release/references/semver.md | 50 + .../scripts/rotate-unreleased.py | 152 +++ .../templates/changelog-section.tmpl.md | 34 + .../templates/release-notes.tmpl.md | 45 + .agents/skills/squashing-commits/SKILL.md | 297 ++++++ .../writing-clearly-and-concisely/SKILL.md | 62 ++ .../elements-of-style.md | 995 ++++++++++++++++++ .claude/skills | 1 + .github/workflows/ci.yml | 20 + .github/workflows/release.yml | 81 ++ LICENSE | 201 ++++ Makefile | 28 +- README.md | 17 + cmd/register.go | 2 + cmd/root.go | 4 +- cmd/root_test.go | 9 +- cmd/version.go | 17 + .../plans/2026-05-28-release-pipeline.md | 758 +++++++++++++ ...026-05-28-creating-release-skill-design.md | 358 +++++++ .../2026-05-28-release-pipeline-design.md | 331 ++++++ main.go | 3 +- skills-lock.json | 72 +- 25 files changed, 3837 insertions(+), 77 deletions(-) create mode 100644 .agents/skills/creating-release/SKILL.md create mode 100644 .agents/skills/creating-release/references/categorization.md create mode 100644 .agents/skills/creating-release/references/keepachangelog.md create mode 100644 .agents/skills/creating-release/references/semver.md create mode 100755 .agents/skills/creating-release/scripts/rotate-unreleased.py create mode 100644 .agents/skills/creating-release/templates/changelog-section.tmpl.md create mode 100644 .agents/skills/creating-release/templates/release-notes.tmpl.md create mode 100644 .agents/skills/squashing-commits/SKILL.md create mode 100644 .agents/skills/writing-clearly-and-concisely/SKILL.md create mode 100644 .agents/skills/writing-clearly-and-concisely/elements-of-style.md create mode 120000 .claude/skills create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE create mode 100644 cmd/version.go create mode 100644 docs/superpowers/plans/2026-05-28-release-pipeline.md create mode 100644 docs/superpowers/specs/2026-05-28-creating-release-skill-design.md create mode 100644 docs/superpowers/specs/2026-05-28-release-pipeline-design.md diff --git a/.agents/skills/creating-release/SKILL.md b/.agents/skills/creating-release/SKILL.md new file mode 100644 index 0000000..9efff98 --- /dev/null +++ b/.agents/skills/creating-release/SKILL.md @@ -0,0 +1,230 @@ +--- +name: creating-release +description: Use when preparing a new release of codeherd — bumping the VERSION file, rotating CHANGELOG.md's `[Unreleased]` block, drafting per-version release notes. +--- + +# Creating a Release + +## Overview + +This skill cuts a release of codeherd in one Claude Code session, semi-automatically. It analyzes commits since the previous tag, classifies them, picks the next SemVer, and writes three artifacts the GitHub release pipeline consumes later: `VERSION`, `CHANGELOG.md`, and a single `docs/release-notes/.md` file. The maintainer reviews the staged diff and approves once at the end. + +**Core principle:** the skill is deterministic about file shape and commit format. It uses judgment only where judgment is required — commit classification, the next-version pick when commit signals are mixed, and release-notes prose. + +## When to Use + +- Cutting any new release of codeherd, including the first (`0.1.0`). +- The current branch is `release/` cut from `main` (recommended; not enforced). +- Working tree is clean and `git fetch --tags origin` is current. + +## When NOT to Use + +- Mid-feature commits — this is a release-prep skill, not a per-commit changelog adder. +- Pre-release / RC tags — out of scope for now. +- Any repo other than `xico42/codeherd`. + +## Iron Requirements (do not skip) + +These are the exact failure modes that occur when this work is done from memory. Treat them as hard rules. + +1. **Write the `VERSION` file** at the repo root. One line, no leading `v`, SemVer 2.0.0. No exceptions. +2. **Release-note filename is `docs/release-notes/.md`** — one file per release, no leading `v`, no `-user`/`-technical` suffix. +3. **Final commit is subject-only**: `chore: bump version ` — not `release:`, not anything else. No body. The release narrative lives in `docs/release-notes/.md`; the bump commit is a marker, not a place to restate the release. This project does not use commit scopes; do not add one. +4. **CHANGELOG `[Unreleased]` body is reset to** `` after rotation. Never left empty. +5. **CHANGELOG.md uses only the six Keep a Changelog 1.1.0 section names** (Added, Changed, Deprecated, Removed, Fixed, Security). No invented sections. Mark a breaking entry by prefixing it with `**BREAKING:**` inside its correct section — e.g. `### Changed` → `- **BREAKING:** rename ...`. The release-note file (which is not bound by Keep a Changelog above its `## Changes` trailer) keeps its own top-level `## Important: breaking changes` block; only `CHANGELOG.md` and the trailer are constrained to the six sections. +6. **Deprecations land in `### Deprecated`**, separately from `### Changed`, even when a single commit is both (e.g. a rename with a deprecation window — that commit appears in both `### Deprecated` for the old form and the relevant `### Added`/`### Changed` for the new form). +7. **Release-note `## Changes` trailer is copied verbatim from CHANGELOG.md.** After `rotate-unreleased.py` writes the `## [] - ` block, extract that block's body (the `### Added`/`### Changed`/... subsections) and paste it under `## Changes` in the release note. No SHA links anywhere — CHANGELOG.md has none, so the trailer has none. +8. **Release note ends with** `**Full Changelog:** https://github.com/xico42/codeherd/compare/...v` after the trailer — or, for the first release, omit the line. +9. **Triage every `chore` and `docs` commit individually** — they are not auto-dropped. Inspect the body and the diff. A runtime dependency bump, a refactor that changes a user-visible log format, a CI change that ships new install artefacts, a docs change to CLI help text — each survives into CHANGELOG.md under the appropriate section. Pure repo upkeep (test additions, internal refactors, formatting, contributor-doc edits) drops. See `references/categorization.md` for the decision rules. +10. **Every prose fragment** (changelog summaries, release-note summary, highlights, upgrade notes, commit body) is authored with the `writing-clearly-and-concisely` skill applied. Use its Limited Context Strategy if context is tight. + +## Workflow + +Run these steps in order. Stop only at the explicit approval gate in step 9 or on a precondition failure. + +### 1. Preconditions + +```sh +test -z "$(git status --porcelain)" || { echo "dirty tree"; exit 2; } +git fetch --tags --quiet origin +``` + +If the working tree is dirty, print `git status --short` and stop. Do not stage anything. + +### 2. Range + +```sh +prev_tag=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n1) +``` + +- `prev_tag` empty → first release. Range is `..HEAD`. Proposed version starts at `0.1.0`. +- `prev_tag` non-empty → range is `${prev_tag}..HEAD`. + +### 3. Collect commits + +```sh +git log --no-merges --reverse \ + --format='%H%x1f%s%x1f%b%x1e' "${prev_tag:+$prev_tag..}HEAD" +``` + +Field separator: US (`0x1f`). Record separator: RS (`0x1e`). This survives subjects/bodies that contain tabs and newlines. + +Skip any commit whose subject is `chore: bump version `. + +### 4. Classify + +Read `references/categorization.md` for the mapping. For each commit produce: + +```json +{ + "sha": "...", + "subject": "...", + "section": "Added | Changed | Deprecated | Removed | Fixed | Security", + "breaking": false, + "user_visible": true, + "summary": "", + "user_summary":"" +} +``` + +Rules: + +- The project uses six commit types: `feat`, `fix`, `sec`, `revert`, `docs`, `chore`. `feat!`/`!` or a `BREAKING CHANGE:` footer sets `breaking: true`. +- Mechanical mapping: `feat` → `Added`, `fix` → `Fixed`, `sec` → `Security`, `revert` → `Removed`. +- `chore` and `docs` require **per-commit analysis** — they are not auto-dropped. Read each body and the diff. User-visible changes (a runtime dependency bump, a refactor with a changed log format, a CI change that ships new install artefacts, a docs change to CLI help text) ship under the matching section. Pure repo upkeep (test additions, internal refactors, formatting, contributor-doc edits) drops. See `references/categorization.md`. +- A renamed/removed command goes in **both** `Deprecated` (old form, for the deprecation window) and `Added`/`Changed` (new form). +- Unrecognised prefix (historical commits using `refactor:`, `ci:`, `test:`, `perf:`, `build:`, `style:`, `security:`, `deprecate:`, etc.) → treat as the `chore` of the corresponding kind (or, for the old `security:` prefix, as `sec`) and triage per the rules above. Flag at the approval gate so the maintainer can confirm. +- `summary` and `user_summary` are authored with `writing-clearly-and-concisely`. + +### 5. Propose bump + +Apply this table to the classification: + +| Signal | Bump (when prev_tag major ≥ 1) | Bump (while prev_tag major == 0) | +| --------------------------------------------------------- | ------------------------------ | -------------------------------- | +| Any `breaking: true` | MAJOR | MINOR | +| Else any `Added` | MINOR | MINOR | +| Else any `Fixed`/`Security` | PATCH | PATCH | +| Else | PATCH | PATCH | + +First release with no `prev_tag` → `0.1.0` (regardless of signal). + +The maintainer may override with `release as patch|minor|major` at invocation time; print both the override and the auto-derived bump in the approval summary. + +### 6. Render templates + +Source: `templates/changelog-section.tmpl.md`, `templates/release-notes.tmpl.md`. + +Variables: + +| Name | Source | +| ------------------ | -------------------------------------------------------------------------------------------------------- | +| `Version` | proposed semver, no leading `v` | +| `Date` | `YYYY-MM-DD`, local | +| `PrevTag` | `prev_tag` (with `v`), empty for first release | +| `ReleaseKind` | `Major`, `Minor`, or `Patch` from step 5 | +| `Sections` | section name → list of `{Summary, UserSummary, Breaking}` (CHANGELOG.md uses `.Summary`; no SHA links) | +| `Breaking` | every record with `breaking: true` | +| `Highlights` | 1–3 hand-written `{Title, Body}` entries. Author with `writing-clearly-and-concisely`. | +| `HasFixed` | true when `Sections.Fixed` is non-empty | +| `Summary` | 2–4 sentence release summary. Author with `writing-clearly-and-concisely`. | +| `UpgradeNotes` | paragraph; omit when there are no migration steps. | +| `ChangelogSection` | body of the `## [] - ` block in CHANGELOG.md after rotation — slurped in step 7, not authored. | + +Render each template by direct text substitution — no template engine. Empty conditional blocks are removed entirely (no blank section headers). + +Render `release-notes.tmpl.md` in two passes: +- Pass A (now): fill every variable except `ChangelogSection`. Leave the `{{ChangelogSection}}` placeholder literal in the buffer. +- Pass B (step 7, after rotation): replace the placeholder with the slurped CHANGELOG.md body. + +### 7. Write files + +```sh +printf '%s\n' "" > VERSION +python3 scripts/rotate-unreleased.py "" "" \ + "" +mkdir -p docs/release-notes +# Slurp the rotated CHANGELOG.md section body (the ### subsections under +# `## [] - `, stopping at the next `## [` or footer link). +awk -v v="" ' + $0 ~ "^## \\[" v "\\]" {grab=1; next} + grab && /^## \[/ {exit} + grab && /^\[[^]]+\]:[[:space:]]/ {exit} + grab {print} +' CHANGELOG.md > /tmp/ch-section.md +# Pass B: substitute {{ChangelogSection}} in the rendered release note with +# /tmp/ch-section.md, then write docs/release-notes/.md. +``` + +`rotate-unreleased.py` handles CHANGELOG.md surgery deterministically (creates the file on first release, otherwise rotates `[Unreleased]` and refreshes footer compare-links). + +### 8. Stage and summarize + +```sh +git add VERSION CHANGELOG.md docs/release-notes/.md +git diff --cached --stat +``` + +Then print, in this order: + +- Proposed version and the SemVer reasoning sentence. +- Any classification warnings (unknown prefixes, mixed signals). +- The first ~20 lines of `CHANGELOG.md` and of `docs/release-notes/.md`. + +### 9. Approval gate + +Ask the maintainer exactly once: + +> Release prep staged for v\. Commit as `chore: bump version ` (y/n)? Edit files first if needed; staged set will be re-read on `y`. + +Wait for `y`. On `n`, stop without unstaging. + +### 10. Commit + +```sh +git commit -m "chore: bump version " +``` + +Subject is exact. Body is the same `Summary` paragraph used at the top of the release note. No `Co-Authored-By` unless the maintainer set it globally. + +## Prose Policy + +Before writing any sentence into `CHANGELOG.md`, `docs/release-notes/.md`, or the commit body: invoke the `writing-clearly-and-concisely` skill at `.agents/skills/writing-clearly-and-concisely/` and apply its rules. Use its Limited Context Strategy (subagent copyedit) when context is tight. + +This is the single source of style for every release artifact this skill produces. No second style guide lives inside `creating-release`. + +## Rationalizations to Reject + +| Excuse | Reality | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| "I'll skip the `VERSION` file — the tag is the source of truth." | The pipeline triggers off `VERSION` changing. No bump, no release. Write the file. | +| "Leading `v` in filenames is more familiar." | The pipeline globs `docs/release-notes/.md`. Leading `v` (or any `-suffix`) breaks the glob. No `v`, no suffix. | +| "Two files — user + technical — are easier to read." | One file. The friendly body comes first, the CHANGELOG.md slurp comes last under `## Changes`. Don't resurrect the split. | +| "`release:` (or any variant) is the conventional subject." | This repo's convention is `chore: bump version ` — see git log. Match it. The project does not use commit scopes, so `chore(release):` and similar are also wrong. | +| "I can fold the breaking note into Changed without the `**BREAKING:**` prefix." | The prefix is the marker readers (and the release-notes pipeline) look for. Keep the bullet under its correct Keep a Changelog section (`Changed`, `Removed`, etc.) and prefix it with `**BREAKING:**`. Do not invent a `### Breaking` section — Keep a Changelog 1.1.0 has none. The release-note's friendly top half has its own `## Important: breaking changes` block; the CHANGELOG.md trailer it copies keeps the `**BREAKING:**` prefix in place. | +| "Every `chore` and `docs` commit belongs in the changelog for completeness." | Keep a Changelog 1.1.0 is for changes users notice. A `chore` or `docs` commit ships in CHANGELOG.md only when its body proves user-visible behaviour changed — see "Chore triage" in `references/categorization.md`. Pure repo upkeep stays in `git log` alone. | +| "I'll write prose first and copyedit later if there's time." | Defer to `writing-clearly-and-concisely` at draft time, not as a backstop. Strunk's rules are non-optional. | +| "First release, so CHANGELOG.md doesn't need a `[Unreleased]` block." | Yes it does. `rotate-unreleased.py` seeds it on the first release. Future bumps depend on it. | + +## Red Flags — Stop and Reread + +- Subject of the bump commit isn't exactly `chore: bump version `, or the commit carries a body. +- `VERSION` file isn't in the staged set. +- A breaking change is in CHANGELOG.md without the `**BREAKING:**` prefix. +- CHANGELOG.md contains a section that isn't one of Added/Changed/Deprecated/Removed/Fixed/Security. +- The release-note `## Changes` trailer disagrees with CHANGELOG.md's `[]` block — it must be a verbatim copy. +- The release-note filename starts with `v`, has a `-user`/`-technical` suffix, or you wrote two files. +- Prose was written without `writing-clearly-and-concisely` applied. +- A `chore` or `docs` commit appears in CHANGELOG.md without its body justifying a user-visible change. +- A `chore` or `docs` commit was dropped without its body being read. + +Any of these → fix before the approval gate. Do not ship. + +## References + +- `references/categorization.md` — commit prefix → section table, authoritative. +- `references/keepachangelog.md` — Keep a Changelog 1.1.0 condensed. +- `references/semver.md` — SemVer 2.0.0 bump rules including the 0.x convention. +- `templates/changelog-section.tmpl.md` — body injected into `CHANGELOG.md`. +- `templates/release-notes.tmpl.md` — `docs/release-notes/.md` (single file; friendly body + verbatim CHANGELOG.md trailer). +- `scripts/rotate-unreleased.py` — CHANGELOG.md surgery (idempotent, deterministic). diff --git a/.agents/skills/creating-release/references/categorization.md b/.agents/skills/creating-release/references/categorization.md new file mode 100644 index 0000000..053dec3 --- /dev/null +++ b/.agents/skills/creating-release/references/categorization.md @@ -0,0 +1,69 @@ +# Commit categorization + +Authoritative mapping from this project's commit prefixes to Keep a Changelog 1.1.0 sections. The skill's classification step (workflow step 4) reads this file. + +This project uses six commit types: `feat`, `fix`, `sec`, `revert`, `docs`, `chore`. Four map mechanically; two (`chore` and `docs`) require per-commit judgment. + +The release note (`docs/release-notes/.md`) inherits the CHANGELOG.md verdict for its trailing `## Changes` section (it copies that block verbatim). The "In release note body" column below covers only the friendly top half — Summary, Highlights, Breaking, Fixes — where prose is hand-authored. + +## Table + +| Prefix | Section in CHANGELOG.md | In release note body | Notes | +| ----------- | ---------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `feat` | Added | yes | `feat!` or `BREAKING CHANGE:` footer → also list under `## Important: breaking changes`. | +| `fix` | Fixed | yes | | +| `sec` | Security | yes | | +| `revert` | Removed | yes | Subject typically `revert: `. | +| `docs` | judgment per commit (default drop) | judgment | Inspect: changes to user-facing docs (CLI help text, README upgrade guide, public-API reference) → `Changed`. Contributor docs, architecture notes, internal design specs → drop. | +| `chore` | judgment per commit (default drop) | judgment | Inspect every chore. See "Chore triage" below. Never silently drop without checking the body. | +| no prefix | Changed | (judgment) | Add a classification warning at the approval gate so the maintainer can retag. | + +Historical commits using prefixes outside the six (`refactor:`, `perf:`, `test:`, `ci:`, `build:`, `style:`, `security:`, `deprecate:`, etc.) are treated as a `chore` of the corresponding kind and triaged per the chore rules below — except `security:`, which maps to `sec` (the modern short form for the same intent). + +## Chore triage + +`chore` is a catchall. Every chore commit goes through this decision: + +1. **Read the body.** Subject alone is not enough. +2. **Does it change user-visible behavior?** Examples that count: a runtime dependency bump on a library shipped with the binary (especially a CVE patch); a refactor that alters logged output, exit codes, or a wire format; a build/CI change that produces a new install path or a renamed artefact; a performance improvement large enough to mention. If yes → include under the matching Keep a Changelog section (`Changed`, `Security`, etc.). +3. **Is it pure repo upkeep?** Examples: test additions, internal refactor with zero observable side effect, formatting/lint fixes, contributor-doc edits, CI tweaks that only affect the maintainer's workflow. If yes → drop. +4. **When in doubt, include.** A bullet the maintainer removes at the approval gate is cheap; a silent drop the maintainer never sees is invisible. + +This project does not use commit scopes, so the chore subject alone carries no hint about what kind of upkeep it is. Read the body and the diff — `go.mod`/`go.sum` touched means a dependency change; `.github/workflows/*` means CI; `*_test.go` only means tests; etc. The triage decision lives in the diff, not the subject. + +## Breaking changes + +A commit is breaking when: + +- The subject uses `!:`, e.g. `feat!: drop /v1`. +- The body contains a paragraph starting with `BREAKING CHANGE:`. +- A command is renamed or removed, even with a deprecation window — the old form is breaking for any caller that pinned it. If the author shipped it without `!` or `BREAKING CHANGE:`, surface the inconsistency at the approval gate and retag. + +In `CHANGELOG.md`, every breaking commit appears under its underlying Keep a Changelog section (`### Added`, `### Changed`, `### Deprecated`, `### Removed`, `### Fixed`, or `### Security`) with a `**BREAKING:**` prefix on the bullet. Keep a Changelog 1.1.0 has no "Breaking" section, so do not invent one. + +```markdown +### Changed + +- **BREAKING:** rename `ch attach` to `ch session attach`; old form prints a deprecation warning for one release. +``` + +In the **release note's friendly top half**, every breaking commit also appears under `## Important: breaking changes` with a user-facing summary. The trailing `## Changes` block is the verbatim CHANGELOG.md copy, so the same bullet shows up there too with its `**BREAKING:**` prefix — that's expected, not a duplication bug. + +## Deprecations + +When a commit deprecates a public surface (a renamed CLI command, an exported Go API, a config key), the **old form** is listed under `### Deprecated`. The **new form** is listed under its own section (`### Added` or `### Changed`). + +Example: a single commit that renames `ch attach` to `ch session attach` produces two classification records, one per surface: + +- record A (old form) → section `Deprecated`, `breaking: true`. This is the record summarized under `## Important: breaking changes` in the release note's friendly top half. +- record B (new form) → section `Added` (or `Changed` if the form already existed under a different name), `breaking: false`. This record does *not* appear in any Breaking block. + +**Anti-double-listing rule:** within `CHANGELOG.md`, every commit appears at most once per Keep a Changelog section. If you find yourself emitting the same bullet in two sections with the same `**BREAKING:**` marker, the second occurrence is a duplicate — collapse it. The two-record split above is correct because the two records represent two distinct user-visible surfaces (the deprecated old form and the new canonical form). + +## Merging commits + +When several commits share an obvious theme (same area, same feature), the **friendly top half** of the release note may merge them into one bullet or one Highlight. `CHANGELOG.md` (and therefore the trailing `## Changes` block) keeps one bullet per logical change. + +## When in doubt + +Pick `Changed` over silent drop. The maintainer reviews the diff at the approval gate. diff --git a/.agents/skills/creating-release/references/keepachangelog.md b/.agents/skills/creating-release/references/keepachangelog.md new file mode 100644 index 0000000..9812707 --- /dev/null +++ b/.agents/skills/creating-release/references/keepachangelog.md @@ -0,0 +1,78 @@ +# Keep a Changelog 1.1.0 — guidelines + +Source: https://keepachangelog.com/en/1.1.0/ + +## Guiding principles (verbatim) + +- Changelogs are for humans, not machines. +- There should be an entry for every single version. +- The same types of changes should be grouped. +- Versions and sections should be linkable. +- The latest version comes first. +- The release date of each version is displayed. +- Mention whether you follow Semantic Versioning. + +## Types of changes (verbatim) + +- **Added** for new features. +- **Changed** for changes in existing functionality. +- **Deprecated** for soon-to-be removed features. +- **Removed** for now removed features. +- **Fixed** for any bug fixes. +- **Security** in case of vulnerabilities. + +These six types are the **only** sections allowed inside a version block in `CHANGELOG.md`. Do not invent new sections. Do not split entries across custom blocks. Group by these names exactly. + +## Marking breaking changes + +Keep a Changelog has no separate "Breaking" section. To call out a breaking change, prefix the affected entry with `**BREAKING:**` and keep it under its correct type. Examples: + +```markdown +### Changed + +- **BREAKING:** rename `ch attach` to `ch session attach`; the old form prints a deprecation warning for one release. + +### Removed + +- **BREAKING:** drop the deprecated `--legacy-flag` argument from `ch create worktree`. +``` + +The per-version release note (`docs/release-notes/.md`) is **not** a Keep a Changelog file in its top half; it carries a top-level `## Important: breaking changes` block to make the impact unmissable. Its trailing `## Changes` section is a verbatim copy of the CHANGELOG.md block and therefore still obeys the six-section rule. + +## File shape + +```markdown +# Changelog + +All notable changes to this project will be 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] + + + +## [] - YYYY-MM-DD + +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security + +[Unreleased]: https://github.com/xico42/codeherd/compare/v...HEAD +[]: https://github.com/xico42/codeherd/compare/v...v +... +[]: https://github.com/xico42/codeherd/releases/tag/v +``` + +Omit any section that has no entries. Newest version on top. Footer compare-links keep every version linkable, satisfying the "Versions and sections should be linkable" principle. + +## Style notes for entries + +- Write for end users. No commit SHAs in this file. +- Present tense, third person ("renames `ch attach`...", not "we renamed"). +- One bullet per logical change. Do not batch multiple unrelated changes into one bullet. +- All prose is authored with the `writing-clearly-and-concisely` skill applied. diff --git a/.agents/skills/creating-release/references/semver.md b/.agents/skills/creating-release/references/semver.md new file mode 100644 index 0000000..8275296 --- /dev/null +++ b/.agents/skills/creating-release/references/semver.md @@ -0,0 +1,50 @@ +# Semantic Versioning 2.0.0 — condensed + +Source: https://semver.org/spec/v2.0.0.html + +## Version format + +``` +..[-][+] +``` + +- ``, ``, `` are non-negative integers without leading zeros. +- Prerelease and build metadata are optional. This skill does not produce them. + +## When to bump + +| Bump | Trigger | +| ----- | ---------------------------------------------------------------------------------------------------- | +| MAJOR | Incompatible / breaking changes to the public API. | +| MINOR | Backwards-compatible feature additions. | +| PATCH | Backwards-compatible bug fixes (including security fixes that don't change behavior). | + +When you bump a higher level, lower levels reset to zero. `1.4.7` + breaking change → `2.0.0`, not `2.4.7`. + +## The 0.x convention + +SemVer §4 says: + +> Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. + +Common practice during `0.y.z` — and what this skill applies — is: + +| Signal at this level | Bump while major == 0 | +| ------------------------------------- | --------------------- | +| Breaking change (would be MAJOR ≥ 1) | **MINOR** | +| Feature addition (MINOR ≥ 1) | MINOR | +| Bug fix / security | PATCH | + +The maintainer can override at invocation time (`release as major` while still at 0.x to declare 1.0.0 explicitly). + +## First release + +The first tagged release of this project is `0.1.0`. Not `0.0.1` and not `1.0.0`. This is hard-coded in the skill's workflow step 5. + +## Precedence rules used by `write-version.sh` (informational) + +- Compare `..` numerically left to right. +- A version without prerelease metadata has higher precedence than the same version with prerelease metadata: `1.0.0` > `1.0.0-rc.1`. +- Build metadata is ignored for precedence. + +The skill rejects any new version that is not strictly greater than the current `VERSION` by these rules. diff --git a/.agents/skills/creating-release/scripts/rotate-unreleased.py b/.agents/skills/creating-release/scripts/rotate-unreleased.py new file mode 100755 index 0000000..92a9092 --- /dev/null +++ b/.agents/skills/creating-release/scripts/rotate-unreleased.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Rotate CHANGELOG.md's [Unreleased] block into a versioned heading. + +Usage: + rotate-unreleased.py [] + +Arguments: + new_version semver, no leading 'v' e.g. 0.2.0 + date YYYY-MM-DD e.g. 2026-05-28 + section_file path to rendered Keep a Changelog section for the new + version (without the '## [version] - date' heading) + prev_tag previous tag with 'v' prefix; empty for first release + +Behavior: + - If CHANGELOG.md is missing, write it from scratch with header, + empty [Unreleased] (placeholder), the new section, and footer + compare-links. + - Otherwise rotate: replace the [Unreleased] body with a placeholder, + insert the new '## [] - ' block immediately below it, and + rewrite the footer link references for [Unreleased] and []. + +Idempotent guard: + If [] already exists in CHANGELOG.md, exit non-zero with a clear + message — the maintainer is re-running on an already-rotated tree. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO = "xico42/codeherd" +HEADER = """\ +# Changelog + +All notable changes to this project will be 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_PLACEHOLDER = "" + + +def die(msg: str, code: int = 2) -> None: + print(msg, file=sys.stderr) + sys.exit(code) + + +def footer_for(version: str, prev_tag: str) -> str: + tag = f"v{version}" + if prev_tag: + version_link = ( + f"[{version}]: https://github.com/{REPO}/compare/{prev_tag}...{tag}" + ) + else: + version_link = ( + f"[{version}]: https://github.com/{REPO}/releases/tag/{tag}" + ) + unreleased = f"[Unreleased]: https://github.com/{REPO}/compare/{tag}...HEAD" + return unreleased, version_link + + +def write_fresh(path: Path, version: str, date: str, section: str, prev_tag: str) -> None: + unreleased_link, version_link = footer_for(version, prev_tag) + section = section.rstrip() + "\n" + body = ( + f"{HEADER}\n" + f"## [Unreleased]\n\n" + f"{UNRELEASED_PLACEHOLDER}\n\n" + f"## [{version}] - {date}\n\n" + f"{section}\n" + f"{unreleased_link}\n" + f"{version_link}\n" + ) + path.write_text(body, encoding="utf-8") + + +def rotate(path: Path, version: str, date: str, section: str, prev_tag: str) -> None: + text = path.read_text(encoding="utf-8") + + if re.search(rf"^## \[{re.escape(version)}\] - ", text, re.MULTILINE): + die(f"CHANGELOG.md already has [{version}] — refusing to rotate twice.") + + unreleased_re = re.compile(r"^## \[Unreleased\]\n", re.MULTILINE) + m = unreleased_re.search(text) + if not m: + die("CHANGELOG.md has no [Unreleased] heading — refusing to guess where to insert.") + + # Find end of the Unreleased body: next "## [" heading, or footer link line. + body_start = m.end() + next_heading = re.search(r"^## \[", text[body_start:], re.MULTILINE) + footer_start = re.search(r"^\[Unreleased\]:\s", text[body_start:], re.MULTILINE) + + if next_heading is None and footer_start is None: + unreleased_body_end = len(text) + else: + candidates = [c.start() for c in (next_heading, footer_start) if c is not None] + unreleased_body_end = body_start + min(candidates) + + head = text[: m.end()] + tail = text[unreleased_body_end:] + + # Build the new section block. + section = section.rstrip() + "\n" + new_section_block = f"\n{UNRELEASED_PLACEHOLDER}\n\n## [{version}] - {date}\n\n{section}\n" + + # Rewrite footer link references. + unreleased_link, version_link = footer_for(version, prev_tag) + new_unreleased_re = re.compile(r"^\[Unreleased\]:\s.*$", re.MULTILINE) + if new_unreleased_re.search(tail): + tail = new_unreleased_re.sub(unreleased_link, tail, count=1) + # Prepend the new version link immediately after the updated Unreleased link line. + tail = re.sub( + re.escape(unreleased_link) + r"\n", + unreleased_link + "\n" + version_link + "\n", + tail, + count=1, + ) + else: + # No footer in tail — append one. + tail = tail.rstrip() + "\n\n" + unreleased_link + "\n" + version_link + "\n" + + path.write_text(head + new_section_block + tail, encoding="utf-8") + + +def main(argv: list[str]) -> None: + if len(argv) < 4 or len(argv) > 5: + die(__doc__ or "bad args") + + version, date, section_path = argv[1], argv[2], argv[3] + prev_tag = argv[4] if len(argv) == 5 else "" + + if not re.match(r"^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$", version): + die(f"not a valid semver: {version}") + if not re.match(r"^\d{4}-\d{2}-\d{2}$", date): + die(f"date must be YYYY-MM-DD: {date}") + if prev_tag and not re.match(r"^v\d+\.\d+\.\d+", prev_tag): + die(f"prev_tag must look like vX.Y.Z: {prev_tag}") + + section = Path(section_path).read_text(encoding="utf-8") + changelog = Path("CHANGELOG.md") + + if not changelog.exists(): + write_fresh(changelog, version, date, section, prev_tag) + return + + rotate(changelog, version, date, section, prev_tag) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/.agents/skills/creating-release/templates/changelog-section.tmpl.md b/.agents/skills/creating-release/templates/changelog-section.tmpl.md new file mode 100644 index 0000000..2869fbf --- /dev/null +++ b/.agents/skills/creating-release/templates/changelog-section.tmpl.md @@ -0,0 +1,34 @@ +{{!-- Body injected into CHANGELOG.md under the version heading. + Render by direct text substitution. No template engine. + - {{Version}}, {{Date}} are required. + - Iterate {{Sections}} in fixed Keep a Changelog order: Added, Changed, Deprecated, Removed, Fixed, Security. + - Omit any section that has no entries (do not print empty headings). + - For each entry in a section, if {{.Breaking}} is true, prefix the bullet with `**BREAKING:**`. + - Entries are human-readable prose, no commit SHAs (per Keep a Changelog). + - Apply writing-clearly-and-concisely to every .Summary. +--}} +## [{{Version}}] - {{Date}} + +### Added + +- {{#if .Breaking}}**BREAKING:** {{/if}}{{.Summary}} + +### Changed + +- {{#if .Breaking}}**BREAKING:** {{/if}}{{.Summary}} + +### Deprecated + +- {{#if .Breaking}}**BREAKING:** {{/if}}{{.Summary}} + +### Removed + +- {{#if .Breaking}}**BREAKING:** {{/if}}{{.Summary}} + +### Fixed + +- {{.Summary}} + +### Security + +- {{.Summary}} diff --git a/.agents/skills/creating-release/templates/release-notes.tmpl.md b/.agents/skills/creating-release/templates/release-notes.tmpl.md new file mode 100644 index 0000000..a123f5a --- /dev/null +++ b/.agents/skills/creating-release/templates/release-notes.tmpl.md @@ -0,0 +1,45 @@ +{{!-- docs/release-notes/.md + Single release-note file. Used verbatim as the GitHub release body. + Render by direct text substitution. Empty conditional blocks are removed + entirely — do not leave bare headings. + + Top half (Summary → Highlights → Breaking → Fixes → Upgrading) is the + friendly announcement. Apply writing-clearly-and-concisely to {{Summary}}, + each {{.Body}}, each {{.UserSummary}}, and {{UpgradeNotes}}. + + Trailing ## Changes section is copied verbatim from the CHANGELOG.md + release block written by rotate-unreleased.py. The skill workflow does + the copy after rotation; do not author it here. +--}} +# What's new in codeherd v{{Version}} + +{{Summary}} + +{{#if Highlights}}## Highlights + +{{#each Highlights}}### {{.Title}} + +{{.Body}} + +{{/each}}{{/if}}{{#if Breaking}}## Important: breaking changes + +{{#each Breaking}}- {{.UserSummary}} +{{/each}} + +{{/if}}{{#if HasFixed}}## Fixes + +{{#each Sections.Fixed}}- {{.UserSummary}} +{{/each}} + +{{/if}}{{#if UpgradeNotes}}## Upgrading + +{{UpgradeNotes}} + +{{/if}}## Changes + +{{!-- Inserted by the skill workflow after rotate-unreleased.py runs: + copy the body of the `## [] - ` block from CHANGELOG.md + (the ### Added/Changed/... subsections), then the compare link below. +--}} +{{ChangelogSection}} +{{#if PrevTag}}**Full Changelog:** https://github.com/xico42/codeherd/compare/{{PrevTag}}...v{{Version}}{{/if}} diff --git a/.agents/skills/squashing-commits/SKILL.md b/.agents/skills/squashing-commits/SKILL.md new file mode 100644 index 0000000..6bb1027 --- /dev/null +++ b/.agents/skills/squashing-commits/SKILL.md @@ -0,0 +1,297 @@ +--- +name: squashing-commits +description: Compose a polished commit message and optionally squash commits when finishing work on a branch or worktree. Use this skill whenever the user says things like "finish this branch", "squash commits", "prepare for PR", "wrap up", "finalize", "commit final", "ready for review", or wants to consolidate branch history into a single, well-crafted commit. Also use when another skill delegates commit message composition here — even for a single file or a single commit where there is nothing to squash. The core value is the structured commit message, not the squash. +--- + +# Squashing Commits + +This skill produces a single, well-crafted commit on a feature branch — and squashes prior commits into it when needed. The output should let anyone reading `git log` months later understand the change without reading the diff: why it was needed, which decisions matter, and what they mean for the system. + +## When to use + +- User is done implementing on a feature branch or worktree +- User wants to squash before opening a PR +- User wants to rewrite messy history into one coherent commit +- Another skill delegates commit message composition here +- Staged/unstaged changes need a commit message — even with nothing to squash + +## Step 1: Gather context + +Determine state: multiple commits to squash, single commit to amend, or fresh uncommitted changes. + +```bash +# Uncommitted changes? +git status --short + +# Base branch + divergence +git merge-base HEAD main +git rev-list --count main..HEAD + +# All commits on this branch since main (skip if count is 0) +git log --reverse --format="%h %s%n%n%b%n---" main..HEAD + +# Full diff — pick the right comparison +# If commits exist on branch: +git diff --stat main..HEAD +git diff main..HEAD +# If only staged/unstaged changes: +git diff --stat HEAD +git diff HEAD + +# Project commit-message conventions (type usage, voice, length) +git log --oneline -30 main + +# Force-push needed after squash? Count commits since main that already exist on the remote. +upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) && \ + git rev-list --count "main..$upstream" +# Output: number of commits on the branch that have already been pushed. +# > 0 → squashing will rewrite published history; push needs --force-with-lease. +# 0 (or no upstream configured) → nothing pushed yet; a normal push works. +``` + +If the user pre-specified which commits to include ("squash everything except commit X", "only the last 3 commits"), record that constraint and use it when picking the squash base in Step 6. The **default** is to squash every commit since `main`. + +Read the diff carefully. The body explains *why*, not *what* — you need to understand the change deeply enough to write about its motivation and consequences. + +## Step 2: Identify the story + +Before writing, answer internally: + +1. **What problem did this branch solve?** What was broken, missing, or inadequate before? +2. **Which decisions are significant?** Not every file change is a decision. Focus on choices that constrain future work, surprise a reader, or have functional consequences. +3. **What are the tradeoffs?** Did a decision close a door or open one? +4. **What's trivial?** Renames, formatting, mechanical config. Don't give these equal weight to architectural decisions. +5. **Do any decisions depend on others?** If decision B consumes the output of decision A, A's paragraph must come first (see "Sequence producers before consumers" in Step 4). + +## Step 3: Choose the type + +This project uses a deliberately small vocabulary: six types, picked so each one carries a clear release-time meaning. + +| Type | When | +|----------|---------------------------------------------------------------------------------------------------| +| `feat` | New feature or significant user-visible improvement | +| `fix` | Bug or problem correction | +| `sec` | Security fix or hardening | +| `revert` | Reverts a previous commit. Subject typically `revert: ` | +| `docs` | Documentation work | +| `chore` | Everything else: refactors, tests, build/CI, dependency bumps, formatting, repo upkeep | + +`chore` is a catchall, not an auto-drop signal. The release-prep workflow inspects every `chore` body individually to decide whether it ships in CHANGELOG.md or stays in `git log` alone (see `.agents/skills/creating-release/references/categorization.md`). A `chore` that changes user-visible behaviour needs a body paragraph that says so plainly — the workflow has nothing else to read. + +**Breaking changes:** mark a breaking commit with `type!:` in the subject (e.g. `feat!: drop /v1`) or a `BREAKING CHANGE:` paragraph in the body. The release-prep workflow reads both markers; pick whichever fits the message better. A breaking commit always carries one of those two signals — `creating-release` will not infer a break from prose alone, so a renamed CLI command, a removed config key, or a changed wire format that ships without one of the markers gets silently classified as non-breaking and skips the release-note's `## Important: breaking changes` block. If a caller could pin it, mark it. + +**Mixed branches:** when a single squash bundles work that spans multiple types (a feature plus its CI plumbing, a fix plus a docs update), pick the type that names the user-visible change. `feat` beats `chore` when both ship together; `fix` beats `docs` when a bug fix is documented alongside. + +## Step 4: Write the commit message + +**REQUIRED SUB-SKILL:** Invoke `writing-clearly-and-concisely` and apply its rules to every paragraph as you draft. Strunk's discipline (omit needless words, prefer active voice, use definite specific concrete language, put statements in positive form) catches the slop this skill's structural rules don't — wandering paragraphs, weak verbs, hedged claims. The structural rules below give shape; the writing skill gives tightness. Apply both. + +Format: + +``` +type: subject + +Problem description. + +Solution detail. +``` + +This project does not use scopes. The subject is `type: subject` only — never `type(area): subject`. + +### Subject line rules + +- All lowercase, no trailing period, max 72 chars +- Imperative verb (`implement`, `fix`, `add`, `bootstrap`) +- Must complete the sentence "This commit will… [subject]" + +### Body rules + +The body is **prose paragraphs**. No bullet lists. Default to English unless the user prefers otherwise. + +**Problem paragraph:** Why the change was needed. The reader should grasp the motivation without reading the diff. + +**Solution paragraphs:** Technical decisions and their *functional consequences*. The diff shows what changed; the body explains why this approach and what it means for users of the system. + +### Writing principles + +**Write in system terms, not author terms.** The problem paragraph states the condition of the project the change addresses — not the author's experience of it. "The diff alone cannot carry intent across months" belongs in a commit; "I kept writing bad commits at the end of branches" does not. Declarative present tense ("This project adopts X.", "X now happens before Y.") reads cleaner than past-tense narrative ("we observed X drifting"). Applies to every commit type: feature, fix, CI, tooling, refactor. + +**Omit provenance of inspiration.** Prior art outside the repo — upstream skills, blog posts, other projects that solve a similar problem, conference talks, an abandoned earlier branch — is out of scope. The reader is looking at *this* repo's history and needs to understand *this* repo's decision. Where the idea came from is irrelevant to whether the decision was right. + +**Spend at most one sentence on methodology.** TDD cycles, baseline evaluations, A/B tests, survey-then-validate processes — the reader cares the change works, not how you proved it. One sentence at most, often zero. If methodology is genuinely load-bearing (regulated environment, reproducible-build requirement, security review trail), it earns its sentence; otherwise it's noise. + +**Lead with functional impact, not implementation detail.** A decision matters because of what it enables or prevents, not because of which file was edited. + +- Good: "A `ch version` subcommand was added alongside, so an installed binary can report what release it came from — and so the pipeline can prove the version actually made it into the artifact." +- Bad: "Adds cmd/version.go with `newVersionCmd()` returning a cobra.Command that prints `cmd.Root().Version`, wired through `cmd.Execute(version string)` from main.go." + +**Stay one level above flag-level detail.** Don't enumerate ldflags, YAML keys, or env var names unless they're load-bearing for the reader. "Builds reproducibly across linux/darwin × amd64/arm64" beats listing `-trimpath`, `CGO_ENABLED=0`, and every matrix cell. + +**Sequence producers before consumers.** If paragraph B references an artifact produced by paragraph A, put A first. Example: the release workflow publishes `docs/release-notes/.md` as the GitHub release body — so the skill that drafts that file must be introduced before the workflow that consumes it. Reorder paragraphs as needed; don't force a chronological "what we did first" structure. + +**Separate significant decisions from trivial ones.** Configuring a registry URL is mechanical; choosing keyless signing has architectural consequences. Give space to decisions that surprise a reader or constrain future work. + +**Explain constraints and tradeoffs.** If a decision closes a door or opens one, say so. "The node ID must point to the component set, not to an individual variant — the CLI rejects variants" saves someone from repeating a mistake. + +**One topic per paragraph.** Each paragraph covers one decision or a tightly related group. + +**No bullet lists.** Prose forces you to connect cause to effect. Bullet lists fragment reasoning. + +**Don't manufacture a paragraph per housekeeping change.** README updates, dependency pins, lockfile bumps, and design-doc links rarely warrant their own paragraph. Fold them into the topical paragraph they support (the README mention rides with the feature paragraph; a version pin rides with the integration that needs it) or, when truly cross-cutting, into a single closing "shipping" paragraph. A paragraph that only says "README was updated and the design lives at X" earns nothing. + +### Anti-patterns + +| Pattern | Problem | +|--------------------------------------|----------------------------------------------------------------| +| Listing every file changed | The diff shows this. Redundant. | +| "Update X, Y, Z" subject | Describes what, not why. Too vague. | +| Elevating trivial changes | Implies equal weight to all changes. | +| Describing the diff in the body | Reader can see the diff. Explain *why*. | +| Bullet-point body | Fragments reasoning. Write paragraphs. | +| Per-commit bullet walk | Don't enumerate the commits being squashed in the body. The reader doesn't care about the intermediate steps — only the end state.| +| Housekeeping-only paragraph | README/deps/design-doc mentions don't justify their own paragraph. Fold them in.| +| Autobiographical framing | "We kept running into X, so we built Y" tells your discovery story. Reader has no shared timeline. State the system condition the change addresses, declarative present tense.| +| Provenance of inspiration | "Inspired by/based on/borrowed from upstream X" is out of scope. Where the idea came from has no bearing on whether it was right for this repo.| +| Methodology in the body | TDD cycles, baseline evals, survey-then-validate — the reader cares the change works, not how you proved it. One sentence at most, often zero.| +| Naming every flag/key | Buries the functional story under config trivia. | +| Producer paragraph after consumer | Forces re-reading. Reorder so first mention introduces. | +| Mixing languages within one message | Pick one (default English; respect user preference) and stay consistent across all paragraphs of this commit.| + +## Step 5: Present and iterate + +Present in this exact order: + +1. **First** — list every commit that will be squashed, so the user sees the destructive scope before reading the draft: + + ``` + The following commits will be squashed into one: + + + <short-sha> <title> + ... + ``` + + Generate from `git log --reverse --format="%h %s" main..HEAD`. Skip this block if there's only one commit or only uncommitted changes — no destruction, nothing to disclose. + +2. **Second** — the full commit message draft, exactly as it will appear in the commit. + +3. **Third** — one follow-up prompt, focused on paragraph-level edits. Use this verbatim or with only minor wording changes: + + > Any paragraph to tighten, reorder, or drop? Anything missing? + + The second clause is a deliberate catchall — it lets the user surface omissions without forcing a second exchange. Do not substitute a different framing like "Does the problem description capture the motivation?" or "Did I miss any tradeoffs?". Those put the user in the role of inspector and ask them to do your analysis. The skill's prompt asks them to do their actual job: edit the prose. + +Wait for explicit user approval before executing in Step 6. The approval gate applies in every case — message approval is required even when nothing destructive happens (single commit, uncommitted changes). When a squash *is* involved, the gate also protects against destroying history; flag that explicitly in the listing block. + +Expect iteration — the first draft rarely lands. When the user asks for a revision (tighten paragraph N, drop a paragraph, reorder, swap subject), re-emit the *full* updated message and ask again. Don't apply edits eagerly between rounds. Don't argue with the request; the user has context you don't. + +## Step 6: Execute the commit + +Only after the user approves the final message: + +### Multiple commits → squash + +```bash +# Soft reset to keep all changes staged +git reset --soft main + +# Commit with the approved message +git commit -m "$(cat <<'EOF' +<the approved message> +EOF +)" +``` + +**Heredoc gotcha:** the `<<'EOF'` form (EOF in single quotes) disables shell expansion — so backticks, `$`, and `!` inside the body are passed through verbatim. Do **not** escape them with backslashes; inside a quoted heredoc the backslash is also literal, so `` \`foo\` `` ends up as the seven-character string `` \`foo\` `` in the commit (backslashes and all), not the intended `` `foo` ``. Write the body exactly as you want it to appear. + +**If the branch is already pushed (Step 1 detected an upstream),** warn the user that squashing requires `git push --force-with-lease`. Do NOT force push without explicit confirmation. + +### Single existing commit → amend + +If the branch already has exactly one commit since `main` and you only need to rewrite its message: + +```bash +git commit --amend -m "$(cat <<'EOF' +<the approved message> +EOF +)" +``` + +No reset, no force push (unless the commit was already pushed). + +### Uncommitted changes → direct commit + +```bash +# Stage as needed +git add <files> + +git commit -m "$(cat <<'EOF' +<the approved message> +EOF +)" +``` + +--- + +After committing, run `git log -1 --format=full` and verify the body rendered cleanly (no escape artefacts, no truncation). If you spot a problem, re-present the corrected message to the user (per Step 5) and only after they re-approve, amend: + +```bash +git commit --amend -m "$(cat <<'EOF' +<corrected message> +EOF +)" +``` + +A silent amend would bypass the approval gate; the gate exists for content, not only for destruction. + +## Step 7: Offer to open the PR + +A commit ready for review is the natural trigger for a PR. Ask: + +> Commit created. Open the PR now? (y/N) + +If yes, proceed with `gh pr create` (or invoke a PR-creation skill if one exists). The fresh commit body should be the primary source for the PR description. If the user declines or stays silent, stop here — the branch is ready when they return. + +Do not block on this step. Do not open the PR without explicit confirmation. + +## Example output + +A commit that bundles a skill, a workflow, a build pipeline, and licensing into one coherent story. Note the producer-before-consumer ordering (skill introduces the release-notes file before the workflow that publishes it) and the functional level of detail (no ldflags, no YAML keys): + +``` +feat: bootstrap codeherd release process + +The repo had no release path. No LICENSE, no installable binaries, and +no convention for drafting per-version notes. Cutting 0.1.0 by hand +would set no precedent and would block downstream tooling — mise +registry submission, version-pinning by users — that depends on signed, +named artifacts. + +A `creating-release` skill bundle codifies how +`docs/release-notes/<version>.md` is drafted: it surveys git history +since the last tag and emits a single materialized list of surviving +features rather than a commit-by-commit log. For a first release with +no prior tag, the skill folds intermediate refactors into the feature +set. The same file is what the release workflow later publishes +verbatim as the GitHub release body, so the skill's output is the +canonical per-version narrative. + +Releases are then triggered by a commit to `main` that touches +`VERSION`. The new `release.yml` workflow reads the file, builds the +linux/darwin × amd64/arm64 matrix, signs every archive and +`checksums.txt` with sigstore cosign, then creates the `v<version>` tag +and the GitHub release with `docs/release-notes/<version>.md` as the +body. + +The build lives in three new Make targets so the same recipe runs +locally, in CI smoke tests, and during release. A new `ch version` +subcommand lets an installed binary report what release it came from +— and lets the pipeline prove the version actually made it into the +artifact. The PR pipeline exercises the full cross-compile matrix on +every change, surfacing toolchain regressions before they reach a +tag. The project ships under Apache 2.0, and the README points users +at the new mise + manual install paths so binary consumers are not +stuck building from source. +``` + +Note how the final sentence folds Apache 2.0 and the README install update into the existing build/release paragraph rather than spawning a "miscellaneous shipping" paragraph of its own. Housekeeping rides with the topic it supports. diff --git a/.agents/skills/writing-clearly-and-concisely/SKILL.md b/.agents/skills/writing-clearly-and-concisely/SKILL.md new file mode 100644 index 0000000..80f1c73 --- /dev/null +++ b/.agents/skills/writing-clearly-and-concisely/SKILL.md @@ -0,0 +1,62 @@ +--- +name: writing-clearly-and-concisely +description: Apply Strunk's timeless writing rules to ANY prose humans will read—documentation, commit messages, error messages, explanations, reports, or UI text. Makes your writing clearer, stronger, and more professional. +--- + +# Writing Clearly and Concisely + +## Overview + +William Strunk Jr.'s *The Elements of Style* (1918) teaches you to write clearly and cut ruthlessly. + +**WARNING:** `elements-of-style.md` consumes ~12,000 tokens. Read it only when writing or editing prose. + +## When to Use This Skill + +Use this skill whenever you write prose for humans: + +- Documentation, README files, technical explanations +- Commit messages, pull request descriptions +- Error messages, UI copy, help text, comments +- Reports, summaries, or any explanation +- Editing to improve clarity + +**If you're writing sentences for a human to read, use this skill.** + +## Limited Context Strategy + +When context is tight: +1. Write your draft using judgment +2. Dispatch a subagent with your draft and `elements-of-style.md` +3. Have the subagent copyedit and return the revision + +## All Rules + +### Elementary Rules of Usage (Grammar/Punctuation) +1. Form possessive singular by adding 's +2. Use comma after each term in series except last +3. Enclose parenthetic expressions between commas +4. Comma before conjunction introducing co-ordinate clause +5. Don't join independent clauses by comma +6. Don't break sentences in two +7. Participial phrase at beginning refers to grammatical subject + +### Elementary Principles of Composition +8. One paragraph per topic +9. Begin paragraph with topic sentence +10. **Use active voice** +11. **Put statements in positive form** +12. **Use definite, specific, concrete language** +13. **Omit needless words** +14. Avoid succession of loose sentences +15. Express co-ordinate ideas in similar form +16. **Keep related words together** +17. Keep to one tense in summaries +18. **Place emphatic words at end of sentence** + +### Section V: Words and Expressions Commonly Misused +Alphabetical reference for usage questions + +## Bottom Line + +Writing for humans? Read `elements-of-style.md` and apply the rules. Low on tokens? Dispatch a subagent to copyedit with the guide. diff --git a/.agents/skills/writing-clearly-and-concisely/elements-of-style.md b/.agents/skills/writing-clearly-and-concisely/elements-of-style.md new file mode 100644 index 0000000..3c66759 --- /dev/null +++ b/.agents/skills/writing-clearly-and-concisely/elements-of-style.md @@ -0,0 +1,995 @@ +# The Elements of Style (1918) + +_Public domain text by William Strunk Jr._ + +## Contents + +- [I. Introductory](#i-introductory) +- [II. Elementary Rules Of Usage](#ii-elementary-rules-of-usage) + - [Rule 1. Form the possessive singular of nouns by adding 's.](#rule-1-form-the-possessive-singular-of-nouns-by-adding-s) + - [Rule 2. In a series of three or more terms with a single conjunction, use a comma after each term except the last.](#rule-2-in-a-series-of-three-or-more-terms-with-a-single-conjunction-use-a-comma-after-each-term-except-the-last) + - [Rule 3. Enclose parenthetic expressions between commas.](#rule-3-enclose-parenthetic-expressions-between-commas) + - [Rule 4. Place a comma before a conjunction introducing a co-ordinate clause.](#rule-4-place-a-comma-before-a-conjunction-introducing-a-co-ordinate-clause) + - [Rule 5. Do not join independent clauses by a comma.](#rule-5-do-not-join-independent-clauses-by-a-comma) + - [Rule 6. Do not break sentences in two.](#rule-6-do-not-break-sentences-in-two) + - [Rule 7. A participial phrase at the beginning of a sentence must refer to the grammatical subject.](#rule-7-a-participial-phrase-at-the-beginning-of-a-sentence-must-refer-to-the-grammatical-subject) +- [III. Elementary Principles Of Composition](#iii-elementary-principles-of-composition) + - [Rule 8. Make the paragraph the unit of composition: one paragraph to each topic.](#rule-8-make-the-paragraph-the-unit-of-composition-one-paragraph-to-each-topic) + - [Rule 9. As a rule, begin each paragraph with a topic sentence, end it in conformity with the beginning.](#rule-9-as-a-rule-begin-each-paragraph-with-a-topic-sentence-end-it-in-conformity-with-the-beginning) + - [Rule 10. Use the active voice.](#rule-10-use-the-active-voice) + - [Rule 11. Put statements in positive form.](#rule-11-put-statements-in-positive-form) + - [Rule 12. Use definite, specific, concrete language.](#rule-12-use-definite-specific-concrete-language) + - [Rule 13. Omit needless words.](#rule-13-omit-needless-words) + - [Rule 14. Avoid a succession of loose sentences](#rule-14-avoid-a-succession-of-loose-sentences) + - [Rule 15. Express co-ordinate ideas in similar form.](#rule-15-express-co-ordinate-ideas-in-similar-form) + - [Rule 16. Keep related words together.](#rule-16-keep-related-words-together) + - [Rule 17. In summaries, keep to one tense.](#rule-17-in-summaries-keep-to-one-tense) + - [Rule 18. Place the emphatic words of a sentence at the end.](#rule-18-place-the-emphatic-words-of-a-sentence-at-the-end) +- [V. Words And Expressions Commonly Misused](#v-words-and-expressions-commonly-misused) + +## I. Introductory + +This handbook summarizes the essentials of plain English style. It focuses on the rules of usage and principles of composition most often broken, offering a compact alternative to exhaustive manuals. Master the guidance here, then look to the best authors for finer points of style. + +## II. Elementary Rules Of Usage + +### Rule 1. Form the possessive singular of nouns by adding 's. + +Follow this rule whatever the final consonant. Thus write, + +Charles's friend + +Burns's poems + +the witch's malice + +This is the usage of the United States Government Printing Office and of the Oxford University Press. + +Exceptions are the possessive of ancient proper names in *-es* and *-is*, the possessive *Jesus'*, and such forms as *for conscience' sake*, *for righteousness' sake*. But such forms as *Achilles' heel*, *Moses' laws*, *Isis' temple* are commonly replaced by + +the heel of Achilles + +the laws of Moses + +the temple of Isis + +The pronominal possessives *hers*, *its*, *theirs*, *yours*, and *oneself* have no apostrophe. + +### Rule 2. In a series of three or more terms with a single conjunction, use a comma after each term except the last. + +Thus write, + +red, white, and blue + +gold, silver, or copper + +He opened the letter, read it, and made a note of its contents. + +This is also the usage of the Government Printing Office and of the Oxford University Press. + +In the names of business firms the last comma is omitted, as, + +Brown, Shipley & Co. + +### Rule 3. Enclose parenthetic expressions between commas. + +The best way to see a country, unless you are pressed for time, is to travel on foot. + +This rule is difficult to apply; it is frequently hard to decide whether a single word, such as *however*, or a brief phrase, is or is not parenthetic. If the interruption to the flow of the sentence is but slight, the writer may safely omit the commas. But whether the interruption be slight or considerable, he must never insert one comma and omit the other. Such punctuation as + +Marjorie's husband, Colonel Nelson paid us a visit yesterday, + +or + +My brother you will be pleased to hear, is now in perfect health, + +is indefensible. + +If a parenthetic expression is preceded by a conjunction, place the first comma before the conjunction, not after it. + +He saw us coming, and unaware that we had learned of his treachery, greeted us with a smile. + +Always to be regarded as parenthetic and to be enclosed between commas (or, at the end of the sentence, between comma and period) are the following: + +\(1\) the year, when forming part of a date, and the day of the month, when following the day of the week: + +February to July, 1916. + +April 6, 1917. + +Monday, November 11, 1918. + +\(2\) the abbreviations *etc.* and *jr.* + +\(3\) non-restrictive relative clauses, that is, those which do not serve to identify or define the antecedent noun, and similar clauses introduced by conjunctions indicating time or place. + +The audience, which had at first been indifferent, became more and more interested. + +In this sentence the clause introduced by *which* does not serve to tell which of several possible audiences is meant; what audience is in question is supposed to be already known. The clause adds, parenthetically, a statement supplementing that in the main clause. The sentence is virtually a combination of two statements which might have been made independently: + +The audience had at first been indifferent. It became more and more interested. + +Compare the restrictive relative clause, not set off by commas, in the sentence, + +The candidate who best meets these requirements will obtain the place. + +Here the clause introduced by *who* does serve to tell which of several possible candidates is meant; the sentence cannot be split up into two independent statements. + +The difference in punctuation in the two sentences following is based on the same principle: + +Nether Stowey, where Coleridge wrote The Rime of the Ancient Mariner, is a few miles from Bridgewater. + +The day will come when you will admit your mistake. + +Nether Stowey is completely identified by its name; the statement about Coleridge is therefore supplementary and parenthetic. The *day* spoken of is identified only by the dependent clause, which is therefore restrictive. + +Similar in principle to the enclosing of parenthetic expressions between commas is the setting off by commas of phrases or dependent clauses preceding or following the main clause of a sentence. + +Partly by hard fighting, partly by diplomatic skill, they enlarged their dominions to the east, and rose to royal rank with the possession of Sicily, exchanged afterwards for Sardinia. + +Other illustrations may be found in sentences quoted under Rules 4, 5, 6, 7, 16, and 18. + +The writer should be careful not to set off independent clauses by commas: see under Rule 5. + +### Rule 4. Place a comma before a conjunction introducing a co-ordinate clause. + +The early records of the city have disappeared, and the story of its first years can no longer be reconstructed. + +The situation is perilous, but there is still one chance of escape. + +Sentences of this type, isolated from their context, may seem to be in need of rewriting. As they make complete sense when the comma is reached, the second clause has the appearance of an afterthought. Further, *and* is the least specific of connectives. Used between independent clauses, it indicates only that a relation exists between them without defining that relation. In the example above, the relation is that of cause and result. The two sentences might be rewritten: + +As the early records of the city have disappeared, the story of its first years can no longer be reconstructed. + +Although the situation is perilous, there is still one chance of escape. + +Or the subordinate clauses might be replaced by phrases: + +Owing to the disappearance of the early records of the city, the story of its first years can no longer be reconstructed. + +In this perilous situation, there is still one chance of escape. + +But a writer may err by making his sentences too uniformly compact and periodic, and an occasional loose sentence prevents the style from becoming too formal and gives the reader a certain relief. Consequently, loose sentences of the type first quoted are common in easy, unstudied writing. But a writer should be careful not to construct too many of his sentences after this pattern (see Rule 14). + +Two-part sentences of which the second member is introduced by *as* (in the sense of *because*), *for*, *or*, *nor*, and *while* (in the sense of *and at the same time*) likewise require a comma before the conjunction. + +If the second member is introduced by an adverb, a semicolon, not a comma, is required (see Rule 5). The connectives *so* and *yet* may be used either as adverbs or as conjunctions, accordingly as the second clause is felt to be co-ordinate or subordinate; consequently either mark of punctuation may be justified. But these uses of *so* (equivalent to *accordingly* or to *so that*) are somewhat colloquial and should, as a rule, be avoided in writing. A simple correction, usually serviceable, is to omit the word *so* and begin the first clause with *as* or *since*: + +| Original | Revision | +| --- | --- | +| I had never been in the place before; so I had difficulty in finding my way about. | As I had never been in the place before, I had difficulty in finding my way about. | + +If a dependent clause, or an introductory phrase requiring to be set off by a comma, precedes the second independent clause, no comma is needed after the conjunction. + +The situation is perilous, but if we are prepared to act promptly, there is still one chance of escape. + +When the subject is the same for both clauses and is expressed only once, a comma is required if the connective is *but*. If the connective is *and*, the comma should be omitted if the relation between the two statements is close or immediate. + +I have heard his arguments, but am still unconvinced. + +He has had several years' experience and is thoroughly competent. + +### Rule 5. Do not join independent clauses by a comma. + +If two or more clauses, grammatically complete and not joined by a conjunction, are to form a single compound sentence, the proper mark of punctuation is a semicolon. + +Stevenson's romances are entertaining; they are full of exciting adventures. + +It is nearly half past five; we cannot reach town before dark. + +It is of course equally correct to write the above as two sentences each, replacing the semicolons by periods. + +Stevenson's romances are entertaining. They are full of exciting adventures. + +It is nearly half past five. We cannot reach town before dark. + +If a conjunction is inserted the proper mark is a comma (Rule 4). + +Stevenson's romances are entertaining, for they are full of exciting adventures. + +It is nearly half past five, and we cannot reach town before dark. + +A comparison of the three forms given above will show clearly the advantage of the first. It is, at least in the examples given, better than the second form, because it suggests the close relationship between the two statements in a way that the second does not attempt, and better than the third, because briefer and therefore more forcible. Indeed it may be said that this simple method of indicating relationship between statements is one of the most useful devices of composition. The relationship, as above, is commonly one of cause or of consequence. + +Note that if the second clause is preceded by an adverb, such as *accordingly*, *besides*, *then*, *therefore*, or *thus*, and not by a conjunction, the semicolon is still required. + +Two exceptions to the rule may be admitted. If the clauses are very short, and are alike in form, a comma is usually permissible: + +Man proposes, God disposes. + +The gate swung apart, the bridge fell, the portcullis was drawn up. + +Note that in these examples the relation is not one of cause or consequence. Also in the colloquial form of expression, + +I hardly knew him, he was so changed, + +a comma, not a semicolon, is required. But this form of expression is inappropriate in writing, except in the dialogue of a story or play, or perhaps in a familiar letter. + +### Rule 6. Do not break sentences in two. + +In other words, do not use periods for commas. + +I met them on a Cunard liner several years ago. Coming home from Liverpool to New York. + +He was an interesting talker. A man who had traveled all over the world and lived in half a dozen countries. + +In both these examples, the first period should be replaced by a comma, and the following word begun with a small letter. + +It is permissible to make an emphatic word or expression serve the purpose of a sentence and to punctuate it accordingly: + +Again and again he called out. No reply. + +The writer must, however, be certain that the emphasis is warranted, and that he will not be suspected of a mere blunder in syntax or in punctuation. + +Rules 3, 4, 5, and 6 cover the most important principles in the punctuation of ordinary sentences; they should be so thoroughly mastered that their application becomes second nature. + +### Rule 7. A participial phrase at the beginning of a sentence must refer to the grammatical subject. + +Walking slowly down the road, he saw a woman accompanied by two children. + +The word *walking* refers to the subject of the sentence, not to the woman. If the writer wishes to make it refer to the woman, he must recast the sentence: + +He saw a woman accompanied by two children, walking slowly down the road. + +Participial phrases preceded by a conjunction or by a preposition, nouns in apposition, adjectives, and adjective phrases come under the same rule if they begin the sentence. + +| Original | Revision | +| --- | --- | +| On arriving in Chicago, his friends met him at the station. | When he arrived (or, On his arrival) in Chicago, his friends met him at the station. | +| A soldier of proved valor, they entrusted him with the defence of the city. | A soldier of proved valor, he was entrusted with the defence of the city. | +| Young and inexperienced, the task seemed easy to me. | Young and inexperienced, I thought the task easy. | +| Without a friend to counsel him, the temptation proved irresistible. | Without a friend to counsel him, he found the temptation irresistible. | + +Sentences violating this rule are often ludicrous. + +Being in a dilapidated condition, I was able to buy the house very cheap. + +Wondering irresolutely what to do next, the clock struck twelve. + +## III. Elementary Principles Of Composition + +### Rule 8. Make the paragraph the unit of composition: one paragraph to each topic. + +If the subject on which you are writing is of slight extent, or if you intend to treat it very briefly, there may be no need of subdividing it into topics. Thus a brief description, a brief summary of a literary work, a brief account of a single incident, a narrative merely outlining an action, the setting forth of a single idea, any one of these is best written in a single paragraph. After the paragraph has been written, examine it to see whether subdivision will not improve it. + +Ordinarily, however, a subject requires subdivision into topics, each of which should be made the subject of a paragraph. The object of treating each topic in a paragraph by itself is, of course, to aid the reader. The beginning of each paragraph is a signal to him that a new step in the development of the subject has been reached. + +The extent of subdivision will vary with the length of the composition. For example, a short notice of a book or poem might consist of a single paragraph. One slightly longer might consist of two paragraphs: + +- A. Account of the work. +- B. Critical discussion. + +A report on a poem, written for a class in literature, might consist of seven paragraphs: + +- A. Facts of composition and publication. +- B. Kind of poem; metrical form. +- C. Subject. +- D. Treatment of subject. +- E. For what chiefly remarkable. +- F. Wherein characteristic of the writer. +- G. Relationship to other works. + +The contents of paragraphs C and D would vary with the poem. Usually, paragraph C would indicate the actual or imagined circumstances of the poem (the situation), if these call for explanation, and would then state the subject and outline its development. If the poem is a narrative in the third person throughout, paragraph C need contain no more than a concise summary of the action. Paragraph D would indicate the leading ideas and show how they are made prominent, or would indicate what points in the narrative are chiefly emphasized. + +A novel might be discussed under the heads: + +- A. Setting. +- B. Plot. +- C. Characters. +- D. Purpose. + +An historical event might be discussed under the heads: + +- A. What led up to the event. +- B. Account of the event. +- C. What the event led up to. + +In treating either of these last two subjects, the writer would probably find it necessary to subdivide one or more of the topics here given. + +As a rule, single sentences should not be written or printed as paragraphs. An exception may be made of sentences of transition, indicating the relation between the parts of an exposition or argument. Frequent exceptions are also necessary in textbooks, guidebooks, and other works in which many topics are treated briefly. + +In dialogue, each speech, even if only a single word, is a paragraph by itself; that is, a new paragraph begins with each change of speaker. The application of this rule, when dialogue and narrative are combined, is best learned from examples in well-printed works of fiction. + +### Rule 9. As a rule, begin each paragraph with a topic sentence, end it in conformity with the beginning. + +Again, the object is to aid the reader. The practice here recommended enables him to discover the purpose of each paragraph as he begins to read it, and to retain this purpose in mind as he ends it. For this reason, the most generally useful kind of paragraph, particularly in exposition and argument, is that in which + +\(a\) the topic sentence comes at or near the beginning; + +\(b\) the succeeding sentences explain or establish or develop the statement made in the topic sentence; and + +\(c\) the final sentence either emphasizes the thought of the topic sentence or states some important consequence. + +Ending with a digression, or with an unimportant detail, is particularly to be avoided. + +If the paragraph forms part of a larger composition, its relation to what precedes, or its function as a part of the whole, may need to be expressed. This can sometimes be done by a mere word or phrase (*again*; *therefore*; *for the same reason*) in the topic sentence. Sometimes, however, it is expedient to precede the topic sentence by one or more sentences of introduction or transition. If more than one such sentence is required, it is generally better to set apart the transitional sentences as a separate paragraph. + +According to the writer's purpose, he may, as indicated above, relate the body of the paragraph to the topic sentence in one or more of several different ways. He may make the meaning of the topic sentence clearer by restating it in other forms, by defining its terms, by denying the contrary, by giving illustrations or specific instances; he may establish it by proofs; or he may develop it by showing its implications and consequences. In a long paragraph, he may carry out several of these processes. + +1 Now, to be properly enjoyed, a walking tour should be gone upon alone. 2 If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. 3 A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. 4 And you must be open to all impressions and let your thoughts take colour from what you see. 5 You should be as a pipe for any wind to play upon. 6 “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. 7 When I am in the country, I wish to vegetate like the country,” which is the gist of all that can be said upon the matter. 8 There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. 9 And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.—Stevenson, Walking Tours. + +1 Topic sentence. 2 The meaning made clearer by denial of the contrary. 3 The topic sentence repeated, in abridged form, and supported by three reasons; the meaning of the third (“you must have your own pace”) made clearer by denying the contrary. 4 A fourth reason, stated in two forms. 5 The same reason, stated in still another form. 6–7 The same reason as stated by Hazlitt. 8 Repetition, in paraphrase, of the quotation from Hazlitt. 9 Final statement of the fourth reason, in language amplified and heightened to form a strong conclusion. + +1 It was chiefly in the eighteenth century that a very different conception of history grew up. 2 Historians then came to believe that their task was not so much to paint a picture as to solve a problem; to explain or illustrate the successive phases of national growth, prosperity, and adversity. 3 The history of morals, of industry, of intellect, and of art; the changes that take place in manners or beliefs; the dominant ideas that prevailed in successive periods; the rise, fall, and modification of political constitutions; in a word, all the conditions of national well-being became the subject of their works. 4 They sought rather to write a history of peoples than a history of kings. 5 They looked especially in history for the chain of causes and effects. 6 They undertook to study in the past the physiology of nations, and hoped by applying the experimental method on a large scale to deduce some lessons of real value about the conditions on which the welfare of society mainly depend.—Lecky, The Political Value of History. + +1 Topic sentence. 2 The meaning of the topic sentence made clearer; the new conception of history defined. 3 The definition expanded. 4 The definition explained by contrast. 5 The definition supplemented: another element in the new conception of history. 6 Conclusion: an important consequence of the new conception of history. + +In narration and description the paragraph sometimes begins with a concise, comprehensive statement serving to hold together the details that follow. + +The breeze served us admirably. + +The campaign opened with a series of reverses. + +The next ten or twelve pages were filled with a curious set of entries. + +But this device, if too often used, would become a mannerism. More commonly the opening sentence simply indicates by its subject with what the paragraph is to be principally concerned. + +At length I thought I might return towards the stockade. + +He picked up the heavy lamp from the table and began to explore. + +Another flight of steps, and they emerged on the roof. + +The brief paragraphs of animated narrative, however, are often without even this semblance of a topic sentence. The break between them serves the purpose of a rhetorical pause, throwing into prominence some detail of the action. + +### Rule 10. Use the active voice. + +The active voice is usually more direct and vigorous than the passive: + +I shall always remember my first visit to Boston. + +This is much better than + +My first visit to Boston will always be remembered by me. + +The latter sentence is less direct, less bold, and less concise. If the writer tries to make it more concise by omitting “by me,” + +My first visit to Boston will always be remembered, + +it becomes indefinite: is it the writer, or some person undisclosed, or the world at large, that will always remember this visit? + +This rule does not, of course, mean that the writer should entirely discard the passive voice, which is frequently convenient and sometimes necessary. + +The dramatists of the Restoration are little esteemed to-day. + +Modern readers have little esteem for the dramatists of the Restoration. + +The first would be the right form in a paragraph on the dramatists of the Restoration; the second, in a paragraph on the tastes of modern readers. The need of making a particular word the subject of the sentence will often, as in these examples, determine which voice is to be used. + +As a rule, avoid making one passive depend directly upon another. + +| Original | Revision | +| --- | --- | +| Gold was not allowed to be exported. | It was forbidden to export gold (The export of gold was prohibited). | +| He has been proved to have been seen entering the building. | It has been proved that he was seen to enter the building. | + +In both the examples above, before correction, the word properly related to the second passive is made the subject of the first. + +A common fault is to use as the subject of a passive construction a noun which expresses the entire action, leaving to the verb no function beyond that of completing the sentence. + +| Original | Revision | +| --- | --- | +| A survey of this region was made in 1900. | This region was surveyed in 1900. | +| Mobilization of the army was rapidly effected. | The army was rapidly mobilized. | +| Confirmation of these reports cannot be obtained. | These reports cannot be confirmed. | + +Compare the _sentence,_ “The export of gold was prohibited,” in which the predicate “was prohibited” expresses something not implied in “export.” + +The habitual use of the active voice makes for forcible writing. This is true not only in narrative principally concerned with action, but in writing of any kind. Many a tame sentence of description or exposition can be made lively and emphatic by substituting a verb in the active voice for some such perfunctory expression as *there is*, or *could be heard*. + +| Original | Revision | +| --- | --- | +| There were a great number of dead leaves lying on the ground. | Dead leaves covered the ground. | +| The sound of a guitar somewhere in the house could be heard. | Somewhere in the house a guitar hummed sleepily. | +| The reason that he left college was that his health became impaired. | Failing health compelled him to leave college. | +| It was not long before he was very sorry that he had said what he had. | He soon repented his words. | + +### Rule 11. Put statements in positive form. + +Make definite assertions. Avoid tame, colorless, hesitating, non-committal language. Use the word *not* as a means of denial or in antithesis, never as a means of evasion. + +| Original | Revision | +| --- | --- | +| He was not very often on time. | He usually came late. | +| He did not think that studying Latin was much use. | He thought the study of Latin useless. | +| The Taming of the Shrew is rather weak in spots. Shakespeare does not portray Katharine as a very admirable character, nor does Bianca remain long in memory as an important character in Shakespeare's works. | The women in The Taming of the Shrew are unattractive. Katharine is disagreeable, Bianca insignificant. | + +The last example, before correction, is indefinite as well as negative. The corrected version, consequently, is simply a guess at the writer's intention. + +All three examples show the weakness inherent in the word *not*. Consciously or unconsciously, the reader is dissatisfied with being told only what is not; he wishes to be told what is. Hence, as a rule, it is better to express even a negative in positive form. + +| Original | Revision | +| --- | --- | +| not honest | dishonest | +| not important | trifling | +| did not remember | forgot | +| did not pay any attention to | ignored | +| did not have much confidence in | distrusted | + +The antithesis of negative and positive is strong: + +Not charity, but simple justice. + +Not that I loved Caesar less, but Rome the more. + +Negative words other than *not* are usually strong: + +The sun never sets upon the British flag. + +### Rule 12. Use definite, specific, concrete language. + +Prefer the specific to the general, the definite to the vague, the concrete to the abstract. + +| Original | Revision | +| --- | --- | +| A period of unfavorable weather set in. | It rained every day for a week. | +| He showed satisfaction as he took possession of his well-earned reward. | He grinned as he pocketed the coin. | +| There is a general agreement among those who have enjoyed the experience that surf-riding is productive of great exhilaration. | All who have tried surf-riding agree that it is most exhilarating. | + +If those who have studied the art of writing are in accord on any one point, it is on this, that the surest method of arousing and holding the attention of the reader is by being specific, definite, and concrete. Critics have pointed out how much of the effectiveness of the greatest writers, Homer, Dante, Shakespeare, results from their constant definiteness and concreteness. Browning, to cite a more modern author, affords many striking examples. Take, for instance, the lines from My Last Duchess, + +Sir, 'twas all one! My favour at her breast, + +The dropping of the daylight in the west, + +The bough of cherries some officious fool + +Broke in the orchard for her, the white mule + +She rode with round the terrace—all and each + +Would draw from her alike the approving speech, + +Or blush, at least, + +and those which end the poem, + +Notice Neptune, though, + +Taming a sea-horse, thought a rarity, + +Which Claus of Innsbruck cast in bronze for me. + +These words call up pictures. Recall how in The Bishop Orders his Tomb in St. Praxed's Church “the Renaissance spirit—its worldliness, inconsistency, pride, hypocrisy, ignorance of itself, love of art, of luxury, of good Latin,” to quote Ruskin's comment on the poem, is made manifest in specific details and in concrete terms. + +Prose, in particular narrative and descriptive prose, is made vivid by the same means. If the experiences of Jim Hawkins and of David Balfour, of Kim, of Nostromo, have seemed for the moment real to countless readers, if in reading Carlyle we have almost the sense of being physically present at the taking of the Bastille, it is because of the definiteness of the details and the concreteness of the terms used. It is not that every detail is given; that would be impossible, as well as to no purpose; but that all the significant details are given, and not vaguely, but with such definiteness that the reader, in imagination, can project himself into the scene. + +In exposition and in argument, the writer must likewise never lose his hold upon the concrete, and even when he is dealing with general principles, he must give particular instances of their application. + +“This superiority of specific expressions is clearly due to the effort required to translate words into thoughts. As we do not think in generals, but in particulars—as whenever any class of things is referred to, we represent it to ourselves by calling to mind individual members of it, it follows that when an abstract word is used, the hearer or reader has to choose, from his stock of images, one or more by which he may figure to himself the genus mentioned. In doing this, some delay must arise, some force be expended; and if by employing a specific term an appropriate image can be at once suggested, an economy is achieved, and a more vivid impression produced.” + +Herbert Spencer, from whose Philosophy of Style the preceding paragraph is quoted, illustrates the principle by the sentences: + +| Original | Revision | +| --- | --- | +| In proportion as the manners, customs, and amusements of a nation are cruel and barbarous, the regulations of their penal code will be severe. | In proportion as men delight in battles, bull-fights, and combats of gladiators, will they punish by hanging, burning, and the rack. | + +### Rule 13. Omit needless words. + +Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all his sentences short, or that he avoid all detail and treat his subjects only in outline, but that he make every word tell. + +Many expressions in common use violate this principle: + +| Original | Revision | +| --- | --- | +| the question as to whether | whether (the question whether) | +| there is no doubt but that | no doubt (doubtless) | +| used for fuel purposes | used for fuel | +| he is a man who | he | +| in a hasty manner | hastily | +| this is a subject which | this subject | +| His story is a strange one. | His story is strange. | + +In especial the expression *the fact that* should be revised out of every sentence in which it occurs. + +| Original | Revision | +| --- | --- | +| owing to the fact that | since (because) | +| in spite of the fact that | though (although) | +| call your attention to the fact that | remind you (notify you) | +| I was unaware of the fact that | I was unaware that (did not know) | +| the fact that he had not succeeded | his failure | +| the fact that I had arrived | my arrival | + +See also under *case*, *character*, *nature*, *system* in Chapter V. + +*Who is*, *which was*, and the like are often superfluous. + +| Original | Revision | +| --- | --- | +| His brother, who is a member of the same firm | His brother, a member of the same firm | +| Trafalgar, which was Nelson's last battle | Trafalgar, Nelson's last battle | + +As positive statement is more concise than negative, and the active voice more concise than the passive, many of the examples given under Rules 11 and 12 illustrate this rule as well. + +A common violation of conciseness is the presentation of a single complex idea, step by step, in a series of sentences or independent clauses which might to advantage be combined into one. + +| Original | Revision | +| --- | --- | +| Macbeth was very ambitious. This led him to wish to become king of Scotland. The witches told him that this wish of his would come true. The king of Scotland at this time was Duncan. Encouraged by his wife, Macbeth murdered Duncan. He was thus enabled to succeed Duncan as king. (51 words.) | Encouraged by his wife, Macbeth achieved his ambition and realized the prediction of the witches by murdering Duncan and becoming king of Scotland in his place. (26 words.) | +| There were several less important courses, but these were the most important, and although they did not come every day, they came often enough to keep you in such a state of mind that you never knew what your next move would be. (43 words.) | These, the most important courses of all, came, if not daily, at least often enough to keep one under constant strain. (21 words.) | + +### Rule 14. Avoid a succession of loose sentences + +This rule refers especially to loose sentences of a particular type, those consisting of two co-ordinate clauses, the second introduced by a conjunction or relative. Although single sentences of this type may be unexceptionable (see under Rule 4), a series soon becomes monotonous and tedious. + +An unskilful writer will sometimes construct a whole paragraph of sentences of this kind, using as connectives *and*, *but*, *so*, and less frequently, *who*, *which*, *when*, *where*, and *while*, these last in non-restrictive senses (see under Rule 3). + +The third concert of the subscription series was given last evening, and a large audience was in attendance. Mr. Edward Appleton was the soloist, and the Boston Symphony Orchestra furnished the instrumental music. The former showed himself to be an artist of the first rank, while the latter proved itself fully deserving of its high reputation. The interest aroused by the series has been very gratifying to the Committee, and it is planned to give a similar series annually hereafter. The fourth concert will be given on Tuesday, May 10, when an equally attractive programme will be presented. + +Apart from its triteness and emptiness, the paragraph above is weak because of the structure of its sentences, with their mechanical symmetry and sing-song. Contrast with them the sentences in the paragraphs quoted under Rule 9, or in any piece of good English prose, as the preface (Before the Curtain) to Vanity Fair. + +If the writer finds that he has written a series of sentences of the type described, he should recast enough of them to remove the monotony, replacing them by simple sentences, by sentences of two clauses joined by a semicolon, by periodic sentences of two clauses, by sentences, loose or periodic, of three clauses—whichever best represent the real relations of the thought. + +### Rule 15. Express co-ordinate ideas in similar form. + +This principle, that of parallel construction, requires that expressions of similar content and function should be outwardly similar. The likeness of form enables the reader to recognize more readily the likeness of content and function. Familiar instances from the Bible are the Ten Commandments, the Beatitudes, and the petitions of the Lord's Prayer. + +The unskillful writer often violates this principle, from a mistaken belief that he should constantly vary the form of his expressions. It is true that in repeating a statement in order to emphasize it he may have need to vary its form. For illustration, see the paragraph from Stevenson quoted under Rule _9_. But apart from this, he should follow the principle of parallel construction. + +| Original | Revision | +| --- | --- | +| Formerly, science was taught by the textbook method, while now the laboratory method is employed. | Formerly, science was taught by the textbook method; now it is taught by the laboratory method. | + +The left-hand version gives the impression that the writer is undecided or timid; he seems unable or afraid to choose one form of expression and hold to it. The right-hand version shows that the writer has at least made his choice and abided by it. + +By this principle, an article or a preposition applying to all the members of a series must either be used only before the first term or else be repeated before each term. + +| Original | Revision | +| --- | --- | +| The French, the Italians, Spanish, and Portuguese | The French, the Italians, the Spanish, and the Portuguese | +| In spring, summer, or in winter | In spring, summer, or winter (In spring, in summer, or in winter) | + +Correlative expressions (*both, and*; *not, but*; *not only, but also*; *either, or*; *first, second, third*; and the like) should be followed by the same grammatical construction, that is, virtually, by the same part of speech. (Such combinations as “both Henry and I,” “not silk, but a cheap substitute,” are obviously within the rule.) Many violations of this rule (as the first three below) arise from faulty arrangement; others (as the last) from the use of unlike constructions. + +| Original | Revision | +| --- | --- | +| It was both a long ceremony and very tedious. | The ceremony was both long and tedious. | +| A time not for words, but action. | A time not for words, but for action. | +| Either you must grant his request or incur his ill will. | You must either grant his request or incur his ill will. | +| My objections are, first, the injustice of the measure; second, that it is unconstitutional. | My objections are, first, that the measure is unjust; second, that it is unconstitutional. | + +See also the third example under Rule 12 and the last under Rule 13. + +It may be asked, what if a writer needs to express a very large number of similar ideas, say twenty? Must he write twenty consecutive sentences of the same pattern? On closer examination he will probably find that the difficulty is imaginary, that his twenty ideas can be classified in groups, and that he need apply the principle only within each group. Otherwise he had best avoid difficulty by putting his statements in the form of a table. + +### Rule 16. Keep related words together. + +The position of the words in a sentence is the principal means of showing their relationship. The writer must therefore, so far as possible, bring together the words, and groups of words, that are related in thought, and keep apart those which are not so related. + +The subject of a sentence and the principal verb should not, as a rule, be separated by a phrase or clause that can be transferred to the beginning. + +| Original | Revision | +| --- | --- | +| Wordsworth, in the fifth book of The Excursion, gives a minute description of this church. | In the fifth book of The Excursion, Wordsworth gives a minute description of this church. | +| Cast iron, when treated in a Bessemer converter, is changed into steel. | By treatment in a Bessemer converter, cast iron is changed into steel. | + +The objection is that the interposed phrase or clause needlessly interrupts the natural order of the main clause. Usually, however, this objection does not hold when the order is interrupted only by a relative clause or by an expression in apposition. Nor does it hold in periodic sentences in which the interruption is a deliberately used means of creating suspense (see examples under Rule 18). + +The relative pronoun should come, as a rule, immediately after its antecedent. + +| Original | Revision | +| --- | --- | +| There was a look in his eye that boded mischief. | In his eye was a look that boded mischief. | +| He wrote three articles about his adventures in Spain, which were published in Harper's Magazine. | He published in Harper's Magazine three articles about his adventures in Spain. | +| This is a portrait of Benjamin Harrison, grandson of William Henry Harrison, who became President in 1889. | This is a portrait of Benjamin Harrison, grandson of William Henry Harrison. He became President in 1889. | + +If the antecedent consists of a group of words, the relative comes at the end of the group, unless this would cause ambiguity. + +The Superintendent of the Chicago Division, who + +| Original | Revision | +| --- | --- | +| A proposal to amend the Sherman Act, which has been variously judged. | A proposal, which has been variously judged, to amend the Sherman Act. | +| — | A proposal to amend the much-debated Sherman Act. | +| The grandson of William Henry Harrison, who | William Henry Harrison's grandson, who | + +A noun in apposition may come between antecedent and relative, because in such a combination no real ambiguity can arise. + +The Duke of York, his brother, who was regarded with hostility by the Whigs + +Modifiers should come, if possible, next to the word they modify. If several expressions modify the same word, they should be so arranged that no wrong relation is suggested. + +| Original | Revision | +| --- | --- | +| All the members were not present. | Not all the members were present. | +| He only found two mistakes. | He found only two mistakes. | +| Major R. E. Joyce will give a lecture on Tuesday evening in Bailey Hall, to which the public is invited, on “My Experiences in Mesopotamia” at eight P. M. | On Tuesday evening at eight P. M., Major R. E. Joyce will give in Bailey Hall a lecture on “My Experiences in Mesopotamia.” The public is invited. | + +### Rule 17. In summaries, keep to one tense. + +In summarizing the action of a drama, the writer should always use the present tense. In summarizing a poem, story, or novel, he should preferably use the present, though he may use the past if he prefers. If the summary is in the present tense, antecedent action should be expressed by the perfect; if in the past, by the past perfect. + +An unforeseen chance prevents Friar John from delivering Friar Lawrence's letter to Romeo. Meanwhile, owing to her father's arbitrary change of the day set for her wedding, Juliet has been compelled to drink the potion on Tuesday night, with the result that Balthasar informs Romeo of her supposed death before Friar Lawrence learns of the non-delivery of the letter. + +But whichever tense be used in the summary, a past tense in indirect discourse or in indirect question remains unchanged. + +The Friar confesses that it was he who married them. + +Apart from the exceptions noted, whichever tense the writer chooses, he should use throughout. Shifting from one tense to the other gives the appearance of uncertainty and irresolution (compare Rule 15). + +In presenting the statements or the thought of some one else, as in summarizing an essay or reporting a speech, the writer should avoid intercalating such expressions as “he said,” “he stated,” “the speaker added,” “the speaker then went on to say,” “the author also thinks,” or the like. He should indicate clearly at the outset, once for all, that what follows is summary, and then waste no words in repeating the notification. + +In notebooks, in newspapers, in handbooks of literature, summaries of one kind or another may be indispensable, and for children in primary schools it is a useful exercise to retell a story in their own words. But in the criticism or interpretation of literature the writer should be careful to avoid dropping into summary. He may find it necessary to devote one or two sentences to indicating the subject, or the opening situation, of the work he is discussing; he may cite numerous details to illustrate its qualities. But he should aim to write an orderly discussion supported by evidence, not a summary with occasional comment. Similarly, if the scope of his discussion includes a number of works, he will as a rule do better not to take them up singly in chronological order, but to aim from the beginning at establishing general conclusions. + +### Rule 18. Place the emphatic words of a sentence at the end. + +The proper place in the sentence for the word, or group of words, which the writer desires to make most prominent is usually the end. + +| Original | Revision | +| --- | --- | +| Humanity has hardly advanced in fortitude since that time, though it has advanced in many other ways. | Humanity, since that time, has advanced in many other ways, but it has hardly advanced in fortitude. | +| This steel is principally used for making razors, because of its hardness. | Because of its hardness, this steel is principally used in making razors. | + +The word or group of words entitled to this position of prominence is usually the logical predicate, that is, the *new* element in the sentence, as it is in the second example. + +The effectiveness of the periodic sentence arises from the prominence which it gives to the main statement. + +Four centuries ago, Christopher Columbus, one of the Italian mariners whom the decline of their own republics had put at the service of the world and of adventure, seeking for Spain a westward passage to the Indies as a set-off against the achievements of Portuguese discoverers, lighted on America. + +With these hopes and in this belief I would urge you, laying aside all hindrance, thrusting away all private aims, to devote yourself unswervingly and unflinchingly to the vigorous and successful prosecution of this war. + +The other prominent position in the sentence is the beginning. Any element in the sentence, other than the subject, may become emphatic when placed first. + +Deceit or treachery he could never forgive. + +So vast and rude, fretted by the action of nearly three thousand years, the fragments of this architecture may often seem, at first sight, like works of nature. + +A subject coming first in its sentence may be emphatic, but hardly by its position alone. In the sentence, + +Great kings worshipped at his shrine, + +the emphasis upon *kings* arises largely from its meaning and from the context. To receive special emphasis, the subject of a sentence must take the position of the predicate. + +Through the middle of the valley flowed a winding stream. + +The principle that the proper place for what is to be made most prominent is the end applies equally to the words of a sentence, to the sentences of a paragraph, and to the paragraphs of a composition. + +## V. Words And Expressions Commonly Misused + +(Some of the forms here listed, as *like I did*, are downright bad English; others, as the split infinitive, have their defenders, but are in such general disfavor that it is at least inadvisable to use them; still others, as *case*, *factor*, *feature*, *interesting*, *one of the most*, are good in their place, but are constantly obtruding themselves into places where they have no right to be. If the writer will make it his purpose from the beginning to express accurately his own individual thought, and will refuse to be satisfied with a ready-made formula that saves him the trouble of doing so, this last set of expressions will cause him little trouble. But if he finds that in a moment of inadvertence he has used one of them, his proper course will probably be not to patch up the sentence by substituting one word or set of words for another, but to recast it completely, as illustrated in a number of examples below and in others under Rules 12 and 13.) + +**All right.** Idiomatic in familiar speech as a detached phrase in the sense, “Agreed,” or “Go ahead.” In other uses better avoided. Always written as two words. + +**As good or better than.** Expressions of this type should be corrected by rearranging the sentence. + +| Original | Revision | +| --- | --- | +| My opinion is as good or better than his. | My opinion is as good as his, or better (if not better). | + +**As to whether.** *Whether* is sufficient; see under Rule 13. + +**Bid.** Takes the infinitive without *to*. The past tense in the sense, _“ordered,”_ is *bade*. + +**But.** Unnecessary after *doubt* and *help*. + +| Original | Revision | +| --- | --- | +| I have no doubt but that | I have no doubt that | +| He could not help see but that | He could not help seeing that | + +The too frequent use of *but* as a conjunction leads to the fault discussed under Rule 14. A loose sentence formed with *but* can always be converted into a periodic sentence formed with *although*, as illustrated under Rule 4. + +Particularly awkward is the following of one *but* by another, making a contrast to a contrast or a reservation to a reservation. This is easily corrected by re-arrangement. + +| Original | Revision | +| --- | --- | +| America had vast resources, but she seemed almost wholly unprepared for war. But within a year she had created an army of four million men. | America seemed almost wholly unprepared for war, but she had vast resources. Within a year she had created an army of four million men. | + +**Can.** Means *am (is, are) able*. Not to be used as a substitute for *may*. + +**Case.** The Concise Oxford Dictionary begins its definition of this word: “instance of a thing's occurring; usual state of affairs.” In these two senses, the word is usually unnecessary. + +| Original | Revision | +| --- | --- | +| In many cases, the rooms were poorly ventilated. | Many of the rooms were poorly ventilated. | +| It has rarely been the case that any mistake has been made. | Few mistakes have been made. | + +See Wood, Suggestions to Authors, pp. 68–71, and Quiller-Couch, The Art of Writing, pp. 103–106. + +**Certainly.** Used indiscriminately by some writers, much as others use *very*, to intensify any and every statement. A mannerism of this kind, bad in speech, is even worse in writing. + +**Character.** Often simply redundant, used from a mere habit of wordiness. + +| Original | Revision | +| --- | --- | +| Acts of a hostile character | Hostile acts | + +**Claim, vb.** With object-noun, means *lay claim to*. May be used with a dependent clause if this sense is clearly involved: “He claimed that he was the sole surviving heir.” (But even here, “claimed to be” would be better.) Not to be used as a substitute for *declare*, *maintain*, or *charge*. + +**Clever.** This word has been greatly overused; it is best restricted to ingenuity displayed in small matters. + +**Compare.** To *compare to* is to point out or imply resemblances, between objects regarded as essentially of different order; to *compare with* is mainly to point out differences, between objects regarded as essentially of the same order. Thus life has been compared to a pilgrimage, to a drama, to a battle; Congress may be compared with the British Parliament. Paris has been compared to ancient Athens; it may be compared with modern London. + +**Consider.** Not followed by *as* when it means “believe to be.” “I consider him thoroughly competent.” Compare, “The lecturer considered Cromwell first as soldier and second as administrator,” where “considered” means “examined” or “discussed.” + +**Data.** A plural, like *phenomena* and *strata*. + +These data were tabulated. + +**Dependable.** A needless substitute for *reliable*, *trustworthy*. + +**Different than.** Not permissible. Substitute *different from*, *other than*, or *unlike*. + +**Divided into.** Not to be misused for *composed of*. The line is sometimes difficult to draw; doubtless plays are divided into acts, but poems are composed of stanzas. + +**Don't.** Contraction of *do not*. The contraction of *does not* is *doesn't*. + +**Due to.** Incorrectly used for *through*, *because of*, or *owing to*, in adverbial phrases: “He lost the first game, due to carelessness.” In correct use related as predicate or as modifier to a particular noun: “This invention is due to Edison;” “losses due to preventable fires.” + +**Folk.** A collective noun, equivalent to *people*. Use the singular form only. + +**Effect.** As noun, means *result*; as verb, means *_to_ bring about*, *accomplish* (not to be confused with *affect*, which means “to influence”). + +As noun, often loosely used in perfunctory writing about fashions, music, painting, and other arts: “an Oriental effect;” “effects in pale green;” “very delicate effects;” “broad effects;” “subtle effects;” “a charming effect was produced by.” The writer who has a definite meaning to express will not take refuge in such vagueness. + +**Etc.** Equivalent to *and the rest*, *and so forth*, and hence not to be used if one of these would be insufficient, that is, if the reader would be left in doubt as to any important particulars. Least open to objection when it represents the last terms of a list already given in full, or immaterial words at the end of a quotation. + +At the end of a list introduced by *such as*, *for example*, or any similar expression, *etc.* is incorrect. + +**Fact.** Use this word only of matters of a kind capable of direct verification, not of matters of judgment. That a particular event happened on a given date, that lead melts at a certain temperature, are facts. But such conclusions as that Napoleon was the greatest of modern generals, or that the climate of California is delightful, however incontestable they _may be_, are not properly facts. + +On the formula *the fact that*, see under Rule 13. + +**Factor.** A hackneyed word; the expressions of which it forms part can usually be replaced by something more direct and idiomatic. + +| Original | Revision | +| --- | --- | +| His superior training was the great factor in his winning the match. | He won the match by being better trained. | +| Heavy artillery has become an increasingly important factor in deciding battles. | Heavy artillery has played a constantly larger part in deciding battles. | + +**Feature.** Another hackneyed word; like *factor* it usually adds nothing to the sentence in which it occurs. + +| Original | Revision | +| --- | --- | +| A feature of the entertainment especially worthy of mention was the singing of Miss A. | (Better use the same number of words to tell what Miss A. sang, or if the programme has already been given, to tell how she sang.) | + +As a verb, in the advertising sense of *offer as a special attraction*, to be avoided. + +**Fix.** Colloquial in America for *arrange*, *prepare*, *mend*. In writing restrict it to its literary senses, *fasten*, *make firm or immovable*, etc. + +**Get.** The colloquial *have got* for *have* should not be used in writing. The preferable form of the participle is *got*. + +**He is a man who.** A common type of redundant expression; see Rule 13. + +| Original | Revision | +| --- | --- | +| He is a man who is very ambitious. | He is very ambitious. | +| Spain is a country which I have always wanted to visit. | I have always wanted to visit Spain. | + +**Help.** See under **But**. + +**However.** In the meaning *nevertheless*, not to come first in its sentence or clause. + +| Original | Revision | +| --- | --- | +| The roads were almost impassable. However, we at last succeeded in reaching camp. | The roads were almost impassable. At last, however, we succeeded in reaching camp. | + +When *however* comes first, it means *in whatever way* or *to whatever extent*. + +However you advise him, he will probably do as he thinks best. + +However discouraging the prospect, he never lost heart. + +**Interesting.** Avoid this word as a perfunctory means of introduction. Instead of announcing that what you are about to tell is interesting, make it so. + +| Original | Revision | +| --- | --- | +| An interesting story is told of | (Tell the story without preamble.) | +| In connection with the anticipated visit of Mr. B. to America, it is interesting to recall that he | Mr. B., who it is expected will soon visit America | + +**Kind of.** Not to be used as a substitute for *rather* (before adjectives and verbs), or except in familiar style, for *something like* (before nouns). Restrict it to its literal sense: “Amber is a kind of fossil resin;” “I dislike that kind of notoriety.” The same holds true of *sort of*. + +**Less.** Should not be misused for *fewer*. + +| Original | Revision | +| --- | --- | +| He had less men than in the previous campaign | He had fewer men than in the previous campaign | + +*Less* refers to quantity, *fewer* to number. “His troubles are less than mine” means “His troubles are not so great as mine.” “His troubles are fewer than mine” means “His troubles are not so numerous as mine.” It is, however, correct to say, “The signers of the petition were less than a hundred,” where the round number *a hundred* is something like a collective noun, and *less* is thought of as meaning a less quantity or amount. + +**Like.** Not to be misused for *as*. *Like* governs nouns and pronouns; before phrases and clauses the equivalent word is *as*. + +| Original | Revision | +| --- | --- | +| We spent the evening like in the old days. | We spent the evening as in the old days. | +| He thought like I did. | He thought as I did (like me). | + +**Line, along these lines.** *Line* in the sense of *course of procedure*, *conduct*, *thought*, is allowable, but has been so much overworked, particularly in the phrase *along these lines*, that a writer who aims at freshness or originality had better discard it entirely. + +| Original | Revision | +| --- | --- | +| Mr. B. also spoke along the same lines. | Mr. B. also spoke, to the same effect. | +| He is studying along the line of French literature. | He is studying French literature. | + +**Literal, literally.** Often incorrectly used in support of exaggeration or violent metaphor. + +| Original | Revision | +| --- | --- | +| A literal flood of abuse. | A flood of abuse. | +| Literally dead with fatigue | Almost dead with fatigue (dead tired) | + +**Lose out.** Meant to be more emphatic than *lose*, but actually less so, because of its commonness. The same holds true of *try out*, *win out*, *sign up*, *register up*. With a number of verbs, *out* and *up* form idiomatic combinations: *find out*, *run out*, *turn out*, *cheer up*, *dry up*, *make up*, and others, each distinguishable in meaning from the simple verb. *Lose out* is not. + +**Most.** Not to be used for *almost*. + +| Original | Revision | +| --- | --- | +| Most everybody | Almost everybody | +| Most all the time | Almost all the time | + +**Nature.** Often simply redundant, used like *character*. + +| Original | Revision | +| --- | --- | +| Acts of a hostile _nature_ | Hostile acts | + +Often vaguely used in such expressions as a “lover of nature;” “poems about nature.” Unless more specific statements follow, the reader cannot tell whether the poems have to do with natural scenery, rural life, the sunset, the untracked wilderness, or the habits of squirrels. + +**Near by.** Adverbial phrase, not yet fully accepted as good English, though the analogy of *close by* and *hard by* seems to justify it. *Near*, or *near at hand*, is as good, if not better. + +Not to be used as an adjective; use *neighboring*. + +**Oftentimes, ofttimes.** Archaic forms, no longer in good use. The modern word is *often*. + +**One hundred and one.** Retain the *and* in this and similar expressions, in accordance with the unvarying usage of English prose from Old English times. + +**One of the most.** Avoid beginning essays or paragraphs with this formula, as, “One of the most interesting developments of modern science is, etc.;” “Switzerland is one of the most interesting countries of Europe.” There is nothing wrong in this; it is simply threadbare and forcible-feeble. + +A common blunder is to use a singular verb in a relative clause following this or a similar expression, when the relative is the subject. + +| Original | Revision | +| --- | --- | +| One of the ablest men that has attacked this problem. | One of the ablest men that have attacked this problem. | + +**Participle for verbal noun.** + +| Original | Revision | +| --- | --- | +| Do you mind me asking a question? | Do you mind my asking a question? | +| There was little prospect of the Senate accepting even this compromise. | There was little prospect of the Senate's accepting even this compromise. | + +In the left-hand column, *asking* and *accepting* are present participles; in the right-hand column, they are verbal nouns (gerunds). The construction shown in the left-hand column is occasionally found, and has its defenders. Yet it is easy to see that the second sentence has to do not with a prospect of the Senate, but with a prospect of accepting. In this example, at least, the construction is plainly illogical. + +As the authors of The King's English point out, there are sentences apparently, but not really, of this type, in which the possessive is not called for. + +I cannot imagine Lincoln refusing his assent to this measure. + +In this sentence, what the writer cannot imagine is Lincoln himself, in the act of refusing his assent. Yet the meaning would be virtually the same, except for a slight loss of vividness, if he had written, + +I cannot imagine Lincoln's refusing his assent to this measure. + +By using the possessive, the writer will always be on the safe side. + +In the examples above, the subject of the action is a single, unmodified term, immediately preceding the verbal noun, and the construction is as good as any that could be used. But in any sentence in which it is a mere clumsy substitute for something simpler, or in which the use of the possessive is awkward or impossible, should of course be recast. + +| Original | Revision | +| --- | --- | +| In the event of a reconsideration of the whole matter's becoming necessary | If it should become necessary to reconsider the whole matter | +| There was great dissatisfaction with the decision of the arbitrators being favorable to the company. | There was great dissatisfaction that the arbitrators should have decided in favor of the company. | + +**People.** *The people* is a political term, not to be confused with *the public*. From the people comes political support or opposition; from the public comes artistic appreciation or commercial patronage. + +**Phase.** Means a stage of transition or development: “the phases of the moon;” “the last phase.” Not to be used for *aspect* or *topic*. + +| Original | Revision | +| --- | --- | +| Another phase of the subject | Another point (another question) | + +**Possess.** Not to be used as a mere substitute for *have* or *own*. + +| Original | Revision | +| --- | --- | +| He possessed great courage. | He had great courage (was very brave). | +| He was the fortunate possessor of | He owned | + +**Prove.** The past participle is *proved*. + +**Respective, respectively.** These words may usually be omitted with advantage. + +| Original | Revision | +| --- | --- | +| Works of fiction are listed under the names of their respective authors. | Works of fiction are listed under the names of their authors. | +| The one mile and two mile runs were won by Jones and Cummings respectively. | The one mile and two mile runs were won by Jones and by Cummings. | + +In some kinds of formal writing, as geometrical proofs, it may be necessary to use *respectively*, but it should not appear in writing on ordinary subjects. + +**Shall, Will.** The future tense requires *shall* for the first person, *will* for the second and third. The formula to express the speaker's belief regarding his future action or state is *I shall*; *I will* expresses his determination or his consent. + +**Should.** See under **Would**. + +**So.** Avoid, in writing, the use of *so* as an intensifier: “so good;” “so warm;” “so delightful.” + +On the use of *so* to introduce clauses, see Rule 4. + +**Sort of.** See under **Kind of**. + +**Split Infinitive.** There is precedent from the fourteenth century downward for interposing an adverb between *to* and the infinitive which it governs, but the construction is in disfavor and is avoided by nearly all careful writers. + +| Original | Revision | +| --- | --- | +| To diligently inquire | To inquire diligently | + +**State.** Not to be used as a mere substitute for *say*, *remark*. Restrict it to the sense of *express fully or clearly*, as, “He refused to state his objections.” + +**Student Body.** A needless and awkward expression meaning no more than the simple word *students*. + +| Original | Revision | +| --- | --- | +| A member of the student body | A student | +| Popular with the student body | Liked by the students | +| The student body passed resolutions. | The students passed resolutions. | + +**System.** Frequently used without need. + +| Original | Revision | +| --- | --- | +| Dayton has adopted the commission system of _government._ | Dayton has adopted government by commission. | +| The dormitory system | Dormitories | + +**Thanking You in Advance.** This sounds as if the writer meant, “It will not be worth my while to write to you again.” In making your request, write, “Will you please,” or “I shall be obliged,” and if anything further seems necessary write a letter of acknowledgment later. + +**They.** A common inaccuracy is the use of the plural pronoun when the antecedent is a distributive expression such as *each*, *each one*, *everybody*, *every one*, *many a man*, which, though implying more than one person, requires the pronoun to be in the singular. Similar to this, but with even less justification, is the use of the plural pronoun with the antecedent *anybody*, *any one*, *somebody*, *some one*, the intention being either to avoid the awkward “he or she,” or to avoid committing oneself to either. Some bashful speakers even say, “A friend of mine told me that they, etc.” + +Use *he* with all the above words, unless the antecedent is or must be feminine. + +**Very.** Use this word sparingly. Where emphasis is necessary, use words strong in themselves. + +**Viewpoint.** Write *point of view*, but do not misuse this, as many do, for *view* or *opinion*. + +**While.** Avoid the indiscriminate use of this word for *and*, *but*, and *although*. Many writers use it frequently as a substitute for *and* or *but*, either from a mere desire to vary the connective, or from uncertainty which of the two connectives is the more appropriate. In this use it is best replaced by a semicolon. + +| Original | Revision | +| --- | --- | +| The office and salesrooms are on the ground floor, while the rest of the building is devoted to manufacturing. | The office and salesrooms are on the ground floor; the rest of the building is devoted to manufacturing. | + +Its use as a virtual equivalent of *although* is allowable in sentences where this leads to no ambiguity or absurdity. + +While I admire his energy, I wish it were employed in a better cause. + +This is entirely correct, as shown by the paraphrase, + +I admire his energy; at the same time I wish it were employed in a better cause. + +Compare: + +| Original | Revision | +| --- | --- | +| While the temperature reaches 90 or 95 degrees in the daytime, the nights are often chilly. | Although the temperature reaches 90 or 95 degrees in the daytime, the nights are often chilly. | + +The paraphrase, + +The temperature reaches 90 or 95 degrees in the daytime; at the same time the nights are often chilly, + +shows why the use of *while* is incorrect. + +In general, the writer will do well to use *while* only with strict literalness, in the sense of *during the time that*. + +**Whom.** Often incorrectly used for *who* before *he said* or similar expressions, when it is really the subject of a following verb. + +| Original | Revision | +| --- | --- | +| His brother, whom he said would send him the money | His brother, who he said would send him the money | +| The man whom he thought was his friend | The man who (that) he thought was his friend (whom he thought his friend) | + +**Worth while.** Overworked as a term of vague approval and (with *not*) of disapproval. Strictly applicable only to actions: “Is it worth while to telegraph?” + +| Original | Revision | +| --- | --- | +| His books are not worth while. | His books are not worth reading (are not worth one's while to read; do not repay reading; are worthless). | + +The use of *worth while* before a noun (“a worth while story”) is indefensible. + +**Would.** A conditional statement in the first person requires *should*, not *would*. + +I should not have succeeded without his help. + +The equivalent of *shall* in indirect quotation after a verb in the past tense is *should*, not *would*. + +He predicted that before long we should have a great surprise. + +To express habitual or repeated action, the past tense, without *would*, is usually sufficient, and from its brevity, more emphatic. + +| Original | Revision | +| --- | --- | +| Once a year he would visit the old mansion. | Once a year he visited the old mansion. | diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0cee69..eaa5cee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,3 +42,23 @@ jobs: with: go-version-file: go.mod - run: make lint + + release-build: + name: Release build smoke (${{ matrix.goos }}/${{ matrix.goarch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make release-build GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - run: make release-archive GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - name: Verify archive contents + run: | + tar -tzf dist/ch-$(cat VERSION)-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + | LC_ALL=C sort | diff - <(printf "LICENSE\nREADME.md\nch\n") diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..07c7779 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: Release + +on: + push: + branches: [main] + paths: [VERSION] + +permissions: + contents: write + id-token: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Read VERSION + id: read + run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + + - name: Skip if tag exists + id: check + run: | + if git ls-remote --exit-code --tags origin "refs/tags/v${{ steps.read.outputs.version }}"; then + echo "Tag v${{ steps.read.outputs.version }} already exists, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Verify release notes exist + if: steps.check.outputs.skip != 'true' + run: test -f docs/release-notes/${{ steps.read.outputs.version }}.md + + - uses: actions/setup-go@v5 + if: steps.check.outputs.skip != 'true' + with: + go-version-file: go.mod + + - name: Build + archive all targets + if: steps.check.outputs.skip != 'true' + run: | + for goos in linux darwin; do + for goarch in amd64 arm64; do + make release-build GOOS=$goos GOARCH=$goarch + make release-archive GOOS=$goos GOARCH=$goarch + done + done + + - name: Checksums + if: steps.check.outputs.skip != 'true' + run: make release-checksums + + - uses: sigstore/cosign-installer@v3 + if: steps.check.outputs.skip != 'true' + + - name: Sign archives + checksums + if: steps.check.outputs.skip != 'true' + run: | + cd dist + for f in *.tar.gz checksums.txt; do + cosign sign-blob --yes \ + --output-signature "$f.sig" \ + --output-certificate "$f.pem" "$f" + done + + - name: Tag + create release + if: steps.check.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + V=${{ steps.read.outputs.version }} + git tag "v$V" + git push origin "v$V" + gh release create "v$V" \ + --title "v$V" \ + --notes-file "docs/release-notes/$V.md" \ + dist/*.tar.gz dist/*.sig dist/*.pem dist/checksums.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..22c5e0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing the Work or + Derivative Works thereof, You may choose to offer, and charge a + fee for, acceptance of support, warranty, indemnity, or other + liability obligations and/or rights consistent with this License. + However, in accepting such obligations, You may act only on Your + own behalf and on Your sole responsibility, not on behalf of any + other Contributor, and only if You agree to indemnify, defend, + and hold each Contributor harmless for any liability incurred by, + or claims asserted against, such Contributor by reason of your + accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Francisco Rodrigues + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. diff --git a/Makefile b/Makefile index fa9d1d2..aecdb0f 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ -BIN_NAME := ch -INSTALL := $(HOME)/.local/bin/$(BIN_NAME) -LDFLAGS := -ldflags "-s -w -X main.version=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev)" +BIN_NAME := ch +INSTALL := $(HOME)/.local/bin/$(BIN_NAME) +LDFLAGS := -ldflags "-s -w -X main.version=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev)" +DIST_DIR := dist +VERSION := $(shell cat VERSION) +RELEASE_LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)" COVERAGE_THRESHOLD := 80 -.PHONY: build install test test-integration coverage lint clean deps setup check vendor tools +.PHONY: build install test test-integration coverage lint clean deps setup check vendor tools release-build release-archive release-checksums deps: go mod download @@ -40,11 +43,26 @@ tools: go install tool clean: - rm -f $(BIN_NAME) coverage.out + rm -rf $(BIN_NAME) coverage.out $(DIST_DIR) format: gofmt -s -w . +release-build: + mkdir -p $(DIST_DIR)/$(GOOS)-$(GOARCH) + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \ + go build -trimpath $(RELEASE_LDFLAGS) \ + -o $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(BIN_NAME) . + +release-archive: + cp LICENSE README.md $(DIST_DIR)/$(GOOS)-$(GOARCH)/ + tar -C $(DIST_DIR)/$(GOOS)-$(GOARCH) -czf \ + $(DIST_DIR)/$(BIN_NAME)-$(VERSION)-$(GOOS)-$(GOARCH).tar.gz \ + $(BIN_NAME) LICENSE README.md + +release-checksums: + cd $(DIST_DIR) && sha256sum *.tar.gz > checksums.txt + check: coverage test-integration lint build @echo "All checks passed" diff --git a/README.md b/README.md index 939898f..c664d6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # codeherd +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) + A CLI for managing parallel agentic coding sessions. It organizes projects and git worktrees, configures per-agent environments with deterministic port allocation, and orchestrates tmux sessions where AI coding agents run independently. It is like a shepherd, but for coding agents. @@ -273,6 +275,20 @@ See [docs/hooks.md](docs/hooks.md) for the full reference including environment ## Install +### Via mise + +```bash +mise use github:xico42/codeherd@latest +``` + +Once codeherd lands in the [mise official registry](https://github.com/jdx/mise/tree/main/registry), this becomes `mise use codeherd@latest`. + +### Manual + +Download the appropriate archive from the [latest release](https://github.com/xico42/codeherd/releases/latest), extract, and place `ch` on your `PATH`. Each release ships archives for `linux` and `darwin` on `amd64` and `arm64`, with sigstore signatures and a `checksums.txt`. + +### From source + Requires Go 1.22+, git, and tmux. ```bash @@ -334,6 +350,7 @@ ch | `ch attach session <project> <branch>` | Attach to a session | | `ch show session <project> <branch>` | Show session details | | `ch delete session <project> <branch>` | Stop a session | +| `ch version` | Print the installed version | ## Development diff --git a/cmd/register.go b/cmd/register.go index 8b9fca6..b656202 100644 --- a/cmd/register.go +++ b/cmd/register.go @@ -31,4 +31,6 @@ func registerCommands(root *cobra.Command) { root.AddCommand(runCmd.Cobra()) root.AddCommand((&TemplateCmd{}).Cobra()) + + root.AddCommand(newVersionCmd()) } diff --git a/cmd/root.go b/cmd/root.go index c94a80a..649ab59 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -67,8 +67,10 @@ func resolveProfileArg(flag string) string { } // Execute runs the root command and returns any error. -func Execute() error { +// version is the build-time version string surfaced via `ch --version` / `ch version`. +func Execute(version string) error { resetAllFlags(rootCmd) + rootCmd.Version = version if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) return fmt.Errorf("%w", err) diff --git a/cmd/root_test.go b/cmd/root_test.go index 4287eef..ebb462d 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -14,7 +14,7 @@ func runCmd(t *testing.T, args ...string) error { orig := os.Args os.Args = append([]string{"ch"}, args...) defer func() { os.Args = orig }() - return cmd.Execute() + return cmd.Execute("test") } // TestExecute_Help exercises Execute() and all init() registrations by @@ -68,3 +68,10 @@ func TestExecute_UnknownCommand(t *testing.T) { t.Error("Execute() with unknown command = nil, want error") } } + +// TestExecute_Version exercises the version subcommand. +func TestExecute_Version(t *testing.T) { + if err := runCmd(t, "version"); err != nil { + t.Errorf("Execute(version) = %v, want nil", err) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..ae7cf9e --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(cmd.OutOrStdout(), cmd.Root().Version) + }, + } +} diff --git a/docs/superpowers/plans/2026-05-28-release-pipeline.md b/docs/superpowers/plans/2026-05-28-release-pipeline.md new file mode 100644 index 0000000..7a64c20 --- /dev/null +++ b/docs/superpowers/plans/2026-05-28-release-pipeline.md @@ -0,0 +1,758 @@ +# Release Pipeline Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Stand up an automated, mise-compatible release pipeline that publishes Apache-2.0 licensed, sigstore-signed `codeherd` binaries for linux/darwin × amd64/arm64 whenever the `VERSION` file changes on `main`, and prove the build steps on every PR before merge. + +**Architecture:** A `LICENSE` file plus three new `make` targets (`release-build`, `release-archive`, `release-checksums`) own all build logic. The existing PR-time `ci.yml` gains a matrix smoke job that calls those targets to catch cross-compile regressions early. A new `release.yml` workflow fires on `main` when `VERSION` is touched: it reads the version, no-ops if the tag already exists, runs the four builds sequentially in one job, signs everything via cosign keyless OIDC, then creates the tag and the GitHub release with the matching `docs/release-notes/<version>.md` as the body. + +**Tech Stack:** GitHub Actions (`actions/checkout@v5`, `actions/setup-go@v5`, `sigstore/cosign-installer@v3`), Go toolchain, `make`, `tar`, `sha256sum`, `cosign`, `gh` CLI. Apache License 2.0. + +**Spec:** `docs/superpowers/specs/2026-05-28-release-pipeline-design.md` + +--- + +## File Structure + +| Path | Status | Purpose | +| --- | --- | --- | +| `LICENSE` | new | Apache 2.0 boilerplate, copyright `2026 Francisco Rodrigues` | +| `Makefile` | modified | Adds `release-build`, `release-archive`, `release-checksums` targets; extends `clean`; adds them to `.PHONY` | +| `.github/workflows/ci.yml` | modified | New `release-build` job (matrix smoke) appended after `lint` | +| `.github/workflows/release.yml` | new | Single-job release publish triggered by `VERSION` change on `main` | +| `README.md` | modified | Apache 2.0 badge near the top; mise + manual install snippets in the existing Install section | + +--- + +## Task 1: Add LICENSE file + +**Files:** +- Create: `LICENSE` + +- [ ] **Step 1: Write the LICENSE file with full Apache 2.0 text** + +Create `LICENSE` with the exact contents below. + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing the Work or + Derivative Works thereof, You may choose to offer, and charge a + fee for, acceptance of support, warranty, indemnity, or other + liability obligations and/or rights consistent with this License. + However, in accepting such obligations, You may act only on Your + own behalf and on Your sole responsibility, not on behalf of any + other Contributor, and only if You agree to indemnify, defend, + and hold each Contributor harmless for any liability incurred by, + or claims asserted against, such Contributor by reason of your + accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Francisco Rodrigues + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. +``` + +- [ ] **Step 2: Verify file is present and copyright line is correct** + +Run: `head -5 LICENSE && echo "---" && grep -n "Copyright 2026" LICENSE` +Expected: first lines show "Apache License / Version 2.0..."; grep prints exactly one line: `199: Copyright 2026 Francisco Rodrigues` (line number may differ if the boilerplate spacing shifts; the important part is exactly one match). + +- [ ] **Step 3: Commit** + +```bash +git add LICENSE +git commit -m "chore: add Apache 2.0 LICENSE" +``` + +--- + +## Task 2: Add release Makefile targets + +**Files:** +- Modify: `Makefile` + +- [ ] **Step 1: Read the current Makefile** + +Run: `cat Makefile` +Expected: top of file shows `BIN_NAME := ch`, `LDFLAGS := -ldflags "-s -w -X main.version=$(shell git describe ...)"`, `.PHONY: build install test test-integration coverage lint clean deps setup check vendor tools`. + +- [ ] **Step 2: Add DIST_DIR, VERSION, and RELEASE_LDFLAGS variables under the existing top-of-file vars** + +Modify the top of `Makefile`. Replace lines 1–4 (`BIN_NAME ... COVERAGE_THRESHOLD := 80`) with: + +```make +BIN_NAME := ch +INSTALL := $(HOME)/.local/bin/$(BIN_NAME) +LDFLAGS := -ldflags "-s -w -X main.version=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev)" +DIST_DIR := dist +VERSION := $(shell cat VERSION) +RELEASE_LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)" +COVERAGE_THRESHOLD := 80 +``` + +- [ ] **Step 3: Extend `.PHONY` to include the new targets** + +Replace line 6 of `Makefile` (`.PHONY: build install test test-integration coverage lint clean deps setup check vendor tools`) with: + +```make +.PHONY: build install test test-integration coverage lint clean deps setup check vendor tools release-build release-archive release-checksums +``` + +- [ ] **Step 4: Add the three release targets after the existing `format` target** + +Append these targets to `Makefile`, after the `format` block (currently ending at line 46 `gofmt -s -w .`). Insert them before `check:`: + +```make +release-build: + mkdir -p $(DIST_DIR)/$(GOOS)-$(GOARCH) + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \ + go build -trimpath $(RELEASE_LDFLAGS) \ + -o $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(BIN_NAME) . + +release-archive: + cp LICENSE README.md $(DIST_DIR)/$(GOOS)-$(GOARCH)/ + tar -C $(DIST_DIR)/$(GOOS)-$(GOARCH) -czf \ + $(DIST_DIR)/$(BIN_NAME)-$(VERSION)-$(GOOS)-$(GOARCH).tar.gz \ + $(BIN_NAME) LICENSE README.md + +release-checksums: + cd $(DIST_DIR) && sha256sum *.tar.gz > checksums.txt +``` + +- [ ] **Step 5: Extend `clean` to remove `dist/`** + +Replace the `clean` block (currently lines 42–43): + +```make +clean: + rm -f $(BIN_NAME) coverage.out +``` + +with: + +```make +clean: + rm -rf $(BIN_NAME) coverage.out $(DIST_DIR) +``` + +- [ ] **Step 6: Sanity-check the modified Makefile parses** + +Run: `make -n release-build GOOS=linux GOARCH=amd64` +Expected: prints the planned commands (`mkdir -p dist/linux-amd64`, `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w -X main.version=0.1.0" -o dist/linux-amd64/ch .`) without error. The `$(VERSION)` substitution must show `0.1.0` (current VERSION contents), not `$(VERSION)` literal. + +- [ ] **Step 7: Commit** + +```bash +git add Makefile +git commit -m "feat(make): add release-build, release-archive, release-checksums targets" +``` + +--- + +## Task 3: Verify Makefile targets end-to-end locally + +This task is verification-only: it proves Task 2's targets actually produce a valid archive before any CI/workflow code references them. + +**Files:** none modified. + +- [ ] **Step 1: Clean state** + +Run: `make clean` +Expected: `dist/`, `ch`, and `coverage.out` removed; command exits 0. + +- [ ] **Step 2: Build and archive for linux/amd64** + +Run: +```bash +make release-build GOOS=linux GOARCH=amd64 +make release-archive GOOS=linux GOARCH=amd64 +``` +Expected: `dist/linux-amd64/ch` exists (~9 MB), and `dist/ch-0.1.0-linux-amd64.tar.gz` exists. + +- [ ] **Step 3: Verify archive content layout** + +Run: `tar -tzf dist/ch-0.1.0-linux-amd64.tar.gz | sort` +Expected output (exactly three lines): +``` +LICENSE +README.md +ch +``` + +- [ ] **Step 4: Verify the embedded version string** + +Run: +```bash +mkdir -p /tmp/release-verify +tar -xzf dist/ch-0.1.0-linux-amd64.tar.gz -C /tmp/release-verify +/tmp/release-verify/ch --version +``` +Expected: output contains `0.1.0` (not a git SHA, not `dev`). The exact format depends on how `main.version` is printed (`ch --version` exists per the `ch` CLI surface). + +- [ ] **Step 5: Run all four archs to confirm the matrix builds** + +Run: +```bash +make clean +for goos in linux darwin; do + for goarch in amd64 arm64; do + make release-build GOOS=$goos GOARCH=$goarch + make release-archive GOOS=$goos GOARCH=$goarch + done +done +make release-checksums +ls dist/ +cat dist/checksums.txt +``` +Expected: `ls dist/` shows four `.tar.gz` files plus `checksums.txt`; the checksums file lists exactly four lines, one per archive, each with a 64-hex sha256. + +- [ ] **Step 6: Clean up** + +Run: `make clean` +Expected: `dist/` removed. + +- [ ] **Step 7: No commit (verification only)** + +Nothing to commit. Move on if all steps above passed. + +--- + +## Task 4: Add CI smoke job for release build + +**Files:** +- Modify: `.github/workflows/ci.yml` + +- [ ] **Step 1: Append the `release-build` job to `ci.yml`** + +Append this block to the end of `.github/workflows/ci.yml` (after the existing `lint:` job, preserving the file's final newline): + +```yaml + + release-build: + name: Release build smoke (${{ matrix.goos }}/${{ matrix.goarch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make release-build GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - run: make release-archive GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - name: Verify archive contents + run: | + tar -tzf dist/ch-$(cat VERSION)-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + | sort | diff - <(printf "LICENSE\nREADME.md\nch\n") +``` + +- [ ] **Step 2: YAML syntax sanity-check** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo OK` +Expected: prints `OK`. If Python is unavailable, fall back to `go tool golangci-lint --help >/dev/null` (no-op proxy) and visually inspect indentation — the new job must align with the existing `unit:`, `integration:`, `lint:` jobs (4-space indent under `jobs:`). + +- [ ] **Step 3: Optional — run actionlint if available** + +Run: `actionlint .github/workflows/ci.yml || echo "actionlint not installed, skipping"` +Expected: either `actionlint not installed, skipping`, or no findings reported. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: add release-build smoke job to PR pipeline" +``` + +--- + +## Task 5: Add release publish workflow + +**Files:** +- Create: `.github/workflows/release.yml` + +- [ ] **Step 1: Write the release workflow** + +Create `.github/workflows/release.yml` with this content: + +```yaml +name: Release + +on: + push: + branches: [main] + paths: [VERSION] + +permissions: + contents: write + id-token: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Read VERSION + id: read + run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + + - name: Skip if tag exists + id: check + run: | + if git ls-remote --exit-code --tags origin "refs/tags/v${{ steps.read.outputs.version }}"; then + echo "Tag v${{ steps.read.outputs.version }} already exists, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Verify release notes exist + if: steps.check.outputs.skip != 'true' + run: test -f docs/release-notes/${{ steps.read.outputs.version }}.md + + - uses: actions/setup-go@v5 + if: steps.check.outputs.skip != 'true' + with: + go-version-file: go.mod + + - name: Build + archive all targets + if: steps.check.outputs.skip != 'true' + run: | + for goos in linux darwin; do + for goarch in amd64 arm64; do + make release-build GOOS=$goos GOARCH=$goarch + make release-archive GOOS=$goos GOARCH=$goarch + done + done + + - name: Checksums + if: steps.check.outputs.skip != 'true' + run: make release-checksums + + - uses: sigstore/cosign-installer@v3 + if: steps.check.outputs.skip != 'true' + + - name: Sign archives + checksums + if: steps.check.outputs.skip != 'true' + run: | + cd dist + for f in *.tar.gz checksums.txt; do + cosign sign-blob --yes \ + --output-signature "$f.sig" \ + --output-certificate "$f.pem" "$f" + done + + - name: Tag + create release + if: steps.check.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + V=${{ steps.read.outputs.version }} + git tag "v$V" + git push origin "v$V" + gh release create "v$V" \ + --title "v$V" \ + --notes-file "docs/release-notes/$V.md" \ + dist/*.tar.gz dist/*.sig dist/*.pem dist/checksums.txt +``` + +- [ ] **Step 2: YAML syntax sanity-check** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK` +Expected: prints `OK`. + +- [ ] **Step 3: Optional — run actionlint** + +Run: `actionlint .github/workflows/release.yml || echo "actionlint not installed, skipping"` +Expected: either `actionlint not installed, skipping`, or no findings reported. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci: add release workflow triggered by VERSION change" +``` + +--- + +## Task 6: Update README + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Add Apache 2.0 badge under the title** + +Modify `README.md` to insert a license badge immediately after the title line. Replace lines 1–2 (`# codeherd\n\n`) with: + +```markdown +# codeherd + +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) + +``` + +- [ ] **Step 2: Replace the existing Install section with mise + manual options** + +Locate the `## Install` section (currently around lines 274–280, content: `Requires Go 1.22+, git, and tmux.` followed by the `make install` snippet). Replace the whole `## Install` section (from `## Install` through the closing of the `make install` code block) with: + +```markdown +## Install + +### Via mise + +```bash +mise use github:xico42/codeherd@latest +``` + +Once codeherd lands in the [mise official registry](https://github.com/jdx/mise/tree/main/registry), this becomes `mise use codeherd@latest`. + +### Manual + +Download the appropriate archive from the [latest release](https://github.com/xico42/codeherd/releases/latest), extract, and place `ch` on your `PATH`. Each release ships archives for `linux` and `darwin` on `amd64` and `arm64`, with sigstore signatures and a `checksums.txt`. + +### From source + +Requires Go 1.22+, git, and tmux. + +```bash +make install # builds and installs to ~/.local/bin/ch +``` +``` + +- [ ] **Step 3: Verify the changes render reasonably** + +Run: `head -5 README.md && echo "---" && sed -n '/^## Install/,/^## Shell Completion/p' README.md` +Expected: title block shows the badge, and the Install section shows three subheadings in order: `### Via mise`, `### Manual`, `### From source`, with the existing `## Shell Completion` heading following immediately after. + +- [ ] **Step 4: Commit** + +```bash +git add README.md +git commit -m "docs(readme): add license badge and install instructions for mise + binaries" +``` + +--- + +## Task 7: Final end-to-end local verification + +This task is verification-only: re-runs the full build + checksum + (mock) sign sequence locally to prove the assembled pipeline pieces still cooperate after all edits. + +**Files:** none modified. + +- [ ] **Step 1: Clean state and rebuild every archive** + +Run: +```bash +make clean +for goos in linux darwin; do + for goarch in amd64 arm64; do + make release-build GOOS=$goos GOARCH=$goarch + make release-archive GOOS=$goos GOARCH=$goarch + done +done +make release-checksums +``` +Expected: command sequence completes; `dist/` contains four `.tar.gz` archives plus `checksums.txt`. + +- [ ] **Step 2: Verify every archive contains exactly LICENSE + README.md + ch** + +Run: +```bash +for f in dist/*.tar.gz; do + echo "== $f ==" + tar -tzf "$f" | sort | diff - <(printf "LICENSE\nREADME.md\nch\n") && echo "OK" +done +``` +Expected: each archive prints `OK`; no `diff` output. + +- [ ] **Step 3: Verify checksums file is well-formed** + +Run: `cd dist && sha256sum -c checksums.txt && cd ..` +Expected: each archive prints `OK`; command exits 0. + +- [ ] **Step 4: Verify embedded version on every binary** + +Run: +```bash +for goos in linux darwin; do + for goarch in amd64 arm64; do + tmp=$(mktemp -d) + tar -xzf dist/ch-0.1.0-$goos-$goarch.tar.gz -C "$tmp" + if [ "$goos-$goarch" = "linux-amd64" ]; then + "$tmp/ch" --version + else + # Cannot exec other arches on this host; just check the binary is non-empty + test -s "$tmp/ch" && echo "$goos-$goarch: binary present" + fi + rm -rf "$tmp" + done +done +``` +Expected: the linux-amd64 binary prints a version line containing `0.1.0`; the other three print `<goos>-<goarch>: binary present`. + +- [ ] **Step 5: Confirm workflows pass static checks** + +Run: +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); yaml.safe_load(open('.github/workflows/release.yml'))" && echo "yaml OK" +``` +Expected: prints `yaml OK`. + +- [ ] **Step 6: Clean up** + +Run: `make clean` +Expected: `dist/` removed. + +- [ ] **Step 7: Push the branch and open a PR** + +Run: +```bash +git push -u origin chore/release-skill # or whatever branch you are on +gh pr create --fill --base main +``` +Expected: PR URL printed. Confirm in the PR's Checks tab that the new `Release build smoke` matrix runs four cells (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64) and all pass alongside the existing `unit`, `integration`, `lint` jobs. Merge once green. + +- [ ] **Step 8: Observe the release workflow firing** + +After merge, watch the Actions tab for the `Release` workflow. Expected: it triggers automatically because the merge commit touches `VERSION`. Wait for completion (~90s), then verify: + +```bash +gh release view v0.1.0 +gh release view v0.1.0 --json assets --jq '.assets[].name' | sort +``` +Expected: release body matches `docs/release-notes/0.1.0.md` verbatim; assets list contains exactly: +``` +ch-0.1.0-darwin-amd64.tar.gz +ch-0.1.0-darwin-amd64.tar.gz.pem +ch-0.1.0-darwin-amd64.tar.gz.sig +ch-0.1.0-darwin-arm64.tar.gz +ch-0.1.0-darwin-arm64.tar.gz.pem +ch-0.1.0-darwin-arm64.tar.gz.sig +ch-0.1.0-linux-amd64.tar.gz +ch-0.1.0-linux-amd64.tar.gz.pem +ch-0.1.0-linux-amd64.tar.gz.sig +ch-0.1.0-linux-arm64.tar.gz +ch-0.1.0-linux-arm64.tar.gz.pem +ch-0.1.0-linux-arm64.tar.gz.sig +checksums.txt +checksums.txt.pem +checksums.txt.sig +``` + +If the release fails partway: +- Tag exists but release does not → `git push --delete origin v0.1.0 && git tag -d v0.1.0`, fix the failing step, re-trigger the workflow by touching `VERSION` again (whitespace is fine — the next run with a present tag would no-op, so the tag must be deleted first). +- Release exists with missing assets → `gh release delete v0.1.0 -y`, then delete the tag as above, then re-trigger. + +- [ ] **Step 9: Smoke-test mise install (optional, off-tree)** + +Run on a clean shell (after release publishes): +```bash +mise use github:xico42/codeherd@0.1.0 +ch --version +``` +Expected: mise downloads the correct archive for the host platform; `ch --version` prints `0.1.0`. This validates the asset filename pattern is mise-compatible, which unblocks issue #15 (registry submission). + +- [ ] **Step 10: Update the auto-memory if anything surprising surfaced** + +If the release workflow needed adjustments not foreseen in the spec (e.g. cosign step required an extra flag on this repo, or `git push origin v$V` needed a different ref format), capture the surprise in a feedback or project memory entry so future release pipeline work benefits. + +--- + +## Notes for the executing engineer + +- **Reproducibility:** every `release-*` target works locally with the same inputs CI uses. If the workflow fails, reproduce locally with the same `GOOS`/`GOARCH` first — only investigate GitHub-specific causes if local repro succeeds. +- **Cosign keyless requires `id-token: write`:** already set in `release.yml`. If you ever fork the workflow to a private repo without OIDC enabled, signing will fail; remove the cosign steps or wire up a real key. +- **`gh release create` needs `GH_TOKEN`:** the workflow passes `secrets.GITHUB_TOKEN` via env. No extra setup needed. +- **VERSION-bump trigger and `chore: bump version`:** the `creating-release` skill commits this message. Any other commit that touches `VERSION` will also fire the release workflow — that is by design; the tag-exists guard makes it safe. +- **Idempotency manual test:** to confirm the guard works, push a no-op whitespace change to `VERSION` after a successful release. The workflow should run and exit cleanly without creating a duplicate release. Skip this test if you trust the spec. +- **Adjusting copyright holder:** the LICENSE bottom block reads `Copyright 2026 Francisco Rodrigues`. If the user prefers a different attribution (e.g. organisation name), change that single line and re-commit before opening the PR. diff --git a/docs/superpowers/specs/2026-05-28-creating-release-skill-design.md b/docs/superpowers/specs/2026-05-28-creating-release-skill-design.md new file mode 100644 index 0000000..57d7d24 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-creating-release-skill-design.md @@ -0,0 +1,358 @@ +# `creating-release` skill — design + +**Status:** approved (brainstorming) +**Date:** 2026-05-28 +**Tracks:** #13 (Create release pipeline) — skill portion only +**Out of scope:** the GitHub Actions release pipeline, target platform matrix, mise-backend artifact naming. Those are tracked separately and will be designed once the skill ships. + +## 1. Goal + +Build a maintainer-facing skill, `creating-release`, that takes the codeherd repo from "main has unreleased commits" to "ready-to-merge release-prep commit", semi-automatically. + +The skill runs locally in a Claude Code session before the release is merged. It: + +1. Reads the previous released tag and computes the commit range to release. +2. Classifies each commit into Keep a Changelog sections plus a Breaking flag. +3. Proposes the next SemVer tag from that classification. +4. Bumps the `VERSION` file at the repo root. +5. Rotates `CHANGELOG.md`'s `[Unreleased]` block into a versioned heading and refreshes the compare-link footer. +6. Generates two per-version release-notes files: `docs/release-notes/<version>-technical.md` (used later as the GitHub release body) and `docs/release-notes/<version>-user.md` (friendly announcement). +7. Stages every change, prints a summary diff, and waits for the maintainer's final approval before committing `chore: bump version <version>`. + +The skill is autonomous on the happy path. It stops only for ambiguous input (no commits since last tag, dirty working tree, missing `CHANGELOG.md` on a non-first release) or to gather the maintainer's final approval before committing. + +## 2. Skill home and distribution + +Skills live in `.agents/skills/` so they are agent-runner-agnostic. Claude Code's local skill discovery path `.claude/skills/` is a symlink to `.agents/skills/`: + +``` +.claude/skills -> ../.agents/skills # already created +.agents/skills/writing-clearly-and-concisely/ # already installed +.agents/skills/creating-release/ # this skill (to be created) +``` + +The symlink is committed to the repo so any clone gets the same Claude-visible layout without a setup step. Both the symlink and the `.agents/` tree are checked in. + +The skill is maintainer-only: it is not packaged as a Claude Code plugin and not advertised through `.claude-plugin/marketplace.json`. + +## 3. Bundled layout + +``` +.agents/skills/creating-release/ +├── SKILL.md # orchestration prompt (entry point) +├── scripts/ +│ ├── last-tag.sh # prints last released tag, or empty for v0.1.0 +│ ├── commits-since.sh # prints "%H%x09%s%x09%b%x1e" for <range> +│ ├── propose-bump.sh # classification JSON -> next semver +│ ├── rotate-unreleased.sh # CHANGELOG.md surgery +│ ├── write-version.sh # bump VERSION + sanity-check +│ └── check-preconditions.sh # dirty tree / branch / missing files +├── templates/ +│ ├── changelog-section.tmpl.md # block injected into CHANGELOG.md +│ ├── technical-notes.tmpl.md # docs/release-notes/<v>-technical.md +│ └── user-notes.tmpl.md # docs/release-notes/<v>-user.md +└── references/ + ├── keepachangelog.md # condensed format rules + ├── semver.md # condensed bump rules + └── categorization.md # commit -> section mapping table +``` + +Every shell script is POSIX `sh`, depends only on `git`, `awk`, `sed`, and `jq`, and uses `set -eu`. Scripts are independently runnable for testing (`bash scripts/propose-bump.sh < fixture.json`) and have no Claude-specific side effects. + +## 4. Prose policy + +All maintainer-facing prose the skill emits — release-notes copy, changelog entries, the final commit body, summary printed before approval — defers to the `writing-clearly-and-concisely` skill installed at `.agents/skills/writing-clearly-and-concisely/`. + +`SKILL.md` must contain an explicit instruction: + +> Before writing any prose into `CHANGELOG.md`, `docs/release-notes/<v>-technical.md`, `docs/release-notes/<v>-user.md`, or the final commit message body, invoke the `writing-clearly-and-concisely` skill and apply its rules. Use the Limited Context Strategy (subagent copyedit) if context is tight. + +This is the single source of style for the release artifacts; no separate style guide lives inside `creating-release`. + +## 5. Inputs, outputs, preconditions + +### Inputs + +- Current working tree at the repo root of `codeherd`. +- Optional flag the maintainer types when invoking the skill: + - `release as patch|minor|major` — override the bump proposed from commit analysis. + - `from <ref>` — override the lower bound of the commit range (default: last tag, or repo root for v0.1.0). + +### Preconditions (`check-preconditions.sh`) + +The skill aborts with a clear error if any precondition fails: + +1. The working tree has no uncommitted changes (`git status --porcelain` is empty). +2. The current branch is *not* `main`. The skill is intended to run on a `release/<version>` branch cut from `main`. (Naming is a convention; not enforced.) +3. `CHANGELOG.md` exists with an `## [Unreleased]` heading, *unless* there are no tags yet (first release). +4. `git fetch --tags origin` has been run recently enough for `last-tag.sh` to be accurate. The skill performs `git fetch --tags --quiet origin` itself before reading tags. + +### Outputs (file system, before commit) + +- `VERSION` — single line, no leading `v`, e.g. `0.1.0\n`. +- `CHANGELOG.md` — `[Unreleased]` block rotated under `## [<version>] - YYYY-MM-DD`; a fresh empty `[Unreleased]` block is inserted at the top; footer compare-links updated. +- `docs/release-notes/<version>-technical.md` — created. +- `docs/release-notes/<version>-user.md` — created. + +### Final commit + +``` +chore: bump version <version> + +<one paragraph summary — same prose used in user-notes opening> +``` + +Author and trailer follow this project's commit-message conventions (the maintainer's git identity; no `Co-Authored-By` unless the maintainer enables it). + +## 6. Workflow (`SKILL.md` orchestration) + +The skill must execute the steps in order and never skip the final approval gate. + +1. **Preconditions.** Run `scripts/check-preconditions.sh`. On non-zero exit, print its stderr verbatim and stop. +2. **Range.** Run `scripts/last-tag.sh`. If empty, this is the first release; range is the repo root..`HEAD` and proposed version is `0.1.0` (overridable). +3. **Collect commits.** Run `scripts/commits-since.sh <prev-tag>..HEAD`. The script prints one record per commit using `%x1e` (RS) as the record separator and `%x09` (TAB) between fields: `sha`, `subject`, `body`. This format survives commit messages that contain newlines. +4. **Classify.** Read `references/categorization.md`. For each commit, produce a JSON record: + ```json + { + "sha": "...", + "subject": "...", + "section": "Added|Changed|Deprecated|Removed|Fixed|Security", + "breaking": false, + "user_visible": true, + "summary": "<one-sentence rewrite, present tense, omit needless words>", + "user_summary": "<rewrite for an end user; omitted when user_visible is false>" + } + ``` + `summary` is the technical/changelog phrasing (still readable, no jargon for its own sake). `user_summary` is the friendly phrasing for `<v>-user.md` and is only generated when `user_visible` is true. + - Use the conventional-commit prefix as a strong hint (`feat → Added`, `fix → Fixed`, etc.). The mapping table in `references/categorization.md` is authoritative. + - Mark `breaking: true` for `<type>!:` subjects, or commits whose body contains `BREAKING CHANGE:`. + - Mark `user_visible: false` for `chore:`, `ci:`, `build:`, `test:`, `style:`, `refactor:` that have no user-observable effect. These are kept out of `<v>-user.md` but still appear in `CHANGELOG.md` and `<v>-technical.md` (under Changed if material; otherwise omitted entirely per Keep a Changelog guidance). + - Each `summary` field is written with the `writing-clearly-and-concisely` skill applied. +5. **Propose bump.** Pipe the classification JSON into `scripts/propose-bump.sh`. The script applies SemVer rules: + - any `breaking: true` → MAJOR (or, while `0.x`, MINOR per SemVer §4) + - else any `Added` → MINOR + - else any `Fixed` or `Security` → PATCH + - else → PATCH (no-op-style releases still bump PATCH) + First release is always `0.1.0` regardless of bump signal. + If the maintainer passed `release as <kind>`, that overrides the script's result; the skill prints both and uses the override. +6. **Render templates.** Fill `templates/changelog-section.tmpl.md`, `templates/technical-notes.tmpl.md`, and `templates/user-notes.tmpl.md` with the classification data. Prose fields use `writing-clearly-and-concisely`. Template variables are listed in §8. +7. **Write files.** + - `scripts/write-version.sh <version>` updates `VERSION` and asserts the new value parses as SemVer. + - `scripts/rotate-unreleased.sh <version> <YYYY-MM-DD>` rewrites `CHANGELOG.md`: copy the existing `[Unreleased]` body, replace it with the rendered new section under `## [<version>] - <date>`, insert a fresh empty `[Unreleased]` block above, and refresh the footer compare-links (`[Unreleased]`, `[<version>]`). For the first release, footer links use the repo-root SHA as the lower bound. + - Write `docs/release-notes/<version>-technical.md` and `docs/release-notes/<version>-user.md`. Create `docs/release-notes/` if missing. +8. **Stage and summarize.** `git add` the four touched paths. Print: + - the proposed version and bump reason; + - the resulting `git diff --staged --stat`; + - the head of each new file (first ~20 lines). +9. **Approval gate.** Ask the maintainer in one message: *"Release prep staged for v<version>. Commit as `chore: bump version <version>` (y/n)? Edit anything first if needed."* Wait for explicit `y`. +10. **Commit.** On `y`, run `git commit -m "chore: bump version <version>" -m "<summary paragraph>"`. On `n`, stop without unstaging — the maintainer keeps the staged tree to edit. + +## 7. Commit classification (`references/categorization.md`) + +| Prefix | Default section | Notes | +| ------------------- | --------------- | --------------------------------------------------------------------- | +| `feat` | Added | `feat!` or `BREAKING CHANGE:` → mark breaking | +| `fix` | Fixed | | +| `perf` | Changed | Note magnitude in summary if commit body has measurements | +| `refactor` | Changed | `user_visible: false` unless body says otherwise | +| `revert` | Removed | Subject form: `revert: <original subject>` — point to the reverted SHA | +| `docs` | omit | Skipped unless the commit changes user-facing docs (READMEs, man-style help) — judgment call | +| `style` | omit | | +| `test` | omit | | +| `chore` | omit | Exception: `chore(deps):` bumps that affect runtime go in Changed | +| `ci`, `build` | omit | | +| `security` | Security | Custom prefix; reserved for security fixes | +| `deprecate` | Deprecated | Custom prefix; reserved for deprecation notices | +| no recognised prefix| Changed | Skill flags these in the summary so the maintainer can re-tag | + +Rules: + +- Commits with subject `chore: bump version <v>` are skipped automatically. +- In `CHANGELOG.md` (Keep a Changelog 1.1.0), breaking changes are marked by prefixing the bullet with `**BREAKING:**` and keeping it under its underlying section (`### Changed`, `### Removed`, etc.). Keep a Changelog has no separate "Breaking" section, so we do not invent one. In the two per-version note files — which are not Keep a Changelog — breaking changes still get their own top-level block (`## Breaking Changes` / `## Important: breaking changes`). +- Multiple commits that share a clear theme (same scope or same feature area) may be merged into a single bullet in `<v>-user.md`, with the technical file still listing each commit separately. + +## 8. Templates + +All three templates use Go `text/template`-style placeholders for clarity; the skill renders them by string substitution since there is no template engine — Claude reads the template, substitutes by hand, applies prose rules. Template variables: + +| Name | Type | Source | +| ----------------- | ----------- | ----------------------------------------------------- | +| `Version` | string | proposed semver, no leading `v` | +| `Date` | string | `YYYY-MM-DD`, local date | +| `PrevTag` | string | previous tag with `v` prefix, or empty for first release | +| `RepoSlug` | string | `xico42/codeherd` (hard-coded; this skill is repo-specific) | +| `ReleaseKind` | string | `Major`, `Minor`, or `Patch`, derived from the bump | +| `Sections` | map | section name → list of `{Summary, UserSummary, SHA, Breaking}` records (`Sections.Fixed` etc. are addressable by section name) | +| `Breaking` | list | breaking-change records across all sections; each has `Summary`, `UserSummary`, `SHA` | +| `Highlights` | list | top 1–3 user-visible items, each `{Title, Body}`, written by Claude for `<v>-user.md` | +| `HasFixed` | bool | true when `Sections.Fixed` is non-empty | +| `Summary` | paragraph | 2–4 sentence release summary, written for both files | +| `UpgradeNotes` | paragraph | omitted when empty (no manual steps, no config changes) | + +### 8.1 `templates/changelog-section.tmpl.md` + +Injected into `CHANGELOG.md` under `## [<Version>] - <Date>`. Mirrors Keep a Changelog 1.1.0 exactly: one heading per non-empty section, bullet per commit, breaking changes prefixed with `**BREAKING:**`. + +```markdown +## [{{Version}}] - {{Date}} + +{{if Breaking}}### Breaking + +{{range Breaking}}- **BREAKING:** {{.Summary}} ({{.SHA}}) +{{end}} +{{end}}{{range $name, $items := Sections}}### {{$name}} + +{{range $items}}- {{.Summary}} ({{.SHA}}) +{{end}} +{{end}} +``` + +### 8.2 `templates/technical-notes.tmpl.md` + +Adapted from the changelog-generator "Technical Release Notes" template. Used verbatim as the GitHub release body by the future pipeline. + +```markdown +# Release v{{Version}} + +**Release Date:** {{Date}} +**Type:** {{ReleaseKind}} <!-- Major | Minor | Patch --> +**Previous:** {{PrevTag or "(initial release)"}} + +## Summary + +{{Summary}} + +{{if Breaking}}## Breaking Changes + +{{range Breaking}}- {{.Summary}} ({{.SHA}}) +{{end}} +{{end}}## Changes + +{{range $name, $items := Sections}}### {{$name}} + +{{range $items}}- {{.Summary}} ([{{.SHA}}](https://github.com/{{RepoSlug}}/commit/{{.SHA}})) +{{end}} +{{end}}{{if UpgradeNotes}}## Upgrade Notes + +{{UpgradeNotes}} +{{end}}**Full Changelog:** https://github.com/{{RepoSlug}}/compare/{{PrevTag}}...v{{Version}} +``` + +### 8.3 `templates/user-notes.tmpl.md` + +Adapted from the changelog-generator "User-Friendly Release Notes" template. Friendly prose, no SHAs. + +```markdown +# What's new in codeherd v{{Version}} + +{{Summary}} + +{{if Highlights}}## Highlights + +{{range Highlights}}### {{.Title}} + +{{.Body}} + +{{end}}{{end}}{{if Breaking}}## Important: breaking changes + +{{range Breaking}}- {{.UserSummary}} +{{end}} +{{end}}{{if HasFixed}}## Fixes + +{{range Sections.Fixed}}- {{.UserSummary}} +{{end}} +{{end}}{{if UpgradeNotes}}## Upgrading + +{{UpgradeNotes}} +{{end}} +``` + +`HasFixed` is true when `Sections.Fixed` is non-empty. `Highlights[].Title` and `Highlights[].Body` are authored by Claude per release, not lifted from commit subjects. + +## 9. `CHANGELOG.md` invariant + +At rest (between releases) the file is: + +```markdown +# Changelog + +All notable changes to this project will be 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 + +<!-- entries appended by maintainers during normal development --> + +## [<version-N>] - YYYY-MM-DD + +... + +## [<version-1>] - YYYY-MM-DD + +... + +[Unreleased]: https://github.com/xico42/codeherd/compare/v<version-N>...HEAD +[<version-N>]: https://github.com/xico42/codeherd/compare/v<version-N-1>...v<version-N> +... +[<first-version>]: https://github.com/xico42/codeherd/releases/tag/v<first-version> +``` + +`rotate-unreleased.sh` is responsible for: + +1. Locating the `## [Unreleased]` line. +2. Copying its body up to the next `## [` heading (or EOF). +3. Inserting `## [<new>] - <date>` immediately above the old `[Unreleased]` heading, with the body that was rendered from §8.1 (the rendered body **replaces** whatever was in `[Unreleased]`; the skill's classification is the source of truth for the release block). +4. Replacing the `[Unreleased]` body with a single placeholder comment `<!-- Add Unreleased entries here. -->`. +5. Rewriting the footer link references: + - `[Unreleased]: ...compare/v<new>...HEAD` + - prepending `[<new>]: ...compare/v<prev>...v<new>` (or `releases/tag/v<new>` for the first release). + +For the first release there is no `[Unreleased]` block yet; the script seeds the entire `CHANGELOG.md` with the header, the rendered `[<new>]` block, an empty `[Unreleased]`, and the footer. + +The script is deterministic: same inputs, identical bytes out. Trailing newline preserved. + +## 10. `VERSION` file + +- Single line, `<major>.<minor>.<patch>`, optional `-<prerelease>` and `+<build>` per SemVer 2.0.0. +- No leading `v`. The `v` prefix is added only for git tags and footer links. +- `write-version.sh` rejects anything that fails the SemVer regex from semver.org. The current value must be strictly less than the new value by SemVer precedence (§11). + +## 11. Edge cases and failure modes + +- **First release with no prior tag.** `last-tag.sh` returns empty; range becomes `<root-sha>..HEAD`. Proposed version is `0.1.0`. `CHANGELOG.md` is created from scratch. The footer link for `[0.1.0]` points to `releases/tag/v0.1.0`. +- **No commits in range.** Skill prints "no commits since v<prev>; nothing to release" and exits zero without writing anything. +- **Dirty tree.** Skill prints `git status --short` and exits non-zero before touching anything. +- **Mid-release abort.** If the maintainer answers `n` at the approval gate, files stay staged. The maintainer may unstage with `git reset` or amend before committing. +- **Re-run on the same branch.** `check-preconditions.sh` detects an already-bumped `VERSION` that equals the proposed value and exits with an explanatory message. +- **Unrecognised commit prefix.** Classified as Changed; the skill emits a warning in the summary and asks the maintainer to retag before approval. +- **`writing-clearly-and-concisely` not installed.** Skill warns once and proceeds, but the spec recommends installing it before running. + +## 12. Out of scope + +- The GitHub Actions release pipeline. A separate spec will cover triggering on `VERSION` change, building binaries, target matrix, and GitHub release publication. +- mise-backend artifact naming. Decided alongside the pipeline. +- Cross-platform build details. +- Publishing the skill outside this repo. It is repo-specific; `RepoSlug` is hard-coded. + +## 13. Acceptance criteria + +The skill is complete when: + +1. Running it on a clean `release/0.1.0` branch cut from `main` produces, with no manual edits, a staged change set containing `VERSION` (`0.1.0`), a new `CHANGELOG.md`, `docs/release-notes/0.1.0-technical.md`, and `docs/release-notes/0.1.0-user.md`. +2. The maintainer can approve and the resulting commit message is `chore: bump version 0.1.0`. +3. Running the skill a second time on the same branch refuses to proceed and prints a clear reason. +4. Every script under `scripts/` runs standalone with documented inputs and exits non-zero on error. +5. Every prose fragment authored by the skill was produced with `writing-clearly-and-concisely` applied (verifiable by spot-check of the diff against Strunk's rules). +6. The symlink `.claude/skills -> ../.agents/skills` is committed and the skill is discoverable as `.claude/skills/creating-release/SKILL.md`. + +## 14. References + +- Keep a Changelog 1.1.0 — https://keepachangelog.com/en/1.1.0/ +- Semantic Versioning 2.0.0 — https://semver.org/spec/v2.0.0.html +- changelog-generator skill (template source) — https://github.com/claude-office-skills/skills/tree/main/changelog-generator +- changelog-automation skill (template source) — https://github.com/wshobson/agents/tree/main/plugins/documentation-generation/skills/changelog-automation +- `.agents/skills/writing-clearly-and-concisely/SKILL.md` — prose authority for every artifact this skill produces. diff --git a/docs/superpowers/specs/2026-05-28-release-pipeline-design.md b/docs/superpowers/specs/2026-05-28-release-pipeline-design.md new file mode 100644 index 0000000..2022423 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-release-pipeline-design.md @@ -0,0 +1,331 @@ +# Release pipeline design + +Status: approved +Date: 2026-05-28 +Issue: [#13](https://github.com/xico42/codeherd/issues/13) +Related: [#15](https://github.com/xico42/codeherd/issues/15) (mise registry submission, follow-up) + +## Problem + +`codeherd` has no release pipeline. There is no published binary, no LICENSE file, and no automated path from a merged `chore: bump version` commit to a GitHub Release that downstream tools like [`mise`](https://mise.jdx.dev/dev-tools/backends/github.html) can consume. + +The [`creating-release`](../../../.claude/skills/creating-release) skill already produces the prerequisites on the release branch: a bumped `VERSION` file, a rotated `CHANGELOG.md`, and a single `docs/release-notes/<version>.md` narrative. What is missing is the build pipeline that picks up from there. + +## Goals + +- Cut Apache 2.0 licensed binary releases automatically when a `VERSION` bump lands on `main`. +- Ship archives for `linux` and `darwin` on `amd64` and `arm64`, in a layout mise's GitHub backend can auto-detect. +- Sign archives so users can verify integrity. +- Catch cross-compile and packaging regressions on the pull request, before merge, not after release fires. +- Keep build commands reproducible locally — the workflow should be a thin wrapper around `make`. + +## Non-goals + +- BSD, Windows, and Solaris builds. Issue #13 lists these; the project depends on tmux which is not first-class on BSD/Solaris and is unavailable on Windows. Cutting an untested artifact for those platforms creates a worse user experience than not shipping at all. Revisit if real users ask. +- Replacing or extending the `creating-release` skill. This spec consumes the skill's output unchanged. +- Mise registry submission. Tracked separately in #15 — gated on at least one successful release from this pipeline. +- Goreleaser or a similar release-orchestration tool. The matrix is small enough that `make` + a GH workflow is simpler than introducing a new dependency. +- Re-running `make check` on the release commit. PR-time CI must already pass to merge; re-running on `main` wastes minutes and risks flake blocking a release. + +## Files touched + +| Path | Status | Purpose | +| --- | --- | --- | +| `LICENSE` | new | Apache License 2.0, full text, copyright `2026 Francisco Rodrigues` | +| `.github/workflows/release.yml` | new | Release publish workflow | +| `.github/workflows/ci.yml` | modified | New `release-build` smoke job | +| `Makefile` | modified | `release-build`, `release-archive`, `release-checksums` targets; `clean` extended | +| `README.md` | modified | License badge, mise install snippet | + +## License: Apache 2.0 + +The CLI ships under Apache 2.0. The decision is documented here because it affects every released artifact. + +- **Why not MIT:** Apache 2.0 adds an explicit patent grant and clearer contributor terms. Once outside contributions arrive — and once a company stands behind the project — the patent clause matters. Apache 2.0 is the de-facto standard for OSS CLIs that expect to grow. +- **Why not BSL or AGPL:** both target SaaS protection. The CLI runs client-side and never serves traffic; locking it down with copyleft or source-available terms scares off contributors and enterprise users without protecting any revenue. The future SaaS backend will live in a separate repo and can carry a different license (BSL is the likely candidate) without affecting the CLI. + +## Makefile targets + +Build logic lives in `make` so it is reproducible locally. The workflow only orchestrates. + +```make +DIST_DIR := dist +VERSION := $(shell cat VERSION) +RELEASE_LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)" + +release-build: + mkdir -p $(DIST_DIR)/$(GOOS)-$(GOARCH) + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \ + go build -trimpath $(RELEASE_LDFLAGS) \ + -o $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(BIN_NAME) . + +release-archive: + cp LICENSE README.md $(DIST_DIR)/$(GOOS)-$(GOARCH)/ + tar -C $(DIST_DIR)/$(GOOS)-$(GOARCH) -czf \ + $(DIST_DIR)/$(BIN_NAME)-$(VERSION)-$(GOOS)-$(GOARCH).tar.gz \ + $(BIN_NAME) LICENSE README.md + +release-checksums: + cd $(DIST_DIR) && sha256sum *.tar.gz > checksums.txt +``` + +**Rationale:** + +- `CGO_ENABLED=0` → static binaries that run across any libc. No glibc-version surprises across distros. +- `-trimpath` → reproducible builds. Embedded paths do not leak `/home/runner/...` from the build agent. +- A dedicated `RELEASE_LDFLAGS` reads the version from `VERSION` instead of from `git describe`. The release workflow creates the `v<version>` tag *after* the build step, so at build time `git describe --tags --always` would return a short SHA and `ch --version` would print the SHA rather than the semantic version. Sourcing from the `VERSION` file is authoritative and matches what the archive filename advertises. The default development `LDFLAGS` (used by `make build` / `make install`) is unchanged. +- Archive root is flat: `ch`, `LICENSE`, `README.md` side-by-side, no nested directory. Mise unpacks and finds `ch` immediately. +- Per-target staging dir (`dist/<goos>-<goarch>/`) keeps copies of `LICENSE` / `README.md` from racing across matrix-style sequential calls. + +`make clean` extends to remove `dist/`. + +**Local repro:** + +```sh +make release-build GOOS=linux GOARCH=amd64 +make release-archive GOOS=linux GOARCH=amd64 +``` + +## CI smoke job (PR side) + +Add to `.github/workflows/ci.yml`: + +```yaml +release-build: + name: Release build smoke (${{ matrix.goos }}/${{ matrix.goarch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make release-build GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - run: make release-archive GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} + - name: Verify archive contents + run: | + tar -tzf dist/ch-$(cat VERSION)-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + | sort | diff - <(printf "LICENSE\nREADME.md\nch\n") +``` + +**Properties:** + +- Runs on every PR alongside the existing `unit`, `integration`, `lint` jobs. Blocks merge if cross-compile or archive packaging breaks. +- Four matrix cells in parallel, ~30s each → ~30s wall clock added. +- `fail-fast: false`: cross-compile breakage is often platform-specific (a darwin-only build tag, an arm64 issue). Surfacing all failing platforms in one PR cycle is worth the small CI cost on a four-cell matrix. +- Archive content verification catches "forgot to include LICENSE" regressions before they hit a release. +- Does NOT publish, sign, or tag. Those steps only matter for the released artifact. + +## Release workflow + +`.github/workflows/release.yml`: + +```yaml +name: Release + +on: + push: + branches: [main] + paths: [VERSION] + +permissions: + contents: write + id-token: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Read VERSION + id: read + run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + + - name: Skip if tag exists + id: check + run: | + if git ls-remote --exit-code --tags origin "refs/tags/v${{ steps.read.outputs.version }}"; then + echo "Tag v${{ steps.read.outputs.version }} already exists, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Verify release notes exist + if: steps.check.outputs.skip != 'true' + run: test -f docs/release-notes/${{ steps.read.outputs.version }}.md + + - uses: actions/setup-go@v5 + if: steps.check.outputs.skip != 'true' + with: { go-version-file: go.mod } + + - name: Build + archive all targets + if: steps.check.outputs.skip != 'true' + run: | + for goos in linux darwin; do + for goarch in amd64 arm64; do + make release-build GOOS=$goos GOARCH=$goarch + make release-archive GOOS=$goos GOARCH=$goarch + done + done + + - name: Checksums + if: steps.check.outputs.skip != 'true' + run: make release-checksums + + - uses: sigstore/cosign-installer@v3 + if: steps.check.outputs.skip != 'true' + + - name: Sign archives + checksums + if: steps.check.outputs.skip != 'true' + run: | + cd dist + for f in *.tar.gz checksums.txt; do + cosign sign-blob --yes \ + --output-signature "$f.sig" \ + --output-certificate "$f.pem" "$f" + done + + - name: Tag + create release + if: steps.check.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + V=${{ steps.read.outputs.version }} + git tag "v$V" + git push origin "v$V" + gh release create "v$V" \ + --title "v$V" \ + --notes-file "docs/release-notes/$V.md" \ + dist/*.tar.gz dist/*.sig dist/*.pem dist/checksums.txt +``` + +### Trigger + +`push` to `main` filtered to changes touching `VERSION`. The skill workflow is: + +1. Run `creating-release` skill on a release branch → bumps `VERSION`, rotates `CHANGELOG.md`, writes `docs/release-notes/<version>.md`, commits `chore: bump version <version>`. +2. Open PR → CI (including the new smoke job) must pass. +3. Merge → release workflow fires on `main`. + +No tag push trigger. No `workflow_dispatch`. The skill commit is the single source of truth. + +### Idempotency + +The "Skip if tag exists" step lets the workflow no-op when re-run on a `VERSION` value that has already shipped. Concrete scenarios it covers: + +- A force-push or rebase that rewrites the `VERSION` commit without changing its content. +- An unrelated commit accidentally rewriting the same `VERSION` value (e.g. a revert). +- A manual re-run from the Actions UI. + +The check uses `git ls-remote` rather than the local tag list to be authoritative against the remote. + +Every subsequent step gates on `steps.check.outputs.skip != 'true'`. The repetition is verbose but is the standard GitHub Actions idiom — there is no clean "early success exit" for a job. + +### Pre-flight: release notes file + +Before any expensive setup (`setup-go`, build matrix, cosign), the workflow verifies `docs/release-notes/<version>.md` exists. If the skill ran correctly the file is there; if a release commit got hand-edited and the file was lost, fail fast. + +### Build phase + +A single job runs the four `goos × goarch` combinations sequentially in one shell loop. Total wall clock is roughly 2 minutes — Go cross-compile is seconds per target. A matrix would parallelize to ~30 seconds but at the cost of an `actions/upload-artifact` + `actions/download-artifact` dance into a separate publish job. For a release that fires roughly weekly, the simpler shape wins. + +A failing target halts the whole release. This is wanted: you cannot publish a partial release anyway. + +### Checksums and signing + +`make release-checksums` produces a single `checksums.txt` with sha256 sums for every archive. + +[Sigstore cosign keyless signing](https://docs.sigstore.dev/cosign/signing/overview/) signs each archive and the checksums file. Keyless means signing happens via the workflow's OIDC token — no signing keys to manage, no secrets to rotate. Output per file: `<file>.sig` (signature) and `<file>.pem` (certificate). Users verify with: + +```sh +cosign verify-blob \ + --certificate ch-0.1.0-linux-amd64.tar.gz.pem \ + --signature ch-0.1.0-linux-amd64.tar.gz.sig \ + --certificate-identity-regexp 'https://github.com/xico42/codeherd' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + ch-0.1.0-linux-amd64.tar.gz +``` + +### Tag and release creation + +The workflow creates and pushes the `v<version>` tag, then `gh release create` uploads: + +- Four archives: `ch-<version>-{linux,darwin}-{amd64,arm64}.tar.gz`. +- Eight signature files: `.sig` and `.pem` per archive. +- `checksums.txt` plus its `.sig` and `.pem`. + +Release notes body comes verbatim from `docs/release-notes/<version>.md` via `--notes-file`. No body editing, no concatenation with the CHANGELOG section — single source of truth. + +### Concurrency + +`concurrency.group: release` with `cancel-in-progress: false` serializes simultaneous VERSION pushes. The second run waits for the first to finish, then either no-ops via the tag-exists check or proceeds for a different version. Prevents race conditions on tag creation when two release commits land back-to-back. + +### Failure recovery + +If a release fails partway: + +- Before the tag step → safe, no state changed, fix and retrigger. +- After tag push but before `gh release create` succeeds → the tag exists with no release. Delete it manually (`git push --delete origin v<version>` plus `git tag -d v<version>` locally), fix the underlying issue, retrigger. +- After `gh release create` → the release exists, possibly with incomplete assets. Delete the release + tag manually, retrigger. + +No automatic rollback. Releases are rare; manual recovery is fine. + +## Mise compatibility + +Asset filenames `ch-<version>-{linux,darwin}-{amd64,arm64}.tar.gz` match [mise's GitHub backend autodetection heuristics](https://mise.jdx.dev/dev-tools/backends/github.html): the OS substring (`linux` / `darwin`), the arch substring (`amd64` / `arm64`), and the `.tar.gz` extension all hit. The `v<version>` tag prefix is the documented default. + +Direct install works without a registry entry once the first release ships: + +```sh +mise use github:xico42/codeherd@0.1.0 +``` + +Mise registry submission is tracked in #15 and gated on a successful first release. + +## README additions + +```md +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) + +## Installation + +### Via mise + +mise use github:xico42/codeherd@latest + +### Manual + +Download the appropriate archive from the [latest release](https://github.com/xico42/codeherd/releases/latest), +extract, and place `ch` on your PATH. +``` + +Once #15 lands, swap the mise snippet to `mise use codeherd@latest`. + +## Acceptance criteria + +- `LICENSE` (Apache 2.0) is at the repo root. +- A PR that breaks `make release-build` for any of the four targets fails CI before merge. +- Merging a `chore: bump version <version>` commit to `main` produces, without manual intervention: + - A `v<version>` git tag. + - A GitHub release titled `v<version>` whose body is `docs/release-notes/<version>.md`. + - Four `ch-<version>-<goos>-<goarch>.tar.gz` archives attached. + - A `checksums.txt` attached. + - A `.sig` + `.pem` for every archive and for `checksums.txt`. +- Re-running the release workflow against a `VERSION` whose tag already exists exits successfully without creating a duplicate release. +- `mise use github:xico42/codeherd@<version>` installs the `ch` binary on linux-amd64, linux-arm64, darwin-amd64, and darwin-arm64. +- A `ch` binary extracted from any released archive prints the matching semantic version when invoked as `ch --version` (no embedded SHA leak). + +## Out of scope, deferred follow-ups + +- Mise official registry entry — #15. +- BSD / Solaris / Windows binaries — revisit when a user asks. +- SBOM generation, SLSA provenance attestation — cosign keyless is the practical floor for v0.x; layer on top later if supply-chain attestation becomes a requirement. +- Container image (`ghcr.io/xico42/codeherd`) — only useful once a SaaS backend exists. diff --git a/main.go b/main.go index 22e24d3..46ce426 100644 --- a/main.go +++ b/main.go @@ -9,8 +9,7 @@ import ( var version = "dev" func main() { - _ = version // set at build time via -ldflags - if err := cmd.Execute(); err != nil { + if err := cmd.Execute(version); err != nil { os.Exit(1) } } diff --git a/skills-lock.json b/skills-lock.json index f936b5a..9a37c79 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,75 +1,11 @@ { "version": 1, "skills": { - "brainstorming": { - "source": "obra/superpowers", + "writing-clearly-and-concisely": { + "source": "obra/the-elements-of-style", "sourceType": "github", - "computedHash": "5cafa1558b0b6bd4d4c71f23d5567b7fcbbcdb4b0a50b0c5f69a80a3cebaf9b8" - }, - "dispatching-parallel-agents": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "534432f7d0af6f567ec505ef251e6bbf0a8f2d12a34d9da9517fb131dd9c8646" - }, - "executing-plans": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "7311ed85870f37e9af1e8354c500e7c4161893d845031eed43dd1d55157da83a" - }, - "finishing-a-development-branch": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "9edba9a38684c060fdc38290f640e1dc0c37de286723ac9be73bacacf7cd6f3d" - }, - "receiving-code-review": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "2760c85d4f4117b0006e7ba755f4bbd61f8f4c185f347999763c97f507274e30" - }, - "requesting-code-review": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "93a837ba79c3c3f0b3e503fd9d8b54c638337d73d200c1af3bca9607264bf0e0" - }, - "subagent-driven-development": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "1f5831dbfcddda9ac3e706f2ca81a3887fef9598159966d27ef672d86e255242" - }, - "systematic-debugging": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "72e9ab72627e4fd8ed26a582e82309a98ecdc4f6e1c99418430ac05682c9e91d" - }, - "test-driven-development": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "126f1ebf6ccd414f42544f6e83d8cc5adb089e1108eaffb7c400701e37eecd9f" - }, - "using-git-worktrees": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "52bbb4b6e80918e83e92a1514f3b3757712154c2a8a42de24919e48a794c54fc" - }, - "using-superpowers": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "4310d74a75f89e81f999bfa5b10886be977f95ee0c7fc1d547f94e32c7c8d7f3" - }, - "verification-before-completion": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "9b446f0c7fe1cfb560b1d34439523b1a76d5f177290007b2c053a1c749a4a8ba" - }, - "writing-plans": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "8c536ae03ef3ab1fa974c0e50a65d803acddf5d4ee7cac68ab25a39b9e338611" - }, - "writing-skills": { - "source": "obra/superpowers", - "sourceType": "github", - "computedHash": "7cd0c064b5a62ffdc9cfff539a7b4d8d4a7792c8f0d544436bea021f08632e7b" + "skillPath": "skills/writing-clearly-and-concisely/SKILL.md", + "computedHash": "c702d9db967bc320da26630c1bb2b6794ec7d058bc535818f72e2d3301c2448a" } } } From c62b41fc0933cb235cb561519712fc9ce2a773ae Mon Sep 17 00:00:00 2001 From: Francisco Rodrigues <ednofco@gmail.com> Date: Thu, 28 May 2026 18:32:17 -0300 Subject: [PATCH 2/2] chore: bump version 0.1.0 --- CHANGELOG.md | 32 +++++++++++++++++++++++++++++ VERSION | 1 + docs/release-notes/0.1.0.md | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 VERSION create mode 100644 docs/release-notes/0.1.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..17df820 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be 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] + +<!-- Add Unreleased entries here. --> + +## [0.1.0] - 2026-05-28 + +### Added + +- `project` command (`list`, `show`, `clone`) manages repo aliases and clone directories. +- `worktree` command (`list`, `create`, `delete`) checks out parallel branches under each project, with `--from <branch>`, `--attach`, and `--agent` flags. +- `session` command (`list`, `create`, `delete`, `show`, `attach`) runs tmux-backed agent and shell sessions, storing session state in tmux user-defined options. +- `ch create session` creates the worktree automatically when the target branch has no checkout. +- TUI dashboard launches when you run `ch` with no subcommand, covers project, worktree, and session views, offers a contextual delete flow and a navigable agent picker, and runs inside a dedicated `codeherd` tmux session unless you pass `--no-tmux`. +- Named agents live under `[agents.<name>]` in `config.toml`, selected at session start via `--agent` or the TUI picker; `[defaults].agent` sets the default. +- Hooks fire during the clone, worktree, file-copy, template, and session stages, configured per project. +- File-copy step copies external files (editor config, prompt files, `.env`) into new worktrees through `src:dst` entries. +- `.herd` template engine ships `port` (deterministic FNV-1a hash per project, branch, and name, in the range 10000–59999) and `env` helpers, and supports dry-run. +- `ch template [dir]` renders `.herd` files outside the worktree lifecycle. +- Opt-in profiles, enabled by `[defaults].profiles_enabled`, isolate personal, work, and client contexts under one install; the active profile resolves from `-p/--profile`, then `CODEHERD_PROFILE`, then `[defaults].main_profile`, and each session carries a `<profile>-` tmux name prefix so two profiles never collide on the same `(project, branch)` pair. +- Every agent and shell session receives `CODEHERD_SESSION`, `CODEHERD_PROJECT`, `CODEHERD_BRANCH`, `CODEHERD_WORKTREE_PATH`, `CODEHERD_CLONE_DIR`, and `CODEHERD_PROFILE`. +- `ch run <agent>` replaces the current process with a registered agent in the foreground, inherits the shell environment, and skips the tmux and worktree lifecycle. +- Shell completion suggests project, branch, agent, and profile names dynamically. + +[Unreleased]: https://github.com/xico42/codeherd/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/xico42/codeherd/releases/tag/v0.1.0 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/docs/release-notes/0.1.0.md b/docs/release-notes/0.1.0.md new file mode 100644 index 0000000..d16333a --- /dev/null +++ b/docs/release-notes/0.1.0.md @@ -0,0 +1,40 @@ +# What's new in codeherd v0.1.0 + +This first tagged release of codeherd introduces a CLI that manages projects, branch worktrees, and tmux-backed agent sessions on your local machine through a `<verb> <subject>` grammar. The headline is a TUI dashboard that runs by default, backed by named agents, per-project hooks, `.herd` templates with deterministic port allocation, and opt-in profiles that isolate multiple contexts under one install. + +## Highlights + +### A dashboard that drives the whole workflow + +Run `ch` with no subcommand and codeherd opens a TUI dashboard inside a dedicated `codeherd` tmux session — pass `--no-tmux` to bypass it. The dashboard gathers every project, worktree, and session in one view: pick an agent from the picker, attach to a running session, or delete a worktree, agent, or shell session independently through the contextual delete flow. Main worktrees stay protected from accidental deletion. When profiles are enabled, Ctrl+P and Ctrl+N cycle through them. + +### Parallel agent sessions without tmux plumbing + +codeherd manages worktrees and tmux sessions for every project and branch. Run `ch create session <project> <branch> --agent claude` and codeherd creates the worktree if needed, starts your configured agent inside it, and lets you reattach later from the CLI or the TUI. + +### Deterministic ports for parallel worktrees + +Every `.herd` template can call `port "name"` to hash the project, branch, and key into a stable port between 10000 and 59999. Two worktrees of the same repo never collide on the same port. + +### Opt-in profiles for isolated contexts + +Set `[defaults].profiles_enabled = true` and each profile gets its own config file, tmux name prefix, and session list. codeherd resolves the active profile from `-p`/`--profile`, then `CODEHERD_PROFILE`, then `[defaults].main_profile` — or switch on the fly with Ctrl+P and Ctrl+N in the TUI. + +## Changes + +### Added + +- `project` command (`list`, `show`, `clone`) manages repo aliases and clone directories. +- `worktree` command (`list`, `create`, `delete`) checks out parallel branches under each project, with `--from <branch>`, `--attach`, and `--agent` flags. +- `session` command (`list`, `create`, `delete`, `show`, `attach`) runs tmux-backed agent and shell sessions, storing session state in tmux user-defined options. +- `ch create session` creates the worktree automatically when the target branch has no checkout. +- TUI dashboard launches when you run `ch` with no subcommand, covers project, worktree, and session views, offers a contextual delete flow and a navigable agent picker, and runs inside a dedicated `codeherd` tmux session unless you pass `--no-tmux`. +- Named agents live under `[agents.<name>]` in `config.toml`, selected at session start via `--agent` or the TUI picker; `[defaults].agent` sets the default. +- Hooks fire during the clone, worktree, file-copy, template, and session stages, configured per project. +- File-copy step copies external files (editor config, prompt files, `.env`) into new worktrees through `src:dst` entries. +- `.herd` template engine ships `port` (deterministic FNV-1a hash per project, branch, and name, in the range 10000–59999) and `env` helpers, and supports dry-run. +- `ch template [dir]` renders `.herd` files outside the worktree lifecycle. +- Opt-in profiles, enabled by `[defaults].profiles_enabled`, isolate personal, work, and client contexts under one install; the active profile resolves from `-p/--profile`, then `CODEHERD_PROFILE`, then `[defaults].main_profile`, and each session carries a `<profile>-` tmux name prefix so two profiles never collide on the same `(project, branch)` pair. +- Every agent and shell session receives `CODEHERD_SESSION`, `CODEHERD_PROJECT`, `CODEHERD_BRANCH`, `CODEHERD_WORKTREE_PATH`, `CODEHERD_CLONE_DIR`, and `CODEHERD_PROFILE`. +- `ch run <agent>` replaces the current process with a registered agent in the foreground, inherits the shell environment, and skips the tmux and worktree lifecycle. +- Shell completion suggests project, branch, agent, and profile names dynamically.