# Phase 01.1.7: Story File Isolation Check [CONDITIONAL] (STORY-123)

**Purpose:** Warn about uncommitted story files that may conflict with current story development. Distinguishes "your story" from "other uncommitted stories" to help users understand the scope of changes.

**When to execute:** Only when `$GIT_AVAILABLE == true` AND uncommitted .story.md files exist beyond the current story.

**Skip conditions:**
- Git not available ($GIT_AVAILABLE == false)
- No uncommitted .story.md files
- Only current story is uncommitted (no others)

**Depends On:** STORY-121 (DEVFORGEAI_STORY environment variable scoping)

**Security Notes:**
- Input validation REQUIRED before any shell command execution (CRITICAL)
- All parameters must match pattern: STORY-[0-9]+ (HIGH)
- No shell metacharacters allowed in story IDs (CRITICAL)

---

## Phase 01.1.7.0: Input Validation [SECURITY - CRITICAL] ⚠️

**MUST EXECUTE FIRST** - Prevents command injection vulnerability (OWASP A03:2021)

```
# Validate story_id parameter before any shell operations
CURRENT_STORY_ID = $STORY_ID  # Set by /dev command parameter extraction

# ========== VALIDATION BLOCK ==========
# CRITICAL: Validate story_id prevents shell injection attacks

# Check 1: Empty validation
IF [[ -z "${CURRENT_STORY_ID}" ]]; then
    Display: "❌ ERROR: story_id cannot be empty"
    Display: "  Expected format: STORY-NNN (e.g., STORY-123)"
    HALT with exit code 1

# Check 2: Pattern validation - Must match STORY-[0-9]+
IF [[ ! "${CURRENT_STORY_ID}" =~ ^STORY-[0-9]+$ ]]; then
    Display: "❌ ERROR: Invalid story_id format: '${CURRENT_STORY_ID}'"
    Display: "  Expected format: STORY-NNN (e.g., STORY-123)"
    Display: "  Invalid characters detected"
    HALT with exit code 1

# Check 3: Length validation - Reasonable upper bound
IF [[ ${#CURRENT_STORY_ID} -gt 20 ]]; then
    Display: "❌ ERROR: story_id exceeds maximum length"
    Display: "  Maximum: 20 characters, got: ${#CURRENT_STORY_ID}"
    HALT with exit code 1

# Validation passed
Display: "✓ Story ID validated: ${CURRENT_STORY_ID}"
```

**Injection Patterns Blocked:**
- Command substitution: `STORY-123 && rm -rf /`
- Pipe operators: `STORY-123 | cat /etc/passwd`
- Variable expansion: `STORY-${USER}`
- Backtick execution: `STORY-\`whoami\``
- Glob patterns: `STORY-*`
- Redirection: `STORY-123 > /tmp/file`

---

## Phase 01.1.7.1: Detect Uncommitted Story Files

```
# Get current story ID from /dev argument (e.g., "STORY-123")
CURRENT_STORY_ID = $STORY_ID  # Set by /dev command parameter extraction

# Find all uncommitted .story.md files via git status
uncommitted_output = Bash(command="git status --porcelain | grep '\\.story\\.md$'")

IF uncommitted_output is empty:
    Display: "✓ No uncommitted story files detected"
    SKIP Phase 01.1.7 (proceed to Phase 01.2)

# Parse uncommitted story files
UNCOMMITTED_STORY_FILES = []
FOR line in uncommitted_output.lines:
    # Format: " M devforgeai/specs/Stories/STORY-100-title.story.md"
    file_path = line.split()[-1]  # Get file path (last field)
    IF file_path ends with ".story.md":
        UNCOMMITTED_STORY_FILES.append(file_path)

Display: "Found {len(UNCOMMITTED_STORY_FILES)} uncommitted story file(s)"
```

---

## Phase 01.1.7.2: Extract Story IDs and Separate Current vs. Others

```
# Extract story IDs from file paths
# Path format: devforgeai/specs/Stories/STORY-NNN-title.story.md

# SECURITY FIX: Parse current story number with error handling
IF ! [[ "${CURRENT_STORY_ID}" =~ ^STORY-([0-9]+)$ ]]; then
    Display: "❌ ERROR: Story ID failed validation (this should not happen)"
    HALT with exit code 1
fi

# Extract number safely with pattern match
current_story_num=${BASH_REMATCH[1]}

STORY_IDS = []
FOR file_path in UNCOMMITTED_STORY_FILES:
    # Extract STORY-NNN from path
    filename = file_path.split("/")[-1]
    match = regex_match(filename, r"^STORY-(\d+)")
    IF match:
        story_num = int(match.group(1))
        STORY_IDS.append({
            "id": f"STORY-{story_num}",
            "number": story_num,
            "path": file_path
        })

# Separate current story from others
CURRENT_STORY_FILE = None
OTHER_STORY_IDS = []

FOR story in STORY_IDS:
    IF story["number"] == int(current_story_num):
        CURRENT_STORY_FILE = story
    ELSE:
        OTHER_STORY_IDS.append(story)

# Check skip condition - only current story uncommitted
IF len(OTHER_STORY_IDS) == 0:
    Display: "✓ Only current story ({CURRENT_STORY_ID}) is uncommitted - no warning needed"
    SKIP Phase 01.1.7 (proceed to Phase 01.2)

Display: "  - Current story: {CURRENT_STORY_ID}"
Display: "  - Other uncommitted stories: {len(OTHER_STORY_IDS)}"
```

---

## Phase 01.1.7.3: Detect Story Ranges (Format Consecutive Numbers)

