# Push-After-Merge Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** After every successful squash-merge of a feature branch into the configured target branch, push the target branch to its remote (default `origin`) so collaborators and CI see merged work without manual intervention. Replace the existing per-turn `agent.auto_push` (which created a remote zombie branch per task) with a single `git.push_after_merge` knob.

**Architecture:** A single insertion point in `GitOps.apply_on_completion()` — right after `squash_merge` confirms `merged=True` and `no_op=False`. Wraps the existing `push_to_remote()` / `has_remote()` helpers in `claw_forge/git/commits.py`. Failure surfaces as a logger warning; the dispatcher continues. Configuration: `git.push_after_merge: bool | str` (str = explicit remote name); CLI override `--push-after-merge[/--no-push-after-merge]` plus `--push-remote NAME`. The old `agent.auto_push` flag, hook, and tests are deleted; users with the deprecated key in their config see a one-line console warning at startup pointing them at the replacement.

**Tech Stack:** Python 3.11, Typer (CLI), pytest, asyncio, subprocess (for git). No new dependencies.

---

## File Structure

| Path | Action | Responsibility |
|---|---|---|
| `claw_forge/git/__init__.py` | Modify | `GitOps.__init__` accepts `push_after_merge`; `apply_on_completion` calls push after successful merge |
| `claw_forge/cli.py` | Modify | Adds `--push-after-merge` / `--push-remote` options; reads `git.push_after_merge` from config; emits deprecation warning for `agent.auto_push`; removes `auto_push` plumbing |
| `claw_forge/agent/hooks.py` | Modify | Removes `auto_push_hook` factory and the `auto_push` parameter on `get_default_hooks` |
| `tests/git/test_git_init.py` | Modify | Adds `TestApplyOnCompletionPush` class with 6 tests |
| `tests/test_auto_push.py` | Delete | Tests for the now-removed Stop hook |
| `tests/test_cli_run_flags.py` | Modify or Add | Adds tests verifying `--push-after-merge` and `--push-remote` parse and propagate |
| `docs/commands.md` | Modify | Documents `--push-after-merge` / `git.push_after_merge`; removes `--auto-push` |
| `README.md` | Modify | Mentions the new option in the run section |
| `CLAUDE.md` | Modify | Adds a "Push-After-Merge" subsection under "Git Worktree Lifecycle" / "Key Conventions" |

---

## Task 1: GitOps takes a `push_after_merge` parameter

**Files:**
- Modify: `claw_forge/git/__init__.py:70-73` (the `__init__` method)
- Test: `tests/git/test_git_init.py` (add to existing `TestGitOps` class)

- [ ] **Step 1: Write the failing test**

Append to `tests/git/test_git_init.py` inside the existing `class TestGitOps:` block:

```python
    def test_init_default_push_after_merge_is_false(self, tmp_path: Path) -> None:
        ops = GitOps(project_dir=tmp_path, enabled=True)
        assert ops.push_after_merge is False

    def test_init_push_after_merge_bool_true(self, tmp_path: Path) -> None:
        ops = GitOps(project_dir=tmp_path, enabled=True, push_after_merge=True)
        assert ops.push_after_merge is True

    def test_init_push_after_merge_custom_remote(self, tmp_path: Path) -> None:
        ops = GitOps(project_dir=tmp_path, enabled=True, push_after_merge="upstream")
        assert ops.push_after_merge == "upstream"
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/git/test_git_init.py::TestGitOps::test_init_default_push_after_merge_is_false tests/git/test_git_init.py::TestGitOps::test_init_push_after_merge_bool_true tests/git/test_git_init.py::TestGitOps::test_init_push_after_merge_custom_remote -v`

Expected: 3 FAILs with `AttributeError: 'GitOps' object has no attribute 'push_after_merge'`.

- [ ] **Step 3: Add the parameter to `GitOps.__init__`**

Edit `claw_forge/git/__init__.py:70-73` to:

```python
    def __init__(
        self,
        project_dir: Path,
        *,
        enabled: bool = True,
        push_after_merge: bool | str = False,
    ) -> None:
        self.project_dir = project_dir
        self.enabled = enabled
        self.push_after_merge = push_after_merge
        self._lock = asyncio.Lock()
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run pytest tests/git/test_git_init.py::TestGitOps -v`

