--- name: create-fix-pr description: Use when opening or updating a GitHub pull request after Phase 5 of `/compliance:analyze-cve` has applied and verified a CVE fix locally. --- # Create Fix PR Opens a GitHub pull request for the CVE fix already applied in `REPO_DIR` (Phase 5). Never commits, pushes, or opens a PR without **explicit approval** — interactive, or `AUTO_APPROVE=yes` recorded once upfront (see [Autonomous Mode](../analyze-cve/references/implementation.md#autonomous-mode---auto-approveyesno)). Direct CVE mode (no `--jira=`/`--jql=`) still creates the PR; it just omits Jira `Fixes:` links and the Jira follow-up comment. Called from **Phase 6** of the [analyze-cve](../analyze-cve/SKILL.md) skill after Phase 5 verification succeeds. --- ## When to Use This Skill Use this skill when: - Phase 5 applied a fix in `REPO_DIR` and verification passed - The user approved creating a GitHub PR - `embargo_status` is not `True` Do **not** use this skill to apply the fix itself (that is Phase 5) or to post the analysis report (that is `report-to-jira` in Phase 4). --- ## Required Inputs From the parent command: | Input | Source | Required | |---|---|---| | `REPO_DIR` | Phase 0.7 | Yes | | `GIT_BRANCH` | Phase 0.7 (mapped release branch, e.g. `release-4.17`) | Yes | | `REPO_URL` | Phase 0.7 | Yes | | `CVE_ID` | Phase 0.5 / direct CVE mode | Yes | | `SOURCE_TICKET` | `--jira=` / `--jql=` | No | | `AUTO_APPROVE` | Phase 0 (`--auto-approve`, default `no`) | Yes | | `FORK_ORG` | Caller environment (e.g. CI/RWS runner) | No — enables [Fork Mode](#fork-mode-fork_org-set) when set | | `PHASE5_FILES` | Phase 5 allowlist (incl. untracked) | Yes | | Module path, old version, new version | Phase 4 / 5 | Yes if a dependency bump | | Short CVE description | Phase 1 | Recommended | | Phase 5 change summary | Phase 5 "Document Changes" | Yes | --- ## Prerequisites Hard requirements **for this skill**. Phase 0 does **not** treat `gh` as required (warn-only). This skill must not assume either tool is present — re-check both, then **fail** if either is missing. Do not create a PR another way. ```bash which git 2>/dev/null || echo "MISSING: git" which gh 2>/dev/null || echo "MISSING: gh" gh auth status 2>/dev/null || echo "MISSING: gh auth" ``` - IF `git` is missing → print install instructions and return `status: failed` (`git_missing`). Stop. Do not commit or push. - IF `gh` is missing → print install instructions (`https://cli.github.com/`) and return `status: failed` (`gh_missing`). Stop. Do not commit or push. - IF `gh auth status` fails → print `gh auth login` instructions and return `status: failed` (`gh_unauthenticated`). Stop. Do not commit or push. - Do not print token or login output that contains credentials. > **Credential rule:** Never print, echo, or log tokens, PATs, or `gh` auth output that contains credentials. Reference credentials only via environment variable names. --- ## Fork Mode (`FORK_ORG` set) By default (no `FORK_ORG`) this skill pushes the fix branch directly to the cloned repo's own `origin` and opens a same-repo branch→base PR. That requires the authenticated `gh`/`git` identity to have **direct write access** to every upstream repo `image-repo-mapping` might resolve to (e.g. `openshift/cert-manager-operator`, `openshift/cert-manager`, `openshift/secrets-store-csi-driver-operator`, ...). In an unattended CI/RWS runner that isn't a collaborator on all of those repos, that's the wrong default. **IF the environment variable `FORK_ORG` is set** (e.g. a bot-owned org), push to a fork under that org instead and open a cross-repo PR — mirrors the fork/upstream model used elsewhere for automated OpenShift PRs. This section only changes **where the branch is pushed** and **how the PR's `--head` is specified**; Step 0 (safety gates), Step 1 (org/repo/base-branch resolution from `origin`), Step 2 (conflicting-PR detection), and Step 3b (commit, allowlist validation) are unchanged and still operate against `REPO_DIR`/`ORG`/`REPO`/`BASE_BRANCH` resolved from `origin`. ### Resolve the fork repo ```bash FORK_REPO="${FORK_ORG}/${REPO}" # REPO = repo name parsed from origin in Step 1, e.g. "cert-manager-operator" ``` ### Ensure the fork exists and is actually a fork of this repo Never assume a repo at `${FORK_REPO}` is the right push target just because the name matches — verify its fork lineage first. Reusing an unrelated (or unexpectedly renamed/transferred) repo that happens to occupy that name would push the fix branch and open a PR from somewhere unintended. ```bash if FORK_JSON=$(gh repo view "${FORK_REPO}" --json isFork,parent 2>/dev/null); then IS_FORK=$(echo "$FORK_JSON" | jq -r '.isFork') PARENT_FULL_NAME=$(echo "$FORK_JSON" | jq -r '.parent.fullName // empty') else IS_FORK="" PARENT_FULL_NAME="" fi ``` - IF `${FORK_REPO}` exists (the `gh repo view` above succeeded) **and** (`IS_FORK` is not exactly `true` **or** `PARENT_FULL_NAME` is not exactly `${ORG}/${REPO}`) → **stop immediately**, do not add the `fork` remote or push anything, and return `status: failed` (`fork_wrong_lineage`). - IF `${FORK_REPO}` exists and lineage matches → skip fork creation below, continue to Step 3a. - IF `${FORK_REPO}` does not exist → create it: ```bash gh repo fork "${ORG}/${REPO}" --org "${FORK_ORG}" --remote=false --clone=false --default-branch-only=false FORK_CREATE_EXIT=$? ``` - IF `FORK_CREATE_EXIT` is non-zero → **stop immediately** and return `status: failed` (`fork_create_failed`). Do not poll, do not add the `fork` remote, do not push — the create call already reported failure, so there is nothing to wait for. - IF `FORK_CREATE_EXIT` is `0` → the fork was accepted; poll briefly, since GitHub creates forks asynchronously: ```bash for i in $(seq 1 10); do gh repo view "${FORK_REPO}" >/dev/null 2>&1 && break sleep 3 done ``` - IF the fork still doesn't exist after polling → return `status: failed` (`fork_create_failed`). Do not fall back to pushing to `origin` — that would silently switch to requiring direct upstream write access, which is exactly what `FORK_ORG` was set to avoid. ### 3a (fork mode). Branch — same as default mode Branch creation/naming is identical to the non-fork case below (`BRANCH_NAME` derived from `CVE_ID`/`SOURCE_TICKET`, checked out from `BASE_BRANCH`). ### 3c (fork mode). Push to the fork, not `origin` ```bash git -C "${REPO_DIR}" remote add fork "https://github.com/${FORK_REPO}.git" 2>/dev/null \ || git -C "${REPO_DIR}" remote set-url fork "https://github.com/${FORK_REPO}.git" # Keep the fork in sync with the upstream base branch before pushing a branch built on top of it, # so the PR diff is just this fix — not a stale-fork drift diff. git -C "${REPO_DIR}" fetch origin "${BASE_BRANCH}" git -C "${REPO_DIR}" push fork "refs/remotes/origin/${BASE_BRANCH}:refs/heads/${BASE_BRANCH}" --force-with-lease 2>/dev/null || true git -C "${REPO_DIR}" push -u fork "${BRANCH_NAME}" ``` Never force-push `${BASE_BRANCH}` on `origin` (the upstream repo) — the force-with-lease sync above only ever targets the `fork` remote's copy of that branch name, never `origin`. ### Step 4 (fork mode). Create the PR across fork → upstream **Do not use `gh pr create --head "${FORK_ORG}:${BRANCH_NAME}"`.** `gh pr create --head` only supports a **user**-owned head repo via the `owner:branch` syntax — per `gh pr create --help`: "Using an organization as the owner is currently not supported" (tracked at [cli/cli#10093](https://github.com/cli/cli/issues/10093)). Since `FORK_ORG` may be a GitHub organization, use the REST API's `head_repo` parameter instead (via `gh api`), which unambiguously names the head repository regardless of whether `FORK_ORG` is a user or an org: ```bash printf '%s' "${PR_BODY}" > "${WORK_CVE}/pr-body.md" PR_URL=$(gh api \ "repos/${ORG}/${REPO}/pulls" \ -f head_repo="${FORK_REPO}" \ -f head="${BRANCH_NAME}" \ -f base="${BASE_BRANCH}" \ -f title="${PR_TITLE}" \ -F body=@"${WORK_CVE}/pr-body.md" \ --jq '.html_url') ``` `head` is the **bare branch name** here (no `owner:` prefix) — disambiguation comes entirely from the separate `head_repo` field, so this works whether `FORK_ORG`/`FORK_REPO` is user- or org-owned. Everything else (title/body construction, stacked-PR update path) is unchanged; `PR_URL` is captured directly from the API response instead of `gh pr create`'s output. ### Auth note `gh auth status` in Prerequisites must reflect a token with `public_repo` (classic PAT) or equivalent fork/push/PR scope on `${FORK_ORG}` and PR-creation scope against the upstream repo — this is set up by the caller before this skill runs (not part of this skill). Never echo the token itself. --- ## Step 0: Safety Gates 1. IF `embargo_status = True` → **abort immediately**. Do not commit, push, create a PR, or mention ticket contents. 2. IF Phase 5 did not complete successfully → return `status: skipped` (`phase5_incomplete`). 3. Confirm there are local changes to commit: ```bash git -C "${REPO_DIR}" status --porcelain git -C "${REPO_DIR}" diff --stat ``` - IF working tree is clean **and** HEAD is already on a fix branch with the Phase 5 remediation committed (dependency bump, source, or config) → **validate that branch's changes against `PHASE5_FILES`** (see the check in Step 3b) before skipping to Step 4. A clean worktree only means nothing is *uncommitted*; it does not prove the existing commit(s) contain only Phase 5 paths. IF the branch diff vs `${BASE_BRANCH}` contains paths outside `PHASE5_FILES` → **always return `status: failed` (`phase5_files_mismatch`) immediately. Never gated by `AUTO_APPROVE` and never prompt.** - IF working tree is clean and HEAD is still `${GIT_BRANCH}` with no Phase 5 commit → return `status: skipped` (`no_local_changes`). 4. IF `AUTO_APPROVE=no` (and the parent hasn't already recorded a yes) → Ask: ``` Phase 5 applied the fix locally. Create a GitHub PR against ? ``` - IF no → return `status: skipped` (`user_declined`). Leave the working tree as-is. - IF yes → continue. Still do not commit until Step 3. IF `AUTO_APPROVE=yes` → treat as yes, skip the prompt, continue. Still do not commit until Step 3. --- ## Step 1: Resolve GitHub Repo and Base Branch ```bash git -C "${REPO_DIR}" remote get-url origin ``` Parse `ORG/REPO` from the origin URL (`github.com/openshift/hypershift.git` → `openshift/hypershift`). If origin is SSH (`git@github.com:org/repo.git`), parse the same way. `BASE_BRANCH` = `GIT_BRANCH` from Phase 0.7 (the mapped release branch the clone is on). Do **not** open the PR against `main` unless that is actually `GIT_BRANCH`. --- ## Step 2: Detect Conflicting Open PRs **Title match only.** Do not inspect PR files, body, module versions, or `go.mod` diffs. ```bash gh pr list --repo "${ORG}/${REPO}" --state open --base "${BASE_BRANCH}" \ --json number,title,url,headRefName,author ``` A PR is a **match** if its **title** contains (case-insensitive) either this `CVE_ID`, or `SOURCE_TICKET` (when set). IF none → continue to Step 3 with strategy `new`. IF any match: - **`AUTO_APPROVE=no`** → present the list and wait for the user: ```text Open PR(s) on base already have this CVE/Jira in the title: # <url> head=<branch> How should I proceed? 1. stack — check out that PR branch, commit this fix on top, push (PR updates) 2. wait — stop; do not open a competing PR 3. independent — new branch from <BASE_BRANCH> (may need rebase later) ``` | Choice | Action | | --- | --- | | `stack` | `git fetch origin <headRefName> && git checkout <headRefName> && git pull`. Re-apply leftover Phase 5 changes if they are not already on that branch (same method as Phase 5 — bump, patch, or config edit). Strategy = `stack`. | | `wait` | Return `status: skipped` (`user_wait_for_pr`) including the existing PR URL. | | `independent` | Warn that a second PR on the same base may need rebase later. If `go.mod` is in this diff, mention possible module-file conflicts. Strategy = `new`. | | No answer | **Do not guess.** Ask again. | - **`AUTO_APPROVE=yes`** → **always `wait`**, automatically, with no exceptions: return `status: skipped` (`user_wait_for_pr_auto`) including the existing PR URL. Never auto-choose `stack` (that pushes commits onto a branch nobody reviewed for this run) or `independent` (that opens a second, possibly duplicate/conflicting PR) unattended. Log clearly that this run stopped because of the existing PR, so a human can revisit with `--auto-approve=no` if a different resolution is actually wanted. --- ## Step 3: Branch, Commit, Push Work only inside `REPO_DIR`. **Vendor:** Phase 5 already ran `go mod vendor`/`make vendor` (if applicable) and included any resulting paths in `PHASE5_FILES` before verification. Do **not** run vendor sync here — doing so after `PHASE5_FILES` was written would generate paths that never make it into the allowlist and get silently dropped from the commit. IF a dependency bump is present in `PHASE5_FILES` but `vendor/` looks out of sync (e.g. `go mod vendor` reports diffs) → stop and tell the user to re-run Phase 5, rather than fixing it here. ### 3a. Branch **New PR (`strategy = new`):** ```bash git -C "${REPO_DIR}" fetch origin "${BASE_BRANCH}" git -C "${REPO_DIR}" checkout "${BASE_BRANCH}" git -C "${REPO_DIR}" pull --ff-only origin "${BASE_BRANCH}" SLUG="$(echo "${CVE_ID}" | tr '[:upper:]' '[:lower:]')" if [ -n "${SOURCE_TICKET}" ]; then BRANCH_NAME="fix/${SLUG}-$(echo "${SOURCE_TICKET}" | tr '[:upper:]' '[:lower:]')" else BRANCH_NAME="fix/${SLUG}" fi git -C "${REPO_DIR}" checkout -b "${BRANCH_NAME}" ``` Re-apply the Phase 5 changes on this branch if checking out `BASE_BRANCH` discarded uncommitted work. Prefer not discarding: stash before checkout if the working tree is dirty, then stash pop onto `BRANCH_NAME`. **Stack (`strategy = stack`):** already on the existing PR branch. Set `BRANCH_NAME` to that head ref. Do not create a new branch. ### 3b. Commit Stage **only `PHASE5_FILES`**, not the whole worktree. That allowlist is written in Phase 5 (`phase5-files.txt`) and includes new untracked files. `go.mod`, `go.sum`, `go.work`, and `vendor/` are common for **dependency version bumps**, but other CVE remediations may only touch source, config, Dockerfiles, or scripts. `WORK_CVE` is in the **command's own workspace** (`.work/`), not inside `REPO_DIR`. Paths in the allowlist are relative to `REPO_DIR`. ```bash WORK_CVE="${AI_HELPERS_WORKSPACE:-.}/.work/compliance/analyze-cve/${CVE_ID}" PHASE5_FILES="${WORK_CVE}/phase5-files.txt" ``` IF `phase5-files.txt` is missing or empty → rebuild it from Phase 5 "Document Changes" (and `phase5-before.status` if present). IF the allowlist still cannot be determined → **always return `status: failed` (`phase5_files_missing`) immediately. Never gated by `AUTO_APPROVE` and never prompt.** Do **not** fall back to whole-worktree `git diff` / `git add -A` / `git add .` in any case. ```bash # inspect current tree for sanity, but do not use it as the stage set git -C "${REPO_DIR}" status --porcelain while IFS= read -r f; do [ -n "${f}" ] || continue git -C "${REPO_DIR}" add -- "${f}" done < "${PHASE5_FILES}" ``` `git add -- <path>` stages modifications, deletions, and untracked files. Examples of what belongs on the allowlist: - Dependency bump: `go.mod`, `go.sum`, and `go.work` / `vendor/` only if Phase 5 changed them - Code/config fix: the source or config files Phase 5 added or edited - Mixed: the union of those paths Do **not** add `.work/`, analysis reports, or credentials. Do **not** add pre-existing dirty files that are absent from `PHASE5_FILES`. Do **not** `git add` `go.mod` / `vendor/` unless they are on the allowlist. **Validate the staged set exactly matches the allowlist.** The loop above only *adds* allowlisted paths — it does not prove the index is free of anything else (e.g. leftover pre-existing staged files). Diff the two sets and reject extras instead of committing them silently: ```bash git -C "${REPO_DIR}" diff --cached --name-only > "${WORK_CVE}/staged-files.txt" EXTRA="$(comm -23 <(sort "${WORK_CVE}/staged-files.txt") <(sort "${PHASE5_FILES}"))" if [ -n "${EXTRA}" ]; then echo "Unstaging paths not in PHASE5_FILES:" echo "${EXTRA}" while IFS= read -r x; do [ -n "${x}" ] || continue git -C "${REPO_DIR}" restore --staged -- "${x}" done <<< "${EXTRA}" fi MISSING="$(comm -23 <(sort "${PHASE5_FILES}") <(sort "${WORK_CVE}/staged-files.txt"))" if [ -n "${MISSING}" ]; then echo "ERROR: PHASE5_FILES paths missing from the staged set:" echo "${MISSING}" exit 1 # return status: failed (phase5_files_incomplete) fi ``` IF any path was rejected → tell the user which paths were unstaged and why before continuing. Re-run the diff after unstaging to confirm the index now matches `PHASE5_FILES` exactly (accounting for deletions). **On a `stack` branch, or when Step 0 skipped ahead because a Phase 5 commit already existed,** run the equivalent check against the branch, not just the index: ```bash git -C "${REPO_DIR}" diff --name-only "${BASE_BRANCH}...HEAD" > "${WORK_CVE}/branch-files.txt" comm -23 <(sort "${WORK_CVE}/branch-files.txt") <(sort "${PHASE5_FILES}") ``` IF that reports any path outside `PHASE5_FILES` → stop; do not push or open/update the PR. **Always return `status: failed` (`phase5_files_mismatch`) immediately, for manual follow-up. Never gated by `AUTO_APPROVE` and never prompt.** **Commit message.** Match the **actual** Phase 5 change, not a canned module bump. Use the repo's own convention when detectable — e.g. many OpenShift repos expect `UPSTREAM: <upstream-pr-or-carry>: <subject>`: ```bash git -C "${REPO_DIR}" log --oneline -20 ``` If the recent history shows the `UPSTREAM:` convention and a dependency bump with a known upstream fix PR number (from the Phase 4 remediation plan): ``` UPSTREAM: <upstream-pr>: Bump <module> to <new> for <CVE_ID> ``` If a dependency bump that is downstream-only / fork / carry (no tracked upstream PR, but repo uses `UPSTREAM:` convention): ``` UPSTREAM: <carry>: Bump <module> to <new> for <CVE_ID> ``` If the repo does not use the `UPSTREAM:` convention, use a plain subject. Dependency bump: ``` Bump <module> to <new> for <CVE_ID> ``` If the fix is **not** a version bump (code, config, workaround), regardless of convention: ``` Fix <CVE_ID>: <short description of the change> ``` Body: one or two sentences describing what changed and why. Include `Fixes: <SOURCE_TICKET>` in Jira mode; omit that line in direct CVE mode. For a bump, include old → new versions. Do not claim a module bump if Phase 5 did not bump a module. ```bash git -C "${REPO_DIR}" commit --signoff -m "${COMMIT_SUBJECT}" -m "${COMMIT_BODY}" ``` Sign off (DCO) by default — most upstream/downstream OpenShift-style repos require it and a missing sign-off is a common, avoidable PR-check failure. Never use `--no-verify` or skip hooks unless the user explicitly asks. ### 3c. Push ```bash git -C "${REPO_DIR}" push -u origin "${BRANCH_NAME}" ``` Default is a normal push. Amending an existing commit and force-pushing is never authorized by `AUTO_APPROVE`; it always requires an interactive user (moot in practice, since `AUTO_APPROVE=yes` never selects `stack` — see Step 2). --- ## Step 4: Create or Update the GitHub PR ### Title Dependency bump: ``` <CVE_ID>: bump <module-short> to <new> [<SOURCE_TICKET>] ``` Omit `[<SOURCE_TICKET>]` in direct CVE mode. Example: `CVE-2026-33186: bump google.golang.org/grpc to v1.79.3 [OCPBUGS-80452]` Non-bump fix: ``` <CVE_ID>: <short description> [<SOURCE_TICKET>] ``` ### Body Read the repo's PR template if one exists (`cat "${REPO_DIR}/.github/PULL_REQUEST_TEMPLATE.md" 2>/dev/null`) and structure the body around it. Otherwise use: **Jira mode** — every Jira key must be a markdown link. Use a `Fixes:` line (do not use a bare key): ```markdown ## Summary Fixes: [<SOURCE_TICKET>](<JIRA_BASE_URL>/browse/<SOURCE_TICKET>) <What Phase 5 changed and why — e.g. bump `<module>` from `<old>` to `<new>`, or a code/config workaround.> <2–3 sentence CVE description from Phase 1> ## Changes - <file- or behavior-level bullets from the Phase 5 diff> ## Verification - `make verify` / `go mod verify` (if modules changed) - `make build` / `go build ./...` - `govulncheck ./...` --- Always review AI generated responses prior to use. AI-assisted change via compliance plugin (`/compliance:analyze-cve`) ``` **Direct CVE mode** — same body **without** the `Fixes:` line and without Jira URLs. Do not include hostnames, cluster names, routes, usernames, passwords, or tokens in the title, body, or comments. ### Create vs update **New PR (default mode — no `FORK_ORG`):** ```bash gh pr create \ --repo "${ORG}/${REPO}" \ --base "${BASE_BRANCH}" \ --head "${BRANCH_NAME}" \ --title "${PR_TITLE}" \ --body "${PR_BODY}" ``` **New PR (fork mode — `FORK_ORG` set):** use the [Fork Mode](#fork-mode-fork_org-set) Step 4 variant (`gh api repos/.../pulls -f head_repo=...`) instead of the `gh pr create` call above. **Stacked on existing PR:** push is enough (to `origin` by default, or `fork` in Fork Mode). Optionally: ```bash gh pr comment "${EXISTING_PR}" --repo "${ORG}/${REPO}" \ --body "Added <CVE_ID> fix: <what Phase 5 changed>." ``` And `gh pr edit` to append the CVE / Jira key to title and the `Fixes:` line if missing. Capture `PR_URL` from `gh pr create` output or `gh pr view --json url` (default mode), or from the `gh api` response (Fork Mode). IF create fails (permissions, fork needed) → show the exact `gh` error (no secrets). If the error indicates missing write access and `FORK_ORG` is not set, suggest re-running with `FORK_ORG` configured (see [Fork Mode](#fork-mode-fork_org-set)) rather than guessing at a fork target. Otherwise give the user the title/body to paste, return `status: failed` (`pr_create_failed`). Do not retry more than once. --- ## Step 5: Post PR URL to Jira (Jira mode only) **Skip this step** if `SOURCE_TICKET` is unset (direct CVE mode) → still `status: success` for the GitHub PR. Use the [report-to-jira](../report-to-jira/SKILL.md) skill's **Follow-up: PR URL comment** section. Do not edit or replace the Phase 4 analysis comment. Do not add/remove labels here. IF posting fails → display the comment in session for manual paste. The GitHub PR is still a success (`jira_followup: failed`). --- ## Return Value **Success:** ```json { "skill": "create-fix-pr", "status": "success", "action": "created | updated", "pr_url": "https://github.com/<org>/<repo>/pull/<n>", "pr_number": 123, "branch": "fix/cve-yyyy-nnnnn-ocpbugs-12345", "base_branch": "release-4.17", "cve_id": "CVE-YYYY-NNNNN", "source_ticket": "OCPBUGS-12345 or null", "jira_followup": "posted | skipped | failed" } ``` **Skipped:** ```json { "skill": "create-fix-pr", "status": "skipped", "reason": "user_declined | no_local_changes | phase5_incomplete | user_wait_for_pr | user_wait_for_pr_auto | embargo" } ``` **Failed:** ```json { "skill": "create-fix-pr", "status": "failed", "reason": "git_missing | gh_missing | gh_unauthenticated | pr_create_failed | commit_failed | phase5_files_missing | phase5_files_mismatch | fork_wrong_lineage | fork_create_failed", "error": "<message without secrets>" } ``` --- ## Integration with analyze-cve Called from **Phase 6** of the [analyze-cve](../analyze-cve/SKILL.md) skill after Phase 5 verification succeeds. Runs after the [Repo Guard](../analyze-cve/references/implementation.md#repo-guard--re-clone-if-missing) has confirmed `REPO_DIR` still exists. --- ## Guardrails - Never commit, push, or open a PR without explicit approval — interactive, or `AUTO_APPROVE=yes` recorded once upfront - Never force-push to `BASE_BRANCH` / `main` / `master` / `release-*` - Never skip git hooks unless the user asks - Never stage paths outside `PHASE5_FILES`; never `git add -A` or `git add .` - Never commit/push/PR without validating the staged (and, for `stack`, the branch) path set against `PHASE5_FILES` and rejecting extras - Never run vendor sync in Phase 6 — it happens in Phase 5, before `PHASE5_FILES` is written - Never push to, or open a PR from, a `FORK_ORG` repo without first verifying its fork lineage (`isFork` + `parent.fullName` match) — never reuse a same-named repo that isn't actually a fork of the target - Never let `AUTO_APPROVE=yes` auto-select `stack` or `independent` on a PR-title conflict — always `wait` unattended - Never let `AUTO_APPROVE=yes` skip a hard-fail case (missing/mismatched `PHASE5_FILES`, ambiguous conflict resolution) — those always require a human, flag or not - Never include secrets in commit messages, PR text, or Jira comments - Stop immediately on embargo - Direct CVE mode must still produce a PR