---
name: qa-loop-ui
description: Presents up to 5 questions from smart-question-generator one at a time, records answers, handles per-question skip and skip-all, and captures per-question response time. Used as the interaction layer of Stage 4 of the /gaai:bootstrap pipeline.
license: ELv2
compatibility: Works in any interactive AI coding agent context (Claude Code, Cursor) where inline Q&A is supported
metadata:
  author: gaai-framework
  version: "1.0"
  category: cross
  track: cross-cutting
  id: SKILL-QA-LOOP-UI-001
  updated_at: 2026-04-29
  status: stable
inputs:
  - question_result       # {questions[], error, fallback} — output of smart-question-generator
outputs:
  - qa_session_result     # {answers[], partial: bool, skipped_all: bool, abort_reason: string|null}
---

# Q&A Loop UI

## Purpose / When to Activate

Activate:
- As Stage 4 of the `/gaai:bootstrap` pipeline, immediately after `smart-question-generator` returns a `question_result` with `questions.length > 0`
- When the bootstrap orchestrator needs to surface ambiguities as a conversational Q&A with the user
- Re-run is NOT expected: one call per bootstrap session (Q&A is one-shot)

When `question_result.questions` is empty (legitimate empty OR fallback), the orchestrator MUST skip this skill entirely and proceed to Stage 5 — do NOT activate with empty questions.

---

## Input Schema

```yaml
question_result:
  questions:                   # from smart-question-generator — already ≤5 items
    - question_text: string    # ≤200 chars
      topic: string            # used as question_id in answers output
      severity: number         # 1–10 — not shown to user; used for future sorting
      options:                 # optional — omitted for open-ended questions
        - label: string        # human-readable option label
          value: string        # machine value
  error: string | null
  fallback: bool
```

**Precondition check (before entering loop):**

```
if question_result.questions.length == 0:
  log: "[qa-loop-ui] no questions to ask — skip loop"
  return {answers: [], partial: false, skipped_all: false, abort_reason: "no_questions"}
```

---

## Process

### Step 1 — Initialize session

```
session_start_ts = current_timestamp_ms()
answers = []
current_index = 0
total_questions = question_result.questions.length
```

Display intro message to user (inline, before first question):

```
Bootstrap found {total_questions} point(s) needing clarification.
I'll ask up to {total_questions} question(s) — you can type "skip" to skip a question
or "skip all" to skip the entire Q&A.
```

### Step 2 — Question loop (sequential)

For each question at `current_index` in `question_result.questions`:

#### Step 2a — Display question

Format the question for inline presentation:

**Without options (open-ended):**

```
[{current_index + 1}/{total_questions}] {question.question_text}
```

**With options:**

```
[{current_index + 1}/{total_questions}] {question.question_text}

Options:
  {for i, opt in enumerate(question.options)}
  {i+1}. {opt.label}  ({opt.value})
  {endfor}

(Type your answer, a number to pick an option, "skip", or "skip all")
```

#### Step 2b — Record question start time

```
question_start_ts = current_timestamp_ms()
```

#### Step 2c — Wait for user input

Await one line of user input. Trim leading/trailing whitespace from the response.

#### Step 2d — Classify response

```
raw_input = trim(user_input)
normalized = lowercase(raw_input)

if normalized == "skip all" OR normalized == "s all":
  # User wants to exit the entire Q&A loop early
  log: "[qa-loop-ui] user requested skip-all at question {current_index + 1}/{total_questions}"
  response_time_ms = current_timestamp_ms() - question_start_ts
  # Record the current question as skipped before exiting
  answers.push({
    question_id: question.topic,
    answer_text: "",
    skipped: true,
    response_time_ms: response_time_ms
  })
  return {
    answers: answers,
    partial: true,          # not all questions were presented
    skipped_all: true,
    abort_reason: null
  }

elif normalized == "skip" OR normalized == "s" OR normalized == "":
  # User is skipping this specific question
  log: "[qa-loop-ui] question '{question.topic}' skipped by user"
  response_time_ms = current_timestamp_ms() - question_start_ts
  answers.push({
    question_id: question.topic,
    answer_text: "",
    skipped: true,
    response_time_ms: response_time_ms
  })
  current_index++
  continue  # advance to next question

else:
  # User provided an answer
  response_time_ms = current_timestamp_ms() - question_start_ts

  # For option-choice questions: normalize numeric input to value
  resolved_answer = raw_input
  if question.options is present AND raw_input is a digit string:
    option_index = parseInt(raw_input) - 1   # 1-based to 0-based
    if option_index >= 0 AND option_index < question.options.length:
      resolved_answer = question.options[option_index].value
      log: "[qa-loop-ui] option choice for '{question.topic}': input={raw_input} → value={resolved_answer}"
    else:
      # Out of range — treat raw input as free-text answer
      log: "[qa-loop-ui] option index out of range for '{question.topic}' (input={raw_input}) — using raw text"

  answers.push({
    question_id: question.topic,
    answer_text: resolved_answer,
    skipped: false,
    response_time_ms: response_time_ms
  })
  current_index++
  continue  # advance to next question
```

