feat: crew PRs carry their brief's Plan line; ship-queue ready and land check it (GST-1751) (#4118)

* feat: crew PRs carry their brief's Plan line; ship-queue ready and land check it (GST-1751)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat: /ship finish and train ready/land check the Plan line (GST-1751)

finish --plan resolves the brief's Plan line before stage 1; train ready,
base-red ready, train land and stamp land read and write the READY note's
plan record like direct ready and land. Review fixes: grant and exception
rows never pass as plan approvals, render --plan stays short when redacted,
no explicit changelog-only exemption, off mode still records a resolved line.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Review-Finding: p1/red-team#2 reader=red-team
Review-Finding: p1/red-team#3 reader=red-team
Review-Finding: p1/specialist-batched#1 reader=testing
Review-Finding: bin/gstack-approvals:56:functional-contract reader=qa
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: cover the plan check at ready, train ready, land-direct, finish and the policy key (GST-1751)

Review-Finding: p1/specialist-batched#3 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: the Plan line stands for the gate's real state, and a broken store never throws (GST-1751)

A row id from an autoplan run resolves to that run's newest gate revision; a
run id needs the plan approval answered and no decision left unanswered.
readStore skips malformed rows and unreadable files; with the key off the
check never throws or prints. Train land rechecks the Plan line before its
merge on every path. /ship's Step 19-C renders Approvals with --plan;
DRIVER.md back under its size limit.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: train land refuses a slot with no plan record under require (GST-1751)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Step 8 plan discovery (lib/ship-audit.ts and the plan-completion sections) skips the Plan record line, so a later Plan: <path> binding is still found; render --plan refuses a line it cannot parse. The review skill's pins drop by 4 bytes.

Review-Finding: p1/red-team#1 reader=red-team
Review-Finding: scripts/resolvers/plan-gates.ts:206:functional-contract reader=qa
Review-Finding: p1/red-team#3 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog fragment passes the prose lint (GST-1751)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: captain.md describes the READY PLAN: line (GST-1751)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: plan discovery splits the Plan line on whitespace and skips record kinds in any case (GST-1751)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: lower the gstack-review size pins after the Step 8 binding shrank 4 bytes (GST-1751)

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:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-07 09:53:38 +00:00
1 parent fd0e3390e9
commit 901f246d74
44 files changed
+870 -46

No files matched your search

