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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gstack/ship-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"queue_mode": "stamp-at-merge",
"release_tool": "bun scripts/gen-agents-digest.ts",
"release_outputs": ["agents-digest/gstack-AGENTS.md"],
"stamp_paths": [],
"stamp_paths": ["lib/ship-policy.ts"],
"stamp_patterns": ["_KEY_VERSION = '{version}'"],
"changelog": "CHANGELOG.md",
"mirror": null,
"history": "squash-ok",
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [1.91.275.0] - 2026-10-27

### Changed
- **Policy key versions are stamped, not hand-moved** (`lib/ship-policy.ts`, `.gstack/ship-policy.json`): `DOCS_CAPS_KEY_VERSION` is 1.91.270.0, the release that shipped `docs.caps` (#3487 was written naming 1.91.258.0, which main stamped for another PR; a slot call never permits an edit beyond the four stamp files, so the constant could not move at the slot). gstack's own policy now lists `lib/ship-policy.ts` under `stamp_paths` with the `_KEY_VERSION = '{version}'` template in `stamp_patterns`: a new key's constant is written as a tentative future version clear of main's pace (the next minor) and `gstack-restamp`'s stamp_paths stage moves it to the allocated version at the slot stamp, reading the base branch's policy, so the stage is live for the PRs after this one; the tree receipt's stamp-only check still accepts that move only when the tentative value equals the base version at the slot, so a PR that adds a key cannot clear the train until #3512 lands. `test/gstack-ship-policy.test.ts` adds the check the incident lacked: a released key's version must be a CHANGELOG heading whose entry names the key, so a tentative value no stamp moved fails (keys from before `review_scope` are exempt).

## [1.91.274.0] - 2026-10-27

**/design-html and /design-review now run the design lint, and its copy tier reports every content and ship-floor tell at its line (hallmark phase 2, GSTA-92; Garry 2026-10-26 "Do it", option a).** The two design skills still called `gstack-design-craft`, which counted tells without positions and kept accepted exceptions in finalized.json. The lint already carried the same ids, so the skills now call it, the copy tier has its own patterns with line numbers, and a page answers a tell with a waiver comment beside it instead of a side record.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.91.274.0
1.91.275.0
2 changes: 1 addition & 1 deletion agents-digest/gstack-AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# gstack digest v1.91.274.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.275.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,
Expand Down
3 changes: 2 additions & 1 deletion docs/ship-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ hard-code:
"queue_mode": "stamp-at-merge",
"release_tool": "bun scripts/gen-agents-digest.ts",
"release_outputs": ["agents-digest/gstack-AGENTS.md"],
"stamp_paths": [],
"stamp_paths": ["lib/ship-policy.ts"],
"stamp_patterns": ["_KEY_VERSION = '{version}'"],
"changelog": "CHANGELOG.md",
"history": "squash-ok",
"stamp_sensitive_gates": ["tests"]
Expand Down
18 changes: 14 additions & 4 deletions lib/ship-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,21 @@ export const SHIP_QUEUE_KEYS_VERSION = '1.91.108.0';
export const TRAIN_KEYS_VERSION = '1.91.146.0';
// Each constant is the release that first reads its key: a policy declaring the key must name at least
// that version as min_gstack_version, so an older gstack refuses the policy by name instead of failing on
// an unknown key. review_scope shipped in 1.91.256.0 (#3481). docs.caps is unreleased: its constant names the
// release expected to stamp it (test/gstack-ship-policy.test.ts holds it newer than every CHANGELOG heading the
// checkout sees); whoever stamps that release under another number moves the constant to the stamped version.
// an unknown key. review_scope shipped in 1.91.256.0 (#3481), docs.caps in 1.91.270.0 (#3487). A new key's
// constant is written as a tentative future version clear of main's pace (the next minor, never the next patch,
// which main takes first and the released-key test below then reds at the pin), in this exact
// `_KEY_VERSION = '<version>'` form: this file is in .gstack/ship-policy.json stamp_paths with that form as a
// stamp_patterns template, so the slot stamp's gstack-restamp stamp_paths stage moves the tentative value to the
// allocated version. The stage reads lines the branch added: a tentative value newer than the fork version moves,
// an untouched released constant stays, and a branch-added line naming the base's own version is re-read as a
// stale stamp and moves too, so never rewrite a released constant's line. Until the tree receipt accepts a
// stamp-form move whose old value is not the base version (#3512), the train's stamp-only check passes such a
// move only when the tentative value equals the base version at the slot, which a next-minor value never does:
// a PR that adds a key cannot clear the train until #3512 lands. test/gstack-ship-policy.test.ts holds
// an unreleased constant newer than every CHANGELOG heading and a released one on a heading whose entry names
// its key, so a value a stamp did not move fails there.
export const REVIEW_SCOPE_KEY_VERSION = '1.91.256.0';
export const DOCS_CAPS_KEY_VERSION = '1.91.258.0';
export const DOCS_CAPS_KEY_VERSION = '1.91.270.0';
export const POLICY_KEY_INTRODUCED: Record<PolicyKey, string> = {
bump: '1.91.73.0', queue_mode: '1.91.73.0', release_tool: '1.91.73.0', release_outputs: '1.91.73.0', stamp_paths: '1.91.73.0',
changelog: '1.91.73.0', mirror: '1.91.73.0', history: '1.91.73.0', preregistration_shas: '1.91.73.0', stamp_sensitive_gates: '1.91.73.0',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.274",
"version": "1.91.275",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
Expand Down
28 changes: 26 additions & 2 deletions test/gstack-ship-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ describe('validatePolicy (schema + containment, pure)', () => {

describe('policy key versions against the CHANGELOG', () => {
test('an unreleased key names a release newer than every heading the checkout can see', () => {
// The base branch's CHANGELOG counts when the checkout has it (a depth-1 CI clone does not): a stamp-at-merge
// The base branch's CHANGELOG counts when the checkout has it (a depth-1 CI clone does not; the file is past 1 MiB,
// so spawnSync needs the explicit maxBuffer or it drops the read silently): a stamp-at-merge
// branch carries main's headings only after the slot merge, and a release stamped under another number
// leaves the constant naming a release that never read the key, which the stamper moves by hand.
const root = path.join(import.meta.dir, '..');
const base = spawnSync('git', ['show', 'origin/main:CHANGELOG.md'], { cwd: root, encoding: 'utf8', timeout: 30_000 });
const base = spawnSync('git', ['show', 'origin/main:CHANGELOG.md'], { cwd: root, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024 });
const text = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8') + (base.status === 0 ? base.stdout : '');
const parse = (v: string) => v.split('.').map(Number);
const cmp = (a: number[], b: number[]) => a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]! || a[3]! - b[3]!;
Expand All @@ -91,6 +92,29 @@ describe('policy key versions against the CHANGELOG', () => {
expect(cmp(parse(version), newest), `${key} is introduced in ${version}, which has no CHANGELOG heading and is not newer than the newest heading ${newest.join('.')}; an unreleased key names the release that will stamp it`).toBeGreaterThan(0);
}
});

test('a released key names the heading whose entry introduces it, so a tentative version no stamp moved fails', () => {
// #3487 was written naming the next version and main stamped that number for another PR first, so the constant
// pointed at a release that never read `docs` while the heading existed and the test above skipped it. A key version that is
// a heading must be the one whose entry names the key (`\`<key>` at a word boundary in the section body, so `\`docs/` paths do not count); lib/ship-policy.ts sits in
// stamp_paths so the slot stamp moves a tentative value there itself. Keys from before the convention are exempt.
const root = path.join(import.meta.dir, '..');
const base = spawnSync('git', ['show', 'origin/main:CHANGELOG.md'], { cwd: root, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024 });
const text = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8') + (base.status === 0 ? base.stdout : '');
const sections = new Map<string, string>();
for (const part of text.split(/^(?=## \[)/m)) {
const heading = /^## \[(\d+\.\d+\.\d+\.\d+)\]/.exec(part);
if (heading && !sections.has(heading[1]!)) sections.set(heading[1]!, part);
}
const parse = (v: string) => v.split('.').map(Number);
const cmp = (a: number[], b: number[]) => a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]! || a[3]! - b[3]!;
for (const [key, version] of Object.entries(POLICY_KEY_INTRODUCED)) {
if (cmp(parse(version), parse(REVIEW_SCOPE_KEY_VERSION)) < 0) continue;
const section = sections.get(version);
if (section === undefined) continue;
expect(new RegExp('`' + key + '(?![A-Za-z0-9_/-])').test(section), `${key} is introduced in ${version}, but that release's CHANGELOG entry never names \`${key}: the constant still holds the tentative version the branch was written with; the slot stamp moves it through stamp_paths, or move it to the release that shipped the key`).toBe(true);
}
});
});

describe("gstack's own .gstack/ship-policy.json", () => {
Expand Down
Loading