Files
gstack/ship/sections/pr-body.md.tmpl
T
Garry Tanandcapy-ai[bot] 6665e20d56 v1.91.236.0 feat(ship-queue): require a current /ship receipt before READY, train build and train merge (#3438)
* feat(ship-queue): require a current /ship receipt before READY, train build and train merge

Garry, 2026-10-25: use the gstack ship skill before merging PRs. ready printed
SHIP_RECEIPT: none as information and returned ok, so the pool was READY'd without
/ship. Now ready, train build (dry run included) and verify --train refuse with
QUEUE_RECEIPT_MISSING or QUEUE_RECEIPT_STALE from the PR body alone; verify accepts
the receipt written for the pinned head when the current head is its stamp commit.

gstack-ship-receipt result is /ship's terminal step (GSTACK_RESULT: skill=ship,
run dir with the head and receipt), unattended /ship may update an existing PR's
body, and INSTALL_UNVERIFIED with no verdict names the project directory to
install from (GSTA-85).

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

* test(ship): pin Step 22 in workflow-required-steps and tighten the ship skeleton

Gate edit: test/fixtures/workflow-required-steps.json pins the new
"## Step 22: Terminal line (every run)" heading (strengthens: a later
edit that drops the terminal line turns the required-steps test red).
Steps 14, 16, 18, 20 and 22 prose tightened so the carve-guards raise
shrinks to 81,600 / 1.6740 (measured skeleton 81,535, union 314,027).

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

* fix: pre-landing review fixes (ready at the stamp commit, screen on a stale merge ref, ship-run.json)

- ready accepts the pin's receipt at a slot's stamp commit (plus the slot-2
  merge of main), as verify --train already did; a real edit stays STALE
