Q&A Loop UI
Purpose / When to Activate
Activate:
- As Stage 4 of the
/gaai:bootstrappipeline, immediately aftersmart-question-generatorreturns aquestion_resultwithquestions.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
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", qa_duration_ms: 0}
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 — Compute time estimate (AC2)
Compute a best-effort estimate of time remaining before displaying the question:
if current_index >= 1:
# Use elapsed time over completed questions as running average
elapsed_ms = current_timestamp_ms() - session_start_ts
avg_per_q_ms = elapsed_ms / current_index
questions_left = total_questions - current_index # includes current question
estimated_remaining_ms = questions_left * avg_per_q_ms
estimated_remaining_s = Math.ceil(estimated_remaining_ms / 1000)
rounded_s = Math.max(5, Math.ceil(estimated_remaining_s / 5) * 5) # round up to nearest 5s
if rounded_s < 60:
time_label = " | ~{rounded_s}s remaining"
else:
time_label = " | ~<{Math.ceil(rounded_s / 60)} min remaining"
else:
time_label = "" # no estimate for the first question
Step 2b — Display question with progress header (AC1)
Display the progress header and active question in bold, then options if applicable:
Without options (open-ended):
**📋 Q&A Progress — Question {current_index + 1} of {total_questions}{time_label}**
**{question.question_text}**
With options:
**📋 Q&A Progress — Question {current_index + 1} of {total_questions}{time_label}**
**{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 2c — Record question start time
question_start_ts = current_timestamp_ms()
Step 2d — Wait for user input
Await one line of user input. Trim leading/trailing whitespace from the response.
Step 2e — 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
})
qa_duration_ms = current_timestamp_ms() - session_start_ts
return {
answers: answers,
partial: true, # not all questions were presented
skipped_all: true,
abort_reason: null,
qa_duration_ms: qa_duration_ms
}
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):
qa_duration_ms = current_timestamp_ms() - session_start_ts
skipped_count = answers.filter(a => a.skipped).length
answered_count = answers.filter(a => !a.skipped).length
log: "[qa-loop-ui] Q&A complete — {answers.length} answers recorded ({skipped_count} skipped, {answered_count} answered), duration={qa_duration_ms}ms"
return {
answers: answers,
partial: false,
skipped_all: false,
abort_reason: null,
qa_duration_ms: qa_duration_ms
}
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:
qa_duration_ms = current_timestamp_ms() - session_start_ts
return {
answers: answers, # partial — contains only answers collected before abort
partial: true,
skipped_all: false,
abort_reason: "user_abort",
qa_duration_ms: qa_duration_ms
}
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).
Orchestrator responsibility after this skill (AC3)
After receiving qa_session_result, the orchestrator MUST log the Q&A session duration per workspace:
gaai_admin(action: "log_qa_session", qa_duration_ms: qa_session_result.qa_duration_ms)
This applies on ALL return paths (normal completion, skip-all, abort, no-questions shortcut).
When qa_duration_ms == 0 (no-questions shortcut), the log is still emitted — it records that
Stage 4 was a no-op, which is a useful observability signal.
Output Schema
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)
qa_duration_ms: number # total milliseconds from session_start_ts to return; 0 for no-questions path
# 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
qa_duration_ms: 18500
# 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
qa_duration_ms: 9100
# 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"
qa_duration_ms: 7800
Quality Checks
partial: trueimpliesanswers.length < question_result.questions.lengthskipped_all: trueimpliespartial: trueabort_reason: nullwhenpartial: false(clean completion)- Every entry in
answershasquestion_idmatching atopicfrom the inputquestion_result.questions skipped: truealways paired withanswer_text: ""skipped: falsealways paired with non-emptyanswer_textresponse_time_msis always a non-negative integer (≥ 0)answers.length≤question_result.questions.length(never more answers than questions)qa_duration_msis always a non-negative integer (≥ 0); 0 only on no-questions shortcutqa_duration_ms≥ sum of allresponse_time_msentries (session duration includes display time)
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-userbybootstrap-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)
- Call
log_qa_sessionitself (orchestrator responsibility — see §Orchestrator responsibility above)