---
phase: 18-stash-operations
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - cmd_stash.py
  - gitsl.py
autonomous: true

must_haves:
  truths:
    - "git stash saves uncommitted changes"
    - "git stash push saves with optional message"
    - "git stash pop restores and removes stash"
    - "git stash apply restores but keeps stash"
    - "git stash list shows all stashes"
    - "git stash drop removes most recent stash"
  artifacts:
    - path: "cmd_stash.py"
      provides: "Stash command handler with subcommand dispatch"
      exports: ["handle"]
    - path: "gitsl.py"
      provides: "Dispatch routing for stash"
      contains: "cmd_stash"
  key_links:
    - from: "gitsl.py"
      to: "cmd_stash.handle"
      via: "dispatch on parsed.command == 'stash'"
      pattern: "parsed\\.command == \"stash\""
    - from: "cmd_stash.py"
      to: "sl shelve"
      via: "run_sl call for save operations"
      pattern: "run_sl\\(\\[\"shelve\""
    - from: "cmd_stash.py"
      to: "sl unshelve"
      via: "run_sl call for restore operations"
      pattern: "run_sl\\(\\[\"unshelve\""
---

<objective>
Create stash command handler with subcommand dispatch and routing.

Purpose: Enable users to save and manage temporary changes using familiar git stash workflow, translated to Sapling's shelve/unshelve commands.

Output: cmd_stash.py with subcommand handlers (push, pop, apply, list, drop) and updated gitsl.py dispatch.

Key complexity: `git stash drop` without arguments requires querying shelve list to find the most recent shelve name before deleting, since sl shelve --delete requires a name argument.
</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/18-stash-operations/18-RESEARCH.md
@cmd_branch.py
@cmd_restore.py
@gitsl.py
@common.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create stash command handler with subcommand dispatch</name>
  <files>cmd_stash.py</files>
  <action>
Create cmd_stash.py with handler for git stash command. This is the most complex handler because it has subcommand dispatch (push, pop, apply, list, drop) and the drop subcommand needs to query shelve list to find the most recent name.

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

import subprocess
import sys
from common import ParsedCommand, run_sl


def _get_most_recent_shelve() -> str | None:
    """Get the name of the most recent shelve, or None if no shelves exist."""
    result = subprocess.run(
        ["sl", "shelve", "--list"],
        capture_output=True,
        text=True
    )
    if result.returncode != 0 or not result.stdout.strip():
        return None

    # Output format: "name    (age)    message"
    first_line = result.stdout.strip().split('\n')[0]
    # Name is first whitespace-separated token
    shelve_name = first_line.split()[0]
    return shelve_name


def _handle_push(args: list) -> int:
    """Handle git stash push -> sl shelve."""
    return run_sl(["shelve"] + args)


def _handle_pop(args: list) -> int:
    """Handle git stash pop -> sl unshelve."""
    return run_sl(["unshelve"] + args)


def _handle_apply(args: list) -> int:
    """Handle git stash apply -> sl unshelve --keep."""
    return run_sl(["unshelve", "--keep"] + args)


def _handle_list(args: list) -> int:
    """Handle git stash list -> sl shelve --list."""
    return run_sl(["shelve", "--list"] + args)


def _handle_drop(args: list) -> int:
    """
    Handle git stash drop -> sl shelve --delete.

    git stash drop without args deletes most recent.
    sl shelve --delete requires a name.
    """
    if args:
        # Specific stash reference provided
        # For v1.2, pass through - sl may error if format wrong
        return run_sl(["shelve", "--delete"] + args)

    # No args: delete most recent
    shelve_name = _get_most_recent_shelve()
    if shelve_name is None:
        print("No stash entries found.", file=sys.stderr)
        return 1

    return run_sl(["shelve", "--delete", shelve_name])


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

    Translations:
    - git stash           -> sl shelve
    - git stash push      -> sl shelve
    - git stash -m "msg"  -> sl shelve -m "msg"
    - git stash pop       -> sl unshelve
    - git stash apply     -> sl unshelve --keep
    - git stash list      -> sl shelve --list
    - git stash drop      -> sl shelve --delete <most-recent>
    """
    args = list(parsed.args)

    # No subcommand = stash (same as push)
    if not args:
        return run_sl(["shelve"])

    subcommand = args[0]
    subargs = args[1:]

    if subcommand == "push":
        return _handle_push(subargs)

    if subcommand == "pop":
        return _handle_pop(subargs)

    if subcommand == "apply":
        return _handle_apply(subargs)

    if subcommand == "list":
        return _handle_list(subargs)

    if subcommand == "drop":
        return _handle_drop(subargs)

    # Check if first arg is a flag (e.g., -m) - treat as push
    if subcommand.startswith("-"):
        return _handle_push(args)

    # Unknown subcommand - pass through to shelve
    return run_sl(["shelve"] + args)
```

Key implementation details:
- `_get_most_recent_shelve()` uses subprocess.run with capture_output to query sl shelve --list
- `_handle_drop()` checks if shelve list is empty and returns git-compatible error message
- Flags like -m are detected and routed to push handler
- Unknown subcommands pass through to shelve
  </action>
  <verify>
    - File exists: `ls cmd_stash.py`
    - Python syntax: `python -m py_compile cmd_stash.py`
    - Import test: `python -c "import cmd_stash; print('OK')"`
  </verify>
  <done>
    - cmd_stash.py exists with handle() function
    - Subcommand dispatch for push, pop, apply, list, drop
    - _get_most_recent_shelve() helper for drop without args
    - Error message for drop with no stashes
  </done>
</task>

<task type="auto">
  <name>Task 2: Update gitsl.py dispatch routing</name>
  <files>gitsl.py</files>
  <action>
Add import at top of gitsl.py (after existing cmd_* imports):

```python
import cmd_stash
```

Add dispatch case in main() function (before the unsupported command handling, after cmd_restore):

```python
if parsed.command == "stash":
    return cmd_stash.handle(parsed)
```

Follow the exact pattern used for branch, restore, and other commands in Phase 17.
  </action>
  <verify>
    - Syntax check: `python -m py_compile gitsl.py`
    - Debug mode test: `GITSL_DEBUG=1 python gitsl.py stash` should show parsed command
    - Debug mode test: `GITSL_DEBUG=1 python gitsl.py stash pop` should show parsed command
  </verify>
  <done>
    - gitsl.py imports cmd_stash
    - Dispatch routes stash command to handler
    - Debug mode shows commands are recognized (not unsupported)
  </done>
</task>

</tasks>

<verification>
Run from project root:

```bash
# Verify handler exists and compiles
python -m py_compile cmd_stash.py gitsl.py

# Test debug mode (no repo needed)
GITSL_DEBUG=1 python gitsl.py stash
GITSL_DEBUG=1 python gitsl.py stash push
GITSL_DEBUG=1 python gitsl.py stash -m "test message"
GITSL_DEBUG=1 python gitsl.py stash pop
GITSL_DEBUG=1 python gitsl.py stash apply
GITSL_DEBUG=1 python gitsl.py stash list
GITSL_DEBUG=1 python gitsl.py stash drop
```

All commands should show debug output, not "unsupported command" message.
</verification>

<success_criteria>
1. cmd_stash.py exists with handle() function and subcommand dispatch
2. gitsl.py imports cmd_stash and routes stash command correctly
3. All files pass Python syntax validation
4. Debug mode confirms stash commands are recognized
5. _handle_drop() returns git-compatible error when no stashes exist
</success_criteria>

<output>
After completion, create `.planning/phases/18-stash-operations/18-01-SUMMARY.md`
</output>
