feat(restamp): the repo's stamp rule (stamp_files, literal stamp_patterns) and restamp_command in the staging worktree

stamp_files is an allowlist applied before every stamp rule; stamp_patterns
are bounded literal {version} templates shared by the restamp writer and the
tree receipt. restamp_command runs first in the staging worktree with the
target version (argument and GSTACK_RESTAMP_VERSION), needs a clean caller
tree, skips gstack's version writer and keeps output confinement; a failure
or a wrong version writes nothing. migrations { dir, id } is the policy key
gstack-pregate regate reads.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-13 01:03:26 +00:00
1 parent 05e6f2e828
commit c47fd042d9
6 files changed
+298 -26

No files matched your search

+31 -3
View File
@@ -43,6 +43,10 @@ prints this table. JSON carries no comments, so examples live in
| `stamp_sensitive_gates` | evidence labels | `[]` | Gates that read VERSION or generated bytes (gstack's own `tests` lane does, through the agents-digest freshness test). The tree receipt never marks them reusable. |
| `pr_count` | `{ default_pr_count, split_requires_owner }` | `{1, true}` | The PR-count row in plan reviews. |
| `maintainers` | GitHub logins | `[]` | Maintainer set for `/review` contributor mode; empty means repository push permission. |
| `stamp_files` | globs | `[]` | The repo's stamp rule: when set, only these files may differ by a stamp (list the version file, manifests, plugin trees and the files holding the PR's own `since:` rows). It applies before every other stamp rule, so a doc `vX` mention outside it is a difference. |
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, `<!-- x-stamp: {version} -->`); matched literally at a version boundary, never as a regex. The restamp writer and the tree receipt use them beside the built-in forms. |
| `restamp_command` | command or `null` | `null` | The repo's own restamp, run first in the staging worktree; see [restamp_command](#restamp-command). |
| `migrations` | `{ dir, id }` or `null` | `null` | The migration directory and a regex with exactly one capture group for the number in a file name (`"^v(\\d+)-"`); `gstack-pregate regate` reports `MIGRATION_COLLISION`. |
Containment, checked by `validate` and again at write time: paths and globs are
repo-relative with no `..` segment, under 256 characters, at most 64 per list,
@@ -161,10 +165,11 @@ or the order moved, the result is `QUEUE_STALE` (exit 3) with the exact
Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles).
0. **restamp_command** (only when the policy sets it): the repo's own restamp, see [restamp_command](#restamp-command).
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version.
2. **release_tool**: the policy command, under the contract below.
3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under `<X>`, above its predecessor's entry. Duplicate headings (`CHANGELOG_DUPLICATE_HEADING`) and a top entry that is not `<X>` (`CHANGELOG_TOP_MISMATCH`) are errors.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites within the declared globs. `X` is the branch's own tentative stamp (its VERSION when it moved past base), else `--was <X>`, else the version an unstamped branch would have taken next (`base + bump`); mentions of a version the base never released (not a CHANGELOG heading at base, not a tag) are rewritten too, so a queue reorder that moves the base under a branch still finds its `since:` fields. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. `X` is the branch's own tentative stamp (its VERSION when it moved past base), else `--was <X>`, else the version an unstamped branch would have taken next (`base + bump`); mentions of a version the base never released (not a CHANGELOG heading at base, not a tag) are rewritten too, so a queue reorder that moves the base under a branch still finds its `since:` fields. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
5. **mirror**: tier 2; declared and validated, reported as not run.
6. **apply**: every changed file is written to the branch only after all stages validated, each against the fingerprinted preimage; a file edited meanwhile is `RESTAMP_CONFLICT` and the applied writes roll back (rollback refuses to overwrite a file whose bytes differ from the recorded postimage).
@@ -199,6 +204,26 @@ implementation (`bun run release:restamp` still runs the PR's package script),
so the receipt names the policy revision, the command, its working directory
and the trust scope (`policy: base origin/main@<sha>` or `head (unreviewed)`).
<a id="restamp-command"></a>
### `restamp_command`: the repo owns the stamp
A repo whose own tool knows its stamp locations sets `restamp_command`.
`gstack-restamp` runs it as the first stage inside its staging worktree, a
clean checkout of HEAD, with the allocated version as the last argument and in
`GSTACK_RESTAMP_VERSION`. The caller's HEAD, index and worktree must be clean
first (`RESTAMP_CONFLICT`, nothing written); the generic path keeps its
working-tree fingerprint seed. Anything the command stages or commits in the
staging worktree is folded back into plain edits; then output confinement
applies as for the release tool (`RESTAMP_OUTPUT_OUTSIDE_ALLOWED`), the
version file must hold the target version (`RESTAMP_VERSION_SOURCE`), and a
non-zero exit is `RESTAMP_RELEASE_TOOL_FAILED`. In every failure nothing
reaches the branch. gstack's version writer is skipped; the CHANGELOG and
`stamp_paths` stages still run (a no-op when the tool already did the work).
Read from the base branch and run only with `--allow-repo-commands`, like
`release_tool`. A tool that fetches, merges the base branch or needs a version
of its own choosing cannot run here; give it a mode that takes the version and
writes only stamps.
<a id="tree-receipt"></a>
## The tree receipt
@@ -225,7 +250,10 @@ the CHANGELOG (a heading move or re-heading, compared as an order-independent
multiset of entries), `stamp_paths` and `release_outputs` (every
removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite with
X the gated version and the replacement the current one, so a dependency pin
that happens to equal the old version is `changed`). Any other byte is a change.
that happens to equal the old version is `changed`; `stamp_patterns`
templates count as stamp forms too). With `stamp_files` set, a file outside it
is `changed (stamp_files: not in the repo's stamp_files allowlist)` whatever
its diff. Any other byte is a change.
Equivalence is decided per gate. A lane in `stamp_sensitive_gates` always
`rerun`s; a verified lane is `reusable` only when the tree is same-modulo-stamps