### Phase 6: Commit & PR

> **TLDR**  -  Commit with conventional message + ticket ID, push, open PR. Order: (1) identity confirm → (2) commit → (3) push → (4) fetch default reviewers → (5) ask DRAFT-or-READY → (6) build payload with heredoc+rawfile → (7) POST with `reviewers`+`fromRef`+`toRef`+`draft` required on Bitbucket (missing fields wipe defaults).

#### Analysis mode: publish, do not commit

When `state.mode === "analysis"` this phase publishes the document and skips everything below. Locked 6 forbids `git add` and `git commit` here, so nothing in the repo is staged.

Destination picker and dispatch: `$HOME/.claude/multi-agent-refs/analysis/render.md`. Local file is pre-selected (Locked 5), multiple destinations allowed.

Full pipeline is the opposite: Phase 1 already wrote the document into the worktree, and `prefs.global.analysisPhase.commitDoc` (default `true`) decides whether it rides along with the commit below.

<!-- progress-contract: applied -->
Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md`  -  lines for each per-repo squash, commit, push attempt (including retries), PR body generate, PR create, Jira comment post.
>
> ```
>   commit ──► push ──► fetch default reviewers ──► draft/ready prompt
>                                                        │
>   ┌──────────────── PR payload ─────────────────────────┘
>   │  title + description (rawfile) + reviewers + fromRef + toRef + draft
>   └──► POST (Bitbucket) or gh pr create --draft (GitHub) ──► PR open
> ```

#### Input contract

Phase 6 consumes the latest Phase 4 triage output object conforming to `$HOME/.claude/schemas/triage-output.schema.json` (`accepted`, `deferred`, `rejected` buckets) plus the working-tree mutations from Phase 3. The commit body cites only `accepted` findings that were resolved; `deferred` items must be linked in the PR description as follow-up work, never silently dropped. If the triage output reports any unresolved blocking accepted finding, Phase 6 refuses to commit  -  the pipeline returns to Phase 3 for rework.

The "Build passes" checklist item below is evidence-gated, not self-asserted: Phase 6 trusts the `buildStatus.ok` that Phase 3 / Phase 4 recorded through `$HOME/.claude/scripts/evidence-gate.mjs` (a pass without a substantiating build/test log is treated as unverified and blocks the commit). The secret scanner (`pre-commit-check.sh`) runs on the staged diff as the final pre-commit gate.

#### Step 0a  -  Plan coverage gate (BLOCKING)

Phase 4 answers whether the diff is correct, not whether everything the plan promised landed: the criteria manifest counts rule IDs, Step 1.45 counts planned tests, and the per-step rollup is rendered in Phase 7, after this commit.

```bash
# One --analysis per doc: state.analysis.docPath[] is an array (one per platform),
# and reading only the first reports a one-platform verdict as the whole run's.
ANALYSIS_ARGS=()
while IFS= read -r d; do [ -n "$d" ] && ANALYSIS_ARGS+=(--analysis "$d"); done \
  < <(jq -r '.analysis.docPath[]? // empty' "$STATE_FILE")
node "$HOME/.claude/scripts/plan-coverage-gate.mjs" --state "$STATE_FILE" \
  ${ANALYSIS_ARGS[@]+"${ANALYSIS_ARGS[@]}"} --root "$WORKTREE" || COVERAGE_GAP=1