Expected: all `TestGitOps` tests PASS (existing 6 + new 3 = 9).

- [ ] **Step 5: Commit**

```bash
git add claw_forge/git/__init__.py tests/git/test_git_init.py
git commit -m "feat(git): GitOps accepts push_after_merge parameter"
```

---

## Task 2: `apply_on_completion` pushes target after successful merge

**Files:**
- Modify: `claw_forge/git/__init__.py:189-271` (the `apply_on_completion` method) and the import block at lines 17-32
- Test: `tests/git/test_git_init.py` (add new `TestApplyOnCompletionPush` class)

- [ ] **Step 1: Write the failing tests**

Append a new test class to `tests/git/test_git_init.py`:

```python
class TestApplyOnCompletionPush:
    """Push target branch to remote after a successful squash-merge."""

    def test_push_disabled_by_default_no_push_call(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(project_dir=git_repo, enabled=True)  # default push_after_merge=False
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.push_to_remote") as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_not_called()

    def test_push_fires_on_successful_merge(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge=True,
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=True),
            patch("claw_forge.git.push_to_remote",
                  return_value={"remote": "origin", "branch": "main",
                                "success": True, "error": None}) as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_called_once_with(git_repo, remote="origin", branch="main")

    def test_push_uses_custom_remote_from_string(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge="upstream",
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=True),
            patch("claw_forge.git.push_to_remote",
                  return_value={"remote": "upstream", "branch": "main",
                                "success": True, "error": None}) as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_called_once_with(git_repo, remote="upstream", branch="main")

    def test_push_skipped_when_remote_missing(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge=True,
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=False),
            patch("claw_forge.git.push_to_remote") as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_not_called()

    def test_push_skipped_on_no_op_merge(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge=True,
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "no_op": True,
                                "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=True),
            patch("claw_forge.git.push_to_remote") as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_not_called()

    def test_push_failure_does_not_raise(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge=True,
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": True, "commit_hash": "abc1234"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=True),
            patch("claw_forge.git.push_to_remote",
                  return_value={"remote": "origin", "branch": "main",
                                "success": False, "error": "auth failed"}),
        ):
            # Must not raise — push failures are warnings, not exceptions.
            result = asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        assert result is not None
        assert result["merged"] is True

    def test_push_skipped_when_merge_failed(
        self, git_repo: Path, tmp_path: Path,
    ) -> None:
        ops = GitOps(
            project_dir=git_repo, enabled=True, push_after_merge=True,
        )
        with (
            patch("claw_forge.git.squash_merge",
                  return_value={"merged": False, "error": "conflict"}),
            patch("claw_forge.git.commit_checkpoint", return_value={"sha": "deadbeef"}),
            patch("claw_forge.git.has_remote", return_value=True),
            patch("claw_forge.git.push_to_remote") as mock_push,
        ):
            asyncio.run(ops.apply_on_completion(
                task_id="t1", slug="x", success=True,
                worktree_path=tmp_path / "wt",
                merge_strategy="auto", target_branch="main",
            ))
        mock_push.assert_not_called()
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/git/test_git_init.py::TestApplyOnCompletionPush -v`

Expected: 7 FAILs — `push_to_remote` and `has_remote` are not yet imported into `claw_forge.git`, and the push branch is not yet wired in `apply_on_completion`.

- [ ] **Step 3: Import the helpers into `claw_forge/git/__init__.py`**

Modify the `from claw_forge.git.commits import …` line at `claw_forge/git/__init__.py:30`:

```python
from claw_forge.git.commits import (
    branch_commit_subjects,
    commit_checkpoint,
    has_remote,
    push_to_remote,
    task_history,
)
```

And add `"has_remote"` and `"push_to_remote"` to `__all__` (sorted alphabetically) at `claw_forge/git/__init__.py:35-58`:

```python
__all__ = [
    "GitOps",
    "branch_age_in_commits",
    "branch_commit_subjects",
    "branch_exists",
    "branch_has_commits_ahead",
    "branch_overlap_files",
    "commit_checkpoint",
    "create_feature_branch",
    "create_worktree",
    "current_branch",
    "delete_branch",
    "detect_default_branch",
    "ensure_gitignore",
    "has_remote",
    "init_or_detect",
    "make_branch_name",
    "make_slug",
    "prune_worktrees",
    "push_to_remote",
    "remove_worktree",
    "squash_merge",
    "switch_branch",
    "sync_worktree_with_target",
    "task_history",
]
```

