# 01.0 — Entry Dispatcher (STORY-658)

**Purpose:** Single source of truth for the dispatch logic invoked from `SKILL.md` § "Phase State Initialization & Entry Dispatcher". Replaces the legacy unconditional `phase-init` call that prevented `/dev --fix` from working when state already existed.

**When this applies:** Every invocation of the `spec-driven-dev` skill — fresh start, resume, remediation, all paths. The dispatcher is the FIRST work the skill performs after parameter extraction.

**Reference:** STORY-658 plan (`/home/bryan/.claude/plans/1-plan-mode-refactored-ember.md`), `dev_preflight.py::_compute_next_action()`.

---

## Decision Matrix (9 cases — must match CLI exactly)

The CLI's `_compute_next_action()` helper computes one of six values for `next_action` based on the workflow state. The skill MUST follow this matrix; do not derive a different action.

| # | State File | Phase | Remediation Mode | Open Items | Cycle Open | `$NEXT_ACTION` | `$NEXT_PHASE_TO_ENTER` |
|---|-----------|-------|------------------|------------|-----------|----------------|------------------------|
| 1 | absent (CLI just created) | 01 | any | n/a | n/a | `begin-phase-01` | `01` |
| 2 | exists | any | any | any | **yes** | `phase-status-resume` | (current phase) |
| 3 | exists | <10 | true | yes | no | `halt-ambiguous` | (current phase) |
| 4 | exists | <10 | false | n/a | no | `phase-status-resume` | (current phase) |
| 5 | exists | <10 | true | no | no | `phase-status-resume` (warn no-op) | (current phase) |
| 6 | exists | =10 | true | yes | no | `phase-cycle-start` | `02` |
| 7 | exists | =10 | true | no | no | `already-complete` (warn no-op) | n/a |
| 8 | exists | =10 | false | n/a | no | `already-complete` | n/a |
| 9 | corrupt / IO error | n/a | n/a | n/a | n/a | `halt-corrupt-state` | n/a |

**Definitions:**
- *Open Items* = `len(open_rec_ids) > 0` OR `qa_recommendations.open_count > 0`. Either signal source counts.
- *Cycle Open* = `state.cycles[-1].completed is None` (Sprint 3 invariant).

---

## Per-Action Execution Detail

### `begin-phase-01` (Case 1)

**Precondition:** `dev-preflight` returned exit 0 with `phase_state: "created"`. State file is now fresh on disk.

**EXECUTE:**

```
Display: "▶ Beginning workflow for ${STORY_ID}"
Display: "  Story file: ${STORY_FILE}"
Display: "  Phase 01 starts now."
$CURRENT_PHASE = "01"
```

**VERIFY:** `devforgeai-validate phase-status ${STORY_ID}` reports `current_phase: "01"`.

**RECORD:** None — state was already recorded by `dev-preflight`.

**Continuation:** Enter Phase Orchestration Loop at phase 01.

---

### `phase-status-resume` (Cases 2, 4, 5)

**Precondition:** State file exists. Either:
- (Case 2) A cycle is already in progress and dominates regardless of phase/flag.
- (Case 4) Mid-workflow with no remediation requested — normal resume.
- (Case 5) Mid-workflow with `--fix` but no open items — `--fix` is a no-op.

**EXECUTE:**

```
$CURRENT_PHASE = ${NEXT_PHASE_TO_ENTER}     # from dev-preflight payload
Display: "▶ Resuming ${STORY_ID} from phase ${CURRENT_PHASE}"
IF $REMEDIATION_MODE == true AND no open items (Case 5):
    Display: "⚠️  Note: --fix requested but no open QA recommendations or coverage gaps."
    Display: "   The flag is a no-op. Proceeding with normal resume."
IF cycle is open (Case 2):
    Display: "ℹ️  Existing remediation cycle is open; will continue inside it."
```

**VERIFY:** `devforgeai-validate phase-status ${STORY_ID}` reports `current_phase` matching `${CURRENT_PHASE}`.

