diff --git a/CHANGELOG.md b/CHANGELOG.md index 883b1c929..609c1bb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.93.20.0] - 2026-11-02 + +### Changed + +- `gstack-factory-scorecard` now counts a PR as "at the cap" once it reaches 2 review passes, the two-round cap the review policy sets, instead of 3. The default `--pass-cap` is 2, and the "Review passes per PR" benchmark reads "median 1 pass, few at the cap". Pass `--pass-cap 3` to get the old count. Rows from earlier weeks in `trend.csv` were counted at the old cap, and each row names the cap it used. + +### Docs + +- CLAUDE.md now describes how gstack versions its own PRs today: a branch writes its entry as `changelog.d/.md` and never edits VERSION, package.json or CHANGELOG.md, and the merge train's stamp assigns the version (`bump: minor` or `bump: major` front matter asks for more than a patch). The old text said to claim a version slot with `bin/gstack-next-version`. +- docs/CHANGELOG_STYLE.md, which CLAUDE.md says to read before writing an entry, now describes the same fragment flow: what goes in the fragment, when to add `bump:` front matter, and what to check after merging main. It no longer tells a branch to bump VERSION or claim a slot with `bin/gstack-next-version`. +- docs/ship-policy.md marks the queue-side procedure as being for repos on `stamp-at-merge` without changelog fragments, and points gstack's own PRs to the fragment flow and `train land`. The old link to that section still works. + ## [1.93.19.0] - 2026-11-02 **`gstack-ship-receipt read --check-prose` no longer reads the Documentation exception line or code fences as counter claims (GST-983, GST-984).** `write --docs` with an owner exception requires the PR body to carry the whole `Documentation exception: …` line word for word, but the prose check read the owner's reason in it as prose, so a reason like "sections 1 and 2 skip the audit" was a `2 skip` claim and no body could pass both checks. That is the deadlock GST-977 fixed for the Review-loop exception line. Separately, on gstack-evals #142, test output pasted in a fenced block ("113 passed, 0 failed") and a quoted code comment ("the first 35 fail") were read as receipt counts and refused. diff --git a/CLAUDE.md b/CLAUDE.md index f2a630b7b..67810a17d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -374,18 +374,22 @@ is not a reason to choose MINOR. Choose **MINOR (X.Y+1.0.0)** or **MAJOR ("ridiculous"), such as an incompatible public-interface change or a genuinely new product-scale release. Make that judgment without another approval question. -Use `bin/gstack-next-version` to check the live release queue before publishing. -If a slot is claimed, advance to the next available version at the chosen bump -level and use `bin/gstack-version-bump` to synchronize release metadata. A higher -base version does not itself require a MINOR bump. Keep the PR ready for Garry to -merge; autonomous version decisions do not authorize merging, deploying or -skipping required validation. +gstack's policy sets `changelog_fragments` (#3654), so a branch never writes a +version and never checks `bin/gstack-next-version` for a slot. The merge +train's stamp (`train land`) assigns base + PATCH at merge. To ask for more, +open the branch's fragment with `bump: minor` (or `major`) front matter. A +higher base version does not itself require a MINOR bump. Keep the PR READY for +the merge train; autonomous version decisions do not authorize merging, +deploying or skipping required validation. -**VERSION and CHANGELOG are branch-scoped.** Each shipping branch gets its own -bump and one entry, written at `/ship` time, that describes what THIS branch -adds versus main, for users, with no branch-internal versions or development -narrative. VERSION stays 4-digit; package.json carries the 3-digit npm -translation owned by `bin/gstack-version-bump`, so never hand-edit it. Read +**The CHANGELOG entry is a branch fragment.** Each shipping branch writes one +entry at `/ship` time as `changelog.d/.md` (entry text only, no +`## [x]` heading) that describes what THIS branch adds versus main, for users, +with no branch-internal versions or development narrative. A branch never edits +VERSION, package.json or a released CHANGELOG.md section: the merge stamp folds +the fragment into CHANGELOG.md under its version, keeps VERSION 4-digit and +writes package.json's 3-digit npm translation through `bin/gstack-version-bump` +([changelog fragments](docs/ship-policy.md#changelog-fragments)). Read [docs/CHANGELOG_STYLE.md](docs/CHANGELOG_STYLE.md) BEFORE writing an entry: it holds the versioning invariant, the merge-main checks, what stays out and the entry format. Always credit community contributions with diff --git a/VERSION b/VERSION index 9bc176bfb..500ba75b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.93.19.0 +1.93.20.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index fe1fc15d2..ca19197ea 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.93.19.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.93.20.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-factory-scorecard b/bin/gstack-factory-scorecard index 76793c70c..1195406bd 100755 --- a/bin/gstack-factory-scorecard +++ b/bin/gstack-factory-scorecard @@ -11,7 +11,7 @@ * gstack-factory-scorecard [--repo owner/name] [--base main] [--since-days 7] * [--crew login,login] [--gate-workflow evals.yml] [--gate-event workflow_dispatch|any] * [--free-workflow free-tests.yml] [--train-prefix train/] [--ready-marker ] - * [--pass-marker ] [--pass-cap 3] [--max-artifact-runs 60] + * [--pass-marker ] [--pass-cap 2] [--max-artifact-runs 60] * [--out-dir ] [--csv ] [--json] [--cache-dir ] * [--fixtures ] [--now ] * diff --git a/docs/CHANGELOG_STYLE.md b/docs/CHANGELOG_STYLE.md index 4a2194b7f..ce6723fe8 100644 --- a/docs/CHANGELOG_STYLE.md +++ b/docs/CHANGELOG_STYLE.md @@ -1,7 +1,9 @@ # CHANGELOG + VERSION style Moved verbatim from CLAUDE.md (token-load reduction). Read this BEFORE -writing any `## [X.Y.Z]` CHANGELOG entry. +writing a CHANGELOG entry. In gstack the entry is a fragment, +`changelog.d/.md`, and the merge step gives it its `## [X.Y.Z.W]` +heading ([changelog fragments](ship-policy.md#changelog-fragments)). ## Versioning and branch-scoped entries @@ -9,15 +11,15 @@ Moved verbatim from CLAUDE.md (#2096 size limit). The autonomous version policy (choose versions without asking; default to PATCH) stays in CLAUDE.md as the single source. -**Versioning invariant (workspace-aware ship).** VERSION is a monotonic ordered -release identifier, not a strict semver commitment. The bump level -(major/minor/patch/micro) expresses intent at ship time. Queue-advancing past a -claimed version within the same bump level is explicitly permitted — if branch A -claims v1.7.0.0 as a MINOR and branch B is also a MINOR, B lands at v1.8.0.0 -(still a MINOR relative to main). Downstream consumers must NOT rely on -"MINOR = feature-only, PATCH = fix-only" as a strict contract. This is why -`bin/gstack-next-version` advances within the chosen bump level rather than -repicking the level when collisions happen. +**Versioning invariant.** VERSION is a monotonic ordered release identifier, +not a strict semver commitment. The bump level (major/minor/patch/micro) +expresses intent. Versions are assigned at merge, in merge order: each stamp +takes the version before it plus the effective bump, which is the policy's +PATCH raised by any folded fragment's `bump:` front matter. If PR A and PR B +both ask for MINOR and land back to back on v1.6.0.0, A lands at v1.7.0.0 and +B at v1.8.0.0 (still a MINOR relative to the version before it). Downstream +consumers must NOT rely on "MINOR = feature-only, PATCH = fix-only" as a +strict contract. **package.json carries the npm-valid translation, not VERSION verbatim.** VERSION stays the 4-digit source of truth (e.g. `1.67.0.0`); package.json and @@ -26,18 +28,25 @@ translation (`1.67.0`), and lockfile `version` fields sync only when the lockfile already exists. `bin/gstack-version-bump` (via `lib/version-source.ts`) owns the translation and judges drift on translated forms — do NOT "fix" the apparent mismatch by hand, and do not write a 4-digit version into -package.json (npm rejects it). Rationale and translation rules live in the +package.json (npm rejects it). A branch never writes either one: the merge +stamp does, through `bin/gstack-version-bump`. Rationale and translation rules live in the `lib/version-source.ts` header; `test/gstack-version-bump.test.ts` pins the contract. -**VERSION and CHANGELOG are branch-scoped.** Every feature branch that ships gets its -own version bump and CHANGELOG entry. The entry describes what THIS branch adds — -not what was already on main. +**The CHANGELOG entry is branch-scoped; the version is not.** Every feature +branch that ships writes one entry as `changelog.d/.md`: the entry +text only, with no `## [x]` heading, and `bump: minor` or `bump: major` front +matter only when the change warrants more than a PATCH. A branch never edits +VERSION, package.json's version or a released CHANGELOG.md section. At merge, +`train land` (or `gstack-ship-queue land`) assigns the version, folds the +fragment into one `## [] - ` section at the top of CHANGELOG.md +and deletes the file. The entry describes what THIS branch adds — not what was +already on main. **The CHANGELOG entry is the diff between main and the shipping branch — what users get when they upgrade. NOT how the branch got there.** A reader landing on the entry should learn what they can do now that they couldn't before; they should not learn -about the branch's internal version bumps, the bugs we caught and fixed mid-branch, +about versions the branch carried during development, the bugs we caught and fixed mid-branch, the plan reviews we ran, or the commits we squashed. That is branch development narrative. It belongs in PR descriptions and commit messages, not CHANGELOG. @@ -62,29 +71,28 @@ the user gets; the path to that system is invisible to them. **When to write the CHANGELOG entry:** - At `/ship` time (Step 13), not during development or mid-branch. - The entry covers ALL commits on this branch vs the base branch. -- `/ship` lints the lines the branch added with `bin/gstack-prose-lint CHANGELOG.md --base origin/ --gate`, - and the Quality gate workflow gates the `## [Unreleased]` block the same way. Rewrite each `PROSE:` line - it names ([docs/troubleshooting.md](troubleshooting.md#prose-lint-found)). -- Never fold new work into an existing CHANGELOG entry from a prior version that - already landed on main. If main has v0.10.0.0 and your branch adds features, - bump to v0.10.1.0 with a new entry — don't edit the v0.10.0.0 entry. +- `/ship` lints the fragment with `bin/gstack-prose-lint changelog.d/.md --gate`. + Rewrite each `PROSE:` line it names ([docs/troubleshooting.md](troubleshooting.md#prose-lint-found)). +- Never fold new work into a CHANGELOG.md section that already landed on main. + New work goes in this branch's fragment, and the merge stamp gives it its own + section at the next version. **Key questions before writing:** 1. What branch am I on? What did THIS branch change? -2. Is the base branch version already released? (If yes, bump and create new entry.) -3. Does an existing entry on this branch already cover earlier work? (If yes, replace - it with one unified entry for the final version.) +2. Does this change warrant more than a PATCH? (If yes, open the fragment with + `bump: minor` or `bump: major` front matter; otherwise add none.) +3. Does this branch's fragment already cover earlier work? (If yes, rewrite it as + one unified entry.) -**Merging main does NOT mean adopting main's version.** When you merge origin/main into -a feature branch, main may bring new CHANGELOG entries and a higher VERSION. Your branch -still needs its OWN version bump on top. If main is at v0.13.8.0 and your branch adds -features, bump to v0.13.9.0 with a new entry. Never jam your changes into an entry that -already landed on main. Your entry goes on top because your branch lands next. +**Merging main never gives your branch a version.** When you merge origin/main into +a feature branch, main may bring new CHANGELOG sections and a higher VERSION. Take them +exactly as main has them. Your branch still carries only its fragment, and never jams its +changes into a section that already landed on main; the merge stamp puts its section on top. **After merging main, always check:** -- Does CHANGELOG have your branch's own entry separate from main's entries? -- Is VERSION higher than main's VERSION? -- Is your entry the topmost entry in CHANGELOG (above main's latest)? +- Is your fragment still in `changelog.d/`, with its own text, separate from main's sections? +- Are VERSION, package.json's version and every released CHANGELOG.md section exactly + main's? (The Version Gate check requires this of a head the merge step did not stamp.) If any answer is no, fix it before continuing. **After any CHANGELOG edit that moves, adds, or removes entries,** immediately run @@ -93,13 +101,9 @@ order. Gaps between version numbers are fine. A branch that ships at v1.6.4.0 wi a prior v1.5.2.0 or v1.5.3.0 entry on main is correct — those were branch-internal version numbers that never landed. Do not back-fill gaps with placeholder entries. -**Never orphan branch-internal versions.** If your branch bumped VERSION several times -during development (v1.5.1.0 → v1.5.2.0 → v1.6.4.0, say) and those earlier entries were -never released to main, the final ship consolidates ALL of them into a single entry at -the final version (v1.6.4.0). Collapse them — delete the old entries and move their -content into the final entry, re-version table columns accordingly. Readers see one -release, not a branch diary. Gaps are fine (v1.6.3.0 → v1.6.4.0 with no v1.5.x -in between on main is correct). +**Keep one entry per branch.** When the branch's work grows after its fragment was +written, rewrite that fragment as one unified entry instead of adding a second file +beside it. Readers see one release, not a branch diary. CHANGELOG.md is **for users**, not contributors. Write it like product release notes: @@ -122,9 +126,9 @@ we got here. Keep out of the CHANGELOG, always: documents what DID ship, not what MIGHT ship. - Version-bump housekeeping when no user-facing work actually landed. -If the diff between the base branch version and this version has no user-facing change -(only merges, only CHANGELOG edits, only placeholder work), the honest entry is one -sentence: "Version bump for branch-ahead discipline. No user-facing changes yet." Stop +If the diff between the base branch and this branch has no user-facing change +(only merges, only CHANGELOG edits, only placeholder work), the honest fragment is one +sentence: "No user-facing changes yet." Stop there. Do not pad. Do not explain the plan that will ship eventually. Do not narrate the branch's history. When real work lands, the entry will replace this at /ship time. diff --git a/docs/recommended-harness.md b/docs/recommended-harness.md index 6da4b14c2..467b5a1c0 100644 --- a/docs/recommended-harness.md +++ b/docs/recommended-harness.md @@ -263,7 +263,7 @@ line whose input could not be read says `unavailable ()` rather than 0. gstack-factory-scorecard --repo owner/name --since-days 7 \ [--crew login,login] [--gate-workflow evals.yml] [--free-workflow free-tests.yml] \ [--train-prefix train/] [--ready-marker 'READY #\d+|SHIP_RECEIPT:'] [--pass-marker '"passes": (\d+)|passes=(\d+)'] \ - [--pass-cap 3] [--out-dir .context/factory-scorecard] [--now ] + [--pass-cap 2] [--out-dir .context/factory-scorecard] [--now ] ``` `--repo` defaults to the origin remote's owner/name on github.com (or @@ -280,7 +280,7 @@ measured later does not count anything that happened after that week. | Merged PRs | `merged_at` in the window, base = default branch; crew by `--crew` logins, else author association OWNER/MEMBER/COLLABORATOR | several merges a day, every day; outside contributions land in the same week | | Lead time, created → merged | PR `created_at` to `merged_at`, median and p90 | median under 24h, p90 under 3 days (DORA "elite" change lead time is under a day) | | Lead time, READY → merged | first PR comment matching `--ready-marker` at or before `merged_at`; PRs with no such comment are left out and counted | under 2h; every merged PR carries a READY comment | -| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs sat at `--pass-cap` (3: pass 1 reviews the whole diff, later passes only the delta, a pass that finds nothing new is the exit) | median 2 passes; none at the cap | +| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs reached `--pass-cap` (2, the [review policy](review-policy.md)'s cap of two rounds, a fix-check included: pass 1 reviews the whole diff, pass 2 only the range since, and a pass that finds nothing blocking is the exit) | median 1 pass; few at the cap | | Gate runs green / red / cancelled | completed runs of the gate workflow (`workflow_dispatch` by default; `--gate-event any`); cost from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment | green on 95%+ of runs, cancelled near 0, cost known for every run | | Known gate cost per merged PR | known cost / merged PRs; unknown-cost runs make it a floor | known, stable or falling week over week | | Red-train rate | runs whose head branch starts with `--train-prefix`, cancelled excluded; red = conclusion other than success; red backstops and screens shown beside it | under 5% (each red train is a bisect plus a rebuild, about two gates) | diff --git a/docs/ship-policy.md b/docs/ship-policy.md index 9d013a084..7fd896a28 100644 --- a/docs/ship-policy.md +++ b/docs/ship-policy.md @@ -112,10 +112,16 @@ refuses the file by name (`POLICY_GSTACK_TOO_OLD`). `.gstack/ship-policy.example.json` shows every key set the way a serial-merge repo with a release script, docs mentions and a mirror would set them. -### Queue-side procedure for gstack + +### Queue-side procedure under `stamp-at-merge` without fragments -Who does what under `stamp-at-merge`, for the PR author and for the thread -running the merge queue: +gstack itself no longer follows these steps. Since #3654 (v1.91.342.0) its +policy sets [`changelog_fragments`](#changelog-fragments), so a gstack PR adds +a fragment and never a version, and trains merge through +[`train land`](#train-land). The steps +below are for a repo on `stamp-at-merge` whose policy has no +`changelog_fragments`. Who does what there, for the PR author and for the +thread running the merge queue: 1. **Open.** The branch carries a tentative stamp of the base's version plus PATCH (`gstack-version-bump write --version --regen-digest`) diff --git a/lib/factory-scorecard.ts b/lib/factory-scorecard.ts index ebaac4c48..9be8ce50d 100644 --- a/lib/factory-scorecard.ts +++ b/lib/factory-scorecard.ts @@ -70,7 +70,7 @@ export interface ScorecardOptions { export const DEFAULTS = { base: 'main', sinceDays: 7, gateWorkflow: 'evals.yml', gateEvent: 'workflow_dispatch', freeWorkflow: 'free-tests.yml', trainPrefix: 'train/', readyMarker: 'READY #\\d+|SHIP_RECEIPT:|gstack-ship-receipt', - passMarker: '"passes"\\s*:\\s*(\\d+)|\\bpasses=(\\d+)|\\bREVIEW_PASS(?:ES)?[:= ]\\s*(\\d+)', passCap: 3, maxArtifactRuns: 60, + passMarker: '"passes"\\s*:\\s*(\\d+)|\\bpasses=(\\d+)|\\bREVIEW_PASS(?:ES)?[:= ]\\s*(\\d+)', passCap: 2, maxArtifactRuns: 60, } as const; export const CREW_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); @@ -399,7 +399,7 @@ export const BENCHMARKS: Record = { merged: 'several merges a day, every day; outside contributions land in the same week', lead_time: 'median under 24h, p90 under 3 days (DORA elite: change lead time under a day)', ready_to_merge: 'under 2h from READY to merge; every merged PR carries a READY comment', - review_passes: 'median 2 passes, none at the cap (a pass that finds nothing new is the exit; what is left at the cap is named in the PR body)', + review_passes: 'median 1 pass, few at the cap (a pass that finds nothing blocking is the exit; a PR at the cap ships with what is left named in the PR body)', gates: 'green on 95%+ of runs, cancelled near 0, cost known for every run', cost_per_merge: 'known, stable or falling week over week', red_train: 'under 5% of trains red (each red train costs a bisect and a rebuild)', diff --git a/package.json b/package.json index e8c077ccf..0ae64ebdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.93.19", + "version": "1.93.20", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/factory-scorecard.test.ts b/test/factory-scorecard.test.ts index 3794a333e..bb26faac3 100644 --- a/test/factory-scorecard.test.ts +++ b/test/factory-scorecard.test.ts @@ -59,7 +59,7 @@ describe('factory-scorecard: recorded window', () => { expect(values.merged).toBe('3 (crew 2, outside 1)'); expect(values.lead_time).toBe('median 8.0h, p90 5.0d'); expect(values.ready_to_merge).toBe('median 1.0h, p90 2.0h (2 of 3 PRs with a READY comment)'); - expect(values.review_passes).toBe('median 2, p90 3 (2 of 3 PRs report a pass count; 1 at the cap of 3)'); + expect(values.review_passes).toBe('median 2, p90 3 (2 of 3 PRs report a pass count; 2 at the cap of 2)'); expect(values.gates).toBe('2 / 1 / 1; known cost $43.00 on 2 of 4 runs'); expect(values.cost_per_merge).toBe('$14.33 (2 runs without a known cost)'); expect(values.red_train).toBe('50% (1 red of 2 trains; 0 red backstop, 0 red screens)');