- [ ] **Step 4: Wire the push call into `apply_on_completion`**

In `claw_forge/git/__init__.py`, replace the block at lines 243-260 (the `if merge_strategy == "auto":` body inside `apply_on_completion`) with:

```python
            if merge_strategy == "auto":
                branch_name = f"{branch_prefix}/{slug}"
                merge_result = await self.merge(
                    branch_name,
                    _target,
                    title=description,
                    steps=steps,
                    task_id=task_id,
                    session_id=session_id,
                    worktree_path=worktree_path,
                )
                if merge_result and not merge_result.get("merged"):
                    _logging.getLogger("claw_forge.git").warning(
                        "Merge failed for %s: %s",
                        branch_name,
                        merge_result.get("error", "unknown"),
                    )
                elif (
                    merge_result
                    and merge_result.get("merged")
                    and not merge_result.get("no_op", False)
                    and self.push_after_merge
                ):
                    remote = (
                        self.push_after_merge
                        if isinstance(self.push_after_merge, str)
                        else "origin"
                    )
                    if await asyncio.to_thread(
                        has_remote, self.project_dir, remote,
                    ):
                        push_result = await asyncio.to_thread(
                            push_to_remote,
                            self.project_dir,
                            remote=remote,
                            branch=_target,
                        )
                        if push_result["success"]:
                            _logging.getLogger("claw_forge.git").info(
                                "Pushed %s → %s",
                                _target, remote,
                            )
                        else:
                            _logging.getLogger("claw_forge.git").warning(
                                "Push of %s → %s failed: %s",
                                _target, remote, push_result["error"],
                            )
                return merge_result
```

- [ ] **Step 5: Run tests to verify they pass**

Run: `uv run pytest tests/git/test_git_init.py::TestApplyOnCompletionPush -v`

Expected: all 7 tests PASS.

Run: `uv run pytest tests/git/ -q`

Expected: full git test suite PASSES (no regressions).

- [ ] **Step 6: Commit**

```bash
git add claw_forge/git/__init__.py tests/git/test_git_init.py
git commit -m "feat(git): push target branch to remote after successful squash-merge"
```

---

## Task 3: CLI flags `--push-after-merge` and `--push-remote`

**Files:**
- Modify: `claw_forge/cli.py:540-665` (the `run` command flag block and config-resolution block) and `claw_forge/cli.py:858` (the `GitOps()` construction)
- Test: `tests/test_cli_run_flags.py` (extend or create)

- [ ] **Step 1: Write the failing test**

Check whether `tests/test_cli_run_flags.py` exists; if not, create it. Append (or create) the test:

```python
"""CLI flag tests for `claw-forge run`."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

from typer.testing import CliRunner

from claw_forge.cli import app


def _stub_dispatch(*_args, **_kwargs) -> None:
    """No-op stand-in so we can assert flag plumbing without running agents."""
    return None


class TestPushAfterMergeFlags:
    def test_default_push_after_merge_off(self, tmp_path: Path) -> None:
        runner = CliRunner()
        captured: dict = {}

        def _capture_git_ops(**kwargs):
            captured.update(kwargs)
            ops = MagicMock()
            ops.push_after_merge = kwargs.get("push_after_merge", False)
            return ops

        with (
            patch("claw_forge.cli.GitOps", side_effect=_capture_git_ops),
            patch("claw_forge.cli._ensure_state_service", return_value=8420),
            patch("claw_forge.cli.asyncio.run"),
        ):
            runner.invoke(app, ["run", "--project", str(tmp_path), "--dry-run"])

        assert captured.get("push_after_merge") is False

    def test_flag_enables_push_after_merge(self, tmp_path: Path) -> None:
        runner = CliRunner()
        captured: dict = {}

        def _capture_git_ops(**kwargs):
            captured.update(kwargs)
            return MagicMock()

        with (
            patch("claw_forge.cli.GitOps", side_effect=_capture_git_ops),
            patch("claw_forge.cli._ensure_state_service", return_value=8420),
            patch("claw_forge.cli.asyncio.run"),
        ):
            runner.invoke(
                app,
                ["run", "--project", str(tmp_path), "--push-after-merge",
                 "--dry-run"],
            )

        assert captured.get("push_after_merge") is True

    def test_push_remote_overrides_default_origin(self, tmp_path: Path) -> None:
        runner = CliRunner()
        captured: dict = {}

        def _capture_git_ops(**kwargs):
            captured.update(kwargs)
            return MagicMock()

        with (
            patch("claw_forge.cli.GitOps", side_effect=_capture_git_ops),
            patch("claw_forge.cli._ensure_state_service", return_value=8420),
            patch("claw_forge.cli.asyncio.run"),
        ):
            runner.invoke(
                app,
                ["run", "--project", str(tmp_path),
                 "--push-after-merge", "--push-remote", "upstream",
                 "--dry-run"],
            )

        assert captured.get("push_after_merge") == "upstream"
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/test_cli_run_flags.py::TestPushAfterMergeFlags -v`