- ready --screen refuses QUEUE_SCREEN_STALE_REF when refs/pull/<n>/merge is
  missing or merges an older head; a finished screen that planned 0 cases is
  never followed, and train build seats that slot unscreened (GSTA-95 on #3431)
- gstack-ship-receipt result writes ship-run.json (a /ship record, not a run
  v1 manifest), validates --head, and prints status=incomplete on gh failures
- bindReceipt takes an optional pr and renders one-line SHIP_RECEIPT lines
- tests for every state-matrix cell the review named; the train fixture's
  merge ref is a real merge commit

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

* fix: cycle-2 review fixes (private screen scratch, no bare-merge receipt reuse, 12-hex --head, one receipt scrub) and align the unattended hook text with the GSTACK_RESULT writers (GSTA-91)

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

* fix: adversarial-review fixes (result refuses an unpushed HEAD, resolves --out and reports unwritable records; ready names a failed merge-ref fetch; unattended /ship never runs gh pr ready)

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

* fix: close review-cycle gaps (a stamp must change a stamp file, result requires a full --head and fails closed without headRefOid, digits-only --pr) with tests for every cycle-3 branch

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

* test: give the fsck-rejected merge-ref spawns a timeout (spawnsync tripwire)

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

* fix: detect a hollow screen from the plan it uploads (no paid-plan artifact or nothing planned), correct two troubleshooting facts, and normalise result --pr

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

* fix(hollow-screen): read manifest.json by name, list artifacts by name, say when the plan is unreadable; lowercase result --head

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

* test(hollow-screen): expired and unreadable run rows, uppercase --head; docs name every SCREEN: literal

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

* fix(train build): say when the repository slug is unreadable instead of counting the screen silently

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

* fix: name an ls-remote failure in ready --screen, refuse result --pr with --body, cap echoed receipt errors, tell an unreadable install verdict from a missing one

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

* test: ls-remote refusal, --pr with --body, capped receipt errors, unreadable slug and verdict (both twins); docs name the new literals

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

* fix: scrub Unicode line separators from receipt text, report an unreadable --body as incomplete, refuse unsupported verdict schemas in the bash twin

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>
2026-10-26 09:03:59 +00:00

258 lines
14 KiB
Cheetah

### Prepare the title (Step 18)
Prepare the title in a private file; Step 19 posts that file, never a shell string:
{{FREE_TEXT_FILE:TITLE_FILE=pr-title}}
1. For an existing open PR/MR (not Step 6.5's early PR, which takes item 2), fill the file from the platform, substituting the
printed name, the matched number and `NEW_VERSION`; no title text passes through you:
```bash
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
gh pr view <pr-number> --json title -q .title | ~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh <new-version> --stdin > "$TITLE_FILE" || exit 1
```
GitLab reads `glab mr view <pr-number> --output json | jq -r .title` into the same pipe.
2. For a new PR/MR, write `v<NEW_VERSION> <type>: <summary>` into the file with your file-write tool.
3. Every created or updated title MUST start with `v$NEW_VERSION `; check the file with
`grep -Eq '^v<new-version>( |$)' "$TITLE_FILE"` and never publish an unprefixed title.
4. **NO_VERSION:** replaces items 1-3: keep an existing title (the platform title
piped straight into the file), or write `<type>: <summary>`; no version prefix.
Save the title file name for Step 19.
---
## Step 19: Create PR/MR
Recheck Step 18's PR/MR lookup and record it. Errors or ambiguous matches STOP publication.
If the open PR/MR or title changed, repeat Step 18's identity/title preparation,
then return here for a new lookup and a fresh body before publishing.
### Resolve Linked Spec before composing the body
1. Resolve the archive directory and branch:
```bash
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG)
CURRENT_BRANCH=$(git branch --show-current)
SPEC_ARCHIVES="$GSTACK_STATE_ROOT/projects/$SLUG/specs"
```
2. Read archive frontmatter as data, never shell source. Select an exact
`spec_branch` match to `CURRENT_BRANCH`; among matches use the newest
`spec_filed_at`. Never infer an issue number from a branch name. If no readable
match or positive integer `spec_issue_number`, omit only `## Linked Spec` and
continue composing the PR. Resolve ambiguous matches before linking an issue.
3. Compare that spec's acceptance criteria with Step 8's results. Only fully
completed Step 8 plan scope permits `Closes #N`, with every spec criterion
verified. Partial, deferred, failed, dropped or unverified scope uses `Linked to #N`
and names the remaining work; never auto-close it. Include the archive filename
and `spec_filed_at`, not a private absolute path; `gstack-post` scans them with the body.
### Issue links
Run `~/.claude/skills/gstack/bin/gstack-issue-links --base <base> --out .gstack/tmp/issues.json`, adding
`--issue <n>` per issue the user named as fixed and `--ci-run <id>` per failing CI run. Interactive:
ask once about its `(promote: fix #N)` references (each `fix #<n>` reply adds `--issue <n>`), and add
`--supersedes <n>`/`--credit <n>` for a `Related:` PR only when the owner confirms; rerun after any change.
The completion message lists unpromoted candidates, `Related:` lines and any `ISSUE_LOOKUP_FAILED`.
### Capy
Run `[ "${GSTACK_CAPY:-}" = 1 ] && echo 'CAPY_PR: on' || echo 'CAPY_PR: off'`. On `CAPY_PR: on`, read
`~/.claude/skills/gstack/ship/sections/capy-pr.md` now: the body opens with its `## What changed`,
`## Why` and `## Approvals` before the sections below. On `CAPY_PR: off`, skip it.
The PR/MR body should contain these sections (never reuse a prior run's body):
```
## Summary
<Read `git log origin/<base>..HEAD --oneline`. Group every substantive commit by
theme, excluding VERSION/CHANGELOG bookkeeping. Do not paste the commit list.>
## Test Coverage
<coverage diagram from Step 7, or "All new code paths have test coverage.">
<If Step 7 ran: "Tests: {before} → {after} (+{delta} new)">
<If Step 7 ran: "Coverage: {X}% value-weighted ({Y}% including {W} weakly covered paths)">
<If Step 7 ran: "Test value: {K} tests written, {R} rejected by the authoring gate, {E} existing tests extended, {W} paths weakly covered (weak = ★, gate-failing or unrated)." Use the singular noun for a count of 1 ("1 test written", "1 existing test extended", "1 path weakly covered").>
<Weak paths and leftover gaps as proposed tests with value cards; each regression test's
"Regression proof — fails at HEAD · passes at base · passes after fix" line; Test value
details for cards whose file type has no known comment syntax.>
<One line per Step 5 lane: "Tests ran: <lane> N" from its receipt's `tests_ran`, or
"Tests ran: <lane> count unavailable" when the runner summary was not recognised. A
ZERO-RUN lane never reaches this body unresolved.>
## Pre-Landing Review
<findings from Step 9 code review, or "No issues found.">
<Gate edits: Step 9's latest `Gate edits:` line verbatim — `none detected (...)`,
`N listed, M read, K findings — J kept, R restored, O open`, `partial (...)` or
`UNAVAILABLE — ...`; never `none detected` for partial or UNAVAILABLE. Under it, each
kept finding's `gate`, path and `reason`, and each open finding's `gate` and path.
Identity and reason only; no hunk text.>
<Outside review: its verdict; `unverified` or `unavailable` is listed as missing coverage with its reason, never as passed.>
## Exploratory QA
<Step 9's current surfaces/charters, reproducers, approved regressions and red/green
proof, fixes and blocked/inconclusive/not-run coverage. Never present stale or
unavailable results as passing.>
## Design Review
<If design review ran: "Design Review (lite): N findings — M auto-fixed, K skipped. AI Slop: clean/N issues.">
<Detector: "clean" | "N findings (rule-id, rule-id)" | "not installed" | "not cached" | "off" — the state the probe printed; rule ids and counts only, finding text and snippets never reach the PR body.>
<If no frontend files changed: "No frontend files changed — design review skipped.">
## Eval Results
<If evals ran: suite names, pass/fail counts, cost dashboard summary. If skipped: "No prompt-related files changed — evals skipped.">
## Greptile Review
<Step 10 complete: list comments with [FIXED] / [FALSE POSITIVE] / [ALREADY FIXED], or "No Greptile comments." for a successful empty fetch.>
<Step 10 unavailable: include `Greptile triage: UNAVAILABLE (dispatch failed)` and the actual reason.>
<Step 10 no_pr: omit this section.>
## Scope Drift
<If scope drift ran: "Scope Check: CLEAN" or list of drift/creep findings>
<If no scope drift: omit this section>
## Plan Completion
<If plan file found: completion checklist summary from Step 8>
<If Step 8 printed the not-run line: that line verbatim.>
<If plan items deferred: list deferred items>
## Linked Spec
<Closes #N only when the Linked Spec check above permits it; otherwise
"Linked to #N (partial delivery — not auto-closing)" with remaining work and
"Close #N manually after follow-up lands." Include archive filename and filed date.
Without a valid match, omit this entire section.>
## Issues
<Issue links stdout, verbatim; omit on ISSUE_LINKS: none.>
## Verification Results
<Step 8.1 obligations executed at Step 9: N PASS, M FAIL, K BLOCKED, J NOT RUN,
not-applicable reasons, unresolved obligations and accepted deferrals.
Unavailable/inconclusive is never PASS.>
## TODOS
<If items marked complete: bullet list of completed items with version>
<If no items completed: "No TODO items completed in this PR.">
<If TODOS.md created or reorganized: note that>
<If TODOS.md doesn't exist and user skipped: omit this section>
## Documentation
<Embed Step 14.5's vetted nonempty `documentation_section` for this invocation:
its saved section file, inserted unchanged by the compose block's `DOCS_SECTION_FILE` lines.
A blocked audit shipped under a user exception has no section file: state its
blocked status, scope and exception here and drop the block's `DOCS_SECTION_FILE` guard
and its `cat -- "$DOCS_SECTION_FILE" && echo &&` step.>
<Always include the status and reviewed scope: updated, current, or blocked with the actual user's named risk exception. Never omit this section or reuse another invocation's audit.>
## Test plan
- [x] <Each executed test lane's command>: <observed passing summary>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
#### Compose the body, then publish through gstack-post
The PR body is world-readable on a public repo. `gstack-post` scans the exact title
and body bytes it sends and passes them to `gh`/`glab` as arguments, so there is no
separate scan here. Wrap any Codex / Greptile / eval output sections in
tool-attributed fences (` ```codex-review ` / ` ```greptile `) so the scanner
WARN-degrades the example credentials those tools quote instead of blocking the PR
(a live-format credential inside the fence still blocks).
Write the body from above into two private files: the first through the
`## Documentation` heading line, the second from `## Test plan` on.
{{FREE_TEXT_FILE:BODY_TOP_FILE=pr-body-top:BODY_REST_FILE=pr-body-rest}}
In a new shell, restore Step 14.5's saved section file path as `DOCS_SECTION_FILE`
and substitute the printed names. The block deletes both drafts; to change the body,
write fresh ones.
```bash
: "${DOCS_SECTION_FILE:?Restore the saved Step 14.5 section file path before composing}"
PR_BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/gstack-pr-body.XXXXXX") || { echo "ERROR: mktemp failed — cannot compose the PR body; refusing to publish." >&2; exit 1; }
BODY_TOP_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<body-top-file-name>"
BODY_REST_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<body-rest-file-name>"
[ -s "$BODY_TOP_FILE" ] && [ -s "$BODY_REST_FILE" ] || { echo "Not composed: $BODY_TOP_FILE or $BODY_REST_FILE is empty, so the body was never written. Write both, then rerun." >&2; exit 1; }
{ awk 1 "$BODY_TOP_FILE" && cat -- "$DOCS_SECTION_FILE" && echo && awk 1 "$BODY_REST_FILE"; } > "$PR_BODY_FILE" || exit 1
rm -f "$BODY_TOP_FILE" "$BODY_REST_FILE"
echo "PR_BODY_FILE: $PR_BODY_FILE"
```
Handoff record first (identifiers survive truncation). Write Step 16 stage 4's
per-lane counts (`{"<lane>":{"pass":N,"fail":N,"skip":N}}`) to
`.gstack/tmp/ship-gate.json` with your file-write tool, then:
```bash
~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr <pr-number> --base <base> --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --body "$PR_BODY_FILE"
```
Add `--receipt .gstack/tmp/tree-receipt.json` only when Step 16 wrote it (gate-ahead).
New PR/MR: omit `--pr`, then rerun with the number after creation and repost the body.
Its `SHIP_RECEIPT:` line opens the completion message. The posted receipt is what
`gstack-ship-queue ready`, `train build` and `verify --train` require (`QUEUE_RECEIPT_MISSING`,
`QUEUE_RECEIPT_STALE`, no flag around them), so /ship runs before every READY and after any
push. `RECEIPT_INVALID` names the bad
field: fix that input file and rerun; nothing was written. `ISSUE_LINKS_MISSING`: add
the named line under `## Issues` and rerun. `PREGATE_STALE` means rerun
`gstack-pregate` on the stamped tree; `PREGATE_REMOTE_UNCLEARED` needs
`gstack-pregate clear` with the run URL. Both stop publication.
In each block below, restore the literal `PR_BODY_FILE` path and substitute Step 18's
title file name; `gstack-post` detects GitHub or GitLab from the remote and the repo's
visibility (an unavailable lookup, including on GitLab, uses the scanner's
public-strict policy). Never re-render the body: every retry sends the same files.
**Existing open PR/MR** (`<pr-number>` from the recheck):
```bash
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
~/.claude/skills/gstack/bin/gstack-post pr-body <pr-number> --body-file "${PR_BODY_FILE:?restore the composed body path}" && \
~/.claude/skills/gstack/bin/gstack-post pr-title <pr-number> --title-file "$TITLE_FILE"
```
It updates the body, then the title, retries a retired-GraphQL edit through REST and
reads the title back. Then resolve the draft state from Step 9's gate findings:
1. Step 6.5 opened this PR as an early draft, the user did not ask for a draft, and no
gate finding is `open`: mark it ready now with `gh pr ready <pr-number>`.
2. Step 6.5 opened it as an early draft and any gate finding is `open`: leave it a
draft; print `Draft kept: Gate edits: N open` and the open findings' `gate` and path.
3. The PR was already ready (not a draft) and any gate finding is `open`: publish only
under Step 17's recorded human authorisation (option B there); the body already
carries the `Gate edits: N open` banner. Without that record, STOP here and report.
Print the existing URL and continue to Step 20; do not run the create command below.
**No open PR/MR:** add `--draft` when any gate finding is `open` (a later agent never
infers approval from the PR existing; the human marks it ready after dispositioning
the open items). Otherwise create it ready:
```bash
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
~/.claude/skills/gstack/bin/gstack-post pr-create --base <base> --title-file "$TITLE_FILE" --body-file "${PR_BODY_FILE:?restore the composed body path}"
```
With open gate findings the same command carries `--draft` before `--title-file`.
Branch on the exit code of either block:
- **0** posted. Remove `$PR_BODY_FILE` and the title file.
- **1** HIGH finding: BLOCKED — do not publish. Rotate and redact at the source, then
compose fresh files.
- **2** MEDIUM findings, printed as `RULE:` lines with a `TOKEN:`. AskUserQuestion per
finding (sterner on public repos, no batch acknowledge). PII offers auto-redact:
write `~/.claude/skills/gstack/bin/gstack-redact --from-file "$PR_BODY_FILE" --auto-redact <ids>`
output back into the body file and rerun the block, which scans again. Only when the
user accepts the findings as they are, rerun the same block with
`--confirm <confirm-token>` added to the command that printed it; any edit needs a new token.
- **3** `gh`/`glab` failed, or **64** no usable remote or CLI: print the error, the
branch name, the remote URL and the block for posting by hand. Do not stop — the code
is pushed — but never report a PR that was not created. With open gate findings,
also say the gate edits were not disclosed and leave the Step 16 open-items report
in the invocation record; never report the disclosure as done.
- Any other exit is an error that blocks publication until its cause is fixed.
**Output the PR/MR URL** — then proceed to Step 20.
---