docs: describe the shipped contracts (Step 13 and 14.5)

CHANGELOG [Unreleased]: docs-caps --base <base> reading origin/<base>'s
policy, the review-late-defer row, the literal <diff-base> prompt rule, the
coauthor-trailers placement and refusals, the policy key versions (review_scope
1.91.256.0 re-pin, docs.caps at the stamping release), the version-less
conditions split between Step 12 and the receipt, and the carry refusal wording.
docs/ship-policy.md, docs/review-policy.md and docs/unattended.md follow the
code: docs-caps --base, the literal diff-base sha, the trailer block, the
gstack-outside-voice run binding and the Step 11 row fields.

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-26 23:14:03 +00:00
1 parent 47e693b304
commit 5ceaa5e1e3
4 files changed
+36 -24

No files matched your search

+10 -7
View File
@@ -3,22 +3,25 @@
## [Unreleased]
### Added
- **Repositories without a version source** (`ship/SKILL.md` Step 12, `ship/sections/changelog.md`, `gstack-ship-receipt write`): a repo with no `.gstack/ship-policy.json` pins, `VERSION` or `package.json#version` ships without a version change: `Version bump: skipped (no version source: <path, reason>)`, no `VERSION` is ever created, the PR title is unprefixed, the run log records `"version":null` and the receipt records `"version": "none"`. A repo with a CHANGELOG but no CHANGELOG entry source prints `CHANGELOG: skipped (none at <path>)` and never creates the file ([ship-policy.md#ship-receipt](docs/ship-policy.md#ship-receipt)).
- **Docs audit record and doc caps** (`lib/ship-receipt-docs.ts`, `gstack-ship-receipt write --docs <json|@file>`, `gstack-ship-receipt docs-caps [--policy <file>]`, policy key `docs.caps: [{ path, max_bytes }]`): Step 14.5's outcome (`updated` | `current` | `blocked`, with the owner's `exception: { accepted_by, date, via, reason }`) is part of the receipt; `blocked` without an exception, or a `--body` that lacks the exception line word for word, refuses with `DOCS_AUDIT_BLOCKED`; a declared doc over its byte cap (or absent) prints `DOCS_CAP: <path> <bytes>/<max> over` and blocks before the verification gate. The `docs` key needs `min_gstack_version` (`DOCS_CAPS_KEY_VERSION`).
- **Repositories without a version source** (`ship/SKILL.md` Step 12, `ship/sections/changelog.md`, `gstack-ship-receipt write`): a repo with no `.gstack/version-path` pin and no `VERSION` ships without a version change: `Version bump: skipped (no version source: <path, reason>)`, no `VERSION` is ever created, the PR title is unprefixed and the run log records `"version":null`; the receipt records `"version": "none"` when the repo also has no `package.json#version`, no policy stamp file and no CHANGELOG. A repo with no CHANGELOG (the policy's `changelog` is null or the file is absent) prints `CHANGELOG: skipped (none at <path>)` and never creates the file ([ship-policy.md#ship-receipt](docs/ship-policy.md#ship-receipt)).
- **Docs audit record and doc caps** (`lib/ship-receipt-docs.ts`, `gstack-ship-receipt write --docs <json|@file>`, `gstack-ship-receipt docs-caps [--base <ref>] [--policy <file>]`, policy key `docs.caps: [{ path, max_bytes }]`): Step 14.5's outcome (`updated` | `current` | `blocked`, with the owner's `exception: { accepted_by, date, via, reason }`) is part of the receipt; `blocked` without an exception, or a `--body` that lacks the exception line word for word, refuses with `DOCS_AUDIT_BLOCKED`; a declared doc over its byte cap (or absent) prints `DOCS_CAP: <path> <bytes>/<max> over` and blocks before the verification gate; the caps come from `origin/<base>`'s policy, never the branch's copy, so a PR cannot raise its own cap, and an unreadable base policy refuses with `POLICY_SOURCE_UNAVAILABLE` (`--policy <file>` is for a repository bootstrapping its policy). The `docs` key needs `min_gstack_version` (`DOCS_CAPS_KEY_VERSION`, the release that stamps this entry).
- **`docs/unattended.md#ship-evidence`**: what an unattended /ship leaves for a parent: the READY evidence through `gstack-ship-receipt result`, the `review_loop` counters, the cross-family Capy subagent as the Step 11 outside voice with its review-log row, and the `CARRY:` lines.
- **Paid case `ship-frozen-scope-convergence`** (behavior, gate; `test/helpers/ship-frozen-scope-actor.ts`, `test/skill-e2e-ship-frozen-scope.test.ts`): the native actor runs the generated Step 9.4 and its repeat decision under `review_scope: frozen` against a fixture with one CRITICAL in the diff and one in untouched code; the contract is the in-diff fix committed, the pre-existing file byte-identical and deferred, pass 2 on the range only, exit through decision 4 and `REVIEW_LOOP: passes=2 blocking_found=2 deferred=1 fixed=1`. First panel 3/3 at $0.66.
- **`gstack-ship-receipt read --check-prose`** (`lib/ship-receipt-prose.ts`): every counter the PR body quotes (`passes=2`, `1 deferred`, `passed: 41`) is compared with the receipt's `review_loop`, `gate` and `ci` integers; a disagreement is `RECEIPT_PROSE_MISMATCH` (exit 1) with a `PROSE:` line naming each field and both values.
- **`review/checklist.md` rules with ids**: `prereg-with-results` (Pass 1, Gate Integrity, ASK: a preregistration and its results in one diff) and `test-keyboard-count-loop` (Pass 2: a browser test that reaches an element by a counted loop of key presses).
- **`bin/gstack-coauthor-trailers`**: the `Co-authored-by` trailers a ported range owes (distinct authors other than the checkout's committer, first-appearance order; `--message <file>` appends the missing ones idempotently), for porting a contributor's PR with their authorship kept ([review-policy.md#porting-a-contributors-work](docs/review-policy.md#porting-a-contributors-work)).
- **`bin/gstack-coauthor-trailers`**: the `Co-authored-by` trailers a ported range owes (distinct authors other than the checkout's committer, first-appearance order; `--message <file>` adds the missing ones to the file's trailer block through `git interpret-trailers` with pinned placement, idempotently, so an existing co-author keeps attribution; a `--base` or range starting with `-` is a usage refusal and a slash-named default branch resolves through `origin/`), for porting a contributor's PR with their authorship kept ([review-policy.md#porting-a-contributors-work](docs/review-policy.md#porting-a-contributors-work)).
- **Result codes** `DOCS_AUDIT_BLOCKED`, `RECEIPT_GIT_TOO_OLD`, `RECEIPT_PROSE_MISMATCH` (`lib/result-codes-release.ts`, `docs/troubleshooting.md`).
### Changed
- **`gstack-ship-receipt carry`**: needs git 2.39 or later for `git patch-id --verbatim` and refuses older gits with `RECEIPT_GIT_TOO_OLD` naming the installed version; a named path whose patch-id git cannot compute refuses with git's own first stderr line instead of comparing an empty id; the per-path walk covers only the branch-touched paths among `CARRY_NAMED_PATHS`.
- **`gstack-ship-receipt carry`**: needs git 2.39 or later for `git patch-id --verbatim` and refuses older gits with `RECEIPT_GIT_TOO_OLD` naming the installed version; a product patch-id git cannot compute at either head refuses with git's own first stderr line instead of comparing an empty id; the per-path walk covers only the paths the branch itself touched and names at most `CARRY_NAMED_PATHS` (20).
- **`gstack-review-scope defer`**: `gh repo view` receives the repository positionally (the visibility read works under `--repo`); the marker search reads one page of 200 open issues, and an exactly-full page prints `DEFERRED_SEARCH: truncated at 200 open issues` and records `filed; the marker search was truncated at 200, so an older issue may duplicate it`.
- **Step 11 late defers** (`lib/review-scope.ts` `LATE_DEFER_PHASE`, `ship/sections/review-army.md` item 3.5): a queued adversarial finding scoped `defer` after the last Step 9 pass is logged as a `review` row with `"phase":"late-defer"`; the receipt's summary counts its deferred findings and never counts it as a pass. `render` shows a filed issue's reason when it is not the plain "already filed".
- **`ship/sections/review-army.md` Step 9.4** reordered: `0. Scope the findings` → `1. Apply the cap before any edit, then classify` → `2. Auto-fix`; scratch files live under `.gstack/tmp/` (`review-findings-p$PASS.json`, `review-classified-p$PASS.json`, `review-deferred.json`). The pass-2+ range paragraph says the outside backends receive `RANGE_BASE..HEAD`, and the specialist and Red Team prompts compute `DIFF_BASE=${RANGE_BASE:-$(git merge-base origin/<base> HEAD)}` in /ship (`scripts/resolvers/review-army.ts`).
- **Step 11 late defers** (`lib/review-scope.ts` `LATE_DEFER_SKILL`, `ship/sections/review-army.md` item 3.5): a queued adversarial finding scoped `defer` after the last Step 9 pass is logged under its own skill, `review-late-defer`; the receipt's summary counts its deferred findings, never counts it as a pass, and the dashboards that pick the newest `review` row never see it (a tokenless `review` row would read UNVERIFIED). `render` shows a filed issue's reason when it is not the plain "already filed".
- **`ship/sections/review-army.md` Step 9.4** reordered: `0. Scope the findings` → `1. Apply the cap before any edit, then classify` → `2. Auto-fix`; scratch files live under `.gstack/tmp/` (`review-findings-p$PASS.json`, `review-classified-p$PASS.json`, `review-deferred.json`). The pass-2+ range paragraph says the outside backends receive `RANGE_BASE..HEAD`, and every dispatched prompt in /ship carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on; `scripts/resolvers/review-army.ts`), because a reviewer is its own process and a shell variable left in the prompt would silently re-read the whole branch.
- **Reviewer fan-out** (`--review-fanout auto|serial|parallel`, /ship only; `docs/review-policy.md#reviewer-fan-out`): when the host reports fewer free subagent slots than selected specialists, `auto` runs them in batches of the free-slot count and prints `Fan-out: N specialists in M batches of K`; /review keeps one parallel batch because of its size pin.
- **`ship/sections/documentation.md`** runs `gstack-ship-receipt docs-caps --policy .gstack/ship-policy.json` under "Continue or recover" before the verification gate and writes `.gstack/tmp/docs-audit.json`; `ship/sections/pr-body.md` names the verbatim exception line in the Documentation section and the receipt write adds `--docs @.gstack/tmp/docs-audit.json`. An unattended run has no owner to accept a blocked audit and stops there.
- **`ship/sections/documentation.md`** clears `.gstack/tmp/docs-audit.json` on first entry, runs `gstack-ship-receipt docs-caps --base <base>` under "Continue or recover" before the verification gate and writes `.gstack/tmp/docs-audit.json`; `ship/sections/pr-body.md` names the verbatim exception line in the Documentation section and the receipt write adds `--docs @.gstack/tmp/docs-audit.json`. An unattended run has no owner to accept a blocked audit and stops there.
- **Policy key versions** (`lib/ship-policy.ts`): `REVIEW_SCOPE_KEY_VERSION` is 1.91.256.0, the release that first read `review_scope` (its earlier value 1.91.249.0 admitted a gstack that fails on the unknown key), so a policy that set `review_scope` with `min_gstack_version` between 1.91.249.0 and 1.91.255.0 must raise it to 1.91.256.0; `DOCS_CAPS_KEY_VERSION` names the release that stamps this entry, and `test/gstack-ship-policy.test.ts` holds every unreleased key newer than the CHANGELOG headings the checkout can see.
- **`gstack-ship-receipt write`** records `version: "none"` only when the repository has no version source at all; a `VERSION` that exists but holds no parsable version, a pin that escapes the repository or an unreadable base policy keeps the no-version refusal. `checkReceiptProse` matches a shared leaf key (`pass` under every gate lane) against any of its counters and never inside a longer word (`3 pass-through`).
### Tests
- `test/gstack-ship-receipt-docs.test.ts` (new): the docs record, exception line and doc caps, `version: "none"` for a version-less repo, the git floor, the named patch-id refusal and the branch-touched walk. `test/review-scope-followups.test.ts` (new): late-defer counting, the 200-issue truncation record, `gh repo view` positional, the two checklist rule ids. `test/ship-workflow-clarity.test.ts`: Step 9.4 order, scratch-file names, the late-defer row, `DIFF_BASE` with `RANGE_BASE` in /ship prompts only. `test/ship-frozen-scope-actor.test.ts` (new): a scripted actor passes the frozen-scope verdict with the real binaries, and the undeclared interactions, an unfixed diff defect and a one-pass exit are named failures. `test/ship-receipt-prose.test.ts` and `test/gstack-coauthor-trailers.test.ts` (new).
+4 -2
View File
@@ -162,14 +162,16 @@ porter's fixes on top, each crediting the original authors.
[--exclude <email>]...` lists every distinct author of the range's commits
(default `origin/<base>..HEAD`) other than the checkout's own committer
identity, in first-appearance order, as `Co-authored-by: Name <email>` lines;
`--message <file>` appends the ones the commit message file lacks, matched by
`--message <file>` adds the ones the commit message file lacks to its trailer
block (`git interpret-trailers`, pinned placement, so an existing co-author
keeps attribution), matched by
email case-folded, so running it twice changes nothing. The `COAUTHORS: <n> in
<range>` line on stderr is the receipt; `--json` prints `{ range, authors,
trailers, appended }`.
## Where this is enforced
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`); in `/ship` every dispatched prompt diffs `DIFF_BASE=${RANGE_BASE:-$(git merge-base origin/<base> HEAD)}`, so pass 2+ hands reviewers the range; the fan-out batches above.
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`); in `/ship` every dispatched prompt carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on), because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
- `ship/sections/review-army.md` Step 9: passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
- `lib/review-scope.ts`, `bin/gstack-review-scope`: `classify`, `defer`, `render`, `summary`; refusals are `REVIEW_SCOPE_INPUT` and `REVIEW_SCOPE_RANGE` ([troubleshooting.md](troubleshooting.md#review-scope-input)).
- `lib/ship-policy.ts`: the `review_scope` key and its `ship: review_scope …` consequence line.
+11 -5
View File
@@ -59,7 +59,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `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`. |
| `review_scope` | `frozen` \| `diff-plus-blocking` \| `all` | `frozen` | The finding scope of `/ship`'s review loop; see [review_scope](#review-scope). `frozen` fixes the diff on pass 1, files blocking findings in unchanged code as issues, and later passes read only the range since the previous pass. `diff-plus-blocking` is the same but fixes a blocking finding in unchanged code in this PR instead of filing it. `all` is the earlier loop: every finding is fixed on every pass. Needs `min_gstack_version` when not `frozen`. |
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --policy .gstack/ship-policy.json` before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` (the caps come from `origin/<base>`'s policy, never the branch's copy; `--policy <file>` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
Containment, checked by `validate` and again at write time: paths and globs are
@@ -609,8 +609,11 @@ stops there. With `--body`, the body's `## Documentation` section must carry
the exception word for word (`Documentation exception: accepted by <owner>
<date> via <ref>: <reason>`), or the write refuses with the same code. Doc
caps: a repository declares `docs.caps: [{ path, max_bytes }]` in its policy,
and Step 14.5 runs `gstack-ship-receipt docs-caps --policy <file>` before the
verification gate; one `DOCS_CAP: <path> <bytes>/<max> ok|over` line per cap
and Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` before the
verification gate, reading the caps from `origin/<base>`'s policy so the branch
cannot raise its own (`--policy <file>` only while bootstrapping a policy; an
unreadable base policy is `POLICY_SOURCE_UNAVAILABLE`); one
`DOCS_CAP: <path> <bytes>/<max> ok|over` line per cap
(`absent/<max> over` for a path the tree lacks), exit 1 on any `over`, whose
repair is trimming the file, never raising the cap in the same PR.
@@ -618,8 +621,11 @@ repair is trimming the file, never raising the cap in the same PR.
(or `--body <file>`) compares every counter the body quotes with the receipt's
integers under `review_loop`, `gate` and `ci`: `passes=2`, `passes: 2` and
`2 passes` are claims about `review_loop.passes`, `1 deferred` about
`review_loop.deferred`, `passed: 41` about `gate.passed` (an underscore in a
key matches `_`, a space or `-`). A body that quotes a different number is
`review_loop.deferred`, `passed: 41` about `gate.passed` (a leaf key shared by
several counters, `passed` under `gate` and `ci` or `pass` under every gate
lane, matches when the number equals any of them; an underscore in a key
matches `_`, a space or `-`; a name inside a longer word, `3 pass-through`, is
not a claim). A body that quotes a different number is
`RECEIPT_PROSE_MISMATCH` (exit 1) and the `PROSE:` line names each field with
both values; prose that quotes no counter is not a claim. Data PRs (evals,
measurements, sweeps) run it before READY so the body's numbers are the run's.
+11 -10
View File
@@ -184,19 +184,20 @@ degraded coverage, as long as the model that reviewed is recorded. Its
review-log row is the same row every Step 11 pass writes
(`ship/sections/adversarial.md`): `skill: "adversarial-review"`,
`phase: "adversarial"`, `source` naming the runner that actually reviewed
(`host-subagent` for the cross-family subagent, the same runner id
`bin/gstack-outside-voice runners` prints and the table below describes;
`codex-cli` for the CLI; `"none"` when no outside pass completed),
(the outside provider that reviewed, with the model recorded, for the
cross-family subagent; `codex` for completed Codex CLI output, as
`ship/sections/adversarial.md` prescribes),
`outside_provider` (the provider the host would have used, as the generated
prose names it), `outside_status` (`completed` for a finished outside pass;
`unavailable` when neither the CLI nor a subagent completed, with
`unavailable_reason`; `disabled` under `codex_reviews: disabled`), `status`
`unavailable` when neither the CLI nor a subagent completed, the cause on the
`OUTSIDE_STATUS` line rather than in the row; `disabled` under
`codex_reviews: disabled`), `status`
(`clean`, `issues_found` or `unavailable`), `completed`, `converged`, and the
subagent's model recorded with the pass (`gstack-outside-voice submit --runner
host-subagent --model <id>` binds it; the row's text names it where the host
reports it). The native in-host adversarial
subagent writes its own row with `source` naming the native host; native
completion never credits outside coverage. A parent reads the rows with
subagent's model recorded with the pass (`gstack-outside-voice run --runner
host-subagent --result <file> --model <id> --prompt <file> --out <file>` binds
it; the row's text names it where the host reports it). The native in-host adversarial
subagent writes its own row with `source: "in-host"`; native completion never
credits outside coverage. A parent reads the rows with
`gstack-review-read` and treats `outside_status: unavailable` as missing
coverage, not as a clean outside review.