# Feature: Research Before Asking

<!-- toc -->
- [When it runs](#when-it-runs)
- [The runner flow](#the-runner-flow)
- [Round ceiling](#round-ceiling)
- [Re-entry after proceed](#re-entry-after-proceed)
- [Asking](#asking)
- [Tracker context in the descriptor](#tracker-context-in-the-descriptor)
- [State](#state)
- [Limits](#limits)
- [Reference](#reference)
<!-- /toc -->

**Pattern**: an unattended run that stops on a maturity blocker or on an open
analysis question waits for a person, and the person often answers with
something that was already in the ticket thread, a linked page or the code.
Research reads those first. The research session gathers and proposes;
`research-gate.mjs` checks every cited source and closes only what is backed; the
run's own check - the maturity score, the open-questions gate - re-runs on the
result and decides. The engine is `research/engine.md`.

## When it runs

| Run | What happens |
|---|---|
| Attended, not autopilot | Nothing automatic. `/multi-agent:research <id>` is a command a person runs; it asks one gap at a time. `research-gate.mjs` in gate mode is a no-op (`smoke-gates-interactive-noop.sh`) |
| Gates active (`MULTI_AGENT_UNATTENDED=1` or `state.autopilot`) | `research-gate.mjs` records its verdict and parks or re-opens the run |
| The autopilot runner | A parked run is routed to research before it is left awaiting an answer (below) |

The split is `lib/unattended.mjs` `gatesActive`, as for every other quality
gate (`features/unattended-gates.md`).

## The runner flow

The continuous-mode runner, step 5c, while the item is still claimed:

1. The dev run (`/multi-agent <id> autopilot`) ends parked. `researchRoute`
   routes two parks: `waitingFor: "maturity"` (or a Phase 0 halt that still
   carries blockers), and `waitingFor: "question"` with
   `pendingQuestion.stepId: "phase-1/open-questions"`. The channels menu, a
   user test and a failed verification are a person's call and are never
   researched.
2. A research session: `/multi-agent:research <id> --autonomous --state <path>`,
   launched with `--disallowedTools` naming `research_ask`, `research_search`
   and `WebSearch` (`RESEARCH_DENIED_TOOLS`). It writes
   `<state dir>/research/{context.json,research.json,research.md}`.
3. The runner, not the session, runs `research-gate.mjs --state <path> --json`
   with the unattended environment.
4. `decision: "proceed"`: the runner launches `/multi-agent:resume <taskId>
   autopilot` and supervises it. If that run parks again on the other kind of
   gap, the loop goes round once more while the ceiling allows.
5. Anything else - `ask`, no verdict, a launch that failed, a session that did
   not end - leaves the run's state as it is. A parked state is recorded as
   `awaiting-answer`, exactly as it would have been without research.

The attempt row carries `researchRounds: <n>` for the rounds this tick used.

Each research and resume launch is prepared like the first one: the runner
checks that `agent-guard.sh` is registered on all three PreToolUse matchers (missing:
no session is launched and the run stays parked), clears any file left under
the new session's name in `pr-requests/`, and passes the unattended environment
(`features/unattended-security.md`, "The runner's launch"). A resumed run that
reaches the Phase 4 hand-off is published by the runner exactly like a first
run: the request is looked up under the resumed session's id.

## Round ceiling

`maxAskRounds` in `~/.claude/autopilot/config.json` (default 2) caps research
rounds per item. The count is summed from `researchRounds` on every
`attempted.jsonl` row of that item - the whole log, not the recent tail the
breaker reads - and not from the run state, because a later run of the same
item is a fresh state file and would start at zero. At the ceiling
the runner logs `maxAskRounds` and parks the run without researching it.

## Re-entry after proceed

| Kind | What the gate wrote | Where resume re-enters |
|---|---|---|
| `maturity` | `state.maturity` re-scored, `state.research.decision: proceed`, `waitingFor: "maturity"` | the maturity step. It re-fetches the item and runs `research-gate.mjs --state "$STATE_FILE" --recheck --descriptor <fresh.json> --json`: the recorded verified findings are applied to the fresh descriptor and re-scored. Exit 0: continue, with the printed `description` as the working description. Exit 1: the normal blocker path (`features/maturity-followup.md`) |
| `open-questions` | closed rows removed from `analysis.openQuestions[]`, the `open-questions` gate re-recorded as pass, `waitingFor` and `pendingQuestion` cleared, `status: in_progress` | Phase 2, as `currentPhase + 1`: the gate is the last step of Phase 1, so the plan is already made and research's `pass` is the gate's latest entry. The answers are in `state.research.resolved[]` and research.md |

Recheck re-verifies on fresh data: a comment deleted since the research no
longer backs its gap.

## Asking

`decision: "ask"` parks the run with `waitingFor: "question"`:

| Kind | `pendingQuestion` | Options |
|---|---|---|
| `maturity` | `id: maturity`, `stepId: phase-0/maturity`, the blockers still reported, then the unverified candidates | `fix`, `continue`, `abort` (`schemas/run-questions.json` `maturity`) |
| `open-questions` | `id: open-questions`, `stepId: phase-1/open-questions`, only the rows still open, then the candidates | `ask-now`, `assume`, `abort`, as `open-questions-gate.mjs` |

The candidates are shown so the person answering starts from what research
found. They were not verified, or they are acceptance criteria or business rules,
which research never decides.

## Tracker context in the descriptor

`lib/issue-fetcher.sh` carries `comments[]` (Jira comments or GitHub issue
comments, the newest 20, at most 1500 characters each and 12000 bytes together),
`commentsTotal`, `links[]` (Jira issue links) and `untrustedFields`, all on the
request that fetches the item. None of it feeds the maturity score.
`issue-fetcher.sh --rescore` re-scores a descriptor on stdin with the same
formula; it is how the gate re-runs the check.

## State

```jsonc
"research": {
  "round": 1,
  "kind": "maturity",               // or "open-questions"
  "decision": "proceed",            // or "ask"
  "reason": "...",
  "dir": ".../research", "json": ".../research.json",
  "context": ".../context.json", "md": ".../research.md",
  "gaps": [{ "id": "description_empty", "category": "description",
             "status": "closed", "sources": [{ "label": "evidence", "ref": "...", "quote": "..." }] }],
  "closed": ["description_empty"], "candidates": [], "open": [], "ignored": [],
  "resolved": [],                   // open-questions: answered rows with their sources
  "maturityBefore": { }, "maturityAfter": { },
  "at": "<ISO time>"
}
```

Written by `research-gate.mjs` only, and declared in `agent-state.schema.json`.

## Limits

- A fetched document in `context.json` `documents[]` is written by the research
  session. The gate proves the quote is in the text it was given, not that the
  text came from the fetcher. The item, its comments and its links come from
  the descriptor the fetcher printed.
- The research session's own token cost is not priced into the attempt's `usd`:
  the run's tracker records phases, and research is not one.
- In an attended or terminal-autopilot run the gate is invoked by the agent,
  and is advisory against an agent that skips it, as every in-run gate is. The
  runner's own invocation is what holds.

## Reference

Scripts: `research-gate.mjs`, the continuous-mode runner (`researchRoute`,
`researchRoundsUsed`, `researchArgv`, `resumeArgv`; its own reference is
`features/autopilot-circuit-breaker.md`), `lib/issue-fetcher.sh`.
Schemas: `research-output.schema.json`, `agent-state.schema.json` `research`,
`prefs.schema.json` `projects.<slug>.research.providers`,
`autopilot-config.schema.json` `maxAskRounds`. Tests:
`test/research-gate.test.mjs`, `test/issue-fetcher-context.test.mjs`,
the runner's tests; smoke `smoke-gates-interactive-noop.sh`.
