mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
Write PR titles, commit messages and branch names in plain English (GST-1825) (#4146)
* Add a check that keeps tracker codes and internal jargon out of PR titles and branch names gstack-plain-title checks the readable front of a title, branch name, PR opening or changelog entry against the repository's .gstack/plain-titles.json; one trailing issue code stays allowed. A Plain title workflow runs it on every new pull request, and /ship's push refuses a never-pushed branch whose name fails it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Move the PR body's machine-read lines into a closing Agent notes section The ship receipt, its runs block and the Plan line now live in one collapsed section at the end of the body, and every reader looks there first while still accepting the old receipt-at-top layout. gstack-post refuses to post a title the plain-title check refuses, and land uses the checked PR title as main's commit title. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Teach /ship to write plain titles, PR openings, commit messages and changelog entries The README now lists what gstack is for, so a PR opening can name the goal it serves. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep tests, size budgets and generated files in step with plain titles and Agent notes Copy lists that install result-code modules now include the plain-title codes, the ship goldens and size ledger are refreshed, the new workflow is classified, and the pinned prose tests read the new title and body layout. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Record the test-profile list change as non-behavioral for the shared test harness Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep text people add below the PR notes, and fix three smaller review findings The notes section now ends at its own closing tag, so a collapsible added after it survives the next post. The opening check splits the body the same way the notes reader does. Wave PR titles no longer name the area, which the plain-title check could refuse. The Capy line in the PR-body steps now puts Approvals in the notes. Review-Finding: p1/specialist-batched#1 reader=testing Review-Finding: p1/red-team#1 reader=red-team Review-Finding: p1/red-team#2 reader=red-team Review-Finding: p1/red-team#3 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Mask credentials in refused-title lines and keep older PRs' plan lines readable A refused title's printed match is masked by the secret scanner first. The Plan line reader falls back to the whole body when the Agent notes section has none, so a body written before this change keeps its plan when its first receipt lands. The notes section also ends at its own closing tag when its opener is split or marked open, or when a collapsible inside it is left unclosed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Print a fixed placeholder when a credential in a refused title cannot be masked Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep a plain-title policy edit from scheduling every paid eval Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Record the paid-eval list change as non-behavioral for the shared test harness Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Recheck the PR title on every edit so a base change cannot turn a refused title green Review-Finding: s11/targeted#1 reader=targeted Review-Finding: s11/adversarial-native#1 reader=adversarial-native Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
892c3b13cd
commit
914e7b6fb3
78 files changed
+1770
-174
No files matched your search
@@ -127,6 +127,12 @@ cheap guards (generated-content freshness, size and prompt budgets, credential s
|
||||
crew names them in `GUARD_CHECKS`), because a paid gate that turns red on a free check
|
||||
is spend wasted.
|
||||
|
||||
A lane writes the public record in plain words: its branch name, PR title, commit subjects
|
||||
and changelog fragment say what changed for someone using the project, with an issue code
|
||||
only at the end (`capy/github-rate-limit-gst-1713`, "Stop gstack from hitting GitHub's rate
|
||||
limit while it waits on PRs (GST-1713)"), and its PR body keeps codes, receipts and lane
|
||||
state in the closing Agent notes section ([ship-policy.md](ship-policy.md#plain-titles)).
|
||||
|
||||
A lane edits only the files it was given. The controller keeps the ownership list in the
|
||||
digest's **In progress**; a lane that needs a file another live lane owns tells the
|
||||
controller, who routes it, and never edits it itself. Everything crosses the crew through
|
||||
|
||||
+99
-3
@@ -1256,8 +1256,9 @@ which carry the workflow consequences the skill prose would otherwise spell out:
|
||||
| `ship: review_scope …` | Step 9.4 item 0 runs `gstack-review-scope classify` on each pass's findings; see [review_scope](#review-scope) |
|
||||
| `ship: changelog_fragments <dir>: …` | Step 12 writes no version; Step 13 writes the entry as `<dir>/<branch-slug>.md` (entry text only, `bump:` front matter only when the change warrants more than the policy bump), never an `[Unreleased]` section, linted with `gstack-prose-lint <file> --gate`; `gstack-ship-queue land` assigns the version; see [changelog fragments](#changelog-fragments) |
|
||||
|
||||
The PR body's first section is the ship receipt (`gstack-ship-receipt write … --body`),
|
||||
written by the pr-body section of `/ship` after the body is composed.
|
||||
The ship receipt (`gstack-ship-receipt write … --body`) goes in the PR body's closing Agent notes
|
||||
section, written by the pr-body section of `/ship` after the body is composed; a body from before that
|
||||
layout keeps its receipt at the top and is still read.
|
||||
|
||||
<a id="ship-receipt"></a>
|
||||
## The ship receipt
|
||||
@@ -2677,4 +2678,99 @@ a bundle the identity is unknown and the receipt is `not-eligible`. When the
|
||||
recorded predecessor head is no longer the predecessor PR's head (checked
|
||||
through `--predecessor-ref` or `gh pr view --predecessor-pr`) the receipt
|
||||
prints `PREDECESSOR MOVED <old> -> <new> — fix: gstack-restamp --after <pr>
|
||||
again (RESTAMP_PREDECESSOR_MOVED)` and refuses `eligible`.
|
||||
again (RESTAMP_PREDECESSOR_MOVED)` and refuses `eligible`.
|
||||
<a id="plain-titles"></a>
|
||||
## Plain titles: `.gstack/plain-titles.json`
|
||||
|
||||
People outside a project read its pull requests and commit log to decide
|
||||
whether it is worth using. So PR titles (and the squash commit titles they
|
||||
become), branch names, the opening of each PR body, changelog entries and commit
|
||||
subjects say in everyday words what changed for someone using the project, for
|
||||
example "Stop gstack from hitting GitHub's rate limit while it waits on PRs".
|
||||
|
||||
**Front for humans, end for machines.** The rule applies to the readable front.
|
||||
A title may end with one group of references, such as `(GST-1713)`,
|
||||
`(GST-1808, GST-1809)` or `(#4122)`; that group is never checked. A branch
|
||||
may end with one tracker code (`capy/github-rate-limit-gst-1713`). A PR body
|
||||
opens with one or two plain sentences (what changed and which goal of the
|
||||
project it serves) and keeps tracker codes, the ship record and other
|
||||
machine-read lines in the Agent notes section at the end, below the
|
||||
`<!-- gstack:agent-notes -->` marker. The checker skips that section.
|
||||
|
||||
What the checker refuses in the front:
|
||||
|
||||
- a tracker code matching one of the policy's `codes` patterns;
|
||||
- a word or phrase from `terms` (whole words, any case; in branch names a
|
||||
hyphen or underscore counts as a space);
|
||||
- in titles and commit subjects, a `word(scope):` prefix, since the scope is
|
||||
usually an internal module name. A bare `fix:` or `docs:` is allowed, and so
|
||||
is a leading version such as `v1.98.0.0 release`;
|
||||
- in the PR opening, a first block that is a heading, code block, list, table,
|
||||
HTML or image, or a paragraph longer than two sentences;
|
||||
- in a changelog entry, a tracker code anywhere, and a listed word in the bold
|
||||
lead. Code spans, code blocks, links and quoted words are skipped.
|
||||
|
||||
### The policy file
|
||||
|
||||
Each repository opts in by committing `.gstack/plain-titles.json`
|
||||
(`gstack-plain-title init` writes an example; `gstack-plain-title validate`
|
||||
checks it). Without the file every check passes with
|
||||
`PLAIN_TITLE: none (no policy)`, so gstack's own word list never blocks
|
||||
another project. The keys:
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `codes` | Regex patterns for tracker codes, matched in any case. Keep them narrow: `\\b(?:GST\|GSTA)-\\d+\\b`, not every `ABC-123`, so `UTF-8` stays readable. |
|
||||
| `terms` | The project's own words that mean nothing to an outside reader. |
|
||||
| `allow` | `{ "text", "reason" }` entries. A refused match inside an allowed phrase passes. Every entry says why. |
|
||||
| `since` | A date (`YYYY-MM-DD`, midnight UTC). PRs created before it are exempt, so nothing already open breaks. |
|
||||
| `exempt_branch_prefixes` | Branch prefixes that gstack's own tools generate; those branches are not checked. |
|
||||
|
||||
gstack's own file lists its tracker prefixes (GST, GSTA, GBRA, GEVA, FOUN, HARN,
|
||||
ALPH) and R-numbers (`R76`), the crew and Captain names, and the words last
|
||||
week's titles used most: voices, outside voice, pregate, fix-check, ledger,
|
||||
receipt, train, wave, lane, restamp and no-stop, with their plurals where
|
||||
they were used. It exempts `release/` (the release PR), `wave/` (wave PRs) and
|
||||
`gstack-land-repair/` (land's repair PR). The allow list is one entry, kept short
|
||||
on purpose:
|
||||
|
||||
| Allowed | Why |
|
||||
|---------|-----|
|
||||
| `R2` | Cloudflare R2, the storage service, not a rules number. |
|
||||
|
||||
"merge train" is not allowed: to a reader outside the crew it still names
|
||||
gstack's own merge process, so titles say "merge several PRs together" instead.
|
||||
|
||||
### Where it runs
|
||||
|
||||
1. **/ship, before posting.** /ship writes a plain title and runs
|
||||
`gstack-plain-title check` on the title, the branch (before its first
|
||||
push), the PR opening and the changelog fragment, and rewrites anything it
|
||||
refuses.
|
||||
2. **gstack-post, before posting.** `gstack-post pr-title` and `pr-create`
|
||||
run the title check first and post nothing when it refuses.
|
||||
3. **The "Plain title" CI check** (`.github/workflows/plain-title.yml`). It
|
||||
runs on every new PR, on each push, and when the title or body is edited.
|
||||
It fails on a refused title and only warns about the branch name, the
|
||||
opening and commit subjects, because those cannot always be fixed in place.
|
||||
It runs on `pull_request_target` with a read-only token and the base
|
||||
repository's checkout, so a PR is checked against the policy and checker
|
||||
already on main and cannot change either; the PR text reaches the checker
|
||||
only as files. The PR that first adds the workflow does not run it for the
|
||||
same reason.
|
||||
|
||||
`gstack-plain-title check` exits 0 when everything passes, the policy is
|
||||
missing, the PR is exempt or there are only warnings; 1 when something is
|
||||
refused; 2 on a usage error; 3 when the policy is invalid. Each finding is
|
||||
one `PLAIN_TITLE:` line naming only the matched words, and the last line is
|
||||
`PLAIN_TITLE: ok | refused=<n> | warned=<n> | exempt (created before <since>) | none (no policy)`.
|
||||
Codes and fixes: [troubleshooting.md](troubleshooting.md#plain-title-refused).
|
||||
|
||||
### Making it a required check
|
||||
|
||||
The rollout takes two merges. The first ships the checker, the writers and the
|
||||
workflow. Once every open PR has a "Plain title" run (an edit to its title or
|
||||
body starts one), a one-line follow-up adds `"Plain title"` to
|
||||
`changelog_fragments.required_checks`, and `gstack-ship-queue land` then
|
||||
refuses a PR whose check is red or missing. Adding it in the first merge would
|
||||
hold every open PR that has never had a run.
|
||||
+3
-3
@@ -698,14 +698,14 @@ A lot of branches die when the interesting work is done and only the boring rele
|
||||
|
||||
### The PR body
|
||||
|
||||
`/ship` writes the PR body from the diff, the receipt and the review, and a reader who was not in the session decides the merge from it. On Capy (`CAPY_PR: on`) the body opens with four sections, in this order, before the standard Summary, Test plan and Documentation sections:
|
||||
`/ship` writes the PR body from the diff, the receipt and the review, and a reader who was not in the session decides the merge from it. The front is for people and the end is for machines: the body opens with one or two plain sentences saying what changed for someone using the project and which README goal it serves, and it ends with one collapsed **Agent notes** section (`<!-- gstack:agent-notes -->` then `<details><summary>Agent notes</summary>`) that holds the ship receipt, the approval and Plan lines, issue codes and the review-loop lines gstack's tools read. The title follows the same shape: plain words first, an issue code only at the end (`Stop gstack from hitting GitHub's rate limit while it waits on PRs (GST-1713)`), checked by `gstack-plain-title` (see [Plain titles](ship-policy.md#plain-titles)). On Capy (`CAPY_PR: on`) the opening is followed by three sections, in this order, before the standard Summary, Test plan and Documentation sections:
|
||||
|
||||
1. **`## What changed`**: one to three captioned before/after images from `gstack-pr-shots` or a UI screenshot, highest-signal first, with a collapsed text version under each.
|
||||
2. **`## Why`**: two or three sentences. The first states the problem in the person's words (the issue, the message, the failing run), then the cause and the user-visible effect.
|
||||
3. **`## Blast radius`**: what else the change touches and the one fact that makes that safe, with the evidence-ladder rung that fact reached (a test that exercises the shared path, a grep over every caller, a measured run). A change to a shared helper, a generated file, a selection list or a policy default always has one; a leaf fix says so in one line.
|
||||
4. **`## Approvals`**: the output of `gstack-approvals render`, verbatim. On a private repository that is the table; on a public repository, or one whose visibility gh cannot read, it is only `Approved: yes|no (record <id>)`, because GitHub keeps every earlier version of a PR description.
|
||||
`## Approvals`, the output of `gstack-approvals render` verbatim, goes inside Agent notes. On a private repository that is the table; on a public repository, or one whose visibility gh cannot read, it is only `Approved: yes|no (record <id>)`, because GitHub keeps every earlier version of a PR description.
|
||||
|
||||
Before the body is composed, `gstack-prose-lint` reads the Why, Summary and Blast radius drafts (`--sections 'Why,Summary,Blast radius' --gate`) and the CHANGELOG entry (`--base origin/<base> --gate`), and a `PROSE_LINT_FOUND` line names the sentence to rewrite; the rest of the body is advisory. The Captain's merge-ask template (`captain/templates/merge-ask.md`) carries the same four sections, so a READY report and its PR say the same thing.
|
||||
Before the body is composed, `gstack-prose-lint` reads the Why, Summary and Blast radius drafts (`--sections 'Why,Summary,Blast radius' --gate`) and the CHANGELOG entry (`--base origin/<base> --gate`), and a `PROSE_LINT_FOUND` line names the sentence to rewrite; the rest of the body is advisory. The Captain's merge-ask template (`captain/templates/merge-ask.md`) carries the same sections, so a READY report and its PR say the same thing.
|
||||
|
||||
### Versions: where `/ship` looks, and when it ships without one
|
||||
|
||||
|
||||
@@ -7377,3 +7377,37 @@ These codes come from `bin/gstack-clean`, which reclaims old codex scratch in `<
|
||||
### `RUNAWAY_INPUT: file=<path> …` (gstack-runaway)
|
||||
|
||||
**Meaning.** That thread's file is not valid JSON, or lacks `cost.tokens`, a parseable `created_at` or numeric `tasks[].cost.tokens`. The thread is skipped (never counted as OK), the rest of the sweep still runs, `SWEEP:` counts it as `unreadable`, and the exit is 1. **Fix.** Fetch that thread again with `tools.thread.get` (the whole object, not a summary) and rerun the scan.
|
||||
|
||||
## Plain titles (gstack-plain-title)
|
||||
|
||||
These codes come from `bin/gstack-plain-title`, which /ship, gstack-post and the "Plain title" CI check run so that PR titles, branch names, PR openings, changelog entries and commit subjects say in everyday words what changed. The rules are in [ship-policy.md](ship-policy.md#plain-titles); the word list is the repository's `.gstack/plain-titles.json`. Each finding prints as `PLAIN_TITLE: <CODE> <kind> "<match>" — <hint>`, naming only the matched words, never the whole title or body.
|
||||
|
||||
<a id="plain-title-refused"></a>
|
||||
### `PLAIN_TITLE: PLAIN_TITLE_REFUSED <kind> "<match>" — …`
|
||||
|
||||
**Meaning.** The front of the PR title (everything before one optional trailing group of references such as `(GST-1713)` or `(#4122)`) holds a tracker code, a word from the policy's `terms` list, or a `word(scope):` prefix. Exit 1; gstack-post posts nothing, and the CI check fails. **Fix.** Rewrite the title as a plain sentence about what changed for someone using the project, for example "Stop gstack from hitting GitHub's rate limit while it waits on PRs (GST-1713)". Editing the title reruns the CI check.
|
||||
|
||||
<a id="plain-branch-refused"></a>
|
||||
### `PLAIN_TITLE: PLAIN_BRANCH_REFUSED <kind> "<match>" — …`
|
||||
|
||||
**Meaning.** The branch name holds a tracker code or a listed word before its last segment. Hyphens and underscores count as spaces, one tracker code at the very end is fine (`capy/github-rate-limit-gst-1713`), and branches gstack generates (`release/`, `wave/`, `gstack-land-repair/`) are exempt. /ship refuses it before the first push; CI only warns, since a pushed branch cannot be renamed in place. **Fix.** Name the next branch in plain words; for this one, open a new branch only if the PR has not been pushed yet.
|
||||
|
||||
<a id="plain-opening-refused"></a>
|
||||
### `PLAIN_TITLE: PLAIN_OPENING_REFUSED <kind> "<match>" — …`
|
||||
|
||||
**Meaning.** The readable part of the PR body (everything above the `<!-- gstack:agent-notes -->` marker) does not start with a plain paragraph: it starts with a heading, code block, list, table, HTML or image, runs past two sentences, or names a tracker code or listed word. /ship refuses it; CI only warns. **Fix.** Start the body with one or two sentences saying what changed and which goal of the project it serves. Move codes and records into the Agent notes section at the end.
|
||||
|
||||
<a id="plain-changelog-refused"></a>
|
||||
### `PLAIN_TITLE: PLAIN_CHANGELOG_REFUSED <kind> "<match>" — …`
|
||||
|
||||
**Meaning.** The changelog entry holds a tracker code, or its bold lead holds a listed word. Code spans, code blocks, links and quoted words are skipped. **Fix.** Drop tracker codes from the entry and rewrite the bold lead in everyday words.
|
||||
|
||||
<a id="plain-commit-refused"></a>
|
||||
### `PLAIN_TITLE: PLAIN_COMMIT_REFUSED <kind> "<match>" — … (warning only)`
|
||||
|
||||
**Meaning.** A commit subject on the PR breaks the title rule. This is always a warning: the squash commit on main takes the PR title, which the title check already guards. **Fix.** Write later commit subjects as plain sentences; nothing needs rewriting now.
|
||||
|
||||
<a id="plain-policy-invalid"></a>
|
||||
### `gstack-plain-title: .gstack/plain-titles.json is not valid JSON or breaks the schema … (PLAIN_POLICY_INVALID)`
|
||||
|
||||
**Meaning.** The policy file does not parse, has an unknown key, a `codes` entry that is not a valid regex or matches empty text, an `allow` entry without a reason, or a `since` that is not a date. Exit 3; nothing was checked. **Fix.** Fix the key the line names, then run `gstack-plain-title validate`.
|
||||
+1
-1
@@ -159,7 +159,7 @@ waiting on the base, not failed ([base_red](ship-policy.md#base-red)).
|
||||
|
||||
The one publication the unattended rules allow: when an open PR for the branch
|
||||
exists and HEAD is its pushed head, Step 18 takes the pr-body section's
|
||||
existing-PR path and posts the body (receipt first) and title through
|
||||
existing-PR path and posts the body (receipt in its Agent notes section) and title through
|
||||
`gstack-post`. It never runs `pr-create`, never pushes a commit and never runs
|
||||
`gh pr ready`; a branch with no open PR, or whose PR head is not HEAD, takes
|
||||
the handoff section. When the run committed anything (a CHANGELOG entry,
|
||||
|
||||
Reference in new issue
Block a user