```

A step is accounted for when it is `completed`, `skipped` + `skipReason`, or `failed` + `failureReason`; a bare `pending` / `in_progress` is what this catches. Every Section 14 row tagged `Add new` must name a file that exists in the tree, and an empty `docPath[]` leaves that half reporting `skipped`, never `passed`.

On exit 1: show the gate's list verbatim, then either return to Phase 3 or have the user mark each step deliberately (`plan-todos.sh` writes the reason). Never proceed without that decision. Exit 2 means the plan was missing or empty: modes without Phase 2 skip this step explicitly.

#### Step 0  -  Multi-Repo Integration Build

**Fires only when** `state.projects.length >= 2` (multi-repo task). Single-repo tasks skip this step silently  -  no prompt, no overhead.

Full contract: `$HOME/.claude/multi-agent-refs/multi-repo-integration-build.md`. Summary:

1. Sort touched-repo basenames → `repoSet`.
2. Look up `prefs.global.multiRepoIntegrationHosts[]` for matching `repoSet`.
3. **Match found** → auto-run: submodule update → resolve package deps → build host scheme → evaluate errors → proceed / block / offer retry.
4. **No match** → learn-once prompt: `[1] register host / [2] mark combo as noHost / [3] skip this run only`. Persist choice. Run immediately if host registered.
5. **Autopilot** refuses to prompt on unknown combos  -  logs a visible skip and proceeds. Teach via a normal-mode run once.

Tracker: `phase-tracker.sh sub 6 0 "Integration build" <status>`  -  visible in live tracker. Progress line `→ integrating <host-scheme> with <N> submodules`.

Why: codegen mismatches (nested vs flat keys, missing entries) only surface when the full dependency chain builds together. Building repos in isolation gives false confidence and produces post-merge fix PRs.

#### Step 1  -  Identity Confirmation

```
Want to use a different author/email?
Current: {identity.name} <{identity.email}>
(y: change / n: continue):
```

If yes -> show identity picker (same as Phase 0 Step 6), update git config in worktree.

#### Step 2  -  Instruction-Driven vs Standard

Branch **deterministically**, no implicit fallback. Read `agent-state.json` and evaluate both conditions:

| `instructionDriven` | `instructionFiles.commit` exists on disk | Action                              |
| ------------------- | ---------------------------------------- | ----------------------------------- |
| `true`              | **yes** (file readable)                  | → Instruction-driven path           |
| `true`              | **no** (missing / unreadable)            | → **Log error, fall back to standard** + set `state.instructionDrivenFallback: true` for Phase 7 audit. Do NOT silently ignore. |
| `false`             |  -                                         | → Standard path                     |

**Instruction-driven path:**

1. Ask: "Want to commit?" (skip in autopilot)
2. Read commit instruction file (e.g. `.instructions/figma/figma-iteration-commit/SKILL.md`)
3. Follow its flow (build validation, test, submodule commits, push, PR, issue management)
4. Log: "Phase 6: instruction-driven commit  -  file={instructionFiles.commit}"

**Standard path (also used as fallback when instructionDriven=true but file missing):**

1. **Local checkout test prompt** (skip in autopilot): If still in worktree AND Phase 5 was skipped, ask the user. Per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix: `AskUserQuestion.label` and `header` stay English (UI button + chip contract); `question` and `description` follow `outputLanguage`. Render the picker accordingly:
   - `question` (in `outputLanguage`)  -  semantically: "Run a quick WIP-checkout test before commit?"
   - `header` (English, ≤12 chars): `"WIP checkout"`
   - `options` (description in `outputLanguage`, label in English):
     - label: `"No  -  continue to commit (Recommended)"`, description (in `outputLanguage`): proceed straight to commit + push + PR
     - label: `"Yes  -  WIP checkout"`, description (in `outputLanguage`): pause so the user can checkout the branch locally and poke around
   - Default: option 1 (No  -  continue to commit)
   - If user picks "Yes  -  WIP checkout": follow Phase 5 Steps 2-6. On `"fix:"` → Phase 3. On `"ok"` → resume Phase 6.
2. Commit confirm prompt (skip in autopilot). Same language matrix:
   - `question` in `outputLanguage` (semantic: "Commit now?")
   - labels English: `"Commit"`, `"Pause and resume later"`
   - No → Pause, user can `resume` later. Yes → continue:
3. If WIP commit exists from Phase 5 or Step 1: `git reset HEAD~1` to unstage, then re-commit properly
4. Stage changes: `git add` with specific files (NOT `git add -A`  -  avoid sensitive files, `.worktrees/`, agent files)
5. **Multi-submodule check**: If project has submodules (e.g. uicomponents + common):
   - Detect which submodules have changes: `git -C {worktree} diff --name-only | cut -d'/' -f1-3 | sort -u`
   - For EACH submodule with changes: stage, commit, push separately
   - Commit message uses same convention but scope reflects submodule: `{type}({submodule-scope}): {description} [{jiraId}]`
6. Commit with convention: `{type}({scope}): {description} [{jiraId}]` or `[#{shortId}]`. **Local-only repos** (state field `projects[i].provider == "local"`) carry the same conventional prefix but drop the `[{jiraId}]` suffix  -  there is no tracker to reference. The taskId (`LOCAL-...`) goes in the commit body footer instead: `Local-Task: {taskId}`.
7. Push to remote  -  **skip per-repo when `projects[i].provider == "local"`** (no `git push`, no upstream config). Log `Phase 6: local-only commit {sha} (no push)` for that repo.
8. Ask: "Want to open a Pull Request?"
   - **Skip the prompt entirely when `state.offlineOnly == true`** (all repos local  -  no PR target exists). Proceed to Phase 7 with `pr.status = "skipped-offline"` for each project.
   - In mixed mode (some local, some remote), only prompt for the remote-backed repos; local ones auto-skip.
   - No -> Phase 7
   - Yes -> Create PR with technical description (see below)
9. **Worktree finalize (gated by `settings.worktreeAutoRemoveOnPr`, default true)**: run **from the project root**  -  step 3 leaves the shell inside the worktree and the script refuses there.

   ```bash
   cd "$PROJECT_ROOT"
   FIN=$(bash $HOME/.claude/scripts/worktree-finalize.sh --json \
     --worktree "$WT_PATH" --project-root "$PROJECT_ROOT" \
     --task-id "$TASK_ID" --project "$PROJECT" --branch "$BRANCH")
   ```

   The script salvages before removing, keeps the branch, and does **not** check it out  -  the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: in single-repo mode that path went away with the worktree, so the write fails, the fields land nowhere, and Phase 7 reads a dead path and silently skips the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).

