---
phase: 06-status-output-emulation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - cmd_status.py
  - tests/test_status_porcelain.py
autonomous: true

must_haves:
  truths:
    - "git status --porcelain outputs 2-char XY codes matching git format"
    - "git status --short outputs same format as --porcelain"
    - "Untracked file shows as ?? filename"
    - "Modified file shows as  M filename (space + M)"
    - "Added file shows as A  filename (A + space)"
    - "Removed file shows as D  filename (D + space)"
    - "Missing file shows as  D filename (space + D)"
    - "git status (no flags) still passthroughs to sl status"
    - "Clean repo with --porcelain outputs empty string"
  artifacts:
    - path: "cmd_status.py"
      provides: "Handler with porcelain/short output transformation"
      exports: ["handle"]
      contains: "transform_to_porcelain"
    - path: "tests/test_status_porcelain.py"
      provides: "E2E tests for porcelain format"
      min_lines: 80
  key_links:
    - from: "cmd_status.py"
      to: "subprocess"
      via: "capture_output pattern from cmd_rev_parse.py"
      pattern: "subprocess\\.run.*capture_output=True"
    - from: "cmd_status.py"
      to: "common.py"
      via: "run_sl for passthrough"
      pattern: "run_sl\\("
---

<objective>
Implement status output emulation for --porcelain and --short flags

Purpose: Enable tools that parse git status output to work with gitsl without modification
Output: Enhanced cmd_status.py with output transformation, E2E tests validating format
</objective>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/06-status-output-emulation/06-RESEARCH.md

# Existing patterns
@cmd_status.py
@cmd_rev_parse.py
@common.py
@tests/conftest.py
@tests/test_cmd_rev_parse.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Enhance cmd_status.py with porcelain output transformation</name>
  <files>
    - cmd_status.py
  </files>
  <action>
Modify cmd_status.py to detect --porcelain/--short/-s flags and transform sl status output to git format.

Key implementation from research:

```python
"""Handler for 'git status' command."""

import subprocess
import sys
from common import ParsedCommand, run_sl

# Status code translation: sl -> git porcelain XY format
# Key insight: sl has no staging area, so:
# - sl M (modified) -> git " M" (working tree change, not staged)
# - sl A (added) -> git "A " (staged addition)
# - sl R (removed) -> git "D " (staged deletion)
SL_TO_GIT_STATUS = {
    'M': ' M',  # Modified in working tree (not staged - sl has no staging)
    'A': 'A ',  # Added (will be committed - equivalent to staged in git)
    'R': 'D ',  # Removed (will be deleted - equivalent to staged deletion)
    '?': '??',  # Unknown/untracked
    '!': ' D',  # Missing (deleted from disk, not via sl rm)
    'I': '!!',  # Ignored (only with --ignored flag)
}


def parse_sl_status_line(line: str) -> tuple:
    """
    Parse a single sl status line.

    Args:
        line: Single line from sl status output, e.g., "M filename.txt"

    Returns:
        Tuple of (status_code, filename) or (None, None) if invalid

    Format: "X filename" where X is single char status
    """
    if not line or len(line) < 3:  # Minimum: "X f"
        return None, None

    if line[1] != ' ':  # Second char must be space
        return None, None

    status_code = line[0]
    filename = line[2:]

    return status_code, filename


def transform_to_porcelain(sl_output: str) -> str:
    """
    Transform sl status output to git porcelain v1 format.

    sl format:  "X filename" (1 char + space + filename)
    git format: "XY filename" (2 chars + space + filename)

    Args:
        sl_output: Complete output from 'sl status' command

    Returns:
        Git-compatible porcelain format output
    """
    lines = []

    for line in sl_output.splitlines():
        status_code, filename = parse_sl_status_line(line)

        if status_code is None:
            continue

        # Map sl status to git XY code
        git_code = SL_TO_GIT_STATUS.get(status_code, '??')

        # Format: XY<space>filename
        lines.append(f"{git_code} {filename}")

    # Return with trailing newline if there's content
    if lines:
        return '\n'.join(lines) + '\n'
    return ''


def handle(parsed: ParsedCommand) -> int:
    """
    Handle 'git status' command.

    Special handling for --porcelain and --short flags to transform
    sl status output into git-compatible format.
    """
    # Check for porcelain/short flags
    needs_transform = '--porcelain' in parsed.args or '--short' in parsed.args or '-s' in parsed.args

    if needs_transform:
        # Remove git-specific flags before calling sl
        sl_args = [a for a in parsed.args
                   if a not in ('--porcelain', '--short', '-s')]

        result = subprocess.run(
            ['sl', 'status'] + sl_args,
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            transformed = transform_to_porcelain(result.stdout)
            sys.stdout.write(transformed)
        else:
            sys.stderr.write(result.stderr)

        return result.returncode

    # Default: passthrough to sl status
    return run_sl(['status'] + parsed.args)
```