**RECORD:** None — resumption is not a state-mutating step.

**Continuation:** Enter Phase Orchestration Loop at `$CURRENT_PHASE`.

---

### `phase-cycle-start` (Case 6 — THE STORY-658 CASE)

**Precondition:** State file exists, `current_phase == "10"`, `--fix` requested, open recommendations or gaps present, no cycle currently open.

**EXECUTE:** Run the precomputed `$NEXT_COMMAND` exactly as supplied by `dev-preflight`:

```bash
# Example (story-specific values are in $NEXT_COMMAND).
# Note: --story-id is a REQUIRED named argument — see cli.py:1638.
# Do NOT pass story-id positionally; that signature is reserved for older
# subcommands (dev-preflight, phase-init). The CLI argparse will reject
# `phase-cycle-start STORY-658 ...` with exit 2 + usage error.
devforgeai-validate phase-cycle-start \
    --story-id=STORY-658 \
    --source=qa-recommendations \
    --trigger="/dev STORY-658 --fix" \
    --rec-ids="REC-STORY-658-C-001,REC-STORY-658-C-002,..." \
    --target-phase=02 \
    --project-root=. \
    --format=json
```

The CLI will:
- Append a new cycle entry to `state.cycles[]` with auto-assigned cycle number
- Reset `state.current_phase` to `--target-phase` (typically `"02"`)
- Reset `state.phases["02"]` to `{status: "pending", steps_completed: [], checkpoint_passed: false}`
- Preserve earlier-phase entries (history retained)

**VERIFY:**

```
exit_code := result of $NEXT_COMMAND
IF exit_code == 0:
    Parse JSON payload.
    $CURRENT_CYCLE = payload.cycle               # integer cycle number
    $CURRENT_PHASE = ${NEXT_PHASE_TO_ENTER}      # typically "02"
    $REMEDIATION_MODE = true
    $REMEDIATION_SOURCE = "qa-recommendations"   # or "remediation-cycle" for legacy gaps
    $OPEN_REC_IDS = (parsed from --rec-ids argument or qa_recommendations payload)
    Display: "✅ Cycle ${CURRENT_CYCLE} opened (source=${REMEDIATION_SOURCE})"
    Display: "   Resetting workflow to phase ${CURRENT_PHASE} for narrowed remediation scope."
ELIF exit_code == 1 AND error contains "still open":
    HALT — "A prior remediation cycle is still open. Use phase-cycle-close first."
    (This case should not occur if the dispatcher correctly evaluated cycle_open=False;
     surface as a defensive halt and ask the user to investigate state.)
ELIF exit_code == 1 AND error contains "Invalid":
    HALT — "Invalid argument to phase-cycle-start: {error}". Verify $NEXT_COMMAND.
ELIF exit_code == 2:
    HALT — "IO error opening cycle: {error}".
ELSE:
    HALT — "Unexpected exit code {exit_code}: {error}".
```

**RECORD:** `devforgeai-validate phase-record ${STORY_ID} --phase=01 --step=0` (the cycle-open record).

**Continuation:** Enter Phase Orchestration Loop at `$CURRENT_PHASE` (typically `"02"`). The remediation-mode branches in Phase 02 Step 1.5 (`phases/phase-02-test-first.md`) and downstream phases will pick up `$REMEDIATION_MODE`, `$OPEN_REC_IDS`, and `$REMEDIATION_SOURCE` from there.

**Cross-reference:** This action subsumes the work that Step 01.9.6 in `01.9-qa-failures.md` used to do for `/dev --fix` invocations on completed (phase 10) stories. Step 01.9.6 remains canonical for any code path that opens a cycle inside Phase 01 itself; the dispatcher invokes the same CLI with the same arguments, so the resulting state file is identical.

---

### `already-complete` (Cases 7, 8)

**Precondition:** State file exists, `current_phase == "10"`, no open items to remediate (or no remediation requested).