10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule  -  e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply with `gh issue edit {issueNo} --body "{updated body}"` and preserve every other section unchanged.
11. **NEVER close or resolve the issue**  -  neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
12. Log: "Phase 6: Commit {sha}  -  PR #{number}, worktree {removed|kept: <reason>}"

#### Step 3  -  PR Description (technical detail for reviewers)

**Section set + markup dialect: `channels/pr.md`  -  read it first.** Phase 7 channels replaces this body with that section set, so build to it. Required reading: `payload-contracts.md`.

Generate a structured PR description based on task type. The PR body targets **code reviewers**  -  it should be technical: what changed, why, architecture decisions, how to verify.

**required**: Run all generated text (PR body, commit message) through the `humanizer` skill before posting. This removes AI-generated patterns (inflated language, filler phrases, repetitive structure) and makes the output sound like a developer wrote it.

**IMPORTANT  -  Issue auto-close prevention**: Never use these keywords in PR title, body, or commit messages  -  they auto-close the issue on merge:

- GitHub: `Closes`, `Close`, `Closed`, `Fixes`, `Fix`, `Fixed`, `Resolves`, `Resolve`, `Resolved` (case-insensitive, followed by `#N`)
- Bitbucket: similar patterns
- Use `Ref: #N`, `Related: #N`, or `See: {jiraId}` instead

**Default Reviewers**: use `prefs.projects[{project}].defaultReviewers` when set; otherwise ask once ("Default reviewers for this project? comma-separated, or skip") and persist the answer there. Per-remote resolution rules are in "Default reviewers (required on every PR)" below  -  this is the prefs half only.

**Standard PR (bug fix, feature, refactor)**  -  invocation shape; body comes from a file so newlines survive:

```bash
REVIEWERS=$(jq -r '.projects["'$PROJECT'"].defaultReviewers // [] | map("--reviewer " + .) | join(" ")' "$PREFS_FILE")
gh pr create --base {baseBranch} --head {branch} $REVIEWERS \
  --title "{type}({scope}): {summary} [{jiraId}]" \
  --body-file /tmp/pr-body-$TASK_ID.md
```

**UI Component PR (when a UI component is detected (SwiftUI/Compose)):**

Same `gh pr create` pattern with component-specific body sections:

- Component Details: file table (Configuration, View, Modifiers, Tests)
- Variants table, Design Tokens Used, Figma URL
- Accessibility: labels, identifiers, platform-minimum tap target (44pt iOS / 48dp Android), Dynamic Type
- Test Coverage: structural/behavioral/snapshot counts
- Checklist: no magic numbers, configuration purity, modifier chain, accessibility, dark mode + RTL, build passes

**Bitbucket PR** (when remote is Bitbucket):

CRITICAL: PR description **MUST** preserve real newlines. Never embed `\n` literally  -  Bitbucket saves it verbatim. Never use HTML entities. Pattern: heredoc → `jq --rawfile` → `curl --data-binary @file`.

Required payload fields: `title`, `description` (via rawfile), `fromRef` (id + repo slug + project key), `toRef` (same structure), `state: "OPEN"`, `open: true`, `closed: false`, `locked: false`, `reviewers` (the fetched list  -  `[]` only if that endpoint really returned empty).

To **update** an existing PR: use the `channels/pr.md` PUT payload  -  required, not illustrative. It carries `reviewers`/`fromRef`/`toRef`/`draft` (a PUT clears omitted fields) and the GET's `version` unmodified (`version + 1` → 409). Refetch after, assert the reviewer count held. GitHub: `--body-file`.

**Default reviewers (required on every PR)**

Each PR MUST ship with default reviewers attached. Pick the branch based on `remoteType`:

##### Bitbucket (`remoteType == "bitbucket"`)

