Skill Tracer
Skills › gstack › ship › pr-body.md

pr-body.md

310 lines · ship/sections/pr-body.md at 3f79d81

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1<!-- AUTO-GENERATED from pr-body.md.tmpl — do not edit directly -->
2<!-- Regenerate: bun run gen:skill-docs -->

<!-- AUTO-GENERATED from pr-body.md.tmpl — do not edit directly --> <!-- Regenerate: bun run gen:skill-docs -->

Raw lines 1 to 2
3### Prepare the title (Step 18)
4
5Prepare the title in a private file; Step 19 posts that file, never a shell string:
6
7```bash
8_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp"
9mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; }
10_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; }
11TITLE_FILE=$(mktemp "${_GT:?}/pr-title.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "TITLE_FILE: $TITLE_FILE (name: ${TITLE_FILE##*/})"
12```
14Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand.
161. 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
17 printed name, the matched number and `NEW_VERSION`; no title text passes through you:
18 ```bash
19 TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
20 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
21 ```
22 GitLab reads `glab mr view <pr-number> --output json | jq -r .title` into the same pipe.
232. For a new PR/MR, write `v<NEW_VERSION> <type>: <summary>` into the file with your file-write tool;
24 `<summary>` names what the user gets, not the mechanism.
253. Every created or updated title MUST start with `v$NEW_VERSION `; check the file with
26 `grep -Eq '^v<new-version>( |$)' "$TITLE_FILE"` and never publish an unprefixed title.
274. **NO_VERSION:** replaces items 1-3: keep an existing title (the platform title
28 piped straight into the file), or write `<type>: <summary>`; no version prefix.
30Save the title file name for Step 19.
32---

Prepare the title (Step 18)

Prepare the title in a private file; Step 19 posts that file, never a shell string:

_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp"
mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; }
_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; }
TITLE_FILE=$(mktemp "${_GT:?}/pr-title.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "TITLE_FILE: $TITLE_FILE (name: ${TITLE_FILE##*/})"

Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand.

  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; <summary> names what the user gets, not the mechanism.
  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.


Raw lines 3 to 33
Runs code bin/gstack-pr-title-rewrite.sh :20
34## Step 19: Create PR/MR
36Recheck Step 18's PR/MR lookup and record it. Errors or ambiguous matches STOP publication.
37If the open PR/MR or title changed, repeat Step 18's identity/title preparation,
38then return here for a new lookup and a fresh body before publishing.

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.

Raw lines 34 to 39
40### Resolve Linked Spec before composing the body
421. Resolve the archive directory and branch:
43 ```bash
44 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}"
45 SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG)
46 CURRENT_BRANCH=$(git branch --show-current)
47 SPEC_ARCHIVES="$GSTACK_STATE_ROOT/projects/$SLUG/specs"
48 ```
492. Read archive frontmatter as data, never shell source. Select an exact
50 `spec_branch` match to `CURRENT_BRANCH`; among matches use the newest
51 `spec_filed_at`. Never infer an issue number from a branch name. If no readable
52 match or positive integer `spec_issue_number`, omit only `## Linked Spec` and
53 continue composing the PR. Resolve ambiguous matches before linking an issue.
543. Compare that spec's acceptance criteria with Step 8's results. Only fully
55 completed Step 8 plan scope permits `Closes #N`, with every spec criterion
56 verified. Partial, deferred, failed, dropped or unverified scope uses `Linked to #N`
57 and names the remaining work; never auto-close it. Include the archive filename
58 and `spec_filed_at`, not a private absolute path; `gstack-post` scans them with the body.

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.
Raw lines 40 to 59
Runs code bin/gstack-paths :44
Runs code bin/gstack-slug :45
60### Issue links
62Run `~/.claude/skills/gstack/bin/gstack-issue-links --base <base> --out .gstack/tmp/issues.json`, adding
63`--issue <n>` per issue the user named as fixed and `--ci-run <id>` per failing CI run. Interactive:
64ask once about its `(promote: fix #N)` references (each `fix #<n>` reply adds `--issue <n>`), and add
65`--supersedes <n>`/`--credit <n>` for a `Related:` PR only when the owner confirms; rerun after any change.
66The completion message lists unpromoted candidates, `Related:` lines and any `ISSUE_LOOKUP_FAILED`.

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.

Raw lines 60 to 67
Runs code bin/gstack-issue-links :62
68### Capy
70Run `[ "${GSTACK_CAPY:-}" = 1 ] && echo 'CAPY_PR: on' || echo 'CAPY_PR: off'`. On `CAPY_PR: on`, read
71`~/.claude/skills/gstack/ship/sections/capy-pr.md` now: the body opens with its `## What changed`,
72`## Why` and `## Approvals` before the sections below. On `CAPY_PR: off`, skip it.
74The PR/MR body should contain these sections (never reuse a prior run's body):
76```
77## Summary
78<Open with the problem in the user's words (what they hit, saw or asked), then the fix
79and what they get now; themes from `git log origin/<base>..HEAD --oneline` after that,
80minus VERSION/CHANGELOG bookkeeping. Never an inventory or a commit list. BAD: "Adds
81gstack-prose-lint, wires Steps 13 and 19, refreshes the goldens." GOOD: "Nobody reads a
82CHANGELOG entry twice, so autopilot words shipped every release. /ship now lints the entry
83before the commit and names each line to rewrite.">
85## Blast radius
86<One or two sentences: what else the change touches (callers, consumers, generated
87files, hosts) and the one fact that makes that safe, with the rung of the evidence
88ladder the fact reached (file:line read, walked failure path, script ran, repro). The
89ladder is the `/investigate` skill's Blast radius section, when present.>
91## Test Coverage
92<coverage diagram from Step 7, or "All new code paths have test coverage.">
93<If Step 7 ran: "Tests: {before} → {after} (+{delta} new)">
94<If Step 7 ran: "Coverage: {X}% value-weighted ({Y}% including {W} weakly covered paths)">
95<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").>
96<Weak paths and leftover gaps as proposed tests with value cards; each regression test's
97"Regression proof — fails at HEAD · passes at base · passes after fix" line; Test value
98details for cards whose file type has no known comment syntax.>
99<One line per Step 5 lane: "Tests ran: <lane> N" from its receipt's `tests_ran`, or
100"Tests ran: <lane> count unavailable" when the runner summary was not recognised. A
101ZERO-RUN lane never reaches this body unresolved.>
103## Pre-Landing Review
104<findings from Step 9 code review, or "No issues found."; then Step 9.4's `REVIEW_LOOP:` line.>
105<Gate edits: Step 9's latest `Gate edits:` line verbatim — `none detected (...)`,
106`N listed, M read, K findings — J kept, R restored, O open`, `partial (...)` or
107`UNAVAILABLE — ...`; never `none detected` for partial or UNAVAILABLE. Under it, each
108kept finding's `gate`, path and `reason`, and each open finding's `gate` and path.
109Identity and reason only; no hunk text.>
110<Outside review: its verdict; `unverified` or `unavailable` is listed as missing coverage with its reason, never as passed.>
112<Step 9.4's saved `## Deferred findings` and `## Review notes` sections, verbatim, as their own
113sections here; omit each when empty. References only: never a closing keyword.>
115## Exploratory QA
116<Step 9's current surfaces/charters, reproducers, approved regressions and red/green
117proof, fixes and blocked/inconclusive/not-run coverage. Never present stale or
118unavailable results as passing.>
120## Design Review
121<If design review ran: "Design Review (lite): N findings — M auto-fixed, K skipped. AI Slop: clean/N issues.">
122<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.>
123<If no frontend files changed: "No frontend files changed — design review skipped.">
125## Eval Results
126<If evals ran: suite names, pass/fail counts, cost dashboard summary. If skipped: "No prompt-related files changed — evals skipped.">
128## Greptile Review
129<Step 10 complete: list comments with [FIXED] / [FALSE POSITIVE] / [ALREADY FIXED], or "No Greptile comments." for a successful empty fetch.>
130<Step 10 unavailable: include `Greptile triage: UNAVAILABLE (dispatch failed)` and the actual reason.>
131<Step 10 no_pr: omit this section.>
133## Scope Drift
134<If scope drift ran: "Scope Check: CLEAN" or list of drift/creep findings>
135<If no scope drift: omit this section>
137## Plan Completion
138<If plan file found: completion checklist summary from Step 8>
139<If Step 8 printed the not-run line: that line verbatim.>
140<If plan items deferred: list deferred items>
142## Linked Spec
143<Closes #N only when the Linked Spec check above permits it; otherwise
144"Linked to #N (partial delivery — not auto-closing)" with remaining work and
145"Close #N manually after follow-up lands." Include archive filename and filed date.
146Without a valid match, omit this entire section.>
148## Issues
149<Issue links stdout, verbatim; omit on ISSUE_LINKS: none.>
151## Verification Results
152<Step 8.1 obligations executed at Step 9: N PASS, M FAIL, K BLOCKED, J NOT RUN,
153not-applicable reasons, unresolved obligations and accepted deferrals.
154Unavailable/inconclusive is never PASS.>
156## TODOS
157<If items marked complete: bullet list of completed items with version>
158<If no items completed: "No TODO items completed in this PR.">
159<If TODOS.md created or reorganized: note that>
160<If TODOS.md doesn't exist and user skipped: omit this section>
162## Documentation
163<Embed Step 14.5's vetted nonempty `documentation_section` for this invocation:
164its saved section file, inserted unchanged by the compose block's `DOCS_SECTION_FILE` lines.
165A blocked audit shipped under a user exception has no section file: state its
166blocked status, scope and exception here and drop the block's `DOCS_SECTION_FILE` guard
167and its `cat -- "$DOCS_SECTION_FILE" && echo &&` step. The exception is one line,
168word for word as Step 14.5 saved it in `.gstack/tmp/docs-audit.json`:
169`Documentation exception: accepted by <accepted_by> <date> via <via>: <reason>`;
170the receipt write below refuses (`DOCS_AUDIT_BLOCKED`) when the body lacks it.>
171<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.>
173## Test plan
174- [x] <Each executed test lane's command>: <observed passing summary>
176🤖 Generated with [Claude Code](https://claude.com/claude-code)
177```
179#### Compose the body, then publish through gstack-post
181The PR body is world-readable on a public repo. `gstack-post` scans the exact title
182and body bytes it sends and passes them to `gh`/`glab` as arguments, so there is no
183separate scan here. Wrap any Codex / Greptile / eval output sections in
184tool-attributed fences (` ```codex-review ` / ` ```greptile `) so the scanner
185WARN-degrades the example credentials those tools quote instead of blocking the PR
186(a live-format credential inside the fence still blocks).
188Write the body from above into two private files: the first through the
189`## Documentation` heading line, the second from `## Test plan` on.
191```bash
192_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp"
193mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; }
194_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; }
195BODY_TOP_FILE=$(mktemp "${_GT:?}/pr-body-top.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "BODY_TOP_FILE: $BODY_TOP_FILE (name: ${BODY_TOP_FILE##*/})"
196BODY_REST_FILE=$(mktemp "${_GT:?}/pr-body-rest.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "BODY_REST_FILE: $BODY_REST_FILE (name: ${BODY_REST_FILE##*/})"
197```
199Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand.
201In a new shell, restore Step 14.5's saved section file path as `DOCS_SECTION_FILE`
202and substitute the printed names. The block deletes both drafts; to change the body,
203write fresh ones.
205```bash
206: "${DOCS_SECTION_FILE:?Restore the saved Step 14.5 section file path before composing}"
207PR_BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/gstack-pr-body.XXXXXX") || { echo "ERROR: mktemp failed — cannot compose the PR body; refusing to publish." >&2; exit 1; }
208BODY_TOP_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<body-top-file-name>"
209BODY_REST_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<body-rest-file-name>"
210[ -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; }
211~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate >&2 || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), then rerun." >&2; exit 1; }
212{ awk 1 "$BODY_TOP_FILE" && cat -- "$DOCS_SECTION_FILE" && echo && awk 1 "$BODY_REST_FILE"; } > "$PR_BODY_FILE" || exit 1
213rm -f "$BODY_TOP_FILE" "$BODY_REST_FILE"
214echo "PR_BODY_FILE: $PR_BODY_FILE"
215```
217Handoff record first (identifiers survive truncation). Write Step 16 stage 4's
218per-lane counts (`{"<lane>":{"pass":N,"fail":N,"skip":N}}`) to
219`.gstack/tmp/ship-gate.json` with your file-write tool, then:
221```bash
222rm -f .gstack/tmp/review-loop.json
223~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --out .gstack/tmp/review-loop.json &&
224~/.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 --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE"
225```
227A Step 5 red-base pause adds `--base-red <pause file>` (a conditional receipt the queue
228refuses, `QUEUE_RECEIPT_CONDITIONAL`). `--docs` reads the record Step 14.5 saved (`{status, scope[, exception]}`): the
229receipt carries it as `docs`, and a `blocked` status without the owner's exception,
230or a body missing the exception line, is `DOCS_AUDIT_BLOCKED` with nothing written.
231When the owner accepted a stop after a fix-check edit, add
232`--review-loop-exception @<file>` (`{accepted_by, date, via, reason, fix_check: {head, findings}}`)
233and put its printed `Review-loop exception:` line in the body; never a prose line under `REVIEW_LOOP:`.
235The summary reads this branch's review log for the invocation's passes (its
236`REVIEW_LOOP:` line is the one the Pre-Landing Review section carries). The `&&`
237matters: a summary that refuses (no pass matched `REVIEW_SINCE`) must not let `write`
238record a stale or empty loop file.
240Add `--receipt .gstack/tmp/tree-receipt.json` only when Step 16 wrote it (gate-ahead).
241New PR/MR: omit `--pr`, then rerun with the number after creation and repost the body.
242Its `SHIP_RECEIPT:` line opens the completion message. The posted receipt is what
243`gstack-ship-queue ready`, `train build` and `verify --train` require (`QUEUE_RECEIPT_MISSING`,
244`QUEUE_RECEIPT_STALE`, no flag around them), so /ship runs before every READY and after any
245push. `RECEIPT_INVALID` names the bad
246field: fix that input file and rerun; nothing was written. `ISSUE_LINKS_MISSING`: add
247the named line under `## Issues` and rerun. `PREGATE_STALE` means rerun
248`gstack-pregate` on the stamped tree; `PREGATE_REMOTE_UNCLEARED` needs
249`gstack-pregate clear` with the run URL. Both stop publication.
251In each block below, restore the literal `PR_BODY_FILE` path and substitute Step 18's
252title file name; `gstack-post` detects GitHub or GitLab from the remote and the repo's
253visibility (an unavailable lookup, including on GitLab, uses the scanner's
254public-strict policy). Never re-render the body: every retry sends the same files.
256**Existing open PR/MR** (`<pr-number>` from the recheck):
258```bash
259TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
260~/.claude/skills/gstack/bin/gstack-post pr-body <pr-number> --body-file "${PR_BODY_FILE:?restore the composed body path}" && \
261~/.claude/skills/gstack/bin/gstack-post pr-title <pr-number> --title-file "$TITLE_FILE"
262```
264It updates the body, then the title, retries a retired-GraphQL edit through REST and
265reads the title back. Then resolve the draft state from Step 9's gate findings:
2671. Step 6.5 opened this PR as an early draft, the user did not ask for a draft, and no
268 gate finding is `open`: mark it ready now with `gh pr ready <pr-number>`.
2692. Step 6.5 opened it as an early draft and any gate finding is `open`: leave it a
270 draft; print `Draft kept: Gate edits: N open` and the open findings' `gate` and path.
2713. The PR was already ready (not a draft) and any gate finding is `open`: publish only
272 under Step 17's recorded human authorisation (option B there); the body already
273 carries the `Gate edits: N open` banner. Without that record, STOP here and report.
275Print the existing URL and continue to Step 20; do not run the create command below.
277**No open PR/MR:** add `--draft` when any gate finding is `open` (a later agent never
278infers approval from the PR existing; the human marks it ready after dispositioning
279the open items). Otherwise create it ready:
281```bash
282TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
283~/.claude/skills/gstack/bin/gstack-post pr-create --base <base> --title-file "$TITLE_FILE" --body-file "${PR_BODY_FILE:?restore the composed body path}"
284```
286With open gate findings the same command carries `--draft` before `--title-file`.
288Branch on the exit code of either block:
289- **0** posted. Remove `$PR_BODY_FILE` and the title file.
290- **1** HIGH finding: BLOCKED — do not publish. Rotate and redact at the source, then
291 compose fresh files.
292- **2** MEDIUM findings, printed as `RULE:` lines with a `TOKEN:`. AskUserQuestion per
293 finding (sterner on public repos, no batch acknowledge). PII offers auto-redact:
294 write `~/.claude/skills/gstack/bin/gstack-redact --from-file "$PR_BODY_FILE" --auto-redact <ids>`
295 output back into the body file and rerun the block, which scans again. Only when the
296 user accepts the findings as they are, rerun the same block with
297 `--confirm <confirm-token>` added to the command that printed it; any edit needs a new token.
298- **64** with stderr saying the current body holds a title `more than once`: nothing was
299 posted. STOP and report the duplicated titles; never rerun with `--replace` yourself,
300 since a human picks which copy stays (docs/troubleshooting.md#gstack-post-pr-body-merge).
301- **3** `gh`/`glab` failed, or any other **64** (no usable remote or CLI): print the error, the
302 branch name, the remote URL and the block for posting by hand. Do not stop — the code
303 is pushed — but never report a PR that was not created. With open gate findings,
304 also say the gate edits were not disclosed and leave the Step 16 open-items report
305 in the invocation record; never report the disclosure as done.
306- Any other exit is an error that blocks publication until its cause is fixed.
308**Output the PR/MR URL** — then proceed to Step 20.
310---

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
<Open with the problem in the user's words (what they hit, saw or asked), then the fix
and what they get now; themes from `git log origin/<base>..HEAD --oneline` after that,
minus VERSION/CHANGELOG bookkeeping. Never an inventory or a commit list. BAD: "Adds
gstack-prose-lint, wires Steps 13 and 19, refreshes the goldens." GOOD: "Nobody reads a
CHANGELOG entry twice, so autopilot words shipped every release. /ship now lints the entry
before the commit and names each line to rewrite.">

## Blast radius
<One or two sentences: what else the change touches (callers, consumers, generated
files, hosts) and the one fact that makes that safe, with the rung of the evidence
ladder the fact reached (file:line read, walked failure path, script ran, repro). The
ladder is the `/investigate` skill's Blast radius section, when present.>

## 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."; then Step 9.4's `REVIEW_LOOP:` line.>
<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.>

<Step 9.4's saved `## Deferred findings` and `## Review notes` sections, verbatim, as their own
sections here; omit each when empty. References only: never a closing keyword.>

## 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. The exception is one line,
word for word as Step 14.5 saved it in `.gstack/tmp/docs-audit.json`:
`Documentation exception: accepted by <accepted_by> <date> via <via>: <reason>`;
the receipt write below refuses (`DOCS_AUDIT_BLOCKED`) when the body lacks it.>
<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.

_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp"
mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; }
_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; }
BODY_TOP_FILE=$(mktemp "${_GT:?}/pr-body-top.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "BODY_TOP_FILE: $BODY_TOP_FILE (name: ${BODY_TOP_FILE##*/})"
BODY_REST_FILE=$(mktemp "${_GT:?}/pr-body-rest.XXXXXX") || { echo "Not sent: mktemp failed in $_GT." >&2; exit 1; }; echo "BODY_REST_FILE: $BODY_REST_FILE (name: ${BODY_REST_FILE##*/})"

Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand.

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.

: "${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; }
~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate >&2 || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), 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:

rm -f .gstack/tmp/review-loop.json
~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --out .gstack/tmp/review-loop.json &&
~/.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 --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE"

A Step 5 red-base pause adds --base-red <pause file> (a conditional receipt the queue refuses, QUEUE_RECEIPT_CONDITIONAL). --docs reads the record Step 14.5 saved ({status, scope[, exception]}): the receipt carries it as docs, and a blocked status without the owner's exception, or a body missing the exception line, is DOCS_AUDIT_BLOCKED with nothing written. When the owner accepted a stop after a fix-check edit, add --review-loop-exception @<file> ({accepted_by, date, via, reason, fix_check: {head, findings}}) and put its printed Review-loop exception: line in the body; never a prose line under REVIEW_LOOP:.

The summary reads this branch's review log for the invocation's passes (its REVIEW_LOOP: line is the one the Pre-Landing Review section carries). The && matters: a summary that refuses (no pass matched REVIEW_SINCE) must not let write record a stale or empty loop 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):

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:

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.
  • 64 with stderr saying the current body holds a title more than once: nothing was posted. STOP and report the duplicated titles; never rerun with --replace yourself, since a human picks which copy stays (docs/troubleshooting.md#gstack-post-pr-body-merge).
  • 3 gh/glab failed, or any other 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.


Raw lines 68 to 310
Names a skill investigate :89
Starts an agent dispatch :130
Runs code bin/gstack-prose-lint :211
Runs code bin/gstack-review-scope :223
Runs code bin/gstack-ship-receipt :224
Runs code bin/gstack-post :260
Involves a person AskUserQuestion :292
Runs code bin/gstack-redact :294