```
# Sort other story numbers
other_numbers = sorted([s["number"] for s in OTHER_STORY_IDS])

# Group into consecutive ranges
# Example: [100, 101, 102, 103, 115, 116, 125]
#       -> [(100, 103), (115, 116), (125, 125)]

STORY_RANGES = []
range_start = other_numbers[0]
range_end = other_numbers[0]

FOR i in range(1, len(other_numbers)):
    current = other_numbers[i]
    previous = other_numbers[i - 1]

    IF current == previous + 1:
        range_end = current
    ELSE:
        STORY_RANGES.append({"start": range_start, "end": range_end})
        range_start = current
        range_end = current

# Close final range
STORY_RANGES.append({"start": range_start, "end": range_end})

# Format ranges for display
FORMATTED_RANGES = []
FOR r in STORY_RANGES:
    IF r["start"] == r["end"]:
        FORMATTED_RANGES.append(f"STORY-{r['start']} (1 file)")
    ELSE:
        count = r["end"] - r["start"] + 1
        FORMATTED_RANGES.append(f"STORY-{r['start']} through STORY-{r['end']} ({count} files)")
```

---

## Phase 01.1.7.4: Display Warning Box

```
# Build warning display
OTHER_COUNT = len(OTHER_STORY_IDS)

Display: ""
Display: "+-------------------------------------------+"
Display: "| WARNING: UNCOMMITTED STORY FILES DETECTED |"
Display: "+-------------------------------------------+"
Display: ""
Display: "Your story: {CURRENT_STORY_ID} (will be modified by this /dev run)"
Display: ""
Display: "Other uncommitted stories: {OTHER_COUNT} file(s)"

FOR range_text in FORMATTED_RANGES:
    Display: "  - {range_text}"

Display: ""
Display: "Impact:"
Display: "  • Git commits will include ONLY your story (scoped via DEVFORGEAI_STORY)"
Display: "  • Pre-commit validation will focus on {CURRENT_STORY_ID}"
Display: "  • Other story files remain uncommitted"
Display: ""
Display: "+-------------------------------------------+"
Display: ""
```

---

## Phase 01.1.7.5: Ask User for Action

```
# Present options to user
USER_CHOICE = AskUserQuestion(
    questions=[
        {
            "question": "How would you like to proceed with {OTHER_COUNT} other uncommitted story files?",
            "header": "Story Scope",
            "multiSelect": false,
            "options": [
                {
                    "label": "Continue with scoped commits (recommended)",
                    "description": "Proceed with development. Git commits will include ONLY {CURRENT_STORY_ID}."
                },
                {
                    "label": "Commit other stories first",
                    "description": "HALT workflow. You'll commit other stories manually, then re-run /dev."
                },
                {
                    "label": "Show me the list",
                    "description": "Display full list of uncommitted story files, then ask again."
                }
            ]
        }
    ]
)
```

---

## Phase 01.1.7.6: Process User Choice

```
IF USER_CHOICE == "Continue with scoped commits (recommended)":
    # Set environment variable for story-scoped commits (STORY-121 integration)
    $DEVFORGEAI_STORY = CURRENT_STORY_ID
    Display: "✓ Proceeding with scoped commits"
    Display: "  DEVFORGEAI_STORY={CURRENT_STORY_ID}"
    PROCEED to Phase 01.2

ELIF USER_CHOICE == "Commit other stories first":
    Display: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    Display: "  WORKFLOW HALTED - Commit Other Stories First"
    Display: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    Display: ""
    Display: "To commit other stories:"
    Display: "  git add devforgeai/specs/Stories/"
    Display: "  git commit -m 'chore: update story files'"
    Display: ""
    Display: "After committing, re-run: /dev {CURRENT_STORY_ID}"
    HALT workflow

ELIF USER_CHOICE == "Show me the list":
    Display: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    Display: "  UNCOMMITTED STORY FILES"
    Display: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    Bash(command="git status --porcelain | grep '\\.story\\.md$'")
    # Re-ask question
    GOTO Phase 01.1.7.5
```

---

## Phase 01.1.7 Error Handling

**Error Scenarios:**

| Scenario | Action | Exit Code |
|----------|--------|-----------|
| Git status fails | Skip step, proceed | 128 |
| Story file not found | Skip with warning | 0 |
| Invalid file paths | Skip bad file, continue | 0 |
| Range formatting fails | Show raw list | 0 |
| Input validation fails | HALT | 1 |
| Permission denied | Proceed with partial info | 0 |

**Key Principle:** Errors in story file isolation check should NOT block the main /dev workflow. This is a convenience feature, not a critical gate.

---

## Phase 01.1.7 Decision Matrix

| File Status | Current Story | Other Stories | Action |
|-------------|---------------|---------------|--------|
| All committed | - | - | SKIP → Go to 01.2 |
| Uncommitted | Uncommitted | None | SKIP → Go to 01.2 |
| Uncommitted | Uncommitted | Yes (any count) | WARN + ASK |
| Committed | - | Uncommitted | SKIP with warning |
| Git error | - | - | ERROR or skip |

---

## Phase 01.1.7 Success Criteria

**SECURITY (CRITICAL):**
- [x] Input validation executed FIRST (pattern: ^STORY-[0-9]+$)
- [x] Empty story_id rejected
- [x] Command injection blocked
- [x] All shell commands use proper quoting

**FUNCTIONAL:**
- [x] Detects all uncommitted .story.md files
- [x] Correctly separates current story from others
- [x] Formats consecutive story numbers as ranges
- [x] Presents 3 user options via AskUserQuestion
- [x] Sets DEVFORGEAI_STORY environment variable on "Continue"
- [x] HALTs workflow on "Commit other stories first"
- [x] Re-asks after "Show me the list"

**Token cost:** ~3,000 tokens