IMPORTANT:
- Use capture_output=True pattern (from cmd_rev_parse.py)
- Use sys.stdout.write() not print() to avoid extra newlines
- Filter out git-specific flags (--porcelain, --short, -s) before calling sl
- Status code mapping: M -> " M" (space first!), A -> "A ", R -> "D "
- Empty output for clean repo (no trailing newline)
  </action>
  <verify>
Manual test in an sl repo:
```bash
cd /tmp && mkdir test-status && cd test-status
sl init
sl config --local ui.username "Test <test@test.com>"

# Create untracked file
echo "test" > untracked.txt
python /path/to/gitsl.py status --porcelain
# Should output: ?? untracked.txt

# Add the file
sl add untracked.txt
python /path/to/gitsl.py status --porcelain
# Should output: A  untracked.txt

# Commit and modify
sl commit -m "add file"
echo "modified" >> untracked.txt
python /path/to/gitsl.py status --porcelain
# Should output:  M untracked.txt

# Test -s flag
python /path/to/gitsl.py status -s
# Should output same format

# Test normal status still works
python /path/to/gitsl.py status
# Should show sl status output (not porcelain)
```
  </verify>
  <done>
cmd_status.py contains SL_TO_GIT_STATUS mapping dict.
cmd_status.py contains transform_to_porcelain function.
cmd_status.py detects --porcelain, --short, -s flags.
cmd_status.py uses capture_output pattern for transformation.
cmd_status.py falls back to passthrough for normal status.
  </done>
</task>

<task type="auto">
  <name>Task 2: Create E2E tests for porcelain status format</name>
  <files>
    - tests/test_status_porcelain.py
  </files>
  <action>
Create tests/test_status_porcelain.py with comprehensive tests for all status code scenarios.

Test scenarios from research:
1. Untracked file: ?? filename
2. Added file: A  filename (A + space)
3. Modified file:  M filename (space + M)
4. Removed file (via sl rm): D  filename
5. Missing file (deleted without sl rm):  D filename
6. Mixed states: multiple files with different statuses
7. Clean repo: empty output
8. -s and --short work same as --porcelain