### Step 3 — Complete loop

When all questions have been presented and answered (or skipped individually):

```
log: "[qa-loop-ui] Q&A complete — {answers.length} answers recorded ({skipped_count} skipped, {answered_count} answered)"

return {
  answers: answers,
  partial: false,
  skipped_all: false,
  abort_reason: null
}
```

Where:
- `skipped_count = answers.filter(a => a.skipped).length`
- `answered_count = answers.filter(a => !a.skipped).length`

### Step 4 — Abort handling (AC5)

If the user terminates the session mid-loop (e.g., Ctrl+C, process signal, or the agent context
is interrupted before all questions are answered), the partially-collected `answers[]` array MUST
be preserved as-is. The orchestrator receives whatever answers were collected up to the interrupt.

Abort is signaled by catching the interrupt and returning:

```
return {
  answers: answers,         # partial — contains only answers collected before abort
  partial: true,
  skipped_all: false,
  abort_reason: "user_abort"
}
```

**Critical:** never discard partial answers on abort. The orchestrator can use partial Q&A answers
for a degraded-mode synthesis pass via `bootstrap-llm-synthesis` (the `qa_answers` input accepts
partial arrays).

---

## Output Schema

```yaml
qa_session_result:
  answers:
    - question_id: string         # equals topic from input question
      answer_text: string         # "" if skipped
      skipped: bool
      response_time_ms: number    # milliseconds from question display to input received
  partial: bool                   # true if loop ended before all questions presented
  skipped_all: bool               # true if user invoked "skip all"
  abort_reason: string | null     # "user_abort" | "no_questions" | null (null = normal completion)

# Normal completion example:
qa_session_result:
  answers:
    - question_id: "project_type"
      answer_text: "saas"
      skipped: false
      response_time_ms: 3200
    - question_id: "deployment_target"
      answer_text: ""
      skipped: true
      response_time_ms: 1100
  partial: false
  skipped_all: false
  abort_reason: null

# Skip-all example (triggered at question 2 of 3):
qa_session_result:
  answers:
    - question_id: "project_type"
      answer_text: "saas"
      skipped: false
      response_time_ms: 4500
    - question_id: "deployment_target"
      answer_text: ""
      skipped: true
      response_time_ms: 890
  partial: true
  skipped_all: true
  abort_reason: null

# Abort example (mid-loop):
qa_session_result:
  answers:
    - question_id: "project_type"
      answer_text: "library"
      skipped: false
      response_time_ms: 6200
  partial: true
  skipped_all: false
  abort_reason: "user_abort"
```

---

## Quality Checks

- `partial: true` implies `answers.length < question_result.questions.length`
- `skipped_all: true` implies `partial: true`
- `abort_reason: null` when `partial: false` (clean completion)
- Every entry in `answers` has `question_id` matching a `topic` from the input `question_result.questions`
- `skipped: true` always paired with `answer_text: ""`
- `skipped: false` always paired with non-empty `answer_text`
- `response_time_ms` is always a non-negative integer (≥ 0)
- `answers.length` ≤ `question_result.questions.length` (never more answers than questions)

---

## Non-Goals

This skill MUST NOT:
- Generate questions (that is `smart-question-generator`'s responsibility)
- Write to memory directly (memory ingest is the orchestrator's responsibility post-consent gate)
- Rank or re-order questions (that is `topic-importance-ranker` / E107bS04's concern)
- Validate answer content for semantic correctness (answers are user-stated, treated as `declared-by-user` by `bootstrap-llm-synthesis`)
- Handle the "skip all" abort path as a separate code path requiring external coordination (per AC3 / E107bS05 note: the skip-all handler is self-contained in this skill; E107bS05 provides the orchestrator-level abort-safe wrapper)
- Re-run the Q&A loop after completion (one-shot per bootstrap session)
- Apply timeouts to individual questions (patience is left to user; no auto-skip on silence)
- Call the LLM (this is a pure interaction skill; no inference calls)
