--- name: commit-pr audience: swarm-plugin description: > Apply when committing, pushing, opening or updating a PR, writing a pull request, creating release notes, or closing out remote CI. Enforces the opencode-swarm invariant audit, release-note fragment workflow, full validation suite, issue comment requirement, and post-PR lifecycle rules. effort: medium --- # Commit & PR Protocol ## Graph-first evidence contract Before publication, use `repo_map` `diff_context` and `impact_cone` as a final drift check, then verify the direct source, Git diff, and tests. Graph evidence is advisory only. If freshness is stale or inconclusive, confidence is low, source is missing, the language is unsupported/dynamic, the graph is absent, or an action fails, publication decisions rely on the direct evidence. Follow every step in order. Do not skip steps. ## Step -1 - Mandatory invariant audit Before any build, test, push, or PR action, read: 1. [`../../../AGENTS.md`](../../../AGENTS.md) 2. [`../../../docs/engineering-invariants.md`](../../../docs/engineering-invariants.md) For every touched invariant, prepare concrete evidence for the PR body. The PR body must include: ```md ## Invariant audit - 1 (plugin init): touched / not touched - - 2 (runtime portability): touched / not touched - - 3 (subprocesses): touched / not touched - - 4 (.swarm containment): touched / not touched - - 5 (plan durability): touched / not touched - - 6 (test_runner safety): touched / not touched - - 7 (test writing): touched / not touched - - 8 (session state): touched / not touched - - 9 (guardrails/retry): touched / not touched - - 10 (chat/system msg): touched / not touched - - 11 (tool registration): touched / not touched - - 12 (release/cache): touched / not touched - ``` If a touched invariant cannot be proven from source and test output, do not push. ### Required validations for touched invariants If invariants 1, 2, or 3 are touched, run all three: ```bash bun run build node scripts/repro-704.mjs node --input-type=module -e "await import('./dist/index.js'); console.log('dist import OK')" ``` If invariant 3 is touched, audit changed source files for subprocess use: ```bash git diff --name-only origin/main..HEAD | xargs -r grep -nE "bunSpawn\(|spawn\(|spawnSync\(" || true ``` If invariant 11 is touched, run: ```bash bun --smol test tests/unit/config --timeout 60000 for f in tests/unit/tools/*.test.ts; do bun --smol test "$f" --timeout 30000; done ``` If invariant 7 is touched, confirm the writing-tests skill was loaded and that new test seams avoid leaking `mock.module`. ## Step 0 - Session start hygiene Run before publication work: ```bash git fetch origin main rm -f .swarm/evidence/pr_body.md rm -f .swarm/evidence/commit-pr-validation.md rm -f .swarm/evidence/publication-evidence.json git status --short ``` Delete ONLY those three exact publication-cache files. NEVER broad-delete `.swarm/evidence/*.json` — that directory also holds authoritative task/gate evidence (`{taskId}.json`), `final-council.json`, and phase-council records (issue #2131 finding 4a). On Windows, prefer temporary save branches over `git stash`. If you must stash, use `git stash push --include-untracked` and verify the stash contents. If this changeset edited any SKILL.md file's wording, also run `file:.swarm/bundled-skills/skill-edit-validation/SKILL.md`'s content-assertion sweep before committing, to catch stale test assertions. ## Step 1 - Commit and PR titles Use conventional commit format `(): ` exactly. - description is lowercase and does not end with a period - allowed types: `feat`, `fix`, `perf`, `revert`, `docs`, `chore`, `refactor`, `test`, `ci`, `build` Choose the PR title type by the main change: - new capability -> `feat` - bug fix only -> `fix` - docs or chore only -> non-bump types The squash merge commit message must match the PR title exactly. > **Note:** The PR title MUST follow `(): ` exactly — CI runs `action-semantic-pull-request` which will fail the `check-title` job if the format is wrong. Do not deviate from this format. ## Step 2 - Release note fragment Create a pending release fragment and do not calculate a version manually. Required file shape: ```text docs/releases/pending/.md ``` The fragment should cover: - what changed - why - migration steps, if any - breaking changes, if any - known caveats #### Ground-truth verification (mandatory) Before finalizing a release note fragment, verify every concrete reference against the actual codebase: - **File paths**: `grep -r "path/to/file"` — confirm each referenced file exists at the stated path. - **Script names**: check `package.json` scripts or `scripts/` directory. - **Attribute/variable names**: grep for the exact identifier in `src/`. - **Environment variables**: grep for the exact name in `src/` and config. Remove or correct any reference that does not resolve. AI-generated fragments frequently hallucinate paths and names — this step is not optional. Bot authors (e.g. `[bot]`, Copilot) and docs/workflow/meta-only changes (no `src/`, `tests/`, `scripts/`, `package.json`, etc.) are exempt from this requirement by CI; all other PRs that touch code must include a fragment. Do not manually edit: - `package.json` version - `CHANGELOG.md` - `.release-please-manifest.json` — exception: reconciliation when the manifest desyncs from actual releases (see below) For the full release-please manifest desync diagnosis and fix protocol, read `references/pr-incident-playbook.md`. ## Step 3 - Mandatory validation suite Run the full validation stack before pushing. The exact commands may be narrowed only when the repo contract or current task explicitly justifies it in evidence, not by intuition. ### Pre-flight `dist/` is not committed (#1047) — do not stage it. Confirm the build succeeds: ```bash bun run build node --input-type=module -e "await import('./dist/index.js'); console.log('dist import OK')" ``` ### Tier 1 - quality Run both linter AND formatter — e.g., `bunx @biomejs/biome@ check --write .` or equivalent — because CI quality gates reject code that passes tests but fails style validation. **Pin the tool version** to match the version in `package.json` (`@biomejs/biome`); unversioned `bunx biome` resolves to a different version than the CI gate uses. (the repo script `bun run lint:ci` runs the pinned Biome version; prefer it over invoking `bunx biome` directly). ```bash bun run typecheck bun run lint:ci bun run scripts/check-tool-registration.ts bun run check:mock-cleanup bun run check:invariants bun run check:cross-contamination bun run check:test-clock bun run check:runtime-src-refs bun run check:events bun run check:retention bun run check:registry-citations bun run check:core-events bun run check:shell-audit bun run check:trajectory-store bun run check:test-file-cap bun run scripts/check-vacuous-assertions.ts bun run check:pending-fragment bun run check:gate-portability bun run check:bare-spawn bun run scripts/check-config-consumption.ts bun run check:test-tmpdir bun run check:bash-portability bun run check:error-channel-discard bun run check:path-identity bun run check:token-formula (cd scripts/swarm-model && node --test) bun run package:smoke ``` The CI quality job also fails closed when either required dependency is not successful before running the expensive checks: ```bash if [[ "$DETECT_RELEASE_RESULT" != "success" || "$RELEASE_OWNER_RESULT" != "success" ]]; then echo "::error::required dependency failed: detect-release=$DETECT_RELEASE_RESULT, release-owner-guard=$RELEASE_OWNER_RESULT" exit 1 fi ``` This list is the FULL blocking CI quality contract (issue #2131 finding 4c): every quality-job step in `.github/workflows/ci.yml` plus the `package:smoke` pack check. `tests/unit/skills/commit-pr-validation-parity.test.ts` derives the list from the workflow — if CI adds a quality step, the parity test fails until this skill teaches it. ### Tier 2 - unit tests ```bash bun run test:unit:ci # CI-equivalent unit gate (quarantine ledgers + retry budget) bun run test:unit:ci ... # scoped mode: run only the listed repo-relative test files ``` `bun run test:unit:ci` runs `scripts/ci/run-unit-tests-local.ts`, the CI-equivalent unit gate with per-platform quarantine ledgers (`scripts/ci/quarantined-tests.txt`, `quarantined-tests-macos.txt`, `quarantined-tests-windows.txt`) and the retry budget. The full serial run is 45–90 min, so use the scoped mode (`bun run test:unit:ci ...`) when the evidence justifies narrowing (e.g. you only touched one module). This closes the gap the hand-rolled loops left (~40% of CI's colocated `src/**` tests were uncovered). ### Tier 3 - integration ```bash bun test tests/integration ./test --timeout 120000 ``` ### Tier 4 - security and adversarial ```bash bun test tests/security --timeout 120000 bun test tests/adversarial --timeout 120000 ``` ### Tier 5 - smoke ```bash bun test tests/smoke --timeout 120000 ``` ### Pre-existing failure handling If a failure looks unrelated, prove it on clean `origin/main` before carrying it into the PR body: ```bash git worktree add /tmp/repro-check origin/main cd /tmp/repro-check bun install --frozen-lockfile bun run build bun --smol test --timeout 120000 cd .. git worktree remove /tmp/repro-check ``` If the failure reproduces on `main`, document it under `## Pre-existing failures`. Do not silently inherit it. ### dist/ is generated, not committed `dist/` is build output and is git-ignored (#1047); do **not** stage or commit it, and there is no `dist-check` drift gate. The authoritative artifact check is `package-check`, which runs `npm pack` and verifies the packed tarball is complete (type declarations, grammar assets), installs it in a temp project, imports it under Node, and runs the CLI. A `package-check` failure is a source / build / `package.json#files` problem — fix the source or manifest and rebuild; never "commit dist to make CI green." CI builds `dist/` itself (the `unit`, `package-check`, and `smoke` jobs run `bun run build`), and release/publish builds from source. ## Step 4 - Workflow changes If any `.github/workflows/*.yml` file changed, every third-party `uses:` must be pinned to a full 40-character SHA. ## Step 5 - History shape Before opening a PR, verify no local-only files are staged: ```bash git diff --name-only HEAD origin/main | grep -E '\.(local\.json|vscode|idea)' || true ``` Prefer a single clean commit for the branch before initial PR publication. Resolve the canonical remote first (see "Canonical remote resolution" below), then squash/push: ```bash git fetch git log --oneline /..HEAD git reset --soft / git commit -m "type(scope): description" git push --force-with-lease -u ``` If a review cycle is already active and inline comments depend on current SHAs, avoid resquashing until threads are resolved. If pushing to a PR branch owned by another agent or bot, push to the PR's actual head branch: ```powershell $prBranch = gh pr view --json headRefName --jq '.headRefName' git fetch $prBranch git push ":$prBranch" --force-with-lease ``` ### Fork PR workflow approval If the PR is from a fork (head repository differs from base repository), GitHub requires explicit workflow approval after every push. CI jobs will remain in "waiting" status until approved: ```bash # List pending runs gh run list --repo / --branch --limit 5 # Approve the pending run gh api -X POST repos///actions/runs//approve ``` This requires `actions: write` permission on the base repository. See the `fork-pr-operations` skill for the full protocol including race conditions, permission requirements, and stale CI verification. ### Pre-push: Push Protection and Canonical Remote Before `git push`, run both checks: 1. **Push protection scan** — scan for literal secret patterns that trigger GitHub push protection: ```bash git log origin/main..HEAD -p | grep -E 'sk_live|ghp_|xox[abprs]-|AKIA|eyJ|AIza' || true ``` 2. **Canonical remote resolution** — push to the canonical-org remote, not a personal fork: ```bash git remote -v # identify the org-owned remote git push -u ``` For full details, patterns, workarounds, and anecdotes, read `references/pr-incident-playbook.md`. ## Step 6 - PR creation Before generating the PR body, read `.swarm/issue-reference.json`. If it exists and contains a `number` field, auto-populate `Closes #` as the first line of the PR body. If the file does not exist, fall back to the `Closes #` placeholder. PR body requirements: - `Closes #` as the first line when the PR resolves an issue - `## Summary` - `## Invariant audit` - `## Test plan` CI enforces both the section ORDER (Summary, then Invariant audit, then Test plan) and that each heading is on its own line with nothing trailing but whitespace (see `.github/workflows/pr-standards.yml`). ### Publication-gate evidence A repository publication gate (`.github/hooks/pr-publication-gate.json` -> `scripts/copilot-pr-publication-gate.sh`) may block `gh pr create`, `gh pr edit`, and `gh pr ready` until publication evidence exists. Before publishing, write: - `.swarm/evidence/pr_body.md` — the exact PR body you will publish (must contain `## Summary`, `## Invariant audit`, and `## Test plan`). - `.swarm/evidence/commit-pr-validation.md` — the validation commands you ran and their results. - `.swarm/evidence/publication-evidence.json` — the versioned, freshness-bound receipt (issue #2131 finding 4b). The gate verifies it against the CURRENT git state and the EXACT body being published: ```bash BODY_SHA256=$(sha256sum .swarm/evidence/pr_body.md | cut -d' ' -f1) cat > .swarm/evidence/publication-evidence.json <" } $body = @" $closesLine ## Summary - - ## Invariant audit - 1 (plugin init): not touched - ## Test plan - [ ] "@ $utf8NoBom = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false $prBodyPath = Join-Path ([System.IO.Path]::GetTempPath()) "pr_body.txt" [System.IO.File]::WriteAllText($prBodyPath, $body, $utf8NoBom) gh pr create --title "(): " --body-file $prBodyPath --base main ``` ### Issue-trace publication receipt If this session is running an issue trace (`/swarm issue --trace`), call `record_issue_publication` with the issue number, PR number, URL, and HEAD sha immediately after `gh pr create` (or after `gh pr edit` / `gh pr ready` for an existing PR). This is what lets the issue-trace workflow reach its terminal `published` state — without it the trace stays at `publication_handoff` and is NOT considered resolved. ## Step 6a - PR monitoring subscription After PR creation, if the project uses PR monitoring (`pr_monitor.enabled: true` in resolved opencode-swarm config), the new PR must be subscribed for background monitoring: - **Automatic (default):** when `pr_monitor.auto_subscribe_on_pr_create` is enabled (default `true`), the subscription is created automatically after `gh pr create` succeeds — no command needed. Verify with `/swarm pr status` if in doubt. - **Manual fallback:** when auto-subscribe is disabled or did not fire, run `/swarm pr subscribe `, which records the subscription and lazy-starts the polling worker. The post-subscription monitoring protocol — event intake, triage (fix / ask / skip), bounded-retry escalation, and terminal-state behavior — lives in the swarm-pr-subscribe skill (`../swarm-pr-subscribe/SKILL.md`). ## Step 6.5 - Issue comment If the PR closes an issue, post a comment on the issue. This is mandatory. The issue comment must include: 1. the PR link 2. what changed 3. how to use it 4. migration steps or "No migration required" Uses the same UTF-8-no-BOM file-write pattern as Step 6 above. ````powershell $comment = @" Fixed in PR #. ## What changed - - ## How to use ```json { "config": "example" } ``` ## Migration No migration required. "@ $utf8NoBom = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false $issueCommentPath = Join-Path ([System.IO.Path]::GetTempPath()) "issue-comment.txt" [System.IO.File]::WriteAllText($issueCommentPath, $comment, $utf8NoBom) gh issue comment --body-file $issueCommentPath ```` If the PR merged before this was done, post the missing issue comment immediately. ## Commit messages PowerShell parens/brackets/backticks/dollar-signs in `git commit -m "..."` fail. Use the same UTF-8-no-BOM file-write pattern (see Step 6), then `git commit -F `. ## Step 7 - Existing PR follow-up and closeout If a PR already exists for the branch: 1. do not open a second PR 2. inspect unresolved PR feedback surfaces before updating or readying the PR: review threads/comments, requested-changes reviews, CI/check failures, mergeability/conflicts, and whether check data belongs to the current head SHA 3. use `../swarm-pr-feedback/SKILL.md` when feedback needs fixes before closeout 4. update the existing PR body when summary, invariant evidence, test counts, caveats, or pre-existing failure notes changed 5. keep the PR draft while follow-up edits are still expected or required checks are still pending 6. mark the PR ready only after the body is current and required remote checks are green, unless the user explicitly wants it ready earlier 7. after any follow-up push or force-push, verify the PR head matches the expected commit and that reported checks belong to the current `headRefOid`: ```powershell gh pr view --json headRefOid,body,isDraft,state,mergeable,mergeStateStatus,statusCheckRollup,url ``` Useful commands: ```powershell gh pr edit --body-file "$env:TEMP\pr_body.txt" gh pr ready gh pr checks --watch --fail-fast ``` ### Conflict closeout After resolving merge conflicts or syncing a stale branch: 1. verify there are no local unmerged paths or conflict markers, 2. push the conflict-resolution commit, 3. verify GitHub reports both `mergeable: MERGEABLE` and `mergeStateStatus: CLEAN`, not merely that local markers are gone, and 4. keep a conflict/branch-drift item in the PR closure ledger when it affected the PR. If GitHub still reports `DIRTY`, `BLOCKED`, or stale checks after local conflict resolution, fetch current `origin/main` again and re-evaluate before claiming the conflict is resolved. ### GitHub auto-merge race condition With a merge queue enabled, prefer queuing over manual freshness rebases. If you encounter "fetch first" errors after a fresh fetch, GitHub may have auto-pushed a merge commit. Recovery: force-push your local commit with `--force-with-lease`. See `references/pr-incident-playbook.md` for full diagnosis and recovery steps. ### Check closeout `gh pr checks --watch --fail-fast` is useful but can lag or flatten matrix and downstream jobs. When the PR checks view looks stale, missing, or inconsistent, use the workflow run as the authoritative detail: > **MCP environments:** When using GitHub MCP tools instead of `gh`, prefer > `get_check_runs` over `get_status`. The `get_status` method uses GitHub's > legacy commit status API: it returns `state: "pending"` even when all GitHub > Actions jobs are green, because Actions creates check-runs (not legacy > statuses). `get_check_runs` returns the actual job results. ```powershell gh run view --json headSha,status,conclusion,jobs,url ``` Keep watching after unit jobs pass; this repository may enqueue integration and smoke jobs later in the same CI run. Do not call the PR green until the current `headRefOid` has all required jobs completed successfully. If a previous run from an older PR head is still in progress or already failed and is blocking the current head's workflow through concurrency, inspect it with `gh run view --json headSha,status,conclusion,jobs,url`. Cancel only obsolete older-head runs that are no longer relevant to the PR head you are validating, then wait for the current-head checks to complete. If you edit the PR body after checks are green, expect PR Standards / title checks to rerun. Re-check before claiming final green or merge-readiness. ### Merge queue (current-base validation) Read `file:.swarm/bundled-skills/merge-queue-readiness/SKILL.md` for the full pre-queue merge-group CI simulation protocol. When `main` has a GitHub **merge queue** enabled, do not rebase or force-push a PR *solely because `main` advanced*. Once required checks and review are green, add the PR to the merge queue; GitHub re-runs the required workflows against the queued change on top of the latest `main` (and any earlier queued PRs) before merging, so manual "freshness" rebases are unnecessary. Still rebase/force-push when there is a **real** reason: a genuine merge conflict, a stale review thread that depends on current SHAs, or a correctness issue that only appears against current `main`. The queue handles up-to-date validation; it does not resolve conflicts for you. Required workflows trigger on both `pull_request` and `merge_group`. PR-only checks (title/body validation) no-op to success on `merge_group` because the PR already satisfied them before being queued. ## Step 8 - Cancelled jobs and skipped dependents If a required GitHub Actions job is `cancelled` and downstream jobs are `skipped`: 1. inspect the run: ```powershell gh run view --json status,conclusion,jobs,url ``` 2. if the cancellation looks like orchestration or infrastructure rather than a code failure, rerun the failed or cancelled jobs: ```powershell gh run rerun --failed ``` 3. re-check the PR until required jobs are green: ```powershell gh pr checks --watch --fail-fast ``` Do not call the PR green or merge-ready while a required job is `cancelled`, `skipped`, `in_progress`, or otherwise non-green unless the user explicitly accepts that state. ## Step 9 - Pre-merge checklist - [ ] invariant audit is complete and current - [ ] required build and validation commands ran for touched invariants - [ ] `test_runner` was not used with broad repo-validation scopes - [ ] release fragment exists and version files are untouched - [ ] `dist/` was NOT staged (it is generated output, not committed — #1047) - [ ] PR body has `Closes`, `## Summary`, `## Invariant audit`, and `## Test plan` - [ ] if this was review follow-up, the PR body was refreshed to match current evidence - [ ] if the PR resolves an issue, the issue comment was posted with PR link, what changed, how to use it, and migration notes - [ ] if any required job was cancelled and dependent jobs skipped, the run was rerun or the non-green state was explicitly accepted by the user - [ ] for high-risk work (security, isolation, IPC, auth, payments, migrations), an independent adversarial review subagent ran before the final substantive push and all confirmed findings were addressed — if this was not done before pushing, run the review now and force-push a corrected commit before marking the PR ready - [ ] all required CI checks are green before calling the PR merge-ready