**EXECUTE:**

```
Display: "ℹ️  ${GUIDANCE}"
# Examples of guidance content:
#  - "Story STORY-658 is already complete at phase 10. Use /qa STORY-658 deep to re-validate, or /dev STORY-658 --fix to remediate."
#  - "Story STORY-658 is already at phase 10 with no open QA recommendations or coverage gaps. Nothing to remediate; --fix is a no-op."
HALT (clean exit — this is not an error)
```

**VERIFY:** N/A — terminal state.

**RECORD:** None.

**Continuation:** None. The skill exits.

---

### `halt-ambiguous` (Case 3)

**Precondition:** Mid-workflow (`current_phase < 10`) with `--fix` requested AND open items present. Two valid interpretations exist; cannot proceed without user input (per `critical-rules.md` Rule 10).

**EXECUTE:**

```
AskUserQuestion:
  Question: "Story ${STORY_ID} is mid-workflow at phase ${CURRENT_PHASE} with ${len(OPEN_REC_IDS)} open QA item(s). Two valid paths:"
  Header:   "Mid-workflow + --fix"
  Options:
    1. "Resume in-progress phase (Recommended)"
       Description: "Continue from phase ${CURRENT_PHASE} as a normal resume. Remediation items will be addressed inside the existing workflow."
    2. "Open new remediation cycle"
       Description: "Discard current phase progress and open a fresh cycle starting at phase 02 with narrowed scope from the QA recommendations."

Branch on user response:
- Option 1: $NEXT_ACTION = "phase-status-resume"; jump to that handler
- Option 2: Compute the equivalent $NEXT_COMMAND for phase-cycle-start with the current open_rec_ids;
            $NEXT_ACTION = "phase-cycle-start"; $NEXT_PHASE_TO_ENTER = "02"; jump to that handler
```

**VERIFY:** User responded explicitly. Do NOT default — HALT if user declines to choose.

**RECORD:** None — the chosen branch's handler will record.

**Continuation:** Whichever branch the user selected.

---

### `halt-corrupt-state` (Case 9)

**Precondition:** `dev-preflight` raised an exception while reading or parsing `phase-state.json`. `next_command` is null.

**EXECUTE:**

```
Display: "❌ ${GUIDANCE}"
# Example: "Phase-state file for STORY-658 could not be read or parsed: <error>.
#           Inspect devforgeai/workflows/STORY-658-phase-state.json manually;
#           do NOT auto-recover."
HALT (do NOT attempt repair; do NOT delete or rewrite the state file)
```

**VERIFY:** N/A — terminal state.

**RECORD:** None.

**Continuation:** None. User must inspect and repair the state file manually before retrying.

**Anti-pattern reminder:** Per `CLAUDE.md` HALT trigger 10, integrity / parse errors are findings, not puzzles. Do not construct environmental explanations (encoding, line endings, path normalization). Surface the error verbatim.

---

## Self-Bootstrap Fallback (when `$NEXT_ACTION` is unset)

If the skill is invoked without going through `/dev` (e.g., direct `Skill(spec-driven-dev)` invocation, `/resume-dev` route, or older `/dev` versions that pre-date STORY-658), the four context markers may be unset.

**Detection:** `$NEXT_ACTION` is empty / null / undefined.

**EXECUTE:**

```bash
result = devforgeai-validate dev-preflight ${STORY_ID} --project-root=. --format=json
exit_code = $?
```

**VERIFY:**

```
IF exit_code == 0 OR exit_code == 2:
    Parse JSON. Populate:
      $NEXT_ACTION         ← result.next_action
      $NEXT_COMMAND        ← result.next_command
      $NEXT_PHASE_TO_ENTER ← result.next_phase_to_enter
      $GUIDANCE            ← result.guidance
      $REMEDIATION_MODE    ← result.remediation_mode
      $REMEDIATION_SOURCE  ← result.remediation_source
      $OPEN_REC_IDS        ← result.open_rec_ids
      $QA_RECOMMENDATIONS_STATUS ← result.qa_recommendations
      $CURRENT_PHASE       ← result.current_phase
ELIF exit_code == 1:
    HALT — "Story not found: ${STORY_ID}". Cannot dispatch.
ELIF exit_code == 3:
    Parse JSON for $NEXT_ACTION (likely "halt-corrupt-state").
    Jump to that handler.
ELSE:
    HALT — "Unexpected dev-preflight exit code: ${exit_code}".
```

