mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 11:57:19 +01:00
The lander must accept each owner-approved gate change before a PR merges (GST-2491) (#4304)
* Let an owner recorded approval close a gate finding in unattended ship (GST-2491) An unattended /ship logs every gate finding open and nothing could close it, so a PR with an owner-approved gate change could never finish. gstack-approvals gate records the owner disposition outside the run; it closes only that fingerprint on a review row whose range holds the head, and finish, the receipt, the PR body and READY all carry it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep the pinned review brief unchanged; document the resume steps for a dispositioned gate finding (GST-2491) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Bind a gate disposition to its gate change and show it as kept to the rest of /ship (GST-2491) A disposition now names the finding's gate_id as well as its fingerprint, so a later loosening of the same line stays open. gstack-review-read shows a closed gate finding as kept with the disposition's approval id, so the existing --blocking-open, --gate-open, draft and Gate edits rules carry it without prose changes, and an unreadable approval record applies no disposition instead of failing the reader. Review-Finding: p1/red-team#1 reader=red-team Review-Finding: s11/outside-voice#1 reader=outside-voice Review-Finding: s11/outside-voice#2 reader=outside-voice Review-Finding: s11/adversarial-native#1 reader=adversarial-native Review-Finding: s11/adversarial-native#2 reader=adversarial-native Review-Finding: s11/adversarial-native#4 reader=adversarial-native Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * List the Gate disposition line in the receipt and prose troubleshooting entries (GST-2491) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Require the lander to accept each owner gate disposition before merging (GST-2491) Land and a new read-only verify --pin re-derive the dispositions the approval store applies at the pin and merge only with --accept-gate-disposition for each one. The gate writer refuses while any worktree holds an open /ship run on the branch, under a lock; a row counts only in the writer's full shape with its digest; receipt write needs --body when a disposition applies; ready and land cross-check the receipt and body against the store. Supersedes #4299. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Regenerate the result-code anchors for the gate-disposition refusal (GST-2491) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Always record the receipt's gate dispositions and refuse a store that disagrees at merge (GST-2491) The receipt always carries gate_dispositions (an empty list when none apply) and write refuses on an unreadable store or a git error. Land, verify and ready refuse a receipt id the merge-time store lacks, naming the likely wrong state root or mirror, and a field-less receipt beside an applied row. A git ancestry error while deriving dispositions refuses instead of reading as not applied. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Count a disposition whose head a rewrite removed as not applied, and expect the receipt to end with gate_dispositions (GST-2491) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Trim and check the gate disposition recorder name, and let a parked run's ended lease stop blocking it (GST-2491) Review-Finding: s11/outside-voice#3 reader=outside-voice Review-Finding: s11/outside-voice#2 reader=outside-voice 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>
This commit is contained in:
1 parent
842fef1f95
commit
b1f78301db
28 files changed
+1050
-38
No files matched your search
@@ -1718,6 +1718,103 @@ Pre-Landing Review and the PR body, `gstack-ship-receipt write` records them as
|
||||
`ruled_notes` (each with its `head`) and refuses a `--body` that does not carry each
|
||||
line word for word, and the READY block prints each as `RULED_NOTE:`.
|
||||
|
||||
<a id="gate-dispositions"></a>
|
||||
**Gate dispositions (GST-2491).** A ruling never closes a gate finding (a loosened
|
||||
gate the review logged with a `gate` tag and the hunk identity `gate_id`). In an
|
||||
interactive session the human answers `review-gate-disposition`; in an unattended
|
||||
one `gstack-review-log` logs every gate finding `open`, and the log stays so. When
|
||||
the owner approved the gate change, the owner's answer goes through the approval
|
||||
record instead, recorded outside the lane's /ship run at the stop head, one finding
|
||||
per row:
|
||||
|
||||
```bash
|
||||
gstack-approvals gate --finding '<the gate finding's fingerprint>' --gate-id <its gate_id> --head <stop head> \
|
||||
--reason '<the gate change, one line>' --words '<the owner's exact words>' --by '<who recorded>'
|
||||
```
|
||||
|
||||
The row is an approved `gstack-approvals` row with scope `gate_disposition
|
||||
<fingerprint> <gate_id> <head>`, ask `gate_disposition <fingerprint> <gate_id> at
|
||||
<head>: <reason>` and the owner's words in `words` (`lib/gate-dispositions.ts`).
|
||||
`gate` refuses a missing `--gate-id` (or a literal `undefined` or `null`), empty
|
||||
`--words`, a reason, words or `--by` with a newline or control character (each is
|
||||
trimmed first), a `--head` that is not a commit, a `--by` naming gstack, a row that
|
||||
`dispositions` would not list (a head that is not 40 hex, for one), and any call
|
||||
while a /ship run is open on the branch in any worktree of the repository: its
|
||||
marker counts while its lease is open, the same test preflight uses, so a run parked
|
||||
before finish stops counting once its 120-minute lease ends. It finds those worktrees
|
||||
through the git common dir, and it checks and appends under a lock there that
|
||||
/ship's preflight also takes to write its marker. `add` refuses the scope and
|
||||
`--source gate`, so `gate` is the only writer. A row counts only when it has every
|
||||
field `gate` writes: an approval-shaped id that its hash reproduces, `source: gate`,
|
||||
`pending: none`, the store's own branch, `date`, `recorded_at`, `by`, one-line
|
||||
`words` and a `digest` over those fields. The digest catches a row that was edited
|
||||
or typed by hand. It isn't authentication: anyone who can read the code can compute
|
||||
it. A gate finding with no `gate_id` is never closed, and a row gstack wrote is
|
||||
ignored. A
|
||||
disposition closes a gate finding only on a review row that carries that exact
|
||||
fingerprint and `gate_id` and whose `range_base..commit` holds the head (a first
|
||||
pass, with no `range_base`: its own commit), so a later, different loosening of the
|
||||
same line (a new `gate_id`) stays open. Anything else closes nothing, and a
|
||||
review-log field (`kept`, `reason`, `disposition`, `gate_dispositions` or any claim
|
||||
the lane writes on its row) never makes one. `gstack-review-read` shows each closed
|
||||
finding as `kept` with `reason: owner disposition <id>: <reason>` and
|
||||
`disposition: <id>`, lists the ids in the row's `gate_dispositions` and grades the
|
||||
row as if the findings were kept, so /ship's gate rules (`--blocking-open`,
|
||||
`--gate-open`, the draft state, the `Gate edits:` counts) read them as kept once it
|
||||
reads the row again. As with rulings, a row derived from a findings file
|
||||
(`status_source: derived`) is not cleared. Nothing is hidden: finish prints each as
|
||||
`GATE_DISPOSITION: <fingerprint> [gate <gate_id>] at <head12> by <who> at <time>:
|
||||
<reason> — "<words>" (approval <id>)`, ending `(… not applied)` when no review row's
|
||||
range holds it; `gstack-approvals dispositions` prints one `Gate disposition: …`
|
||||
line each for the PR body; `gstack-ship-receipt write` records them as
|
||||
`gate_dispositions` and refuses a `--body` that does not carry each line word for
|
||||
word; and the READY block prints each as `GATE_DISPOSITION:`
|
||||
([troubleshooting](troubleshooting.md#gate-finding-open-unattended)).
|
||||
|
||||
<a id="gate-disposition-acceptance"></a>
|
||||
**Who authorizes a disposition.** A lane is a same-user agent, so it can still forge
|
||||
a well-formed disposition row in its own approval store. What authorizes a
|
||||
disposition is the lander's explicit acceptance: `gstack-ship-queue land --pin`
|
||||
merges one only when the lander passes `--accept-gate-disposition <approval id>`
|
||||
for it.
|
||||
|
||||
Land works the dispositions out again at the pin from the PR branch's approval
|
||||
store: the local record and its mirror. A lander on another machine reads the
|
||||
mirror the owner recorded into, so set `GSTACK_APPROVALS_DIR` to that directory
|
||||
when it isn't the default. Land counts every disposition whose head the pin
|
||||
contains. In a full clone, a row whose head commit is gone (the branch was
|
||||
rewritten and garbage-collected, or the name reused) doesn't apply, because the
|
||||
pin's history is all there and can't contain it. A pin missing from the clone
|
||||
refuses, and so does a shallow clone that can't prove a row's ancestry: deepen it
|
||||
with `git fetch --unshallow` and rerun. It never trusts the receipt or the PR body for this, because the lane
|
||||
writes both. It prints each one as a `GATE_DISPOSITION:` line, then
|
||||
`GATE_DISPOSITIONS: applied=<ids> accepted=<ids> store=<file>`. It refuses with
|
||||
[`QUEUE_GATE_DISPOSITION`](troubleshooting.md#queue-gate-disposition), and merges
|
||||
nothing, when an applied disposition isn't accepted, an accepted id isn't applied,
|
||||
the receipt's `gate_dispositions` leave one out, change one or add one, or the PR
|
||||
body lacks its `Gate disposition:` line. The review rows stay on the lane's
|
||||
machine, so land can't tell which finding a disposition closed. It asks the lander
|
||||
to accept every disposition the store holds for the pin, which is never fewer.
|
||||
|
||||
`gstack-ship-queue verify --pr <n> --pin <sha> [--accept-gate-disposition <id>]...`
|
||||
runs the same check read-only (`pinAcceptance`, the function land calls), for a
|
||||
lander that merges some other way, such as a REST merge pinned to the head. It
|
||||
exits 0 only when every disposition is accepted. `ready` prints the same lines and
|
||||
refuses the same receipt and body mismatches, but asks for no acceptance.
|
||||
`gstack-ship-receipt write` refuses to write a receipt while a disposition applies
|
||||
at HEAD unless `--body` carries each `Gate disposition:` line.
|
||||
|
||||
The receipt always records `gate_dispositions`, as an empty list when none
|
||||
applies, and write refuses, writing nothing, when the store or git can't be read.
|
||||
Land, verify and ready refuse when the receipt's set and the store's set at the pin
|
||||
differ either way. A receipt id the store lacks usually means the lander reads
|
||||
another `GSTACK_STATE_ROOT` or approvals mirror than the owner recorded into, or
|
||||
that the row was removed. A git error while checking a head refuses too. A receipt
|
||||
with no `gate_dispositions` field was written before receipts recorded it, and it
|
||||
lands only when the store applies nothing. The receipt is written by the lane, like
|
||||
every other gate signal it carries. The lander's acceptance guards against a
|
||||
mistaken or missing store, not against a lane that falsifies its own receipt.
|
||||
|
||||
<a id="test-lane-exceptions"></a>
|
||||
**Test-lane exceptions (GST-1595).** A lane `accept` carries from CI refuses with
|
||||
`inventory_uncovered` when an inventory file is in no run's `tree.json`; when bun
|
||||
@@ -2512,6 +2609,7 @@ release-file rows):
|
||||
| the PR head is the pin | [`QUEUE_LAND_PIN_MOVED`](troubleshooting.md#queue-land-pin-moved) |
|
||||
| the PR body's /ship receipt is bound to the pin | `QUEUE_RECEIPT_MISSING`, `QUEUE_RECEIPT_STALE` |
|
||||
| the receipt's `base` is the queue's base | [`QUEUE_RECEIPT_BASE_MISMATCH`](troubleshooting.md#queue-receipt-base-mismatch) |
|
||||
| every owner gate disposition the approval store applies at the pin is accepted with `--accept-gate-disposition <id>`, every accepted id is applied, and the receipt and PR body list exactly those ([gate dispositions](#gate-disposition-acceptance)) | [`QUEUE_GATE_DISPOSITION`](troubleshooting.md#queue-gate-disposition) |
|
||||
| a READY note from `ready` exists at the pin | `QUEUE_NOT_READY` |
|
||||
| no freeze is active (with `--repair`: one is, and its repair slot is free or already this pin; a backstop freeze left from before `release_gate` only warns; see [direct freeze](#direct-freeze)) and no land lock is held | [`QUEUE_TRAIN_FROZEN`](troubleshooting.md#queue-train-frozen), `QUEUE_NOT_READY`, `QUEUE_LAND_LOCKED` |
|
||||
| VERSION, the `package.json` version, the CHANGELOG and `.gstack/version-path` are unchanged since the merge base | [`QUEUE_LAND_STAMPED`](troubleshooting.md#queue-land-stamped) |
|
||||
|
||||
+61
-1
@@ -3727,9 +3727,12 @@ results, prereg or eval path, and names no money, security, secret or auth topic
|
||||
needs the reader again (GST-1195). With `--body`, write also refuses, writing
|
||||
nothing, a body that does not carry each line the receipt requires as one line,
|
||||
word for word: every `Ruled note: …` line `gstack-approvals rulings` prints,
|
||||
every `Gate disposition: …` line `gstack-approvals dispositions` prints (and while
|
||||
a gate disposition applies at HEAD, write refuses to run without `--body`),
|
||||
finish's `MECHANICAL:` lines as `Mechanical retake: …`, and accept's
|
||||
`TEST_LANE_EXCEPTION:` lines as `Test-lane exception: …`
|
||||
([ruled notes](ship-policy.md#ruled-notes),
|
||||
[gate dispositions](ship-policy.md#gate-dispositions),
|
||||
[test-lane exceptions](ship-policy.md#test-lane-exceptions)).
|
||||
|
||||
**Fix.** Regenerate it with `gstack-ship-receipt write`; never hand-edit the
|
||||
@@ -5777,6 +5780,33 @@ doesn't describe what would merge. Nothing was merged.
|
||||
**Fix.** The lane runs /ship and `ready --pr <n> --no-screen` at the new head
|
||||
and sends a new READY; land that pin.
|
||||
|
||||
<a id="queue-gate-disposition"></a>
|
||||
### `QUEUE_GATE_DISPOSITION`
|
||||
|
||||
**Meaning.** The approval store holds an owner gate disposition that applies at the
|
||||
pin, and `land --pin` or `verify --pin` wasn't told to accept it. Or an
|
||||
`--accept-gate-disposition` id names no disposition the store applies there. Or the
|
||||
receipt's `gate_dispositions`, or the PR body's `Gate disposition:` lines, differ
|
||||
from the store (`ready` refuses those too). Nothing was merged. The
|
||||
`GATE_DISPOSITION:` lines above the refusal are what the store holds at the pin;
|
||||
the receipt and the body aren't trusted for this, because the lane writes them.
|
||||
A receipt id the store lacks usually means this lander reads another
|
||||
`GSTACK_STATE_ROOT` or approvals mirror than the owner recorded into, or that the row
|
||||
was removed after the receipt was written. A pin missing from the clone, a shallow
|
||||
clone that can't prove whether a row's head is in the pin's history, and any other
|
||||
git error while checking a head also refuse here. In a full clone, a row whose head
|
||||
commit is gone after a rewrite and gc just doesn't apply.
|
||||
|
||||
**Fix.** Read each `GATE_DISPOSITION:` line: the fingerprint, gate id, head, who
|
||||
recorded it, the reason and the owner's words. If the owner did approve that gate
|
||||
change, rerun with `--accept-gate-disposition <approval id>` for each one. If not,
|
||||
don't accept it; the lane restores the gate. When the store can't be read, or holds
|
||||
none while the receipt lists some, point `GSTACK_APPROVALS_DIR` at the directory the
|
||||
owner recorded into. In a shallow clone, run `git fetch --unshallow` and rerun.
|
||||
When the receipt or the body is what differs, the lane reruns
|
||||
/ship and `ready` at the pin. See
|
||||
[gate dispositions](ship-policy.md#gate-disposition-acceptance).
|
||||
|
||||
<a id="queue-land-checks"></a>
|
||||
### `QUEUE_LAND_CHECKS`
|
||||
|
||||
@@ -6860,6 +6890,36 @@ PR whose older receipt lost its parent branch, or a depth, targeted or exception
|
||||
refusal, run /ship on the new head. When `--head` is not the checked-out HEAD,
|
||||
check it out and carry there.
|
||||
|
||||
<a id="gate-finding-open-unattended"></a>
|
||||
### An unattended /ship stops on an open gate finding
|
||||
|
||||
**Meaning.** The review logged a loosened gate as a gate finding, and an unattended
|
||||
session cannot answer `review-gate-disposition`, so `gstack-review-log` stored it
|
||||
`"action":"open","actor":"unattended"` and the repeat decision printed `NEXT: stop
|
||||
(a blocking finding is still open …)`. A ruling (`gstack-approvals rule`) never
|
||||
closes a gate finding, and nothing the lane writes on its review row does either.
|
||||
|
||||
**Fix.** When the owner approved the gate change, record the owner's answer at the
|
||||
stop head the review read, with the `fingerprint` and `gate_id` from the review row,
|
||||
while no /ship run is open on that branch in any worktree (a parked run's marker stops
|
||||
counting once its 120-minute lease ends, as it does for preflight):
|
||||
|
||||
```bash
|
||||
gstack-approvals gate --finding '<fingerprint>' --gate-id <gate_id> --head <stop head> \
|
||||
--reason '<the gate change, one line>' --words '<the owner's exact words>' --by '<who recorded>'
|
||||
```
|
||||
|
||||
Then resume the parked /ship at the same head with no push, starting by reading
|
||||
`gstack-review-read` again: the finding now reads `kept` with its `disposition`, the
|
||||
row reads `CURRENT`, so the repeat decision has no `--blocking-open` for it, Step 17
|
||||
leaves it out of `--gate-open` and the PR is no draft on its account. finish prints
|
||||
`GATE_DISPOSITION: …`, and the PR body carries the `Gate disposition: …` line
|
||||
`gstack-approvals dispositions` prints. When the owner did not approve it, restore
|
||||
the gate. The lander then accepts each disposition by its approval id:
|
||||
`gstack-ship-queue land --pr <n> --pin <sha> --accept-gate-disposition <id>`, or
|
||||
`gstack-ship-queue verify` with the same flags before a merge that doesn't go
|
||||
through land. See [gate dispositions](ship-policy.md#gate-dispositions).
|
||||
|
||||
<a id="fix-check-open"></a>
|
||||
### `FIX_CHECK_OPEN`
|
||||
|
||||
@@ -7027,7 +7087,7 @@ against `gate.passed: 41`) with a different value. The receipt is written from
|
||||
the run; the prose was typed, so a reader of the body would learn a number the
|
||||
run did not produce. The `PROSE:` line names each field with both values. The
|
||||
`## Approvals` table `gstack-approvals` renders, the `Review-loop exception: …`,
|
||||
`Documentation exception: …`, `Ruled note: …`, `Mechanical retake: …` and
|
||||
`Documentation exception: …`, `Ruled note: …`, `Gate disposition: …`, `Mechanical retake: …` and
|
||||
`Test-lane exception: …` lines the receipt requires, and the inside of a
|
||||
closed code fence are never read as claims.
|
||||
|
||||
|
||||
Reference in new issue
Block a user