---
phase: 20-critical-safety-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - cmd_commit.py
  - cmd_checkout.py
  - tests/test_commit.py
  - tests/test_checkout.py
  - tests/test_branch.py
autonomous: true

must_haves:
  truths:
    - "git commit -a commits only tracked modified files, never adds untracked files"
    - "git checkout -f <branch> discards uncommitted changes and switches branches"
    - "git checkout -m <branch> merges uncommitted changes during branch switch"
    - "git branch -D <name> deletes bookmark label only, never strips commits"
  artifacts:
    - path: "cmd_commit.py"
      provides: "Commit command with -a flag safety"
      contains: "args = [a for a in args if a not in"
    - path: "cmd_checkout.py"
      provides: "Checkout command with -f/-m flag translation"
      contains: "-C"
    - path: "tests/test_commit.py"
      provides: "SAFE-01 test coverage"
      contains: "test_commit_a_does_not_add_untracked"
    - path: "tests/test_checkout.py"
      provides: "SAFE-02, SAFE-03 test coverage"
      contains: "TestCheckoutForce"
  key_links:
    - from: "cmd_commit.py"
      to: "sl commit"
      via: "run_sl with filtered args"
      pattern: "run_sl.*commit"
    - from: "cmd_checkout.py"
      to: "sl goto"
      via: "run_sl with translated flags"
      pattern: "run_sl.*goto"
---

<objective>
Implement critical safety fixes for flags with semantic differences between git and Sapling.

Purpose: Protect users from destructive or unexpected behavior when using git flags that have different meanings in Sapling. The `-a` flag on commit and `-f`/`-m` flags on checkout have semantic differences that can cause data loss or unexpected file additions.

Output: Modified command handlers with safety translations and comprehensive E2E test coverage for all 4 SAFE requirements.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/20-critical-safety-fixes/20-RESEARCH.md

# Existing files to modify:
@cmd_commit.py
@cmd_checkout.py
@tests/test_commit.py
@tests/test_checkout.py
@tests/test_branch.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement safety flag handling</name>
  <files>cmd_commit.py, cmd_checkout.py</files>
  <action>
Modify `cmd_commit.py` to remove `-a` and `--all` flags before passing to sl:

```python
def handle(parsed: ParsedCommand) -> int:
    """Handle 'git commit' command."""
    args = list(parsed.args)

    # SAFE-01: Remove -a/--all flags
    # git -a: stages tracked modified/deleted files only
    # sl -A: adds untracked files too (DANGEROUS semantic difference)
    args = [a for a in args if a not in ('-a', '--all')]

    return run_sl(["commit"] + args)
```

Modify `cmd_checkout.py` to translate `-f`/`--force` to `-C` and pass through `-m`/`--merge` when switching branches (goto path):

1. Add flag translation function:
```python
def _translate_goto_flags(args: List[str]) -> List[str]:
    """Translate git checkout flags to sl goto flags."""
    result = []
    for arg in args:
        if arg in ('-f', '--force'):
            # SAFE-02: git checkout -f -> sl goto -C (clean)
            result.append('-C')
        elif arg in ('-m', '--merge'):
            # SAFE-03: git checkout -m -> sl goto -m (same semantics)
            result.append('-m')
        else:
            result.append(arg)
    return result
```

2. Apply translation in the goto path (when `is_revision` is True):
- Line 147: Change `return run_sl(["goto"] + args)` to `return run_sl(["goto"] + _translate_goto_flags(args))`
- Line 155: Change `return run_sl(["goto"] + args)` to `return run_sl(["goto"] + _translate_goto_flags(args))`

Note: Do NOT apply translation in revert paths (file restoration) - these flags only apply to branch switching.
  </action>
  <verify>
Run `python -c "import cmd_commit; import cmd_checkout"` - no import errors.
Inspect both files and verify:
- cmd_commit.py filters `-a` and `--all`
- cmd_checkout.py translates `-f`/`--force` to `-C` in goto paths
- cmd_checkout.py passes `-m`/`--merge` through in goto paths
  </verify>
  <done>
Both command handlers implement safety flag handling. Code patterns match the existing `-D` to `-d` translation in cmd_branch.py.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add E2E tests for safety requirements</name>
  <files>tests/test_commit.py, tests/test_checkout.py, tests/test_branch.py</files>
  <action>
Add to `tests/test_commit.py`:

```python
class TestCommitSafety:
    """SAFE-01: git commit -a should not add untracked files."""

    def test_commit_a_does_not_add_untracked(self, sl_repo_with_commit: Path):
        """git commit -a ignores untracked files (removes flag)."""
        # Create untracked file
        (sl_repo_with_commit / "untracked.txt").write_text("new content\n")

        # Modify tracked file
        readme = sl_repo_with_commit / "README.md"
        readme.write_text("modified content\n")

        # Add the modified file (so we have something to commit)
        run_command(["sl", "add", "README.md"], cwd=sl_repo_with_commit)

        # Commit with -a - should NOT add untracked.txt
        result = run_gitsl(["commit", "-a", "-m", "Test commit"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Untracked file should still be untracked
        status = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "untracked.txt" in status.stdout, \
            "untracked.txt was added by commit -a (should remain untracked)"

    def test_commit_all_flag_removed(self, sl_repo_with_commit: Path):
        """git commit --all (long form) also ignores untracked files."""
        (sl_repo_with_commit / "untracked2.txt").write_text("new\n")
        readme = sl_repo_with_commit / "README.md"
        readme.write_text("modified\n")
        run_command(["sl", "add", "README.md"], cwd=sl_repo_with_commit)

        result = run_gitsl(["commit", "--all", "-m", "Test"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        status = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "untracked2.txt" in status.stdout
```

