---
phase: 03-execution-pipeline
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
  - common.py
  - cmd_status.py
  - tests/test_execution.py
autonomous: true

must_haves:
  truths:
    - "sl commands execute via subprocess without deadlock"
    - "Exit code from sl propagates exactly to caller"
    - "stdout from sl appears on caller's stdout in real-time"
    - "stderr from sl appears on caller's stderr in real-time"
  artifacts:
    - path: "common.py"
      provides: "run_sl() with subprocess.run() passthrough"
      contains: "subprocess.run"
    - path: "tests/test_execution.py"
      provides: "E2E tests for execution pipeline"
      min_lines: 50
  key_links:
    - from: "common.py"
      to: "subprocess.run"
      via: "run_sl function"
      pattern: "subprocess\\.run\\(\\[\"sl\""
    - from: "cmd_status.py"
      to: "common.run_sl"
      via: "import and call"
      pattern: "run_sl\\("
---

<objective>
Implement subprocess execution with E2E tests

Purpose: Satisfy EXEC-02 through EXEC-05 - real sl execution with proper I/O and exit code propagation
Output: Working run_sl() function, E2E tests proving correct behavior
</objective>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/03-execution-pipeline/03-RESEARCH.md
@.planning/phases/03-execution-pipeline/03-01-SUMMARY.md
@common.py
@cmd_status.py
@tests/conftest.py
@tests/helpers/commands.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement run_sl() with subprocess passthrough</name>
  <files>common.py</files>
  <action>
Replace the stub run_sl() in common.py with real implementation:

```python
import subprocess

def run_sl(args: List[str]) -> int:
    """
    Execute sl command with I/O passthrough.

    Args:
        args: Arguments to pass to sl (command and flags)

    Returns:
        Exit code from sl process

    Notes:
        - stdin=None, stdout=None, stderr=None (defaults) mean child
          inherits parent's file descriptors
        - stdout appears on caller's stdout in real-time
        - stderr appears on caller's stderr in real-time
        - Child receives SIGINT directly (same process group)
    """
    result = subprocess.run(["sl"] + args)
    return result.returncode
```

Key points from research:
- Do NOT use capture_output=True (breaks real-time display)
- Do NOT use stdout=PIPE (causes buffering)
- Use defaults for stdin/stdout/stderr (None = inherit)
- Return result.returncode directly (don't hardcode 0)
  </action>
  <verify>
# Test that run_sl actually calls sl (may fail if sl not installed, that's OK)
python -c "from common import run_sl; exit(run_sl(['--version']))" 2>&1 | head -1
  </verify>
  <done>run_sl() uses subprocess.run() with default I/O inheritance</done>
</task>

<task type="auto">
  <name>Task 2: Create E2E execution tests</name>
  <files>tests/test_execution.py</files>
  <action>
Create tests/test_execution.py with E2E tests for execution pipeline.

IMPORTANT: These tests require Sapling (sl) to be installed. Use pytest.mark.skipif to skip if sl is not available.

```python
"""
E2E tests for execution pipeline (EXEC-02 through EXEC-05).

These tests verify:
- EXEC-02: Script executes sl commands via subprocess
- EXEC-03: Exit code propagates to caller
- EXEC-04: stdout passes through
- EXEC-05: stderr passes through
"""

import shutil
import subprocess
from pathlib import Path

import pytest

from conftest import run_gitsl


# Skip all tests if sl is not installed
sl_available = shutil.which("sl") is not None
pytestmark = pytest.mark.skipif(not sl_available, reason="Sapling (sl) not installed")


class TestExitCodePropagation:
    """EXEC-03: Exit code from sl propagates exactly to caller."""

    def test_successful_command_returns_zero(self, git_repo: Path):
        """sl status on valid repo returns 0."""
        result = run_gitsl(["status"], cwd=git_repo)
        assert result.exit_code == 0

    def test_failed_command_returns_nonzero(self, tmp_path: Path):
        """sl status on non-repo returns non-zero."""
        # tmp_path is NOT a git repo, so sl status should fail
        result = run_gitsl(["status"], cwd=tmp_path)
        assert result.exit_code != 0


class TestStdoutPassthrough:
    """EXEC-04: stdout from sl appears on caller's stdout."""

    def test_status_output_appears(self, git_repo: Path):
        """sl status output appears in stdout."""
        result = run_gitsl(["status"], cwd=git_repo)
        # Empty repo status should contain something
        # (exact output varies, just verify we got something)
        assert result.stdout != "" or result.stderr != ""

    def test_version_output_from_sl(self, git_repo: Path):
        """Verify sl is actually being called (not gitsl --version)."""
        # Run gitsl status and check we don't get gitsl version output
        result = run_gitsl(["status"], cwd=git_repo)
        assert "gitsl version" not in result.stdout


class TestStderrPassthrough:
    """EXEC-05: stderr from sl appears on caller's stderr."""

    def test_error_appears_on_stderr(self, tmp_path: Path):
        """Error message from sl appears in stderr."""
        # Run in non-repo directory to trigger error
        result = run_gitsl(["status"], cwd=tmp_path)
        # sl should output error about not being in a repo
        assert result.stderr != "" or result.exit_code != 0
```

Notes:
- Uses git_repo fixture from conftest.py (creates real git repo)
- Sapling can work with git repos, so git_repo fixture works
- Tests focus on observable behavior, not internal implementation
  </action>
  <verify>python -m pytest tests/test_execution.py -v --tb=short</verify>
  <done>test_execution.py has tests for EXEC-02 through EXEC-05, passes if sl installed</done>
</task>

<task type="auto">
  <name>Task 3: Verify full integration</name>
  <files>gitsl.py, cmd_status.py</files>
  <action>
Run integration verification to prove end-to-end works:

1. Verify gitsl.py dispatches to cmd_status
2. Verify cmd_status calls run_sl
3. Verify run_sl executes sl via subprocess
4. Run all tests to ensure no regressions

Manual verification commands (run these):
```bash
# Create temp git repo
cd $(mktemp -d)
git init
echo "test" > file.txt

# Run gitsl status (should call sl status)
python /path/to/gitsl.py status

# Verify exit code propagation
python /path/to/gitsl.py status; echo "Exit code: $?"

# Run in non-repo to verify error propagation
cd /tmp
python /path/to/gitsl.py status; echo "Exit code: $?"
```

No file changes needed for this task - it's verification only.
  </action>
  <verify>
python -m pytest tests/ -v --tb=short
echo "All tests pass"
  </verify>
  <done>Full test suite passes, execution pipeline verified working</done>
</task>

</tasks>

<verification>
All execution requirements verified:
- EXEC-02: run_sl() calls subprocess.run(["sl"] + args)
- EXEC-03: Exit code test shows 0 for success, non-zero for failure
- EXEC-04: stdout test shows sl output reaches caller
- EXEC-05: stderr test shows error messages reach caller

Signal handling (Ctrl+C) is automatically correct because:
- subprocess.run() with default settings keeps child in same process group
- SIGINT goes to entire process group
- No special handling needed

Run full test suite:
```bash
python -m pytest tests/ -v
```
</verification>

<success_criteria>
1. common.py contains run_sl() with subprocess.run() (no PIPE, no capture_output)
2. tests/test_execution.py contains tests for EXEC-02 through EXEC-05
3. Tests pass when sl is installed
4. Tests skip gracefully when sl is not installed
5. Exit codes propagate correctly (verified by E2E test)
6. All existing tests still pass
</success_criteria>

<output>
After completion, create `.planning/phases/03-execution-pipeline/03-02-SUMMARY.md`
</output>