Expected: FAILs with errors like `Got unexpected extra arguments (--push-after-merge)`.

- [ ] **Step 3: Add the CLI flags to `claw-forge run`**

In `claw_forge/cli.py`, immediately after the `auto_push: str | None = typer.Option(...)` block at lines 550-559, add two new options:

```python
    push_after_merge: bool = typer.Option(
        False, "--push-after-merge/--no-push-after-merge",
        help=(
            "After every successful squash-merge into the target branch, "
            "push the target branch to its remote.  Skipped silently if "
            "the configured remote doesn't exist.  No-op merges (content "
            "already on target) are skipped to save a round-trip.  "
            "[default: disabled]"
        ),
    ),
    push_remote: str = typer.Option(
        "origin", "--push-remote",
        help=(
            "Remote name to push to when --push-after-merge is set.  "
            "Default: origin."
        ),
    ),
```

- [ ] **Step 4: Resolve effective values from CLI flag and config**

After the existing `effective_auto_push` block (around `claw_forge/cli.py:660-665`), add:

```python
    # push_after_merge: CLI flag takes priority over config.
    # Config form: ``git.push_after_merge: true`` or ``"upstream"``.
    # CLI form: ``--push-after-merge --push-remote upstream``.
    effective_push_after_merge: bool | str
    if push_after_merge:
        effective_push_after_merge = (
            push_remote if push_remote != "origin" else True
        )
    else:
        _config_pam = cfg.get("git", {}).get("push_after_merge", False)
        if isinstance(_config_pam, str) and _config_pam:
            effective_push_after_merge = _config_pam
        else:
            effective_push_after_merge = bool(_config_pam)
```

- [ ] **Step 5: Pass through to GitOps**

Replace `claw_forge/cli.py:858` (the `git_ops = GitOps(...)` line) with:

```python
    git_ops = GitOps(
        project_dir=project_path,
        enabled=git_enabled,
        push_after_merge=effective_push_after_merge,
    )
```

- [ ] **Step 6: Run tests to verify they pass**

Run: `uv run pytest tests/test_cli_run_flags.py::TestPushAfterMergeFlags -v`

Expected: all 3 tests PASS.

- [ ] **Step 7: Commit**

```bash
git add claw_forge/cli.py tests/test_cli_run_flags.py
git commit -m "feat(cli): add --push-after-merge and --push-remote flags"
```

---

## Task 4: Remove deprecated `agent.auto_push` plumbing

**Files:**
- Modify: `claw_forge/cli.py:550-559, 660-665, 1488` (remove `auto_push` flag and pass-through)
- Modify: `claw_forge/agent/hooks.py` (remove `auto_push_hook` and the `auto_push` parameter on `get_default_hooks`)
- Delete: `tests/test_auto_push.py`

- [ ] **Step 1: Run pre-removal grep to inventory call sites**

Run:
```bash
grep -rn "auto_push\|auto-push" claw_forge/ tests/ docs/ README.md CLAUDE.md
```

Expected output: lines in `claw_forge/cli.py`, `claw_forge/agent/hooks.py`, `tests/test_auto_push.py`, `docs/commands.md`, `docs/agent-skill.md`, `docs/middleware/hashline.md`, `docs/superpowers/plans/2026-03-21-computation-awareness.md`. The plans file is historical, leave it. The other docs are addressed in Task 5.