```python
"""
E2E tests for git status --porcelain and --short (FLAG-01, FLAG-02).

Tests exact byte-for-byte format matching for tooling compatibility.
"""

import shutil
from pathlib import Path

import pytest

from conftest import run_gitsl
from helpers.commands import run_command


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


class TestPorcelainUntracked:
    """Untracked files show as ?? filename."""

    def test_untracked_file_format(self, sl_repo: Path):
        """Untracked file shows as '?? filename'."""
        (sl_repo / "untracked.txt").write_text("content\n")

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo)

        assert result.exit_code == 0
        assert result.stdout == "?? untracked.txt\n"

    def test_multiple_untracked_files(self, sl_repo: Path):
        """Multiple untracked files each show as ??."""
        (sl_repo / "a.txt").write_text("a\n")
        (sl_repo / "b.txt").write_text("b\n")

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo)

        assert result.exit_code == 0
        assert "?? a.txt" in result.stdout
        assert "?? b.txt" in result.stdout


class TestPorcelainAdded:
    """Added files show as A  filename (A + space)."""

    def test_added_file_format(self, sl_repo: Path):
        """Added file shows as 'A  filename'."""
        (sl_repo / "new.txt").write_text("content\n")
        run_command(["sl", "add", "new.txt"], cwd=sl_repo)

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo)

        assert result.exit_code == 0
        # Exact format: A<space><space>filename
        assert "A  new.txt" in result.stdout


class TestPorcelainModified:
    """Modified files show as  M filename (space + M)."""

    def test_modified_file_format(self, sl_repo_with_commit: Path):
        """Modified tracked file shows as ' M filename'."""
        readme = sl_repo_with_commit / "README.md"
        readme.write_text("Modified content\n")

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo_with_commit)

        assert result.exit_code == 0
        # Exact format: <space>M<space>filename
        assert " M README.md" in result.stdout


class TestPorcelainRemoved:
    """Removed files (via sl rm) show as D  filename."""

    def test_removed_file_format(self, sl_repo_with_commit: Path):
        """File removed via sl rm shows as 'D  filename'."""
        run_command(["sl", "rm", "README.md"], cwd=sl_repo_with_commit)

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo_with_commit)

        assert result.exit_code == 0
        # Exact format: D<space><space>filename
        assert "D  README.md" in result.stdout


class TestPorcelainMissing:
    """Missing files (deleted without sl rm) show as  D filename."""

    def test_missing_file_format(self, sl_repo_with_commit: Path):
        """File deleted from disk (not via sl rm) shows as ' D filename'."""
        (sl_repo_with_commit / "README.md").unlink()

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo_with_commit)

        assert result.exit_code == 0
        # Exact format: <space>D<space>filename
        assert " D README.md" in result.stdout


class TestPorcelainCleanRepo:
    """Clean repo outputs empty string."""

    def test_clean_repo_empty_output(self, sl_repo_with_commit: Path):
        """Clean repo with --porcelain outputs nothing."""
        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo_with_commit)

        assert result.exit_code == 0
        assert result.stdout == ""


class TestPorcelainMixedStates:
    """Multiple files with different states."""

    def test_mixed_states(self, sl_repo_with_commit: Path):
        """Repo with added, modified, and untracked files."""
        # Modify existing
        (sl_repo_with_commit / "README.md").write_text("modified\n")
        # Add new tracked file
        (sl_repo_with_commit / "added.txt").write_text("added\n")
        run_command(["sl", "add", "added.txt"], cwd=sl_repo_with_commit)
        # Create untracked
        (sl_repo_with_commit / "untracked.txt").write_text("untracked\n")

        result = run_gitsl(["status", "--porcelain"], cwd=sl_repo_with_commit)

        assert result.exit_code == 0
        assert " M README.md" in result.stdout
        assert "A  added.txt" in result.stdout
        assert "?? untracked.txt" in result.stdout


class TestShortFlag:
    """--short and -s produce same format as --porcelain."""

    def test_short_flag(self, sl_repo: Path):
        """--short produces porcelain format."""
        (sl_repo / "file.txt").write_text("content\n")

        result = run_gitsl(["status", "--short"], cwd=sl_repo)

        assert result.exit_code == 0
        assert "?? file.txt" in result.stdout

    def test_s_flag(self, sl_repo: Path):
        """-s produces porcelain format."""
        (sl_repo / "file.txt").write_text("content\n")

        result = run_gitsl(["status", "-s"], cwd=sl_repo)

        assert result.exit_code == 0
        assert "?? file.txt" in result.stdout


class TestNormalStatusPassthrough:
    """Normal status (no flags) still passthroughs to sl."""

    def test_normal_status_passthrough(self, sl_repo: Path):
        """git status without flags runs sl status passthrough."""
        (sl_repo / "file.txt").write_text("content\n")

        result = run_gitsl(["status"], cwd=sl_repo)

        assert result.exit_code == 0
        # sl status uses single char code, not porcelain format
        # Output should contain "? file.txt" (sl format), NOT "?? file.txt"
        assert "? file.txt" in result.stdout or "file.txt" in result.stdout
```

IMPORTANT:
- Use sl_repo and sl_repo_with_commit fixtures from conftest.py
- Use run_gitsl from conftest.py for gitsl execution
- Use run_command from helpers.commands for sl commands
- Test exact format: "A " is 2 chars (A + space), " M" is 2 chars (space + M)
- Skip tests if sl not available
  </action>
  <verify>
Run tests:
```bash
python -m pytest tests/test_status_porcelain.py -v
```

All tests should pass (or skip if sl not installed).
  </verify>
  <done>
tests/test_status_porcelain.py exists with tests for all status code scenarios.
Tests cover: untracked (??), added (A ), modified ( M), removed (D ), missing ( D).
Tests cover: clean repo, mixed states, --short flag, -s flag, normal passthrough.
All tests pass when sl is available.
  </done>
</task>

</tasks>

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

Manual verification of exact format:
```bash
cd /tmp && mkdir test-porcelain && cd test-porcelain
sl init
sl config --local ui.username "Test <test@test.com>"

# Verify exact byte format with cat -A (shows $ for newlines, ^ for special chars)
echo "test" > file.txt
python /path/to/gitsl.py status --porcelain | cat -A
# Should show: ?? file.txt$

sl add file.txt
python /path/to/gitsl.py status --porcelain | cat -A
# Should show: A  file.txt$  (note: A, space, space, filename)

sl commit -m "add"
echo "modified" >> file.txt
python /path/to/gitsl.py status --porcelain | cat -A
# Should show:  M file.txt$  (note: space, M, space, filename)
```
</verification>

<success_criteria>
1. `git status --porcelain` outputs exact 2-char XY codes matching git format
2. Untracked: `??`, Added: `A `, Modified: ` M`, Removed: `D `, Missing: ` D`
3. `git status --short` and `-s` produce same format as `--porcelain`
4. `git status` (no flags) still passthroughs to sl status
5. Clean repo outputs empty string (no trailing newline)
6. All E2E tests pass
7. Requirements FLAG-01 and FLAG-02 satisfied
</success_criteria>

<output>
After completion, create `.planning/phases/06-status-output-emulation/06-01-SUMMARY.md`
</output>