**RECORD:** None.

**Continuation:** Re-enter Step B (Dispatch) of SKILL.md with the populated markers.

---

## Backward Compatibility

If the CLI's `dev-preflight` is older than STORY-658 (does not emit `next_action` field), the dispatcher MUST fall back gracefully:

```
IF $NEXT_ACTION is unset AFTER self-bootstrap:
    # Older CLI; derive locally from existing fields
    IF result.phase_state == "created":
        $NEXT_ACTION = "begin-phase-01"
        $NEXT_PHASE_TO_ENTER = "01"
    ELIF result.phase_state == "exists":
        IF result.current_phase == "10" AND result.remediation_mode AND result.qa_recommendations.open_count > 0:
            $NEXT_ACTION = "phase-cycle-start"
            $NEXT_PHASE_TO_ENTER = "02"
            $NEXT_COMMAND = (manually constructed phase-cycle-start invocation; see Case 6)
        ELIF result.current_phase == "10":
            $NEXT_ACTION = "already-complete"
        ELSE:
            $NEXT_ACTION = "phase-status-resume"
            $NEXT_PHASE_TO_ENTER = result.current_phase
    $GUIDANCE = "(derived locally from legacy CLI; upgrade dev-preflight for canonical guidance)"
```

This duplication is a temporary cost of additive rollout. After every consumer is on the STORY-658 CLI, the fallback can be removed. Until then, treat divergence between Python `_compute_next_action()` and this fallback as a bug.

---

## Anti-Pattern: Re-Calling `phase-init`

After the dispatcher runs, do **NOT** invoke `devforgeai-validate phase-init ${STORY_ID}` separately. The legacy SKILL.md contract did this unconditionally; that call:

- Returns exit 0 (created) only when state did not exist before the call.
- Returns exit 1 (state exists) when the dispatcher already created or detected state.
- Returns exit 2 (invalid story ID) — already caught upstream by `dev-preflight`.

All useful behaviors of `phase-init` are now subsumed by `dev-preflight + dispatcher`. Re-calling it produces noise (exit 1) or duplicate work; in the worst case (older versions) it actively confused Claude into halting (the original STORY-658 failure).

**Enforcement:** Phase 01's `Entry Gate` in `phase-01-preflight.md` line 5 still shows the legacy `phase-init` for backward compatibility, but it is documented as a no-op when reached via the dispatcher (see SKILL.md Phase Orchestration Loop step 1).

---

## References

- **SKILL.md:** `src/claude/skills/spec-driven-dev/SKILL.md` § "Phase State Initialization & Entry Dispatcher"
- **CLI:** `src/claude/scripts/devforgeai_cli/commands/dev_preflight.py::_compute_next_action()` (canonical decision matrix)
- **Cycle CLI:** `src/claude/scripts/devforgeai_cli/commands/phase_commands.py::phase_cycle_start_command()` (lines 1559+)
- **Step 01.9.6:** `references/preflight/01.9-qa-failures.md` (mid-workflow remediation entry; complementary to this dispatcher)
- **Plan:** `/home/bryan/.claude/plans/1-plan-mode-refactored-ember.md` (STORY-658 architecture and rollout strategy)
- **Critical rules:** `.claude/rules/core/critical-rules.md` Rule 10 (Ask, don't assume) — applies to `halt-ambiguous`
- **HALT trigger 10:** `CLAUDE.md` (integrity findings are not puzzles) — applies to `halt-corrupt-state`