- [ ] **Step 2: Remove the CLI flag**

Delete the `auto_push: str | None = typer.Option(...)` block at `claw_forge/cli.py:550-559`.

- [ ] **Step 3: Replace the `effective_auto_push` resolution with a deprecation warning**

Replace the block at `claw_forge/cli.py:660-665` (`# auto_push: CLI flag takes priority...`) with:

```python
    # Deprecated: ``agent.auto_push`` was a per-turn Stop-hook push that
    # created a remote branch per task.  Replaced by ``git.push_after_merge``
    # which only pushes the target branch after a successful merge.
    if cfg.get("agent", {}).get("auto_push"):
        console.print(
            "[yellow]warning: agent.auto_push is deprecated and ignored; "
            "use git.push_after_merge: true instead.[/yellow]",
        )
```

- [ ] **Step 4: Remove the `auto_push=effective_auto_push` argument**

In `claw_forge/cli.py:1488` and surrounding lines (the `get_default_hooks(...)` call), remove the `auto_push=effective_auto_push,` line.

- [ ] **Step 5: Remove `auto_push_hook` and parameter from `agent/hooks.py`**

In `claw_forge/agent/hooks.py`:

- Delete the entire `def auto_push_hook(...)` block (lines ~349-406).
- In `get_default_hooks` (around line 555-648), remove the `auto_push: str | None = None,` parameter and the entire `if auto_push is not None:` block (lines ~642-648). Update the docstring to drop the `When auto_push is set...` paragraph.

- [ ] **Step 6: Delete the auto-push test file**

```bash
rm tests/test_auto_push.py
```

- [ ] **Step 7: Run the full suite**

Run: `uv run pytest tests/ -q`

Expected: all tests PASS (the only previously-passing tests we removed are in `tests/test_auto_push.py`; if any other test imported from there or from `auto_push_hook`, that's a regression — fix the import, don't restore the helper).

- [ ] **Step 8: Run lint**

Run: `uv run ruff check claw_forge/ tests/`

Expected: 0 errors. (Watch for unused-import warnings on `auto_push_hook`.)

- [ ] **Step 9: Commit**

```bash
git add -u
git commit -m "refactor(cli): remove deprecated agent.auto_push in favour of git.push_after_merge"
```

---

## Task 5: Documentation updates

**Files:**
- Modify: `docs/commands.md`
- Modify: `README.md`
- Modify: `CLAUDE.md`

- [ ] **Step 1: Update `docs/commands.md` — remove `--auto-push`, add `--push-after-merge`**

In `docs/commands.md`, find the `--auto-push` row in the `claw-forge run` flags table (around line 468) and **replace** it with:

```markdown
| `--push-after-merge` | bool | `False` | Push the target branch (e.g. `main`) to its remote after every successful squash-merge.  Skipped silently if the remote is missing or the merge was a no-op. |
| `--push-remote` | string | `"origin"` | Remote name used by `--push-after-merge`.  Override for forks (e.g. `upstream`). |
```

Find the `### AutoPush (\`--auto-push\`)` section (around line 2615) and **replace** the entire section (until the next `###` heading) with:

```markdown
### Push-After-Merge (`--push-after-merge`)

Pushes the target branch (default `main`, configurable via `git.target_branch`) to its remote after every successful squash-merge.  This keeps the remote in sync with merged work for collaborators and CI without polluting the remote with per-task feature branches.

**Default: disabled.** Must be explicitly opted in.

**Behavior:**
- Fires only when a squash-merge produces a real new commit on the target branch — no-op merges (content already reachable) are skipped to save a round-trip.
- Skipped silently if the configured remote doesn't exist (so it's safe to leave on in repos without an `origin`).
- Push failure is logged as a warning; the dispatcher continues.  The next successful merge's push will catch the remote up.
- No remote feature branches are created — only the target branch is pushed.

**Examples:**

```bash
# Push to origin/main after every merge
claw-forge run --push-after-merge

# Push to upstream/main (forked repo)
claw-forge run --push-after-merge --push-remote upstream

# Or via config
# git:
#   push_after_merge: true            # uses 'origin'
#   # push_after_merge: "upstream"    # explicit remote
```
```