+10 -3
View File
@@ -256,8 +256,9 @@ not a new ask ([carry-forward](unattended.md#carry-forward)).
what it covers; a gate answered with `--words-by agent:<id>` is listed under that agent as its own decision, not the owner's words); `gstack-approvals render` puts it in the report and the PR body. Before
training the PR the controller compares that row's words and `gate_rev` with the ask it
relayed and the reply it carried back; a row that does not match is not READY (shape A).
The brief's done line names the row. A plan approval is not a READY field; the
[READY contract](#the-ready-contract) is unchanged.
The brief's done line names the row. The READY block carries the resolved plan as its
`PLAN:` line, which the [READY template](../captain/templates/READY.md) refuses when it is
missing, says `warn`, or names another plan than the brief's `Plan:` line.
**Exemptions.** Read-only lanes (triage, research, measurement, a review voice), the daily
flaky pass's repairs, slot stamps, CHANGELOG-only conflict resolves and a size or budget
@@ -271,8 +272,14 @@ run, `--kind feature`: one reviewer reading the plan and the draft code together
revision and no carry check (docs/unattended.md). `--full` brings back the full run.
A crew may add exemptions
in its rules file with the owner's words and date; it does not remove these. The brief
says which applies, `Plan: required | approved: <run> | exempt: <reason>`, so an
says which applies, `Plan: required | approved: <run id or approval row id> | exempt: <reason>`, so an
already-approved plan is not planned twice and exempt work does not wait on an ask.
The lane copies that line into the PR body (`gstack-approvals render --plan "<line>"` under
`## Approvals`), and `gstack-ship-queue ready` prints `PLAN:` and records it in the READY note
(docs/ship-policy.md#plan-record): an `approved:` run id or row id must resolve to the owner's
words, never a gate the lane answered itself; an `exempt:` reason must be on this list or a
crew's `crew:<rule>`. Crew sessions warn by default; `plan_record: require` or
`GSTACK_PLAN_RECORD=require` makes ready and land refuse.
The copy-paste brief is [`captain/templates/brief.md`](../captain/templates/brief.md): the
plan and the ask as step 1, the build after the word as step 2, /ship to READY as step 3,
+1
View File
@@ -80,6 +80,7 @@ branch that changes the policy.
| `bookkeeping` | glob list | `[]` | <a id="bookkeeping"></a>Root-anchored globs of bookkeeping tables a verify or ratchet step rewrites (size baselines, serial lists, limits), each ending in `.tsv`, `.csv` or `.txt` and never under `.gstack/` or `.github/` (`["scripts/*-baseline.tsv"]`). A matching file changed at most 20 lines after review binds, first touched or not, takes finish's [one scoped reader](review-policy.md#bounded-post-review-edits) instead of Steps 5–11.5; the reader still reads every changed row. Read from the base revision only. Needs `min_gstack_version`. |
| `mechanical_retake` | `[{ path, template, fields, regen }]` | `[]` | A version or rules constant a branch may have to retake after a base merge, next to `bookkeeping`'s bounded kind: `path` is one literal file, `template` the whole line with one `{n}` (`const RULE_NUMBERS := {n}`), `fields` the `{ glob, field }` fixture fields that carry the same value, and `regen` the [`.gstack/generated.json`](pregate.md#generated) entry ids whose command regenerates its goldens. A post-review edit finish proves to be only that retake routes as `mechanical=`, with no reader and no review round; see [mechanical retakes](#mechanical-retake). At most 8 constants and 16 fields or ids each; no glob, `..`, `.gstack/` or `.github/` path. Read from the base revision only. Needs `min_gstack_version`. |
| `test_code` | glob list | `[]` | Root-anchored globs of test code the built-in test names miss, never under `.gstack/` or `.github/`, and never matching a file there (`["tests/**/*.gd"]` for GDScript checks). A matching file changed after review routes as test code, one scoped reader of its test hunks, instead of behavior, beside the built-in globs; shared `helpers/` and `fixtures/` files stay behavior. See [test code and Godot UID sidecars](#test-code). Read from the base revision only. Needs `min_gstack_version`. |
| `plan_record` | `off` \| `warn` \| `require` | `off` | <a id="plan-record"></a>Whether `gstack-ship-queue ready` and `land` (direct, stamp and train land) and `/ship`'s `gstack-ship-release finish --plan "<line>"` check the PR body's `Plan:` line (GST-1751). The line is the brief's (`Plan: approved: <run id or row id>`, `Plan: exempt: <reason>` or `Plan: required`), written under `## Approvals` by `gstack-approvals render --plan`. `ready` resolves it with lib/plan-record.ts against every branch's approval rows on the lane machine: a run id needs the newest gate revision's plan approval answered `approved`, in the owner's words, with no decision of that revision left unanswered; a row id needs an approved row that is not a ruling or grant, and a row from an autoplan run stands for that run's gate the same way; a broken or unreadable store refuses (`PLAN_STORE_UNAVAILABLE`) and never throws, and under `off` changes nothing; an exemption must be one docs/captain.md lists or `crew:<rule>`; a diff of changelog fragments only needs no line. `finish` resolves the brief's line the same way before its first stage. `ready` (train ready included) prints `PLAN:` and writes the resolved record into the READY note; `land` checks that the note carries the PR body's current line and never reads the store. `warn` prints and continues; `require` refuses (`PLAN_RECORD_MISSING`, `PLAN_UNRESOLVED`, `PLAN_NOT_APPROVED`, `PLAN_EXEMPT_UNKNOWN`, `PLAN_STORE_UNAVAILABLE`, `QUEUE_PLAN_MISMATCH`). A Capy session (`GSTACK_CAPY=1`) under `off` gets `warn`; `GSTACK_PLAN_RECORD=require` is a crew's opt-in. gstack never infers a feature from the diff, title or changelog. Read from the base revision only. Needs `min_gstack_version`. |
| `ci_record_timeout_s` | integer seconds, at least 60 | `600` | <a id="ci-record-timeout"></a>How long `gstack-ship-release accept` lets one `gstack-evidence record-ci` run before it stops that lane. A value over 3600 is clamped to 3600. accept prints the bound it used as `CI_RECORD_TIMEOUT: <n>s (default)`, `(origin/<base> ci_record_timeout_s)` or `(… <value> is over the 3600s ceiling)`. Raise it when record-ci runs long, for example when it reruns a per_file remainder of CI-skipped files here (harness-evals #271 needed about 745 s). Read from the base revision only, so a branch can't raise its own bound. Needs `min_gstack_version`. |
| `base_red` | `{ setup, select, timeout_sec }` or `null` | `null` | How `/ship` Step 5 proves a failure is the base branch's own before pausing on it; see [base_red](#base-red). Read from the base revision only. Needs `min_gstack_version`. |
| `changelog_fragments` | `{ dir, required_checks? }` or `null` | `null` | PRs carry unversioned [CHANGELOG fragments](#changelog-fragments) in `dir` (one flat, repo-relative directory, no `..`) and the merge step ([`land`](#land)) assigns the version (under `merge: direct`, the [release PR](#release-stamp) does). `required_checks` (up to 50 distinct names) are the checks [`train land`](#stamped-trains-under-changelog_fragments-train-land) requires present and green on a stamp commit. `null` keeps `[Unreleased]` sections and per-PR stamps. Needs `min_gstack_version`. |
+68
View File
@@ -2094,6 +2094,74 @@ all the command prints `SHOT_PNG: unavailable (...)` and exits 0 instead.
**Fix.** Screenshot `shot.html` with your own browser at 880 px wide, or use `text.md`.
<a id="plan-record-missing"></a>
### `PLAN: ... (PLAN_RECORD_MISSING)`
**Meaning.** The PR body has no `Plan:` line, so the PR reads as `Plan: required`
(docs/captain.md, Plan before build). gstack never guesses from the diff, title or
changelog whether a change is a feature. Under `plan_record: warn` (the Capy crew default)
`gstack-ship-queue ready` prints the line and continues; under `require` it refuses.
**Fix.** Copy the brief's Plan line into the PR body:
`gstack-approvals render --plan "Plan: approved: <run id>"` (or `exempt: bug fix (R65)`) prints it
under `## Approvals`. A PR whose only changes are changelog fragments needs no line.
<a id="plan-unresolved"></a>
### `PLAN: ... (PLAN_UNRESOLVED)`
**Meaning.** `Plan: approved: <token>` names no approval row on this machine. A run id must
match a row source `autoplan:<id>@gate_rev=<n>` in any branch's file of the repository's
approval store (the state root plus every Drive mirror); a row id must exist and be a plan
approval, not a `ruled_note`, `review_loop_grant` or `test_lane_exception` row.
**Fix.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` after the owner's answer,
or `gstack-approvals add` with their exact words for a plan approved outside /autoplan, then
cite the run id or the row id it printed.
<a id="plan-not-approved"></a>
### `PLAN: ... (PLAN_NOT_APPROVED)`
**Meaning.** The line says `required`, the newest gate revision's row for the cited run is
pending or rejected, or the gate was answered in an agent's own words (`--words-by`, shown
as "its own decision (not the owner's words)"). A lane approving its own plan is the skip
this check exists to catch (GST-1751).
**Fix.** Relay the gate to the owner through the Captain, answer it with their exact words
(`gstack-autoplan answer … --owner-reply "<words>"`), import it, and cite the run.
<a id="plan-exempt-unknown"></a>
### `PLAN: ... (PLAN_EXEMPT_UNKNOWN)`
**Meaning.** `Plan: exempt: <reason>` names a reason outside the list: `bug fix`, `s1`,
`flaky`, `stamp`, `changelog-resolve`, `cap-repin`, `audited-list`, `read-only`,
or `crew:<rule>` for an exemption a crew's rules file records. A trailing
parenthetical such as `(R65)` is ignored. A diff of changelog fragments only needs
no line.
**Fix.** Use the exemption the brief names. A change none of them covers is a feature
and needs `Plan: required` and a plan run.
<a id="plan-store-unavailable"></a>
### `PLAN: ... (PLAN_STORE_UNAVAILABLE)`
**Meaning.** The line cites an approval, but this machine has no approval store: no
state-root `approvals/` directory and no Drive mirror. That is reported apart from a
missing row, because nothing could be read.
**Fix.** Run `gstack-ship-queue ready` on the lane machine that imported the approval, or
`gstack-approvals sync` there. `land` checks the plan record `ready` wrote into the READY
note and does not need the store.
<a id="queue-plan-mismatch"></a>
### `gstack-ship-queue land: ... (QUEUE_PLAN_MISMATCH)`
**Meaning.** Under `plan_record: require`, land found a READY note with no plan record
(written before the key, or by a `ready` that refused nothing in warn mode), or a note
whose Plan line differs from the PR body's current line.
**Fix.** The lane fixes the PR body's Plan line if needed and reruns
`gstack-ship-queue ready --pr <n>` at the pin.
## /ship token budget and fresh-context handoffs (gstack-ship-budget)
The flow, the BUDGET line field by field and the meters are in [ship-budget.md](ship-budget.md).