Add to `tests/test_checkout.py`:

```python
class TestCheckoutForce:
    """SAFE-02: git checkout -f translates to sl goto -C."""

    def test_checkout_force_discards_changes(self, sl_repo_with_commit: Path):
        """git checkout -f <branch> discards uncommitted changes."""
        # Create a bookmark to switch to
        run_command(["sl", "bookmark", "target-branch"], cwd=sl_repo_with_commit)

        # Make uncommitted changes
        readme = sl_repo_with_commit / "README.md"
        readme.write_text("uncommitted changes\n")

        # Force checkout to target branch
        result = run_gitsl(["checkout", "-f", "target-branch"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Changes should be discarded
        assert readme.read_text() == "# Test Repository\n"

    def test_checkout_force_long_form(self, sl_repo_with_commit: Path):
        """git checkout --force <branch> also discards changes."""
        run_command(["sl", "bookmark", "force-target"], cwd=sl_repo_with_commit)

        readme = sl_repo_with_commit / "README.md"
        readme.write_text("uncommitted\n")

        result = run_gitsl(["checkout", "--force", "force-target"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0
        assert readme.read_text() == "# Test Repository\n"


class TestCheckoutMerge:
    """SAFE-03: git checkout -m translates to sl goto -m."""

    def test_checkout_merge_preserves_changes(self, sl_repo_with_commit: Path):
        """git checkout -m <branch> merges local changes during switch."""
        # Create another commit on a different bookmark
        run_command(["sl", "bookmark", "feature"], cwd=sl_repo_with_commit)
        (sl_repo_with_commit / "feature.txt").write_text("feature content\n")
        run_command(["sl", "add", "feature.txt"], cwd=sl_repo_with_commit)
        run_command(["sl", "commit", "-m", "Add feature file"], cwd=sl_repo_with_commit)

        # Go back to original commit
        run_command(["sl", "goto", ".^"], cwd=sl_repo_with_commit)

        # Make uncommitted changes to a different file
        readme = sl_repo_with_commit / "README.md"
        readme.write_text("local changes\n")

        # Checkout with merge - should attempt to merge local changes
        result = run_gitsl(["checkout", "-m", "feature"], cwd=sl_repo_with_commit)
        # May succeed or fail depending on conflicts, but should not error on flag
        # The key is that -m is passed to sl goto, not rejected

    def test_checkout_merge_long_form(self, sl_repo_with_commit: Path):
        """git checkout --merge <branch> also merges local changes."""
        run_command(["sl", "bookmark", "merge-target"], cwd=sl_repo_with_commit)

        readme = sl_repo_with_commit / "README.md"
        readme.write_text("local changes\n")

        # --merge should translate to -m for sl goto
        result = run_gitsl(["checkout", "--merge", "merge-target"], cwd=sl_repo_with_commit)
        # Flag should be accepted (translated to -m)
```

Verify existing test in `tests/test_branch.py`:

Confirm `TestBranchForceDelete.test_branch_force_delete_preserves_commits` exists and tests SAFE-04.
The test creates a bookmark, makes a commit, force deletes with -D, and verifies the commit still exists.
This test already exists (lines 97-122), no changes needed.
  </action>
  <verify>
Run all safety tests:
```bash
cd /Users/vmakaev/NonWork/gitsl && python -m pytest tests/test_commit.py::TestCommitSafety tests/test_checkout.py::TestCheckoutForce tests/test_checkout.py::TestCheckoutMerge tests/test_branch.py::TestBranchForceDelete -v
```
All tests should pass.
  </verify>
  <done>
- 2 new tests for SAFE-01 (commit -a, commit --all)
- 2 new tests for SAFE-02 (checkout -f, checkout --force)
- 2 new tests for SAFE-03 (checkout -m, checkout --merge)
- SAFE-04 already has test coverage (test_branch_force_delete_preserves_commits)
  </done>
</task>

</tasks>

<verification>
Run full test suite to ensure no regressions:
```bash
cd /Users/vmakaev/NonWork/gitsl && python -m pytest tests/ -v
```

Specifically verify safety requirements:
```bash
cd /Users/vmakaev/NonWork/gitsl && python -m pytest tests/test_commit.py tests/test_checkout.py tests/test_branch.py -v -k "safety or force or merge or Force"
```
</verification>

<success_criteria>
1. SAFE-01: `git commit -a` does not add untracked files (test passes)
2. SAFE-02: `git checkout -f <branch>` discards uncommitted changes (test passes)
3. SAFE-03: `git checkout -m <branch>` merges local changes (test passes)
4. SAFE-04: `git branch -D <name>` preserves commits (existing test passes)
5. All existing tests continue to pass (no regressions)
6. Full test suite passes: `python -m pytest tests/ -v`
</success_criteria>

<output>
After completion, create `.planning/phases/20-critical-safety-fixes/20-01-SUMMARY.md`
</output>