Find every other `--auto-push` reference in `docs/commands.md`, `docs/agent-skill.md`, and `docs/middleware/hashline.md` and update them to reference `--push-after-merge` instead. (The historical plans file under `docs/superpowers/plans/` stays unchanged — it's a record.)

- [ ] **Step 2: Update `README.md`**

Find any mention of `--auto-push` in `README.md`. Replace it with `--push-after-merge` and a one-line description, or simply remove it if the README didn't go into detail.

If the README has a "run flags" or "config" section, add:
```markdown
- `--push-after-merge` — push the target branch to remote after every successful merge (off by default).
```

- [ ] **Step 3: Update `CLAUDE.md`**

In `CLAUDE.md`, under the "Git Worktree Lifecycle" section (search for `### Git Worktree Lifecycle`), append a new bullet to the existing list:

```markdown
- **Post-merge target push** (`git.push_after_merge`, opt-in, default `false`): when set to `true` (or a remote name string), `apply_on_completion()` pushes the target branch to its remote after every non-no-op squash-merge.  Replaces the deprecated per-turn `agent.auto_push` — only the target branch is pushed, never per-task feature branches.  Push failures are logged as warnings and don't fail the task; the next merge's push will catch the remote up.
```

Also remove the `Long unattended runs (CI, overnight, --auto-push)` mention if it appears in the same file's "Key Conventions" section, replacing with `--push-after-merge` if relevant.

- [ ] **Step 4: Run a final grep to check no stale `--auto-push` refs remain in docs (excluding plans/)**

Run:
```bash
grep -rn "auto_push\|auto-push" claw_forge/ tests/ docs/ README.md CLAUDE.md | grep -v "docs/superpowers/plans/"
```

Expected: only the historical plan in `docs/superpowers/plans/` (which we deliberately skip) — no other matches.

- [ ] **Step 5: Commit**

```bash
git add docs/ README.md CLAUDE.md
git commit -m "docs: replace --auto-push with --push-after-merge throughout"
```

---

## Task 6: Final lint + full test pass

- [ ] **Step 1: Run lint over both source and tests**

Run: `uv run ruff check claw_forge/ tests/`

Expected: 0 errors. (Memory: lint must pass on `tests/` too — narrowed paths miss test-file E501s.)

- [ ] **Step 2: Run full test suite with coverage**

Run: `uv run pytest tests/ -q --cov=claw_forge --cov-report=term-missing`

Expected: all tests PASS, coverage ≥ 90% (CI gate).

- [ ] **Step 3: Run mypy**

Run: `uv run mypy claw_forge/ --ignore-missing-imports`

Expected: 0 errors. Watch for `bool | str` typing issues on `push_after_merge` — annotate with `bool | str` consistently.

- [ ] **Step 4: Final commit if any fix-up was needed**

If steps 1-3 surfaced anything, fix and commit:

```bash
git add -u
git commit -m "chore: lint and type fixes for push_after_merge"
```

If everything passed first time, no commit is needed.

---

## Self-Review Checklist

**Spec coverage:**
- ✅ Add `git.push_after_merge` config (bool or str) — Task 1, Task 3
- ✅ CLI flag `--push-after-merge` and `--push-remote` — Task 3
- ✅ Wire into `apply_on_completion` — Task 2
- ✅ Skip on no-op merge — Task 2 step 1 test
- ✅ Skip when remote missing — Task 2 step 1 test
- ✅ Push failure non-fatal — Task 2 step 1 test
- ✅ Remove `agent.auto_push` plumbing — Task 4
- ✅ Migration warning for users with deprecated config — Task 4 step 3
- ✅ Docs updates (CLAUDE.md, docs/commands.md, README.md) — Task 5
- ✅ Lint and full test pass — Task 6

**Type consistency:**
- `push_after_merge: bool | str` — used identically in `GitOps.__init__` (Task 1), `apply_on_completion` (Task 2), CLI resolution (Task 3). Ruled to be `True` / `"<remote-name>"` / `False`.
- `effective_push_after_merge: bool | str` in CLI matches `GitOps.push_after_merge` annotation.
- `_target` and `branch_name` from existing code unchanged.

**No placeholders:** All steps contain executable code or commands. No TBDs, no "implement appropriate handling," no "similar to Task N."
