mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 23:14:33 +01:00
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(restamp): gate-ahead keeps the lane CHANGELOG entry when both PRs took the same tentative stamp (GBRA-102 diagnosis fixture) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit43c75a6bb6) * test(tree-receipt): a real gbrain stamp pair is same-modulo-stamps under gbrain committed policy (W1 acceptance) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit6cf53f9fed) * feat(ship-policy): gate_command, full_e2e and min_gstack_version; unknown keys stay fail-closed; validate prints introducing versions Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit878a7123d7) * feat(ship-queue): gstack-ship-queue ready, stamp, verify and gate-ahead (WIP, tests next) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit409a5957ad) * test(ship-queue): stamp journal and resume, verify from a separate clone, gate-ahead attempts, ready Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitade554087b) * docs(ship-policy): Run a shared merge queue; new keys and compatibility rule; troubleshooting anchors; /ship pointer Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7fc73e8892) * fix(ship): shared-queue pointer rides the show-effective ship: line (ship skeleton has 2 bytes of parity headroom) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit5b33d4a015) * test(windows): curate the shared-queue tests out of the Windows subset (shebang spawns) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit8deee14728) * test(fixtures): register the gbrain stamp-pair fixture as free-only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit563a07aa03) * test(ship-queue): receipt digest contract names lib/ship-queue.ts for its pregate lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit501637c258) * feat(pregate): timing check flags elapsed-time bounds and sleep-then-assert in added test lines (W4) lib/pregate/checks/timing.ts prints TIMING_ASSERTION: <file>:<line> <pattern> and a TIMING_ASSERTIONS summary; gstack-timing-ok: <reason> in any comment syntax on the line or the line above escapes one. It warns (PREGATE_TIMING_ASSERTION): measured on gstack (9 of 47 test-touching commits, 263 hits in the tree) and gbrain (37 of 95 commits; 31 of 38 sites of the five timing-flake fixes), blocking would stop a fifth to two fifths of test changes. Adds the /review checklist rule and the /investigate timing-flake playbook. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita3b86b10b2) * feat(update-check): an equal VERSION with a moved main is an upgrade, via the compare API (W12) When VERSION matches but ls-remote's main SHA differs from this checkout's HEAD, bin/gstack-update-check asks the GitHub compare API (status, ahead_by) through the existing receipted curl and prints UPGRADE_AVAILABLE <ver> <ver>+<n> <sha>; a diverged or unreadable compare is CHECK_FAILED (unknown), and behind/identical is up to date. Never fetches. The cache gains an IDENTITY line (head, compared remote SHA, upstream): an outcome replays only for the same HEAD and upstream, a same-remote refresh reuses it without another compare, and a git checkout ignores a legacy one-line cache (non-git installs keep the one-line format). The snooze is keyed by the SHA. gstack-upgrade's question, snooze note and What's New step handle the four-field line; gstack-skill-end reports it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit421e66d2a4) * fix(pregate): treat planning and evidence markdown as an empty lane A docs-only diff with no .gstack/pregate.json failed lanes as PREGATE_NO_LANE. Planning and evidence Markdown is now a docs/no-tests empty lane. Unmapped source files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commite8793e59e8) * fix(pregate): keep test-read markdown on its test lane Unmarked Markdown that no test depends on is a docs/no-tests empty lane. A file a test reads, including a fixture, still selects that test through the import graph. Unmapped source and unreferenced test-tree files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commit7aabbddf3d) * test(pregate): reviewer edges for the docs/no-tests empty lane (#3215) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2fb3fa8613) * test(pregate): requires-remote on a gbrain-shaped repo (ci:ubicloud, UBI_OWNER) (W10) A base pregate.json with gbrain's dependencies and remote_gate "bun run ci:ubicloud" (a package script whose stub needs UBICLOUD_API_KEY, names VMs from UBI_OWNER and prints PREGATE_REMOTE). Under 8 cores the test tier is one obligation naming the command; --remote passes the caller's UBI_OWNER and key through and clears it on the report; without the key the gate's own error shows and the obligation stays uncleared. #3190's path needed no change. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit4d69ff02df) * test(pregate): the ship receipt counts the timing check (7 checks) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitcb03ef67e7) * test(context-budget): gstack-upgrade eager ceiling for the W12 equal-version upgrade question (ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(approval-classes): gstack-approval-classes and ship-receipt citation enforcement (W2, WIP) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit847c7dae3d) * feat(approval-classes): tests, docs, /review and /ship wiring (W2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit269bd5b8b5) * feat(review): state-machines specialist and gstack-state-matrix round-trip check with a captured #6792 regression (W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1a634979c6) * fix(review-army): keep the --simplification force flag pinned; approval test timeouts Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit789d392bce) * test(free-fixtures): register the state-matrix-6792 fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1bacef296d) * test(state-matrix): cover state-machines NEVER_GATE in gstack-specialist-stats Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit859063d56f) * test(fixtures): register the pregate-timing fixtures as free-only (W4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-queue): full_e2e needs bound gate evidence in ready and verify, CI reads every check-run page and commit status, next: quotes its paths (review #3235 P1-1, P1-5, P2-1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): timing reads a native diff and never passes an unread one The timing check's git diff honored diff.external, textconv and diff.noprefix, so a configured driver hid every added line and the check passed. Run it with --no-ext-diff --no-textconv and explicit a/ b/ prefixes, and report a git diff that fails or does not finish as incomplete (PREGATE_INCOMPLETE), which a warn downgrade does not soften. Review of #3235, sibling of P1-4 (lib/pregate/checks/timing.ts:189). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only affirmative APPROVED evidence counts, explicit tokens are authoritative, native diffs fail closed (review #3235 P1-2, P1-3, P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(state-matrix): detect reads the native diff, ignoring diff.external and textconv (review #3235 P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(e2e): review-army-state-matrix behavior case on the #6792 fixture (field list 2, W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-army): tighten the State Machines dispatch line so /ship stays under its parity size ratio with main's #3231 review-army text Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only top-level approval lines count; quoted, fenced, indented-code and commented text never does (review #3235 r2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): approval evidence is the first non-blank line only, at column 0 (review #3235 r3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: keep gstack-upgrade under its size budget, exempt state-matrix's fetch hint, fit the periodic plan's 37 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the scope-signal clarity check follows helper-printed signals; the hermetic update-check fixture writes the identity cache Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor: move the release-path result codes to lib/result-codes-release.ts (module-size ratchet); fit the periodic plan's 38 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave B Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.116.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: copy lib/result-codes-release.ts wherever a fixture copies lib/result-codes.ts; the scope-signal check requires the helper's computed emission Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: state-matrix stats test resolves its slug under its own state root (home containment) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-docs-lane-review out of the Windows subset (runBin shebang, like the other pregate fixtures) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-remote-gbrain out of the Windows subset (runBin shebang, like pregate-remote) 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> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Nick <surlymo@apache.org>
579 lines
35 KiB
Markdown
579 lines
35 KiB
Markdown
# The repo ship policy: `.gstack/ship-policy.json`
|
||
|
||
`/ship` reads one machine-readable file per repository that says how releases
|
||
are stamped and merged: the bump level, the queue mode, the repo's own release
|
||
tool, the paths where version mentions are rewritten, the CHANGELOG, a paired
|
||
mirror, the history policy and the preregistered evals. Without the file
|
||
nothing changes: every key has a default that reproduces today's behavior.
|
||
|
||
The file is **read from the committed copy on the base branch**
|
||
(`origin/<base>`), never from the PR's working tree, so a PR cannot smuggle a
|
||
command into its own gate. **File presence never authorizes execution:**
|
||
`gstack-restamp` runs `release_tool` only under `/ship` or with
|
||
`--allow-repo-commands`; standalone it lists the commands and exits 3 with
|
||
`repo commands not executed (pass --allow-repo-commands)`. A PR that
|
||
introduces or changes the policy runs under the reviewed bootstrap path
|
||
(`--policy-from head --allow-repo-commands`) and its receipt says
|
||
`policy: head (unreviewed)`.
|
||
|
||
Tools: `gstack-ship-policy init|validate|show-effective|check-history`,
|
||
`gstack-restamp`, `gstack-tree-receipt`, `gstack-ship-receipt`,
|
||
`gstack-ship-queue` ([shared merge queue](#run-a-shared-merge-queue)),
|
||
`gstack-evidence bundle|ancestor`. They share the exit table (`0` ok, `1`
|
||
fail, `2` usage, `3` refused or needs a flag) and end every error line with a
|
||
code from [troubleshooting.md](troubleshooting.md#queue-aware-ship-policy-restamp-tree-receipt-ship-receipt).
|
||
|
||
<a id="schema-v1"></a>
|
||
## Schema v1
|
||
|
||
`gstack-ship-policy init` writes every key at its default; `init --explain`
|
||
prints this table. JSON carries no comments, so examples live in
|
||
`.gstack/ship-policy.example.json`. Unknown keys are errors, and there is no
|
||
extension mechanism: a key from a newer gstack, or a misspelled one, fails
|
||
closed (`POLICY_INVALID`) instead of being ignored. Every key has an
|
||
introducing version (`init --explain` and `validate` print it). A key newer
|
||
than 1.91.98.0 set to a non-default value requires `min_gstack_version` at
|
||
least at its introducing version, and a gstack older than `min_gstack_version`
|
||
refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
|
||
`MIN_READER: gstack <version>`, the floor every lane reading the policy needs.
|
||
|
||
| Key | Values | Default | Meaning |
|
||
|---|---|---|---|
|
||
| `bump` | `patch` \| `ask` | `ask` | `patch` skips the MINOR/MAJOR prompt in `/ship` Step 12 and `gstack-restamp --next` bumps PATCH; `ask` keeps the prompt. |
|
||
| `queue_mode` | `claim-at-open` \| `stamp-at-merge` | `claim-at-open` | See [stamp-at-merge](#stamp-at-merge). `claim-at-open` is today's queue (`gstack-next-version` claims a slot when the PR opens). |
|
||
| `release_tool` | command or `null` | `null` | The repo's own release command (`bun run release:restamp`). Runs in an isolated staging worktree under the [release-tool contract](#release-tool-contract). |
|
||
| `release_outputs` | globs | `[]` | Paths the release tool may write beyond the version files, lockfiles, CHANGELOG and `stamp_paths`. Anything else is `RESTAMP_OUTPUT_OUTSIDE_ALLOWED`. |
|
||
| `stamp_paths` | globs | `[]` | Where exact `(was X)`, `since: X` and `vX` mentions of the branch's tentative version are rewritten on a restamp. |
|
||
| `changelog` | path or `null` | `CHANGELOG.md` | Re-headed on a restamp: the PR's `## [Unreleased]` (or old-version) section moves to the top as `## [<version>] - <date>`. |
|
||
| `mirror` | relative path or `null` | `null` | A paired checkout to restamp identically (tier 2: validated and confined today, not yet run). |
|
||
| `history` | `merge-only` \| `squash-ok` | `squash-ok` | `merge-only` makes `/ship` refuse a squash or rebase plan; `gstack-ship-policy check-history` reports a rewritten branch (`HISTORY_POLICY_VIOLATION`). |
|
||
| `preregistration_shas` | commit ids | `[]` | Each must be an ancestor of HEAD before push: `gstack-evidence ancestor --from-policy`; the ship receipt records the result. |
|
||
| `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`. |
|
||
| `gate_command` | command or `null` | `null` | The repo's full pre-merge gate. `gstack-ship-queue gate-ahead` and `ready` print it for the caller to run (in the background or in CI); gstack never executes it. Needs `min_gstack_version`. |
|
||
| `full_e2e` | `true` \| `false` | `false` | `true`: every merge needs a passing full-gate result bound to a tree equal to the merging one modulo stamps; `gstack-ship-queue ready`, `stamp` and `verify` refuse without one (`QUEUE_GATE_MISSING`): a gate-ahead record, or the lane's `--gate-exit 0 --gate-log <file>` from its run on the gated head. Needs `min_gstack_version`. |
|
||
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
|
||
|
||
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,
|
||
and a glob's regex expansion under 512 characters; `release_tool` is under 512
|
||
characters with no newline; a matched file that resolves through a symlink to
|
||
outside the repository is refused (`POLICY_CONTAINMENT`). A mirror may start
|
||
with `..` (a sibling checkout) but has no interior `..`; mirror writes are
|
||
confined to that directory.
|
||
|
||
## gstack's own files
|
||
|
||
gstack's policy (`.gstack/ship-policy.json`, tracked beside `generated.json`
|
||
and `pregate.json` under the otherwise ignored `.gstack/`) runs the repo on
|
||
stamp-at-merge: the version is allocated once, when the PR is next in the queue,
|
||
every bump is PATCH, and `/review` reads the maintainer set from the file. It
|
||
names the one release tool `gstack-version-bump --regen-digest` used to
|
||
hard-code:
|
||
|
||
```json
|
||
{
|
||
"bump": "patch",
|
||
"queue_mode": "stamp-at-merge",
|
||
"release_tool": "bun scripts/gen-agents-digest.ts",
|
||
"release_outputs": ["agents-digest/gstack-AGENTS.md"],
|
||
"stamp_paths": [],
|
||
"changelog": "CHANGELOG.md",
|
||
"history": "squash-ok",
|
||
"stamp_sensitive_gates": ["tests"]
|
||
}
|
||
```
|
||
|
||
`.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
|
||
|
||
Who does what under `stamp-at-merge`, 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 <base+patch> --regen-digest`)
|
||
and its CHANGELOG section headed with that version directly under
|
||
`# Changelog`; `## [Unreleased]` is also accepted. Nothing is claimed and
|
||
`gstack-next-version` is not consulted; the tentative version is only the
|
||
guess that makes the common case free. CI runs on this head as usual.
|
||
2. **Register.** The queue records the PR and its intended order.
|
||
3. **Stamp when next.** With every check green on the current head and
|
||
`origin/main` at the SHA the queue assumed, the PR owner (never the queue)
|
||
runs on the branch:
|
||
`gstack-restamp --next --base main --allow-repo-commands --pr <n> --expect-base <main-sha> --expect-order <pr,...>`.
|
||
When the base has not moved past the tentative stamp it prints
|
||
`RESTAMP: X -> X (noop)` and `FILES: (none)`, and the green head is the
|
||
merge candidate with no further CI. When the base moved, it stamps base +
|
||
PATCH, re-heads the CHANGELOG above the base's top entry, reruns the
|
||
release tool, prints `STAMP_VERSION: <X>` and the stamp diff, and refuses
|
||
with `QUEUE_STALE` (nothing written) when the base or order moved after
|
||
`--expect-base` was read. The owner commits and pushes the stamp commit,
|
||
lets CI run on it, and reports `READY #<n> <sha> STAMP_VERSION=<X>`.
|
||
4. **Merge.** The queue verifies on that exact SHA: `VERSION` equals
|
||
`STAMP_VERSION`, the CHANGELOG's top heading is `[<X>]` and the heading
|
||
below it is the base's current version, every check is green on that SHA,
|
||
and, when a stamp commit was pushed, it changed only `VERSION`,
|
||
`package.json`, `CHANGELOG.md` and the release outputs
|
||
(`gstack-tree-receipt --gated <green-head> --current <sha>` prints the
|
||
per-file verdict). It then squash-merges with the merge API's expected-head
|
||
`sha` set to that SHA, so a late push is rejected by the server. "VERSION
|
||
above main" is replaced by "VERSION equals the restamp's STAMP_VERSION on
|
||
the SHA being merged, and that version is base + PATCH at merge time".
|
||
5. **Not next.** A PR waiting behind another may gate ahead locally with
|
||
`gstack-restamp --after <predecessor-pr>`; it pushes nothing. When the
|
||
predecessor lands, the waiting PR merges `origin/main` in (never a rebase),
|
||
keeping its own CHANGELOG section above the newly merged one, and step 3
|
||
runs.
|
||
|
||
Until this policy is on `origin/main`, `gstack-restamp --next` reads the base's
|
||
`bump: ask` and falls back to a MICRO bump; pass `--bump patch` on the first
|
||
PR stamped after the flip if its base predates it.
|
||
|
||
<a id="run-a-shared-merge-queue"></a>
|
||
## Run a shared merge queue
|
||
|
||
`gstack-ship-queue` strings `gstack-restamp`, `gstack-tree-receipt` and the
|
||
ship receipt into the coordinator protocol, so nobody proves a stamp diff by
|
||
hand. Lanes own their PRs and never merge; the coordinator owns the order and
|
||
every merge. Each step ends in a line the other side acts on, then
|
||
`GSTACK_RESULT: skill=ship-queue status=complete|refused run=<path>`. Nothing
|
||
here pushes.
|
||
|
||
**1. Lane: report ready (unstamped).** After the gate and pregate pass on the
|
||
pushed head:
|
||
|
||
```bash
|
||
gstack-pregate run --allow-repo-commands
|
||
git push
|
||
gstack-ship-queue ready --pr 42
|
||
```
|
||
|
||
```
|
||
READY #42 <head> STAMP_VERSION=unstamped
|
||
pr: https://github.com/<owner>/<repo>/pull/42
|
||
head: <head>
|
||
version: unstamped
|
||
base: origin/main@<sha>
|
||
ci: green at <head12> (passed=31 failed=0 pending=0 skipped=2)
|
||
PREGATE: pass tree=<tree12> head=<head12> checks=6
|
||
gate: command="<gate_command>" full_e2e=no gate-ahead=none
|
||
SHIP_RECEIPT: pr=42 head=<head12> …
|
||
```
|
||
|
||
It refuses (`QUEUE_NOT_READY`) when the working tree has tracked edits, HEAD
|
||
is not the PR's pushed head, CI at that exact head is not green, or the pregate
|
||
result is not a current pass. Send the block as printed.
|
||
|
||
**2. Lane behind another PR: gate ahead.** Gate the tree the queue will have
|
||
once the predecessor lands, while it is still open:
|
||
|
||
```bash
|
||
git fetch origin pull/41/head
|
||
gstack-ship-queue gate-ahead --after 41 --pr 42 --predecessor-ref FETCH_HEAD --allow-repo-commands
|
||
```
|
||
|
||
prints `GATE_AHEAD: pr=42 attempt=<id> gated=<sha> tree=<tree> merge_tree=<tree>
|
||
predecessor=#41@<sha12> version=<N+1>`, the attempt's own `worktree:`, its
|
||
`record:` file, `GATE_COMMAND:` (the policy's `gate_command`) and the exact
|
||
`next:` line. Run the gate there, in the background or in CI, then bind it:
|
||
|
||
```bash
|
||
(cd <worktree> && <gate command>) > gate.log 2>&1
|
||
gstack-ship-queue gate-ahead --record --pr 42 --attempt <id> --exit $? --log gate.log
|
||
```
|
||
|
||
`GATE_RECORD: pr=42 attempt=<id> exit=0 gated_tree=<tree> log_sha256=<sha256>
|
||
current=yes`. The record keeps the log and removes only that attempt's
|
||
worktree, and refuses (`GATE_AHEAD_ATTEMPT_INVALID`) when the worktree no
|
||
longer holds the gated commit. If the predecessor moves, prepare a new
|
||
attempt; attempts never share a worktree, the newest is current, and
|
||
recording an older one later prints `current=no (superseded by <id>)`.
|
||
`gate-ahead --list --pr 42` shows them all. Send the record file with READY.
|
||
|
||
**3. Coordinator: call the slot.** Name the version, the base sha and the
|
||
order: "#42 next at 1.2.7.0, base `<sha>`, order 41,42".
|
||
|
||
**4. Lane: stamp.** A gate-ahead lane first merges `origin/main` (with the
|
||
predecessor in it) into its branch, never a rebase. Then:
|
||
|
||
```bash
|
||
gstack-ship-queue stamp --pr 42 --version 1.2.7.0 --expect-gated <gated> \
|
||
--expect-base <sha> --expect-order 41,42 --allow-repo-commands
|
||
```
|
||
|
||
`<gated>` is the head the gate ran on: the pushed head the lane gated, or the
|
||
record's `gated=` after a gate-ahead. The preflight refuses before anything is
|
||
written unless the tree to stamp equals the gated one modulo stamps, the base
|
||
is `<sha>` and, under `full_e2e`, a passing gate result is bound to the gated
|
||
head. Then it runs the restamp, commits, reruns the tree receipt and prints
|
||
|
||
```
|
||
QUEUE_STAMP: 42 1.2.7.0 head=<sha> gated=<sha> tree=same-modulo-stamps receipt=<sha256>
|
||
```
|
||
|
||
Each stage is journaled in `.gstack/tmp/ship-queue/stamp-42.json`; the
|
||
identical command resumes at the first unfinished stage, or returns the stored
|
||
`QUEUE_STAMP` when it already finished. A refusal prints
|
||
`QUEUE_STAMP_REFUSED: 42 head=<sha> committed=no|yes <sha> stage=<stage>`
|
||
and the coded line (`QUEUE_STALE` carries a `recompute:` command). Push the
|
||
stamp commit, wait for CI on it, run `ready` again
|
||
(`READY #42 <head> STAMP_VERSION=1.2.7.0`) and send it with the
|
||
`QUEUE_STAMP` line.
|
||
|
||
**5. Coordinator: verify, then merge.** From the coordinator's own clone:
|
||
|
||
```bash
|
||
gstack-ship-queue verify --pr 42 --gated <gated> --receipt <sha256> --expect-head <head> [--gate-exit 0 --gate-log <file>]
|
||
gstack-ship-queue verify --pr 42 --record <gate-ahead record> --receipt <sha256> --expect-head <head>
|
||
```
|
||
|
||
It fetches `refs/pull/42/head` and reruns the tree receipt there, so
|
||
`QUEUE_STAMP` is never a pasted claim: `VERIFY: 42 1.2.7.0 head=<sha>
|
||
gated=<sha> tree=same-modulo-stamps receipt=<same sha256>`. Under `full_e2e`,
|
||
`--gated` also needs the lane's passing gate log from its run on that head
|
||
(`--gate-exit 0 --gate-log <file>`), else `QUEUE_GATE_MISSING`. With a gate-ahead
|
||
record, whose gated commit never left the lane's machine, it first recomputes
|
||
the merge with `git merge-tree --write-tree <branch head> <predecessor head>`
|
||
and compares it with the recorded merge tree, rebuilds the gated tree from
|
||
the recorded stamp patch and compares it with the recorded `gated_tree`,
|
||
proves that patch is stamps only, checks the predecessor did not move, and
|
||
prints `merge_tree:`, `gated_tree:` and `gate:` lines. The last line is
|
||
`MERGE_CONDITION: sha=<head>`: merge with that expected head. Any
|
||
`QUEUE_VERIFY_MISMATCH` means do not merge.
|
||
|
||
**Rolling out `gate_command` and `full_e2e`.** Commit them (with
|
||
`min_gstack_version` at their introducing version, which `init --explain`
|
||
prints) only after every lane's gstack is at least that version, the crew's
|
||
install floor (`GSTACK_MIN_REVISION`). A lane on an older gstack refuses the
|
||
policy by name rather than reading it differently.
|
||
|
||
<a id="ship"></a>
|
||
## What `/ship` reads from it
|
||
|
||
`/ship` Step 3 runs `show-effective --base <base>` and follows its `ship:` lines,
|
||
which carry the workflow consequences the skill prose would otherwise spell out:
|
||
|
||
| Line | Effect |
|
||
|---|---|
|
||
| `ship: history merge-only: …` | never rebase, squash or force-push; `check-history --plan <merge\|squash\|rebase>` refuses a rewrite plan (`HISTORY_POLICY_VIOLATION`, exit 3) |
|
||
| `ship: queue_mode stamp-at-merge: …` | Step 12 items 1–4 are replaced by `gstack-restamp --next --base <base> --pr <n> --expect-base <sha>` when the PR is next, `--after <predecessor-pr>` when it is not; the restamp's own `next:` line says what follows (NEW_VERSION, CHANGELOG entry text only, the merge's expected head) |
|
||
| `ship: shared merge queue: …` | in a coordinator-run queue, `gstack-ship-queue ready`, `gate-ahead` and `stamp` replace the restamp calls; see [Run a shared merge queue](#run-a-shared-merge-queue) |
|
||
| `ship: gate-ahead: …` | with `.gstack/tmp/gate-ahead.json` present, Step 16 reuses only the lanes `gstack-tree-receipt … --json > .gstack/tmp/tree-receipt.json` marks reusable; stamp-sensitive and not-eligible lanes run live; `--force-gate <reason>` is the coordinator's call |
|
||
| `ship: bump patch: …` | Step 12 never asks for MINOR or MAJOR; PATCH or MICRO by size |
|
||
| `ship: preregistration: …` | `gstack-evidence ancestor --from-policy --base <base>` must print only `ANCESTOR: ok` before push |
|
||
| `ship: release_tool: …` | the command runs only inside `gstack-restamp` (staging worktree, outputs confined to `release_outputs`) |
|
||
|
||
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.
|
||
|
||
<a id="stamp-at-merge"></a>
|
||
## `queue_mode: stamp-at-merge`
|
||
|
||
Under `stamp-at-merge` nothing is claimed when a PR opens. The stamp happens
|
||
when the PR is next, from the base branch's version at that moment, through
|
||
the repo's own release tool, across every stamp location at once:
|
||
|
||
```bash
|
||
gstack-restamp --next --base main --allow-repo-commands --pr 42 \
|
||
--expect-base <sha of origin/main the queue assumed> --expect-order 40,41,42
|
||
```
|
||
|
||
prints `ship, next in queue at <version>`, `STAMP_VERSION: <version>`, one
|
||
line per stage, the diff limited to stamp files, `MERGE_CONDITION: sha=<head>`
|
||
(pass it to the merge API's expected-head `sha` so a check-to-merge race is
|
||
rejected by the server) and the `GSTACK_RESULT:` line. `claims: ignored
|
||
(stamp-at-merge)` says the legacy claim reader was not consulted. When the base
|
||
or the order moved, the result is `QUEUE_STALE` (exit 3) with the exact
|
||
`recompute` command; nothing is written.
|
||
|
||
Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
|
||
|
||
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, 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).
|
||
|
||
Idempotent: a second run with the same version is `noop`. `--dry-run` prints
|
||
the full diff and writes nothing (not even the journal). `--version <X>` stamps
|
||
exactly X; `--bump` overrides the level; `--queue-mode` overrides the policy.
|
||
|
||
### Gate-ahead: `--after <pr>`
|
||
|
||
A reorder is cheap only when the gate can run before the predecessor merges.
|
||
`gstack-restamp --after <pr> --predecessor-ref <ref>` merges HEAD and the
|
||
predecessor's head in a throwaway worktree (`.gstack/tmp/gate-ahead-worktree`),
|
||
resolving only version-file and CHANGELOG-ordering conflicts, stamps N+1
|
||
relative to the predecessor's version there, commits, and records
|
||
`.gstack/tmp/gate-ahead.json` (`predecessor_head`, `branch_head`,
|
||
`gated_head`, `gated_tree`, `version`, `evidence_branch_key`). The PR branch's
|
||
history is never touched, so squash-history PRs stay clean. The gate runs in
|
||
that worktree; `gstack-evidence bundle` there exports the evidence.
|
||
|
||
<a id="release-tool-contract"></a>
|
||
### The release-tool contract
|
||
|
||
The release tool runs in an isolated staging worktree seeded from the working
|
||
tree's content fingerprint (`gstack-wtree`), with the repo's ignored
|
||
`node_modules` linked in. Its outputs are enumerated: the version files,
|
||
lockfiles, the CHANGELOG, `stamp_paths` and `release_outputs`. A write outside
|
||
that set refuses the whole restamp before anything reaches the branch
|
||
(`RESTAMP_OUTPUT_OUTSIDE_ALLOWED`); a non-zero exit is
|
||
`RESTAMP_RELEASE_TOOL_FAILED`. Preimages and postimages of every write are
|
||
recorded in the journal. Pinning the command string does not freeze its
|
||
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
|
||
|
||
```bash
|
||
gstack-tree-receipt --from .gstack/tmp/gate-ahead.json --predecessor-pr 41 --bundle evidence-bundle.json
|
||
gstack-tree-receipt --gated <sha> --current HEAD --json
|
||
```
|
||
|
||
Compares two **committed heads** (a working-tree fingerprint can name objects
|
||
unreachable once the gate VM is gone) and prints the gated and current heads,
|
||
both tree ids, the version pair, the predecessor the gate assumed, the diff
|
||
limited to stamp files, one line per file with its rule and verdict, one line
|
||
per gate, then two verdict lines:
|
||
|
||
```
|
||
TREE: same-modulo-stamps | changed (<residual paths>)
|
||
gate-reuse: eligible (reusable: tests; rerun: digest) | not-eligible (<what changed>) — fix: <rerun command> (GATE_REUSE_NOT_ELIGIBLE)
|
||
```
|
||
|
||
Hunks are classified per file type: the version file (old → new only), the
|
||
manifest (`version` key only), npm and bun lockfiles (root `version` keys
|
||
only, where `gstack-version-bump` writes; a dependency entry is `changed`),
|
||
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`; `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
|
||
and the execution identity in the bundle equals the current one: Bun and Node
|
||
versions, OS and arch, every lockfile's identity (root version normalized
|
||
away), the `.github/workflows` tree id, runtime pin files and
|
||
`package.json#engines`, and the runner image when either side knows it. Without
|
||
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`.
|
||
|
||
`/ship` Step 16 consults the receipt; when it says `eligible`, a repeated full
|
||
gate needs `--force-gate <reason>`, and the coordinator makes the call.
|
||
|
||
### The evidence bundle
|
||
|
||
`gstack-evidence` is machine-local by design, so the portable form is a bundle:
|
||
|
||
```bash
|
||
gstack-evidence bundle --out evidence-bundle.json --label tests --label e2e --require tests,e2e \
|
||
--selection tests='bun run test' --run-url tests=https://github.com/.../actions/runs/1
|
||
```
|
||
|
||
A bundle binds to a committed head (`repo.commit`, `repo.tree`) and carries the
|
||
comparison material (version source, CHANGELOG path, `stamp_paths`,
|
||
`release_outputs`), the execution identity above, the required-lane manifest,
|
||
and per lane the command, `cmd_sha256`, exit, timestamp, executed selection,
|
||
run URL, log hash and whether it is `verified` (produced by the ledger on a
|
||
clean committed tree at this head) or not (dirty tree, other commit, or an
|
||
`--import`ed assertion). A fresh coordinator machine reads it with
|
||
`gstack-tree-receipt --bundle` after the gate VM is gone; the receipt refuses
|
||
reuse when the bundle was produced on another tree or lacks the identity
|
||
(`EVIDENCE_IDENTITY_UNKNOWN`).
|
||
|
||
<a id="ship-receipt"></a>
|
||
## The ship receipt
|
||
|
||
The handoff record is a fenced block, identifiers first, validated against
|
||
`gstack-artifact schema ship-receipt`:
|
||
|
||
````
|
||
```gstack-ship-receipt
|
||
{ "schema_version": 1, "pr": 42, "head": "<sha>", "version": "1.2.7.0", "base": "main",
|
||
"gated_tree": "<tree>", "predecessor": "<sha>", "tree": "same-modulo-stamps",
|
||
"gate_reuse": "eligible (reusable: tests; rerun: digest)", "gate": { "tests": { "pass": 10, "fail": 0, "skip": 1 } },
|
||
"ci": { "checks": 3, "failed": 0 }, "spend_usd": 0.42, "session_kind": "unattended",
|
||
"artifacts_consumed": "1/1", "run": "<run id>", "thread": "<thread id>", "policy": "base origin/main@<sha>" }
|
||
```
|
||
````
|
||
|
||
`gstack-ship-receipt write --pr <n> --receipt <tree-receipt.json> --gate '<json>' --ci '<json>' --body <pr-body.md>`
|
||
makes it the first section of the PR body (`/ship` Step 18) and prints
|
||
`SHIP_RECEIPT: pr=… head=… version=…` for the first line of the completion
|
||
message. `/land-and-deploy` reads it with `gstack-ship-receipt read --pr <n>
|
||
--json` (`RECEIPT_MISSING`, `RECEIPT_INVALID`).
|
||
|
||
### The adoption census (P7)
|
||
|
||
```bash
|
||
gstack-ship-receipt census --repo garrytan/gstack --since v1.91.70.0 --cohort threads.txt
|
||
```
|
||
|
||
reads merged PRs since the tag through `gh`, parses each receipt, deduplicates
|
||
by `run` and `thread`, and prints `CENSUS: … unattended=<n> consumed=<n>
|
||
adoption=<consumed threads>/<cohort> durable=yes`. The cohort file lists one
|
||
thread id per line (threads that never invoke gstack count in the
|
||
denominator); without it the denominator is the threads the receipts name.
|
||
|
||
## History policy and preregistration (C7)
|
||
|
||
Under `history: merge-only`, `/ship` refuses a squash or rebase plan and merges
|
||
the base branch instead: `gstack-ship-policy check-history --plan squash`
|
||
prints `HISTORY: refused (…) (HISTORY_POLICY_VIOLATION)` with exit 3, and
|
||
without `--plan` it prints `HISTORY: ok` or `HISTORY: rewritten …
|
||
(HISTORY_POLICY_VIOLATION)` (exit 1) when the pushed branch is no longer an
|
||
ancestor of HEAD. `gstack-evidence ancestor
|
||
--from-policy --base main` prints one `ANCESTOR: ok|missing <sha>` line per
|
||
`preregistration_shas` entry (`PREREGISTRATION_NOT_ANCESTOR`, exit 1), and the
|
||
ship receipt records the result in `preregistration`.
|
||
|
||
## Contributor mode
|
||
|
||
`/review` runs `gstack-contributor-mode detect` at Step 1 (decision D10). The PR
|
||
author is outside the maintainer set when `maintainers` in `.gstack/ship-policy.json`
|
||
on the base branch is non-empty and does not list the login (`source=policy`);
|
||
with an empty list, when `gh api repos/<owner>/<repo>/collaborators/<login>/permission`
|
||
reports less than push permission (`source=collaborators`, an egress receipt
|
||
`contributor-mode` precedes the call); when neither is available the mode is off
|
||
and `CONTRIBUTOR_SOURCE_UNAVAILABLE` names what was missing. `--contributor`
|
||
forces it on (`source=flag`). `CODEOWNERS` is file ownership, not maintenance
|
||
authority, and is never read. The printed line is
|
||
`CONTRIBUTOR_MODE: on|off author=<login> source=flag|policy|collaborators|off`,
|
||
followed when on by the evidence-first instructions: a table per change
|
||
(evidence value, risk surfaces, `accept` / `supersede` / `decline`), where
|
||
`accept` keeps a direct path for a correct outside implementation judged by the
|
||
same bar as a maintainer change, `supersede` rewrites from our own plan only after
|
||
a plain-language approval, and the credit lines (`Contributed by @handle`,
|
||
`Co-Authored-By:`, `Supersedes #N`) appear only when a diagnosis or a test carried
|
||
over. `gstack-contributor-mode instructions` prints the same text without detecting.
|
||
|
||
## Banned terms
|
||
|
||
`.gstack/banned-terms.json` lists terms the repository forbids outside named
|
||
locations: `terms` (a string, or `{ "term", "allowed_paths", "reason" }`) and a
|
||
top-level `allowed_paths` that every term inherits. Globs follow the same
|
||
containment rules as the ship policy (repo-relative, no `..`, under 256
|
||
characters). `gstack-banned-terms check --base <ref>` scans the lines the branch
|
||
adds since its merge base (or whole files with `--files <csv>`), reading the term
|
||
list from the committed copy on the base by default (`--policy-from head` reads
|
||
the working tree). Each finding is one line,
|
||
`BANNED_TERM: "<term>" <file>:<line> allowed: <locations> — <text>`, and the
|
||
last line is `BANNED_TERMS: ok | found=<n> | none` (`none`: the repository keeps
|
||
no list). Exit 0 for ok or none, 1 when found, 3 when the file is invalid
|
||
(`BANNED_TERMS_INVALID`, also from `validate`). `/review` turns each finding into
|
||
a Step 4 finding and `/document-release` into a documentation finding;
|
||
`gstack-banned-terms init` writes a commented example.
|
||
|
||
<a id="approval-classes"></a>
|
||
## Approval classes
|
||
|
||
`.gstack/approval-classes.json` names the changes that need the owner's
|
||
approval before they ship: upgrade-time data rewrites, trust or visibility
|
||
changes, existing-user default changes, data repairs, anything the owner wants
|
||
cited. Each entry in `classes` has a `name` (lowercase, dashes) and at least
|
||
one matcher: `paths` (globs of changed files), `patterns` (literal text in an
|
||
added line, case-sensitive) or `migrations: true` (the files the ship policy's
|
||
`migrations` key describes). `allowed_paths` exempts files, such as tests;
|
||
`description` is for people. Globs follow the ship policy's containment rules.
|
||
|
||
Classes come only from the copy committed on `origin/<base>`, so a branch can
|
||
neither declare nor drop its own; `gstack-approval-classes validate` checks the
|
||
working-tree copy before you commit it, and `init` writes an example.
|
||
`gstack-approval-classes check --base <ref>` scans the branch's changes since
|
||
the merge base and prints one `APPROVAL_CLASS: <class> <file>:<line>` line per
|
||
hit, one `APPROVAL_SCOPE: <class>@<digest12>` line per declared class with hits,
|
||
and `APPROVAL_CLASSES: ok | found=<n> | none` last (exit 0, 1 when found, 3 when
|
||
the base file is invalid or `origin/<base>` does not resolve). Built-in
|
||
detectors only report: the ship policy's migration files, when no class sets
|
||
`migrations: true`, print as `(built-in, report-only)` and need no citation.
|
||
|
||
The digest covers the class's effects: each matched file with its blob at the
|
||
scanned head, and each matched added line. The scan reads git's native diff
|
||
(`--no-ext-diff --no-textconv`), so a configured external diff driver or text
|
||
conversion cannot hide a line; when the content diff cannot be read, or its
|
||
added lines disagree with git's own count, `check` and the receipt refuse with
|
||
`APPROVAL_SOURCE_UNAVAILABLE` instead of passing. Adding, removing or editing an
|
||
effect changes the token, so an approval given earlier no longer binds.
|
||
|
||
**Enforcement.** `gstack-ship-receipt write` refuses with
|
||
`APPROVAL_CITATION_MISSING`, writing nothing, unless each declared class with
|
||
hits has a line in the `--body` file (or an `--approval` value) of the form
|
||
|
||
```
|
||
Approved by <owner> <YYYY-MM-DD> via <ref> [for <class>@<digest12>[,<class>@<digest12>]]
|
||
```
|
||
|
||
that resolves and binds:
|
||
|
||
- A plan sha (7 to 64 hex characters) must match the plan of an autoplan run
|
||
passed with `--approval-run <run dir>` whose gate is APPROVED, and the plan
|
||
must name the scope token, or the class and every effect path.
|
||
- A GitHub comment or review URL must be written by a `maintainers` login from
|
||
the base's ship policy that equals the cited owner. A review must be in
|
||
state `APPROVED`; a comment-only, dismissed or changes-requested review is
|
||
not an approval. The evidence is the body's first non-blank line only. It
|
||
must start at column 0 with `Approved` or `Approve` (any case, optionally
|
||
in `**` bold; no list marker, quote or indent) and name the token, or the
|
||
class and every effect path. Nothing later in the body counts, so a
|
||
rejection that quotes or nests an example approval approves nothing. For
|
||
example, a comment whose first line is
|
||
`Approved data-repair@0123456789ab: preview-first, never at upgrade.`
|
||
verifies; one that opens with any other line does not.
|
||
- A token is authoritative. Evidence that names the class with a different
|
||
digest approved earlier effects: it does not bind, and it never falls back
|
||
to matching the class and paths.
|
||
- Anything else (a thread code, a chat message, a URL gstack cannot read) is
|
||
recorded `citation: unverified` for the coordinator, and only when the line
|
||
attests which scope it covers with `for <class>@<digest12>` matching the
|
||
current token.
|
||
|
||
The receipt's `approval` object records the source, `status` (`checked`, or
|
||
`unavailable` when `origin/<base>` does not resolve), one record per class
|
||
(scope token, effects, `citation: verified | unverified`, owner, date, ref,
|
||
`binding: token | paths | attested`, evidence) and any report-only hits; the
|
||
`SHIP_RECEIPT:` line ends with `approval=verified`, `approval=unverified:<classes>`,
|
||
`approval=none` or `approval=unchecked`. A repository without the file gets no
|
||
`approval` object unless a built-in detector reported.
|
||
|
||
A data repair is a class to declare, and /review checks its shape: explicit,
|
||
preview-first, and never reachable from an upgrade or migration path.
|