From 5ceaa5e1e3afc377300653e9b4dec04deb337221 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 23:14:03 +0000 Subject: [PATCH] docs: describe the shipped contracts (Step 13 and 14.5) CHANGELOG [Unreleased]: docs-caps --base reading origin/'s policy, the review-late-defer row, the literal 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> --- CHANGELOG.md | 17 ++++++++++------- docs/review-policy.md | 6 ++++-- docs/ship-policy.md | 16 +++++++++++----- docs/unattended.md | 21 +++++++++++---------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cde5ac5e6..023796c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: )`, 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 )` 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 `, `gstack-ship-receipt docs-caps [--policy ]`, 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: / 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: )`, 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 )` 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 `, `gstack-ship-receipt docs-caps [--base ] [--policy ]`, 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: / over` and blocks before the verification gate; the caps come from `origin/`'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 ` 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 ` 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 ` 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/ 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=`: 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 ` 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). diff --git a/docs/review-policy.md b/docs/review-policy.md index cd9ec9849..c185e4416 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -162,14 +162,16 @@ porter's fixes on top, each crediting the original authors. [--exclude ]...` lists every distinct author of the range's commits (default `origin/..HEAD`) other than the checkout's own committer identity, in first-appearance order, as `Co-authored-by: Name ` lines; -`--message ` appends the ones the commit message file lacks, matched by +`--message ` 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: in ` 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/ 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=`: 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. diff --git a/docs/ship-policy.md b/docs/ship-policy.md index 417348ab1..826262307 100644 --- a/docs/ship-policy.md +++ b/docs/ship-policy.md @@ -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 ` 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: / 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 ` (the caps come from `origin/`'s policy, never the branch's copy; `--policy ` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: / 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 via : `), 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 ` before the -verification gate; one `DOCS_CAP: / ok|over` line per cap +and Step 14.5 runs `gstack-ship-receipt docs-caps --base ` before the +verification gate, reading the caps from `origin/`'s policy so the branch +cannot raise its own (`--policy ` only while bootstrapping a policy; an +unreadable base policy is `POLICY_SOURCE_UNAVAILABLE`); one +`DOCS_CAP: / ok|over` line per cap (`absent/ 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 `) 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. diff --git a/docs/unattended.md b/docs/unattended.md index aca65c841..ce32a227c 100644 --- a/docs/unattended.md +++ b/docs/unattended.md @@ -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 ` 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 --model --prompt --out ` 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.