refactor(ce-pr-description, git-commit-push-pr): hand off PR body via temp file

Change ce-pr-description's output contract from {title, body} to
{title, body_file}. The body is written to an OS temp file via
mktemp + heredoc and the caller substitutes it back with
"$(cat "$body_file")". This avoids emitting the body text twice
(once as the skill's output, again when the caller formats the
gh pr command), which was wasteful on large PR descriptions.

Also add a narrow AGENTS.md exception for value-producing
preparatory commands (VAR=$(cmd1) && cmd2 "$VAR"), so the skill
can use the natural mktemp + heredoc pattern without manually
threading the path across two bash calls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Trevin Chow
2026-04-16 22:06:11 -07:00
parent 1995e3d790
commit c8d4e676bc
3 changed files with 42 additions and 32 deletions
+1 -1
View File
@@ -136,7 +136,7 @@ Why: shell-heavy exploration causes avoidable permission prompts in sub-agent wo
- [ ] Never instruct agents to use `find`, `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `wc`, or `tree` through a shell for routine file discovery, content search, or file reading
- [ ] Describe tools by capability class with platform hints — e.g., "Use the native file-search/glob tool (e.g., Glob in Claude Code)" — not by Claude Code-specific tool names alone
- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no action chaining (`cmd1 && cmd2`, `cmd1 ; cmd2`) and no error suppression (`2>/dev/null`, `|| true`). Boolean conditions within if/while guards (`[ -n "$X" ] || [ -n "$Y" ]`) are fine — that is normal conditional logic, not action chaining. Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures
- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no action chaining (`cmd1 && cmd2`, `cmd1 ; cmd2`) and no error suppression (`2>/dev/null`, `|| true`). Two narrow exceptions: boolean conditions within if/while guards (`[ -n "$X" ] || [ -n "$Y" ]`) are fine — that is normal conditional logic, not action chaining. **Value-producing preparatory commands** (`VAR=$(cmd1) && cmd2 "$VAR"`) are also fine when `cmd2` strictly consumes `cmd1`'s output and splitting would require manually threading the value through model context across bash calls (e.g., `BODY_FILE=$(mktemp -u) && cat > "$BODY_FILE" <<EOF ... EOF`). Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures
- [ ] **Pre-resolution exception:** `!` backtick pre-resolution commands run at skill load time, not at agent runtime. They may use chaining (`&&`, `||`), error suppression (`2>/dev/null`), and fallback sentinels (e.g., `|| echo '__NO_CONFIG__'`) to produce a clean, parseable value for the model. This is the preferred pattern for environment probes (CLI availability, config file reads) that would otherwise require runtime shell calls with chaining. Example: `` !`command -v codex >/dev/null 2>&1 && echo "AVAILABLE" || echo "NOT_FOUND"` ``
- [ ] Do not encode shell recipes for routine exploration when native tools can do the job; encode intent and preferred tool classes instead
- [ ] For shell-only workflows (e.g., `gh`, `git`, `bundle show`, project CLIs), explicit command examples are acceptable when they are simple, task-scoped, and not chained together
@@ -1,12 +1,12 @@
---
name: ce-pr-description
description: "Write or regenerate a value-first pull-request description (title + body) for the current branch's commits or for a specified PR. Use when the user says 'write a PR description', 'refresh the PR description', 'regenerate the PR body', 'rewrite this PR', 'freshen the PR', 'update the PR description', 'draft a PR body for this diff', 'describe this PR properly', 'generate the PR title', or pastes a GitHub PR URL / #NN / number. Also used internally by git-commit-push-pr (single-PR flow) and ce-pr-stack (per-layer stack descriptions) so all callers share one writing voice. Input is a natural-language prompt. A PR reference (a full GitHub PR URL, `pr:561`, `#561`, or a bare number alone) picks a specific PR; anything else is treated as optional steering for the default 'describe my current branch' mode. Returns structured {title, body} for the caller to apply via gh pr edit or gh pr create — this skill never edits the PR itself and never prompts for confirmation."
description: "Write or regenerate a value-first pull-request description (title + body) for the current branch's commits or for a specified PR. Use when the user says 'write a PR description', 'refresh the PR description', 'regenerate the PR body', 'rewrite this PR', 'freshen the PR', 'update the PR description', 'draft a PR body for this diff', 'describe this PR properly', 'generate the PR title', or pastes a GitHub PR URL / #NN / number. Also used internally by git-commit-push-pr (single-PR flow) and ce-pr-stack (per-layer stack descriptions) so all callers share one writing voice. Input is a natural-language prompt. A PR reference (a full GitHub PR URL, `pr:561`, `#561`, or a bare number alone) picks a specific PR; anything else is treated as optional steering for the default 'describe my current branch' mode. Returns structured {title, body_file} (body written to an OS temp file) for the caller to apply via gh pr edit or gh pr create — this skill never edits the PR itself and never prompts for confirmation."
argument-hint: "[PR ref e.g. pr:561 | #561 | URL] [free-text steering]"
---
# CE PR Description
Generate a conventional-commit-style title and a value-first body describing a pull request's work. Returns structured `{title, body}` for the caller to apply — this skill never invokes `gh pr edit` or `gh pr create`, and never prompts for interactive confirmation.
Generate a conventional-commit-style title and a value-first body describing a pull request's work. Returns structured `{title, body_file}` for the caller to apply — this skill never invokes `gh pr edit` or `gh pr create`, and never prompts for interactive confirmation. The body is written to an OS temp file (via `mktemp`) rather than emitted inline, so callers can pass it to `gh pr edit/create` via `cat` substitution without the body being tokenized twice (once in the skill's return, once in the caller's heredoc).
Why a separate skill: several callers need the same writing logic without the single-PR interactive scaffolding that lives in `git-commit-push-pr`. `ce-pr-stack`'s splitting workflow runs this once per layer as a batch; `git-commit-push-pr` runs it inside its full-flow and refresh-mode paths. Extracting keeps one source of truth for the writing principles.
@@ -49,9 +49,9 @@ Steering text is always optional. If present, incorporate it alongside the diff-
Return a structured result with two fields:
- **`title`** -- conventional-commit format: `type: description` or `type(scope): description`. Under 72 characters. Choose `type` based on intent (feat/fix/refactor/docs/chore/perf/test), not file type. Pick the narrowest useful `scope` (skill or agent name, CLI area, or shared label); omit when no single label adds clarity.
- **`body`** -- markdown following the writing principles below.
- **`body_file`** -- absolute path to an OS temp file (created via `mktemp`) containing the body markdown that follows the writing principles below. Do not emit the body inline in the return.
The caller decides whether to apply via `gh pr edit`, `gh pr create`, or discard. This skill does NOT call those commands itself.
The caller decides whether to apply via `gh pr edit`, `gh pr create`, or discard, reading the body from `body_file` (e.g., `--body "$(cat "$BODY_FILE")"`). This skill does NOT call those commands itself. No cleanup is required — `mktemp` files live in OS temp storage, which the OS reaps on its own schedule.
---
@@ -122,7 +122,7 @@ gh pr view <pr-ref> --json number,state,title,body,baseRefName,baseRefOid,headRe
Key JSON fields: `headRefOid` (PR head SHA — prefer over indexing into `commits`), `baseRefOid` (base-branch SHA), `headRepository` + `headRepositoryOwner` (PR source repo), `isCrossRepository`. There is no `baseRepository` field — the base repo is the one queried by `gh pr view` itself.
If the returned `state` is not `OPEN`, report `"PR <number> is <state> (not open); cannot regenerate description"` and exit gracefully without output. Callers expecting `{title, body}` must handle this empty case.
If the returned `state` is not `OPEN`, report `"PR <number> is <state> (not open); cannot regenerate description"` and exit gracefully without output. Callers expecting `{title, body_file}` must handle this empty case.
**Determine whether the PR lives in the current working directory's repo** by parsing the URL's `<owner>/<repo>` path segments and comparing against `git remote get-url origin` (strip `.git` suffix; handle both `git@github.com:owner/repo` and `https://github.com/owner/repo` forms). If the URL repo matches `origin`'s repo, route to the local-git path (Case A). Otherwise route to the API-only path (Case B). Bare numbers and `#NN` forms implicitly target the current repo → Case A.
@@ -344,9 +344,26 @@ Assemble the body in this order:
---
## Step 9: Return `{title, body}`
## Step 9: Return `{title, body_file}`
Return the composed title and body to the caller. Do not call `gh pr edit`, `gh pr create`, or any other mutating command. Do not ask the user to confirm. The caller owns apply.
Write the composed body to an OS temp file, then return the title and the file path to the caller. Do not call `gh pr edit`, `gh pr create`, or any other mutating command. Do not ask the user to confirm. The caller owns apply.
Generate a unique temp path and write the body to it in a single bash call, then print the path so the caller can capture it:
```bash
BODY_FILE=$(mktemp -u -t ce-pr-body) && cat > "$BODY_FILE" <<'__CE_PR_BODY_END__'
<the composed body markdown goes here, verbatim>
__CE_PR_BODY_END__
echo "$BODY_FILE"
```
This is the value-producing preparatory pattern that AGENTS.md explicitly permits: `cat` strictly consumes the path `mktemp -u` just produced, and splitting would require manually threading the path through two bash calls. The final `echo "$BODY_FILE"` surfaces the path to the caller as the bash tool's stdout.
Design notes:
- `mktemp -u -t ce-pr-body` prints a unique path like `/tmp/ce-pr-body.abc123` (Linux) or `/var/folders/.../T/ce-pr-body.abc123` (macOS) without creating the file. The `-u` flag ("unsafe, don't create") is used because the heredoc write creates the file itself — creating it twice is pointless, and some sandboxes (Claude Code's `Write` tool among them) refuse to write to files they haven't `Read` first. This form is portable across macOS (BSD) and Linux (GNU) and avoids the macOS quirk where mid-template `XXXXXX` doesn't substitute. No `.md` suffix is needed — `gh pr edit --body "$(cat <path>)"` reads the bytes regardless of extension.
- The sentinel marker `__CE_PR_BODY_END__` is used verbatim — not `EOF`, because PR bodies can legitimately contain literal `EOF` in code blocks and collide with the heredoc terminator. Quoting the sentinel (`<<'__CE_PR_BODY_END__'`) disables shell expansion inside the heredoc, so `$VAR`, backticks, and `${...}` in the body stay literal.
- Why not use the native file-write tool (e.g., `Write` in Claude Code): the tool commonly refuses to overwrite a file the session hasn't `Read` yet and is often sandboxed to the workspace directory, so it can't write to `/tmp`. A bash heredoc write sidesteps both constraints. The body is emitted as output tokens exactly once (in the bash command parameter) — identical cost to a Write tool call. The savings versus the old flow come from not re-emitting the body again in the caller's `gh pr edit/create` invocation.
Format the return as a clearly labeled block so the caller can extract cleanly:
@@ -354,11 +371,13 @@ Format the return as a clearly labeled block so the caller can extract cleanly:
=== TITLE ===
<title line>
=== BODY ===
<body markdown>
=== BODY_FILE ===
<absolute path to the mktemp body file>
```
If Step 1 exited gracefully (closed/merged PR, invalid range, empty commit list), return no title or body — just the reason string.
Do not emit the body markdown itself in the return block. The caller reads it from `BODY_FILE` via `cat` substitution inside its `gh pr edit`/`gh pr create` invocation. This avoids tokenizing the body twice (once in the return, once in the caller's tool call) and sidesteps heredoc-escaping bugs when the body contains shell metacharacters.
If Step 1 exited gracefully (closed/merged PR, invalid range, empty commit list), do not create a body file — just return the reason string.
---
@@ -69,24 +69,21 @@ Read the current PR description to drive the compare-and-confirm step later:
gh pr view --json body --jq '.body'
```
**Generate the updated title and body** — load the `ce-pr-description` skill with the PR URL from DU-2 (e.g., `https://github.com/owner/repo/pull/123`). The URL preserves repo/PR identity even when invoked from a worktree or subdirectory where the current repo is ambiguous. If the user provided a focus (e.g., "include the benchmarking results"), append it as free-text steering after the URL. The skill returns a `{title, body}` block without applying or prompting.
**Generate the updated title and body** — load the `ce-pr-description` skill with the PR URL from DU-2 (e.g., `https://github.com/owner/repo/pull/123`). The URL preserves repo/PR identity even when invoked from a worktree or subdirectory where the current repo is ambiguous. If the user provided a focus (e.g., "include the benchmarking results"), append it as free-text steering after the URL. The skill returns a `{title, body_file}` block without applying or prompting — the body is in an OS temp file rather than inline, to avoid tokenizing it twice.
If `ce-pr-description` returns a "not open" or other graceful-exit message instead of a `{title, body}` pair, report that message and stop.
If `ce-pr-description` returns a "not open" or other graceful-exit message instead of a `{title, body_file}` pair, report that message and stop.
**Evidence decision:** `ce-pr-description` preserves any existing `## Demo` or `## Screenshots` block from the current body by default. If the user's focus asks to refresh or remove evidence, pass that intent as steering text — the skill will honor it. If no evidence block exists and one would benefit the reader, invoke `ce-demo-reel` separately to capture, then re-invoke `ce-pr-description` with updated steering that references the captured evidence.
**Compare and confirm** — briefly explain what the new description covers differently from the old one. This helps the user decide whether to apply; the description itself does not narrate these differences.
**Compare and confirm** — briefly explain what the new description covers differently from the old one. This helps the user decide whether to apply; the description itself does not narrate these differences. Summarize from the body already in conversation context (from the file-write tool call that produced `body_file`); do not re-read the temp file, which would re-emit the body and defeat the handoff's token savings.
- If the user provided a focus, confirm it was addressed.
- Ask the user to confirm before applying.
If confirmed, apply with the returned title and body:
If confirmed, apply with the returned title and body file:
```bash
gh pr edit --title "<returned title>" --body "$(cat <<'EOF'
<returned body>
EOF
)"
gh pr edit --title "<returned title>" --body "$(cat "<returned body_file>")"
```
Report the PR URL.
@@ -137,7 +134,7 @@ Priority order for commit messages and PR titles:
Use the current branch and existing PR check from context. If the branch is empty, report detached HEAD and stop.
If the PR check returned `state: OPEN`, note the URL -- this is the existing-PR flow. Continue to Step 4 and 5 (commit any pending work and push), then go to Step 7 to ask whether to rewrite the description. Only run Step 6 (which generates a new description via `ce-pr-description`) if the user confirms the rewrite; Step 7's existing-PR sub-path consumes the `{title, body}` that Step 6 produces. Otherwise (no open PR), continue through Steps 6, 7, and 8 in order.
If the PR check returned `state: OPEN`, note the URL -- this is the existing-PR flow. Continue to Step 4 and 5 (commit any pending work and push), then go to Step 7 to ask whether to rewrite the description. Only run Step 6 (which generates a new description via `ce-pr-description`) if the user confirms the rewrite; Step 7's existing-PR sub-path consumes the `{title, body_file}` that Step 6 produces. Otherwise (no open PR), continue through Steps 6, 7, and 8 in order.
### Step 4: Branch, stage, and commit
@@ -205,21 +202,18 @@ When evidence is not possible (docs-only, markdown-only, changelog-only, release
- **For a new PR** (no existing PR found in Step 3): invoke with `base:<base-remote>/<base-branch>` using the already-resolved base from earlier in this step, so `ce-pr-description` describes the correct commit range even when the branch targets a non-default base (e.g., `develop`, `release/*`). Append any captured-evidence context or user focus as free-text steering (e.g., "include the captured demo: <URL> as a `## Demo` section").
- **For an existing PR** (found in Step 3): invoke with the full PR URL from the Step 3 context (e.g., `https://github.com/owner/repo/pull/123`). The URL preserves repo/PR identity even when invoked from a worktree or subdirectory; the skill reads the PR's own `baseRefName` so no `base:` override is needed. Append any focus steering as free text after the URL.
`ce-pr-description` returns a `{title, body}` block. It applies the value-first writing principles, commit classification, sizing, narrative framing, writing voice, visual communication, numbering rules, and the Compound Engineering badge footer internally. Use the returned values verbatim in Step 7; do not layer manual edits onto them unless a focused adjustment is required (e.g., splicing an evidence block captured in this step that was not passed as steering text).
`ce-pr-description` returns a `{title, body_file}` block — the body lives in an OS temp file rather than inline to avoid double-tokenizing it. It applies the value-first writing principles, commit classification, sizing, narrative framing, writing voice, visual communication, numbering rules, and the Compound Engineering badge footer internally. Use the returned values verbatim in Step 7; do not layer manual edits onto them unless a focused adjustment is required (e.g., splicing an evidence block captured in this step that was not passed as steering text — in that case, edit the body file directly before applying).
If `ce-pr-description` returns a graceful-exit message instead of `{title, body}` (e.g., closed PR, no commits to describe, base ref unresolved), report the message and stop — do not create or edit the PR.
If `ce-pr-description` returns a graceful-exit message instead of `{title, body_file}` (e.g., closed PR, no commits to describe, base ref unresolved), report the message and stop — do not create or edit the PR.
### Step 7: Create or update the PR
#### New PR (no existing PR from Step 3)
Using the `{title, body}` returned by `ce-pr-description`:
Using the `{title, body_file}` returned by `ce-pr-description`:
```bash
gh pr create --title "<returned title>" --body "$(cat <<'EOF'
<returned body>
EOF
)"
gh pr create --title "<returned title>" --body "$(cat "<returned body_file>")"
```
Keep the title under 72 characters; `ce-pr-description` already emits a conventional-commit title in that range.
@@ -228,13 +222,10 @@ Keep the title under 72 characters; `ce-pr-description` already emits a conventi
The new commits are already on the PR from Step 5. Report the PR URL, then ask whether to rewrite the description.
- If **yes**, run Step 6 now to generate `{title, body}` via `ce-pr-description` (passing the existing PR URL as `pr:`), then apply the returned title and body:
- If **yes**, run Step 6 now to generate `{title, body_file}` via `ce-pr-description` (passing the existing PR URL as `pr:`), then apply the returned title and body file:
```bash
gh pr edit --title "<returned title>" --body "$(cat <<'EOF'
<returned body>
EOF
)"
gh pr edit --title "<returned title>" --body "$(cat "<returned body_file>")"
```
- If **no** -- skip Step 6 entirely and finish. Do not run delegation or evidence capture when the user declined the rewrite.