### Prepare the title (Step 18) Prepare the title in a private file; Step 19 posts that file, never a shell string: ```bash _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/" gh pr view --json title -q .title | ~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh --stdin > "$TITLE_FILE" || exit 1 ``` GitLab reads `glab mr view --output json | jq -r .title` into the same pipe. 2. For a new PR/MR, write `v : ` 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( |$)' "$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 `: `; 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. The PR/MR body should contain these sections (never reuse a prior run's body): ``` ## Summary ..HEAD --oneline`. Group every substantive commit by theme, excluding VERSION/CHANGELOG bookkeeping. Do not paste the commit list.> ## Test Coverage ## Pre-Landing Review ## Exploratory QA ## Design Review ## Eval Results ## Greptile Review ## Scope Drift ## Plan Completion ## Linked Spec ## Verification Results ## TODOS ## Documentation ## Test plan - [x] : 🤖 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. ```bash _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. ```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_REST_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" [ -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" ``` 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** (`` from the recheck): ```bash TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" ~/.claude/skills/gstack/bin/gstack-post pr-body --body-file "${PR_BODY_FILE:?restore the composed body path}" && \ ~/.claude/skills/gstack/bin/gstack-post pr-title --title-file "$TITLE_FILE" ``` It updates the body, then the title, retries a retired-GraphQL edit through REST and reads the title back. If Step 6.5 opened this PR as an early draft and the user did not ask for a draft, mark it ready now: `gh pr ready `. Print the existing URL and continue to Step 20; do not run the create command below. **No open PR/MR:** ```bash TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" ~/.claude/skills/gstack/bin/gstack-post pr-create --base --title-file "$TITLE_FILE" --body-file "${PR_BODY_FILE:?restore the composed body path}" ``` 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 ` 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 ` 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. - Any other exit is an error that blocks publication until its cause is fixed. **Output the PR/MR URL** — then proceed to Step 20. ---