feat(ship-receipt): carry across regenerated outputs and onto a reviewed tree after a rebase

A base merge whose only product-side change is a registered output the
pregate regen check proved fresh now carries the receipt. A rebased head
carries when its tree is one the receipt covers and its product patch-id
against the current base matches (RECEIPT_CARRY_TREE_MISMATCH otherwise).
Docs: merge main into a reviewed branch, never rebase it.

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-28 23:00:32 +00:00
1 parent 4c745eec10
commit 9fe6c01835
12 files changed
+412 -97

No files matched your search

+8
View File
@@ -231,6 +231,14 @@ a push included. The sequence that works unattended on Capy:
10. After the slot call, the stamp commit keeps the pin's receipt; re-READY with the same
receipt, no second /ship.
When the base moves under a reviewed branch, merge it in; never rebase. A merge that
leaves the product diff unchanged carries the receipt with `gstack-ship-receipt carry`
(pregate at HEAD, no review rerun), and a merge whose resolution changed product code
needs a fresh /ship. A rebase carries only onto a tree the receipt already covers with
the same product diff; any other rebase is a full /ship
([ship-policy.md](ship-policy.md#receipt-carry)). A controller never asks a lane to
rebase a reviewed branch.
## Merge trains
gstack's queue tool drives trains when the repository's `.gstack/ship-policy.json` carries
+36 -6
View File
@@ -891,6 +891,16 @@ line, and the codes a refusal ends with are
<a id="receipt-carry"></a>
## Carrying a receipt across a base merge or a test-only fix
**Merge main into a reviewed branch; never rebase it.** What happens to the
receipt when the base moves:
| What changed since /ship | The receipt |
|---|---|
| a merge of the base, product diff unchanged (tests, release metadata, registered outputs the pregate regenerated fresh) | carries; no review reruns |
| a merge of the base whose resolution changed product code | a fresh /ship (`RECEIPT_CARRY_PRODUCT_CHANGED`) |
| a rebase or force-push onto a tree the receipt covers, product diff against the new base unchanged | carries (`carry.ancestry: rewritten`) |
| any other rebase or force-push | a fresh /ship (`RECEIPT_CARRY_TREE_MISMATCH`) |
Since the release that added `review_scope`. A `/ship` receipt names the head it
reviewed, so a conflict-only merge of the base or a test-only fix used to
invalidate it and cost a full re-run on identical product code.
@@ -921,8 +931,13 @@ newer tentative one the stamp moved (a `*_KEY_VERSION` written as the next
minor, #3512; a version older than the old one or of another width is a hand
edit). A logic change in a
stamped source file is product. Lockfiles are product unless the policy names
them. A base merge contributes no product
change of its own. A receipt carried twice keeps the reviewed head as
them. An output of an entry with a `command` in origin/<base>'s
`.gstack/generated.json` is `generated`, not product, only when the
`--pregate` result passed for HEAD shows the `regen` check passing with those
entries fresh; without that evidence, or for an entry with no command or one
only the branch registers, the output is product (U1 on gstack-evals paid a
full rerun because a merge changed its generated index's diff). A base merge
contributes no product change of its own. A receipt carried twice keeps the reviewed head as
`carry.from` and adds up the merges and paths. The carried receipt records
`carry: { from, reason, product_patch_id, changed_paths, merge_commits,
range_diff, covers, reruns }` with the `git range-diff` evidence, prints
@@ -933,10 +948,25 @@ skill=ship status=complete` line, and its summary line ends with
A changed product path, or a product patch-id that differs, is
[`RECEIPT_CARRY_PRODUCT_CHANGED`](troubleshooting.md#receipt-carry-product-changed)
(exit 3; the refusal names the paths), and a receipt whose head is not in HEAD's
history (a rebase or force-push) or a working tree with uncommitted changes is
[`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused): run `/ship`
on the new head. The queue's `ready`, `train build` and `verify --train` read the
(exit 3; the refusal names the paths), and a working tree with uncommitted
changes is [`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused).
**Rewritten history.** `write` records `head_tree` (the reviewed head's tree)
and `product_patch_id` (its product patch against the base), and every carry
rebinds both to the new head and keeps the earlier trees in `carry.trees`.
When the receipt's head is no longer in HEAD's history, the carry follows only
when HEAD's tree is one of those trees and HEAD's product patch-id against the
current base equals the recorded one; it records `carry.ancestry: rewritten`
and `range_diff: tree-equal`, and needs neither the old commit nor its
history, so a fresh clone carries too. The tree alone is not enough: a stacked
branch rebased onto main after its base closed unmerged keeps its tree while
its diff grows by the base's code, and that is `RECEIPT_CARRY_PRODUCT_CHANGED`.
Any other tree is
[`RECEIPT_CARRY_TREE_MISMATCH`](troubleshooting.md#receipt-carry-tree-mismatch),
which names HEAD's tree and the covered trees. A receipt from before
`head_tree` existed, whose head is not in this clone, is
`RECEIPT_CARRY_REFUSED`: run `/ship` on the new head. A refusal never
rewrites the body. The queue's `ready`, `train build` and `verify --train` read the
carried receipt like any other.
**Delta /ship: eligibility, then checks, then publication.** `/ship` Step 1 runs
+30 -13
View File
@@ -4694,26 +4694,43 @@ rebased; /ship's own rule is to merge the base in, never rebase a reviewed head.
### `RECEIPT_CARRY_REFUSED`
**Meaning.** `gstack-ship-receipt carry` could not carry the PR's /ship receipt
to HEAD: the PR body has no valid receipt, the receipt's head is not an ancestor
of HEAD (a rebase or force-push dropped it), the working tree has uncommitted
or untracked changes (a carry certifies a commit, never a tree that differs from
it), or git could not compare the two product diffs or read the working tree's
status. The receipt in the body is unchanged and still names the old head, so
`gstack-ship-queue ready` refuses `QUEUE_RECEIPT_STALE`.
to HEAD: the PR body has no valid receipt, the working tree has uncommitted or
untracked changes (a carry certifies a commit, never a tree that differs from
it), the receipt's head left HEAD's history and the receipt records no tree to
compare (a receipt written before `head_tree`, whose head is not in this
clone), or git could not compare the two product diffs or read the working
tree's status. The receipt in the body is unchanged and still names the old
head, so `gstack-ship-queue ready` refuses `QUEUE_RECEIPT_STALE`.
**Fix.** Keep the reviewed head in HEAD's history (merge the base in instead of
rebasing), commit or stash every working-tree change, then rerun
**Fix.** Commit or stash every working-tree change, then rerun
`gstack-ship-receipt carry --pr <n>` at the new head. When the history is
already rewritten, run /ship on the new head.
already rewritten and the receipt predates `head_tree`, run /ship on the new
head; next time merge the base in instead of rebasing.
<a id="receipt-carry-tree-mismatch"></a>
### `RECEIPT_CARRY_TREE_MISMATCH`
**Meaning.** The branch was rebased or force-pushed after /ship, so the
receipt's head is no longer in HEAD's history, and HEAD's tree is not a tree
the receipt covers (its `head_tree` or a tree in `carry.trees`). No review saw
this code. The refusal names HEAD's tree and the covered trees; the body is
unchanged.
**Fix.** Run /ship on the new head. A rebase carries only onto a reviewed tree
whose product diff against the new base is unchanged; merging the base in
instead keeps every later carry available
([ship-policy.md](ship-policy.md#receipt-carry)).
<a id="receipt-carry-product-changed"></a>
### `RECEIPT_CARRY_PRODUCT_CHANGED`
**Meaning.** Between the receipt's head and HEAD the product diff against the
base changed: at least one path that is neither a test, release metadata nor
part of the base merge has a different patch than the reviewed head had. The
reviewed verdict does not cover HEAD, so the receipt is not carried. The
refusal names the changed product paths.
base changed: at least one path that is neither a test, release metadata, a
registered output the pregate regenerated fresh, nor part of the base merge has
a different patch than the reviewed head had. After a rebase it means HEAD has
the reviewed tree but the base moved under it, so the diff a reviewer would see
is not the reviewed one. The reviewed verdict does not cover HEAD, so the
receipt is not carried. The refusal names the changed product paths.
**Fix.** Run /ship on the new head. The receipt carries only across a
conflict-only base merge, a test-only change or release metadata, which the
+3 -2
View File
@@ -226,8 +226,9 @@ now names HEAD with a `carry` object holding the `git range-diff` evidence, and
`ship-run.json` gains `carried_from` and `carry_reason`; the `review_loop`
counters are kept, because the reviewed product diff is unchanged. `CARRY:
current` means nothing moved. `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`
or `(RECEIPT_CARRY_REFUSED)` with the refused result line means the product
diff changed or the reviewed head left HEAD's history, so /ship runs again;
, `(RECEIPT_CARRY_TREE_MISMATCH)` or `(RECEIPT_CARRY_REFUSED)` with the refused
result line means the product diff changed, or history was rewritten onto a
tree no review saw, so /ship runs again;
`(RECEIPT_GIT_TOO_OLD)` means the machine's git predates 2.39 and cannot run
`patch-id --verbatim` ([ship-policy.md](ship-policy.md#receipt-carry)).