Files
gstack/docs/ship-policy.md
T
2026-10-16 23:35:07 +00:00

30 KiB
Raw Blame History

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), 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.

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. 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_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.
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 stamp and verify refuse without one (QUEUE_GATE_MISSING). 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:

{
  "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.

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:

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:

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:

(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:

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:

gstack-ship-queue verify --pr 42 --gated <gated> --receipt <sha256> --expect-head <head>
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>. 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.

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: 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.

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:

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:

  1. restamp_command (only when the policy sets it): the repo's own restamp, see restamp_command.
  2. version: gstack-version-bump write --version <X> in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when restamp_command owns the version.
  3. release_tool: the policy command, under the contract below.
  4. 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.
  5. 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.
  6. mirror: tier 2; declared and validated, reported as not run.
  7. 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.

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)).

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.

The tree receipt

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 reruns; 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:

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 --imported 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).

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)

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.