# Ticket 1.6 — Question / Ask-User Bridge

**Phase**: 1 — OpenCode Bridge (Core)
**Priority**: High
**Depends on**: 1.2, 1.4

## Description

Handle the question flow, which is SEPARATE from permissions. When the agent asks the user a question (e.g., "Which library should we use?", "Do you want to proceed?"), the relay must present the question with its options in the browser and return the user's answer to OpenCode.

In claude-relay, this is handled through the `canUseTool` callback when toolName is "AskUserQuestion". In OpenCode, questions have their own dedicated SSE events and REST endpoints.

### OpenCode Question Model

- **SSE event**: `question.asked` with properties `{ id, sessionID, questions[], tool? }`
- **Each question**: `{ question, header, options[], multiple?, custom? }` — note: `multiple` (not `multiSelect`), `custom` (boolean, default true, controls whether free-text "Other" is available)
- **Reply endpoint**: `POST /question/:requestID/reply` with `{ answers: Array<Array<string>> }` — each answer is an array of selected labels
- **Reject endpoint**: `POST /question/:requestID/reject` — skips the question
- **List pending**: `GET /question/` returns all pending questions

## Acceptance Criteria

### AC1: Receives question event and forwards to browsers

```gherkin
Given the SSE consumer receives a "question.asked" event with properties:
  { id, sessionID, questions: [{ question, header, options: [{ label, description }], multiple?, custom? }], tool? }
When the bridge processes it
Then it sends to all WebSocket clients:
  {
    type: "ask_user",
    toolId: "<properties.id>",
    questions: mapped from properties.questions with:
      - question → question
      - header → header
      - options → options
      - multiple → multiSelect (field name mapping)
      - custom → custom (whether free-text "Other" is available)
  }
And it stores the question in a pending questions map
```

### AC2: Receives user answer and forwards to OpenCode

```gherkin
Given a pending question with toolId "q123"
When a browser sends { type: "ask_user_response", toolId: "q123", answers: { "0": "Option A" } }
Then the bridge converts the answers object to OpenCode's format: Array<Array<string>>
  e.g., { "0": "Option A" } → [["Option A"]]
  e.g., { "0": "Opt A, Opt B" } for multi-select → [["Opt A", "Opt B"]]
And it POSTs to /question/q123/reply with { answers: [["Option A"]] }
And it removes the question from the pending map
And it broadcasts { type: "ask_user_resolved", toolId: "q123" } to all clients
```

### AC3: Supports multi-select questions

```gherkin
Given a question with multiSelect: true and 4 options
When the user selects options 1 and 3
Then the bridge sends both selected options in the answer
And OpenCode receives the multi-select response
```

### AC4: Supports free-text "Other" responses

```gherkin
Given a question with predefined options
When the user selects "Other" and types custom text
Then the bridge sends the custom text as the answer
And OpenCode receives the free-text response
```

### AC5: Browser reconnection replays pending questions

```gherkin
Given there is a pending question
When a new browser connects via WebSocket
Then it receives the pending ask_user message immediately
```

### AC6: Question timeout

```gherkin
Given a pending question
When no response is received within 10 minutes
Then the bridge logs a warning
And it keeps the question pending (does NOT auto-answer)
And it sends a reminder push notification
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `onQuestion(event)` | Unit | Creates pending entry, broadcasts to clients |
| `onAnswer(toolId, answers)` | Unit | Calls REST client, removes pending, broadcasts resolved |
| `getPendingQuestions()` | Unit | Returns all pending questions |
| `handleMultiSelect(toolId, selections)` | Unit | Sends multiple selections correctly |
| `handleFreeText(toolId, text)` | Unit | Sends custom text correctly |
| End-to-end flow | Fixture | SSE question → browser answer → REST reply |
| Reconnection replay | Fixture | Connect → receive pending question → answer |

## API Shape

```ts
// src/lib/question-bridge.ts
export class QuestionBridge {
  constructor(options = {})  // { restClient, pushService }

  onQuestion(event)                          // Called by SSE consumer
  onAnswer(toolId, answers)                  // Called by WebSocket handler
  getPending()                               // Returns Map of pending questions

  // Events emitted:
  // 'forward_to_clients' — { type: "ask_user", ... }
  // 'resolved'           — { type: "ask_user_resolved", ... }
}
```

## Reference

- Claude-relay ask-user handling: `../claude-relay/lib/sdk-bridge.js` (AskUserQuestion in canUseTool)
- Claude-relay ask-user response: `../claude-relay/lib/project.js` (ask_user_response handler)
- OpenCode question routes: `../opencode/packages/opencode/src/server/routes/question.ts`
- OpenCode question module: `../opencode/packages/opencode/src/question/index.ts`
- SSE events: `question.asked`, `question.replied`, `question.rejected`
- Reply endpoint: `POST /question/:requestID/reply` with `{ answers: Array<Array<string>> }`
- Reject endpoint: `POST /question/:requestID/reject`
- List pending: `GET /question/`
- Field mapping: OpenCode `multiple` → frontend `multiSelect`
- Answer format mapping: frontend `{ [idx]: "label" }` → OpenCode `Array<Array<string>>`