Bitbucket does NOT auto-assign default reviewers via REST API. Fetch and include them on create. Filter out PR author (409 on self-review).

Steps:

1. Resolve repo numeric ID: `GET /rest/api/1.0/projects/{key}/repos/{slug}` → `.id`
2. Fetch default reviewers: `GET /rest/default-reviewers/1.0/projects/{key}/repos/{slug}/reviewers?sourceRepoId=X&targetRepoId=X&sourceRefId=refs/heads/{branch}&targetRefId=refs/heads/{baseBranch}`
3. Filter out PR author: `jq '[.[] | select(.name != $author) | {user: {name: .name}}]'`
4. Inject into payload's `reviewers` field

If default-reviewers endpoint returns empty array → proceed with `reviewers: []`.

##### GitHub (`remoteType == "github"`)

GitHub auto-requests reviews from `CODEOWNERS` when branch protection requires it. For repos WITHOUT CODEOWNERS, add reviewers explicitly.

Steps:

1. Check CODEOWNERS exists (`.github/CODEOWNERS`, `CODEOWNERS`, `docs/CODEOWNERS`  -  `git cat-file -e`)
2. If CODEOWNERS present → let GitHub auto-request; create PR with no extra reviewers
3. If no CODEOWNERS → resolve reviewers from (in order): branch protection `required_pull_request_reviews` via `gh api`, then `prefs.projects[{project}].githubDefaultReviewers[]`, then skip
4. Create PR: `gh pr create --repo {owner}/{repo} --base {baseBranch} --head {branch} --title "..." --body-file pr-body.md ${REVIEWER_USERS:+--reviewer "$REVIEWER_USERS"} ${REVIEWER_TEAMS:+--reviewer "$REVIEWER_TEAMS"}`

PR author cannot appear in `--reviewer`  -  strip from list.

**Preferences entry** (set once per project, persists): `"githubDefaultReviewers": ["@alice", "@bob", "my-org/backend-team"]` under `projects.{name}`. Save user picks  -  prepend, dedup, max 10.

**Common rule:** never hardcode reviewer IDs  -  derive from repo config or preferences.

##### Draft vs ready-for-review

Before creating PR, ask: "DRAFT or READY? [1/2], default 1"  -  DRAFT blocks merge/delays notifications, READY notifies reviewers + CI. Skip in `autopilot` → default READY.

- **Bitbucket**: `"draft": true` in payload (DC 8.x+). Promote: `draft: false` PUT. Older: prefix title `[DRAFT]`.
- **GitHub**: `--draft`. Promote: `gh pr ready {N}`.
- Persist in `prefs.projects[{project}].defaultPrMode` (`"draft"` or `"ready"`).

**How to detect UI Component PR**: If any of these are true -> use Component PR template:

- Files include `*Configuration.swift` or `*+Modifiers.swift`
- Phase 0 Step 7 detected instruction-driven mode with figma workflow
- Task description mentions "component", "view", "UI element"

---

#### v2.1.0+ Multi-Repo Mode

Active when `state.projects[].length > 1`. The single-repo flow above is preserved verbatim  -  multi-repo is an extension, not a replacement.

##### Per-repo commit (shared message, per-repo scope)

For each repo in `state.projects[]` (sequentially, in dependency order from Phase 2):

1. `cd "$WT_PATH"`  -  switch into the repo's worktree.
2. Build the commit message using the **shared subject** + **per-repo scope** rule:
   ```
   {type}({repo-scope}): {shared subject} [{tracker-id}]

   Part of multi-repo change  -  see sibling PR(s) in repo `{other repo names}`.
   ```
   `repo-scope` is the repo's short name (e.g. `common`, `uicomponents`). The subject and `[tracker-id]` are identical across all repos.
3. `git add` (specific files only  -  same rules as single-repo Step 5)
4. `git -C "$WT_PATH" commit -m "$MESSAGE"` (heredoc body)  -  using identity already pinned in Phase 0 Step 8.
5. Capture `state.projects[i].commit = "<sha>"`.

##### Push-must-succeed policy (gated by `prefs.global.settings.pushMustSucceed`, default `true`)

The build passed → the commit MUST reach the remote. Silent push failures are unacceptable.

For each repo (sequentially), per push attempt:

```
1. git -C "$WT_PATH" fetch origin "$BASE_BRANCH"
2. If origin/$BASE_BRANCH advanced beyond local base:
     git -C "$WT_PATH" rebase "origin/$BASE_BRANCH"
     - on rebase conflict: abort, escalate (see step 5)
3. git -C "$WT_PATH" push origin HEAD:refs/heads/"$BRANCH"
4. On non-zero exit:
     state.projects[i].pushAttempts += 1
     if pushAttempts < settings.pushRetryMax (default 5):
       sleep $((pushAttempts * 2))   # back-off: 2,4,6,8s
       goto 1
     else:
       goto 5
5. After settings.pushRetryMax failures, prompt user:
     [1] Resolve conflict manually in $WT_PATH, then resume #{taskId}
     [2] Switch to a different base branch (re-prompt baseBranch, restart push loop)
     [3] Skip this repo  -  leave commit local, mark state.projects[i].pushStatus = "skipped"
     [4] Pause Phase 6, exit cleanly
```

Persist `state.projects[i].pushStatus` ∈ `"pushed" | "rebase-pushed" | "skipped" | "paused"`. A `"skipped"` repo must be reported in Phase 7 with explicit "manual push required" note.

The legacy single-repo path uses the same loop with `state.projects[].length === 0`; semantics unchanged.

##### Per-repo PR with cross-links

After all repos are pushed, open PRs sequentially. Each PR's body cross-links its siblings.

```bash
PR_URLS=()
for proj in $(jq -r '.projects[].name' "$STATE_FILE"); do
  # ... build base PR body using existing per-stack template (Standard / UI Component / etc)
  # then APPEND a Sibling PRs section:
  cat >> "$PR_BODY_FILE" <<EOF

## Sibling PRs (multi-repo task)
This PR is part of a multi-repo change. Sibling PRs (review together):
EOF
  for sibling in "${PR_URLS[@]}"; do
    echo "- $sibling" >> "$PR_BODY_FILE"
  done
  # PR open via existing remoteType branch (gh / Bitbucket REST)
  PR_URL=$(open_pr_for_repo "$proj")
  PR_URLS+=("$PR_URL")
done
# After all PRs are open, go BACK to the first N-1 PRs and update their bodies
# to include the sibling URLs that were not yet known when they were created.
update_sibling_links "${PR_URLS[@]}"
```

**Then finalize each repo's worktree**, serially and only now  -  `update_sibling_links` updates one PR per repo and the loop `cd`s per repo, so removing repo 1's worktree mid-loop breaks 2..N. Run `worktree-finalize.sh` once per repo with that repo's `--worktree` / `--project-root` / `--project`; each skips or removes independently.

`update_sibling_links` does an UPDATE call per PR (Bitbucket: the `channels/pr.md` PUT payload, current `version`; GitHub: `gh pr edit --body-file`). Idempotent.

##### GitHub issue body  -  list all PRs

Step 10's "Issue body update" extends to multi-repo: instead of a single `### Pull Requests` line, write one row per repo:

```
### Pull Requests
- **common:** {pr-1-url}
- **uicomponents:** {pr-2-url}
```

Match the existing template's table/list shape  -  do not introduce new markers (per existing single-repo rule).

##### `recentGroups` update on success

After ALL repos in the task have `pushStatus ∈ {"pushed", "rebase-pushed"}`:

1. Find the matching `recentGroups[]` entry by **order-independent compare** of `repos` array (sort + JSON.stringify).
2. If found: `entry.count += 1`, `entry.lastUsed = ISO timestamp`, bump to front of array (LRU).
3. If not found: prepend a new entry `{ label: <auto-generated>, repos: [...], count: 1, lastUsed: ... }`. Cap the array at 10 (drop oldest).

A task ending with one or more `pushStatus === "skipped"` repos does NOT bump `recentGroups` count  -  only fully-successful multi-repo tasks earn LRU bumps.

##### Telemetry

```bash
for proj in $(jq -r '.projects[].name' "$STATE_FILE"); do
  $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 commit.created   repo=$proj sha=$SHA
  $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 push.attempted   repo=$proj attempts=$N status=$STATUS
  $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 pr.opened        repo=$proj url=$URL number=$N
done
$HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 multi_repo.completed repos=$REPOS skipped=$SKIPPED
```

**Token forwarding:** the commit-message and PR-body generators run on a model. Forward those calls into the tracker so Phase 7's Cost Breakdown captures Phase 6:

```bash
LOG_METRIC_FORWARD_TO_TRACKER=1 $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 commit.message_generated \
  model=<sonnet|opus> tokens_in=$IN tokens_out=$OUT duration_ms=$DUR
LOG_METRIC_FORWARD_TO_TRACKER=1 $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 6 pr.body_generated \
  model=<sonnet|opus> tokens_in=$IN tokens_out=$OUT duration_ms=$DUR
```

Best-effort. See `$HOME/.claude/multi-agent-refs/progress-contract.md#token-telemetry-forwarding`.
