make-pr · git:20261005.417553d · 2026-10-05 · sha256 ea711f377d3a8c4f

make-pr git:20261005.417553dA

Immutable. This exact content never changes and is served at /api/v1/blob/ea711f377d3a8c4f.

---
name: make-pr
description: >
  catstack-local overlay for the draft-pr skill: adds extra gates before
  publishing a PR that touches engine/hooks, engine|corpus|product skills --
  hook e2e coverage, three-harness install, and ecosystem boundaries.
  Loaded automatically instead of draft-pr for PRs in this repo.
---

# make-pr (catstack overlay)

Use `draft-pr`'s schema, confirmation rules, and diff-atomicity gate exactly
as documented -- this file only adds repo-local rules on top.

## Review unit from path

Declare exactly one review unit that matches the dominant changed paths:

| Paths | Review Unit |
| --- | --- |
| `engine/` (hooks, engine skills, scripts) | `engine-runtime` |
| `corpus/skills/` | `corpus-lesson` |
| `product/skills/` | `product-skill` |

One review unit per PR. Repo-root `tests/` and the `docs/ecosystem.md`
inventory row ride along; every other doc, including the root `README.md`,
is its own `docs` unit. `drafter.config.json` is the only source of these
rules, and preflight reads it rather than a copy. A change whose
code reads another unit's output is two stacked PRs, producer first -- not
one PR with the coupling explained in Slice Rationale.

## Stack on top, or overwrite the branch

A PR that already exists gets one of two treatments, decided from its current
diff against its base -- never from how much work went into it, and never by
asking the user which they would prefer:

- **Stack on top** when the claim still holds and what is being added is
  small: a fix to code the PR already ships, the base merged in, or docs for
  the same claim. Push another commit to the same branch, or open a new PR
  based on it.
- **Overwrite the branch** when the diff no longer matches the claim. Any one
  of these is enough:
  - it carries work already on the base
  - it mixes review units
  - it ships a second claim

  Save the old head as `backup/pr<number>-<short-sha>`, say where it went,
  then `git push --force-with-lease=<branch>:<old-sha>` and rewrite the title
  and body to the slice that remains.

The record that gets read later is the PR's final diff and body, so a branch
whose diff stopped matching its title is rewritten, not explained in Slice
Rationale. `--force-with-lease` and the backup branch are the floor, not a
precaution to skip: a lease failure means another session moved that branch,
so re-read it and decide again instead of forcing past it.

## Order of operations

Run these in order. Do not draft a PR body, then discover the review unit
from a mixed working tree.

1. **Choose one publish path before the first push.** Pick the repo's
   merge-queue / stack tool when the repo uses one, otherwise the host's
   ordinary open-PR command. Keep that choice for the whole slice — do not
   push with one tool and open the PR with another. In this repo that means
   `mergify stack push` (then refresh title/body); elsewhere it is usually
   `gh pr create` / `gh pr edit`.
2. **Classify the slice before drafting the body.** Run commit-scoped
   preflight in dry-run mode first; that is enough to learn the unit and
   the gate list:

```sh
python3 engine/skills/make-pr/scripts/preflight.py --base origin/main --commit-only --dry-run
```

3. Draft the description from the declared Review Unit.
4. **Check the draft against the same commit-scoped slice** (pass the
   description file into preflight), then push and publish only via the
   path chosen in step 1.

## Preflight (run first — before the body)

```sh
python3 engine/skills/make-pr/scripts/preflight.py --base origin/main --commit-only --dry-run
python3 engine/skills/make-pr/scripts/preflight.py --base origin/main --commit-only --body-file <draft-description.md>
```

Default classification is **commit-scoped**: paths from
`git diff --name-only <merge-base>`. Untracked files are ignored so a
parking-lot file cannot expand or mix the review unit. Use `--staged` to
add the index; use `--include-untracked` only when you mean to classify
those files (it fails when untracked count is high — park them or pass
`--paths`). Prefer `--commit-only` (or the default) for Invoker and PR
units.

Claims about the repository's own past (how long, how many, who wrote it,
never/always, first/last) are banned from PR descriptions. The premises a
change depends on are proven in the session, so the description never needs
one. `--body-file` sends the draft to the background judge with the
`pr-description-history-claims` dictionary; a hit fails preflight, and so does
a draft no judge could read. Delete the sentence rather than adding evidence
beside it.

A PR that adds rule lines to skill prose also runs
`engine/skills/make-pr/scripts/rule_scope_check.py`, which sends each added
rule to the judge with the `incident-scoped-rule` dictionary: a rule whose main
sentence is one named tool, label, command or error message fails preflight, and
so does a rule no judge could read. Rewrite the lead as the general lesson and
keep the name as an example. It runs beside
`scripts/ci/check_no_dated_provenance.py`, which reads shapes rather than
meaning and stays as it is.

It reads the commit-scoped diff, prints the review unit from the table above,
fails on any mix of review units (printing one `split` line per unit), and
runs every gate below for the hooks and skills actually touched. Paste its
output into the PR's Test Plan. The sections below describe what it runs; you
only run them by hand if it fails.

A failing preflight or `validate-pr-body.mjs` is a stop, in headless and
merge-gate runs too: do not emit a publishable body that says "needs
splitting" or "not ready to merge". Output the `split` lines and the failing
check's message instead, so whatever publishes gets nothing to publish
(fail-safe defaults, Saltzer & Schroeder 1975,
https://web.mit.edu/Saltzer/www/publications/protection/Basic.html).

## Extra gate: hook e2e coverage

Before publishing any PR that touches `engine/hooks/<name>/`:

```sh
python3 scripts/ci/check_hook_test_coverage.py engine/hooks/<name>
```

Must pass for every touched hook that has a `detect.py`. If it fails, add
the missing positive test (reproduces the bad case the hook exists to
catch, asserts it fires) or negative test (a clean case, asserts it stays
silent) first -- do not publish around the gate. This is in addition to,
not instead of, `draft-pr`'s own diff-atomicity gate.

## Extra gate: skills three-harness install

Before publishing any PR that adds or changes a skill under
`engine/skills/`, `corpus/skills/`, or `product/skills/`:

```sh
python3 scripts/ci/check_skill_test_coverage.py --base <actual-pr-base> --head HEAD
python3 scripts/ci/check_skills_three_harnesses.py
python3 scripts/ci/check_ecosystem_boundaries.py
```

All must pass. The coverage command is diff-aware: each changed skill must
change its own colocated or explicitly mapped test in that direct PR slice;
tests inherited from a parent or child stack slice do not count. A skill MUST install to Claude, Cursor, and Codex (unless listed
in `CLAUDE_ONLY_SKILLS`). Do not publish a skill that only documents a
single harness. Do not land a skill in the wrong bucket.

`engine/hooks/auto-pr`'s delivered instruction already tells the agent to run these
checks as part of its auto-triggered flow; a human asking for a PR
interactively should run them too before publishing.

## Extra gate: fixture vs live on ship closeout

On stack/PR closeout for workers or integrations whose Goal includes live
side effects (Linear, deploy, live mine, external APIs):

- Require an explicit **fixture vs live** split in the Test Plan and Summary.
- Either include live evidence from the same turn, or prefix unsettled live
  claims with `{{CAT-UNVERIFIED: <claim> -- cannot verify: <reason>}}`.
- Visual Proof that only shows UI registration must not be framed as product
  e2e of the live side effect.

Follow `prove-it-ship-gate` and Invoker or locally installed `prove-it`.