---
description: Deep-research engine; routes a question to light (inline) / normal (workflow) / deep and runs the research, writing a persisted session.
when_to_use: >-
  Use when the operator asks a research question, needs a briefing on a topic,
  wants a fact-checked comparison, or says "research X", "compare X and Y",
  "what do we know about X", "look into X", "go dig into X", "find out about X".
  This is the PA's DEFAULT for any research work; reach for it before ad-hoc web
  searching, so every research question leaves a session trail.
allowed-tools: Bash, Read, WebSearch, WebFetch, Grep, Glob, Write, Workflow
argument-hint: <question> [--mode light|normal|deep] [--gate operatorPresent|background|worker] [--intent "..."] [--output shortAnswer|article|structured]
context: inline
---

# /research; the deep-research engine

You are running the `deep-research` engine. One engine, three modes. This skill is
its conversational face: it picks a mode, creates a persisted session via the
`research` CLI, and either **executes light mode inline** (here, now), **dispatches
the `research-normal.js` Workflow** for normal mode, or **dispatches the
`research-deep.js` Workflow** for deep mode (identical dispatch shape to normal,
gated by a mandatory draft-plan confirm; see the deep section below).

**The CLI is the state layer, not the engine.** `research run` creates the session
shell and prints the plan; it does NOT search, fetch, verify, or synthesise. In
light mode, *you* do that work by following the recipe in this file. Never expect
the CLI to gather evidence for you.

`$ARGUMENTS` holds the invocation. The first non-flag text is the **question**;
`--mode`, `--gate`, `--intent`, `--output` are optional flags with the exact enum
spellings shown in the argument hint.

Set once at the top of every run. The research CLI ships as part of
`@stravica-ai/digital-operator-pa`; invoke via `pnpm exec`. Ask the tool for the
absolute paths the Workflow dispatch needs instead of hard-coding them:

```bash
RUN="pnpm exec research"
# Resolve sessions/briefs/workflows/schema dirs from the installed package.
eval "$($RUN paths --json | node -e '
  let s=""; process.stdin.on("data",c=>s+=c).on("end",()=>{
    const p=JSON.parse(s);
    for (const [k,v] of Object.entries(p)) console.log(`${k.toUpperCase()}=${JSON.stringify(v)}`);
  })')"
# SESSIONSDIR, BRIEFSDIR, WORKFLOWSDIR, SCHEMADIR, CLI are now shell vars.
```

Every session dir lives at `$SESSIONSDIR/<sessionId>`; the workflow scripts
and briefs live under `$WORKFLOWSDIR` and `$BRIEFSDIR` respectively (inside
`node_modules/@stravica-ai/digital-operator-pa/dist/…`). Never hard-code the install
path; it is a moving target across version bumps.

---

## Step 1; Decide the mode

Explicit `--mode` always wins. If the caller did not pass one, apply the
tiering heuristic and **state the choice out loud** when you report:

- **Simple fact-find**; single-hop, one authoritative source likely (a spec
  detail, a definition, a version number, "does X support Y") → **light**.
- **Direct comparison / brief** that decomposes into 2–4 independent
  sub-questions ("compare X and Y", "X vs Y for our use") → **normal**.
- **Open-ended / multi-facet**; needs a literature-style sweep across many angles
  ("everything about X", "survey the landscape of Y") → **deep**.

- **Mixed fact-plus-analysis question**; facts to establish plus an allocation,
  a trade-off or a recommendation to reason out from them → **research first, then
  a writer pass**: route the fact part as one research run (light or normal by the
  rules above), then hand its `answer.md` to a writer dispatch for the analysis.
  Never one research run for both: analysis claims have no source to quote, so they
  trip the unsourced guard (the 2026-10-19 normal run aborted on exactly this mix).

Guardrail: the deep tier is for *decomposable breadth*. A context-interdependent
task ("review this codebase's architecture") stays **normal**-shaped; fewer,
bigger workers; not deep.

Say it plainly, e.g. `Mode: light (fact-find).` before you spend anything.

---

## Step 2; Clarify intent (before spending)

Detect the execution posture from observable state; do not guess the operator's
mood:

- **Operator-present** (interactive): `$CLAUDE_CODE_ENTRYPOINT` is a normal
  interactive entrypoint (e.g. `cli`) and/or a TTY is attached, and the caller did
  not pass `--background`.
- **Background / scheduled**: `$CLAUDE_CODE_ENTRYPOINT` is `headless`/`cron`, no
  TTY, or the caller passed `--background`.

  ```bash
  echo "entrypoint=${CLAUDE_CODE_ENTRYPOINT:-unset} tty=$([ -t 0 ] && echo yes || echo no)"
  ```

Behaviour:

- **Operator-present + ambiguous question** (scope, region, timeframe, or which
  "X" is unclear): ask **2–3 clarifying questions**, OR state a one-line intent
  draft and ask the operator to confirm, **before** creating the session. Pass the
  clarified intent through as `--intent "..."`. This applies to **all modes**.
- **Deep mode; mechanical consent gate (never skip):** deep **never auto-routes**;
  it runs only on an explicit `--mode deep`. A heuristic that *recommends* deep (Step 1)
  MUST stop and ask; it may not self-dispatch. A **background/scheduled run may never
  self-select deep**; it narrows to normal and records the assumption in the manifest via
  `--intent`. When deep is explicitly requested, present a **draft-plan confirm before
  dispatch**: show the operator the 4–12 facets and a cost estimate
  (`~N facets → ~M agents, est. X–Y M tokens`), and dispatch only on explicit agreement.
  Consent is **per-run**, not per-session; agreeing to one deep run is not standing
  permission.
- **Background / scheduled**: **never block.** Narrow scope conservatively, decide
  the most useful interpretation yourself, and pass `--intent "..."` reflecting the
  narrowing so the assumption lands in the manifest. Then continue.

If the question is already unambiguous, skip straight to Step 3; do not manufacture
questions.

---

## Gate policy; three postures

`--gate` records how this run handles a **human gate**; a fetch blocked by a login
wall, paywall, Cloudflare challenge, or captcha. It rides on the manifest and every
searcher/worker reads it. Pick it alongside intent (Step 2), from the observable posture:

- **`operatorPresent`**; the operator is live and expects to clear gated routes. On a
  gate, do **not** drop the source: escalate to the operator in plain language
  ("Cloudflare check on <url>; can you clear it?"), let them clear it in the **shared
  headed Playwright MCP browser**, then continue the fetch. The headed browser is shared
  parent state with exactly one consumer (you); so `operatorPresent` is for **light**
  runs (you fetch inline) or a single supervised worker, **not** normal-mode workflows
  (their searcher subagents can't attach to your browser). Mechanics + the queue-sweep
  flow: `$WORKFLOWSDIR/../docs/operator-present-flow.md` (shipped with the tool).
- **`background`**; scheduled/headless, or the operator isn't watching. On a gate:
  **drop the source and log it**. In normal mode the `research-normal.js` workflow
  persists each searcher's `gatedDropped[]` to `gates.json` (via `research gates
  --append`); in light mode you caveat-and-drop (recipe step 5). A later
  `operatorPresent` session sweeps the queue with `research gates --list` / `--retry`
  (see the flow doc). This is the safe default for any non-interactive run.
- **`worker`**; the manifest default for dispatched agents. A worker drops-and-logs
  like `background` and **never** self-upgrades to headed browsing. Full worker-posture
  **park → escalate → resume** automation (and a `research resume` verb) is **v2;
  deferred** (not in the deep-mode release); do not attempt it here. For v1;
  including deep mode; a worker's gates queue exactly like background's.

If unsure and the operator is present, `operatorPresent` for light fact-finds that may
hit a paywall; otherwise `background`. Pass the choice through as `--gate <policy>` at `run`.

---

## Step 3; Route by mode

| Mode | Action | Cost (estimate; measured) |
|---|---|---|
| `light` | Execute the **Light-mode recipe** below, inline. | Estimate: none in this doc; runs inline, no workflow agents. Measured: none recorded. |
| `normal` | Create the session, then **dispatch the `research-normal.js` Workflow** (below). | Estimate: none in this doc. Measured: 6.9M tokens, 104 agents on a five-sub-question brief (2026-10-19), aborted at 34 percent unsourced. |
| `deep` | Confirm the **draft plan** with the operator (Step 2), then **dispatch the `research-deep.js` Workflow**; same shape as normal (below). | Estimate: ~250 agents / 5–8M tokens (est.). Measured: none recorded. |

### Normal; dispatch the workflow

Normal mode is a full pipeline (plan → 2–4 searchers → extract → 3-vote verify →
1 deepen round → synthesize), and it runs as a harness **Workflow script**, not
inline. You create the session, hand its id to the workflow, and report the result.

1. **Create the session** (the workflow reads posture + intent straight off the
   manifest, so pass the clarified `--intent` and the right `--gate`):

   ```bash
   OUT=$($RUN run --mode normal --question "<question>" --gate <policy> \
           ${INTENT:+--intent "$INTENT"}) || { echo "research run failed:"; echo "$OUT"; exit 1; }
   SID=$(printf '%s\n' "$OUT" | sed -n 's/^created //p')
   echo "Mode: normal. Session: $SID. Dispatching research-normal.js…"
   ```

2. **Dispatch the workflow** via the `Workflow` tool. `$WORKFLOWSDIR`, `$SESSIONSDIR`,
   `$BRIEFSDIR`, `$SCHEMADIR`, `$CLI` all come from the `research paths --json` call
   at the top of the run:

   ```
   Workflow({
     scriptPath: "<WORKFLOWSDIR>/research-normal.js",
     args: {
       sessionId:   "<SID>",
       sessionsDir: "<SESSIONSDIR>",
       briefsDir:   "<BRIEFSDIR>",
       schemaDir:   "<SCHEMADIR>",
       cli:         "<CLI>"
     }
   })
   ```

   Substitute the real `$WORKFLOWSDIR` / `$SESSIONSDIR` / `$BRIEFSDIR` / `$SCHEMADIR`
   / `$CLI` / `$SID` values you captured. The workflow runs its own agents
   (searchers/extractors/verifiers/synthesizers), merges every searcher source into
   `sources.json` and every gated drop into `gates.json` straight after Search,
   writes `findings/sq-N.json` + `answer.md` into the session, round-trips them
   through `research show`, and calls **`research mark done <id>`** itself at the
   end; do **not** mark it done by hand. On an abort it calls `research mark aborted
   <id>` itself instead.

3. **When the workflow returns**, it hands back a summary object
   (`status`, `runStats`, `answerPath`, per-SQ `findings` receipts). Verify and report:

   ```bash
   $RUN show "$SID"                       # expect status: done, findings per sub-question
   cat "$SESSIONSDIR/$SID/answer.md"
   ```

   Report inline: the session answer (from `answer.md`, rendered per `outputMode`),
   `Mode: normal. Session: <id>.`, the confidence spread, and the **refuted-claims
   transparency** section. If the workflow returns `status: "aborted"` (an unsourced
   guardrail), surface that plainly with its `reason`; do not present an aborted run
   as a finished answer. An early abort (before Verify) leaves the source registry
   and gate queue but no answer; a late abort (after synthesis) also leaves a partial
   `answer.md` whose unsourced assertions are tagged `[UNSOURCED]`; report it as
   partial, never as done.

Do not attempt partial normal-mode work by hand; the workflow owns the pipeline.

### Deep; confirm the draft plan, then dispatch the workflow

Deep mode reuses the **same dispatch shape** as normal; a full pipeline (4–12 facet
DAG plan → search/extract → central+supporting verify with a mixed panel → up to 3
deepen rounds → synthesize → judge → finalise), run as a harness **Workflow script**.
Two things differ from normal: the script name (`research-deep.js`, by the
`research-<mode>.js` convention) and a **mandatory draft-plan confirm** before dispatch
(the consent gate; Step 2).

Deep is expensive (~250 agents / 5–8M tokens, est.). **Never dispatch deep without an
explicit `--mode deep` and an explicit operator go-ahead on the draft plan** (Step 2's
mechanical consent gate).

1. **Create the session** (the workflow reads posture + intent straight off the
   manifest, exactly as normal; pass the clarified `--intent` and the right `--gate`):

   ```bash
   OUT=$($RUN run --mode deep --question "<question>" --gate <policy> \
           ${INTENT:+--intent "$INTENT"}) || { echo "research run failed:"; echo "$OUT"; exit 1; }
   SID=$(printf '%s\n' "$OUT" | sed -n 's/^created //p')
   echo "Mode: deep. Session: $SID."
   ```

2. **Confirm the draft plan** (consent gate, per-run). Surface the planned facets and the
   cost estimate to the operator; `~N facets → ~M agents, est. X–Y M tokens`; and get
   explicit agreement before spending. If the operator declines, stop; do not dispatch.
   (Background/scheduled runs never reach here; they narrowed to normal in Step 2.)

3. **Dispatch the workflow** via the `Workflow` tool. The args contract is **identical to
   normal's**; the router passes only `{sessionId, sessionsDir, briefsDir, schemaDir, cli}`;
   `question`/`intent`/`gatePolicy`/`outputMode` are read off the `manifest.json` by the
   workflow, never passed as args. The one difference from normal is `scriptPath`:

   ```
   Workflow({
     scriptPath: "<WORKFLOWSDIR>/research-deep.js",
     args: {
       sessionId:   "<SID>",
       sessionsDir: "<SESSIONSDIR>",
       briefsDir:   "<BRIEFSDIR>",
       schemaDir:   "<SCHEMADIR>",
       cli:         "<CLI>"
     }
   })
   ```

   Substitute the real `$WORKFLOWSDIR` / `$SESSIONSDIR` / `$BRIEFSDIR` / `$SCHEMADIR`
   / `$CLI` / `$SID` values you captured. Like normal, the workflow runs its own agents,
   writes `findings/sq-N.json` + `sources.json` + `answer.md` (plus deep's `rounds.json` /
   `judge.json` / `cost.json`) into the session, round-trips them through `research show`,
   and calls **`research mark done <id>`** itself; do **not** mark it done by hand.

4. **When the workflow returns**, verify and report exactly as normal
   (`research show "$SID"`, read `answer.md`), plus deep's advisory `judge.json` score and
   `cost.json` footer. Report `Mode: deep. Session: <id>.`, the confidence spread, the
   **refuted-claims transparency** section, and the confirmed-synthesis results. If it
   returns `status: "aborted"`, surface that plainly with its `reason`; do not present an
   aborted run as a finished answer.

---

## Light-mode recipe (execute inline)

Light mode = main-thread, **3–10 targeted tool calls**, no decomposition, no
verify. One aggregated finding covers the whole question. Follow every step; the
round-trip validation in Step 6 is the acceptance gate; do not skip it.

### 1. Create the session

Choose `--gate` (default `worker`; `operatorPresent` only if the operator is live
and expects to clear gated routes) and `--output` (default `shortAnswer`). All enum
values are **camelCase**; `operatorPresent`, not `operator-present`; `shortAnswer`,
not `short-answer`.

```bash
OUT=$($RUN run --mode light --question "<question>" --gate worker \
        --output shortAnswer ${INTENT:+--intent "$INTENT"}) || {
  echo "research run failed:"; echo "$OUT"; exit 1; }
SID=$(printf '%s\n' "$OUT" | sed -n 's/^created //p')
SESS="$SESSIONSDIR/$SID"
echo "Mode: light (fact-find). Session: $SID."
```

A non-zero exit from the CLI is a **hard failure**: report what it printed on
stderr and stop; do not fabricate a session id.

### 2. Gather evidence (3–10 tool calls)

Use `WebSearch` to locate the authoritative source, `WebFetch` to read it, `Bash`
for local reads, and `pa knowledge search "<query>"` for the operator's KB. Prefer
**primary/official** sources (the spec, the vendor docs, the source repo); avoid
SEO content farms. As you go, assign each source a **stable id** (`src-1`, `src-2`,
…) and note its `kind` (`web` for WebSearch/WebFetch, `kb` for `pa knowledge`,
`codebase` for local reads) and the exact URL. Collect **direct quotes** that
support the answer; you will need `text` + `sourceId` for each.

Stay within 3–10 calls. If you can't answer in that budget, the question was
probably normal-shaped; say so and re-route rather than grinding.

### 3. Write the aggregated finding

Write `<sess>/findings/sq-1.json`. Light mode has no decomposition, so
`subQuestionId` is `"sq-1"` and this one finding is the whole answer. Required
fields, no extras (validator: `src/store/findings.mjs`):

- `subQuestionId`: `"sq-1"`
- `summary`: the shortAnswer, 1–3 sentences.
- `quotes`: array of `{ "text": "...", "sourceId": "src-N", "importance": "central|supporting|incidental" }`. `importance` is optional but include it.
- `sources`: array of the source ids this finding draws on, e.g. `["src-1","src-2"]`.
- `confidence`: `"low" | "medium" | "high"`; your honest read of source quality/agreement.
- `verificationVotes`: **`[]`**; light mode never verifies.
- `unsourced`: `true` only if any claim in the summary has no supporting quote; otherwise `false`.

Write it with the `Write` tool (cleaner than a heredoc for quote-heavy JSON). The
tool's schema is the enforcement layer; Step 6 round-trips it. Example shape:

```json
{
  "subQuestionId": "sq-1",
  "summary": "…1–3 sentence answer…",
  "quotes": [
    { "text": "…exact quote…", "sourceId": "src-1", "importance": "central" }
  ],
  "sources": ["src-1"],
  "confidence": "high",
  "verificationVotes": [],
  "unsourced": false
}
```

### 4. Write the source registry

Write `<sess>/sources.json`. One `items[]` entry per source id you used; ids must
be unique (validator: `src/store/sources.mjs`). Each item needs `id` + `kind`
(enum: `web | playwright | kb | codebase | prior-session`); `url`, `title`,
`fetchedAt` (ISO 8601 UTC), `checksum` are optional but include `url`/`title`.

```json
{
  "items": [
    { "id": "src-1", "kind": "web", "url": "https://…", "title": "…", "fetchedAt": "2026-07-07T15:00:00Z" }
  ]
}
```

Every `sourceId` in a quote and every id in `finding.sources[]` MUST exist in
`sources.items[]`; keep them in sync.

### 5. (Gate handling)

If a `WebFetch` in Step 2 is blocked by a login wall / Cloudflare / captcha, branch on
the run's `--gate` posture (see "Gate policy; three postures" above):

**`operatorPresent`** (the operator is live); **escalate, do not drop.** Ask the operator
to clear the gate in the shared headed Playwright MCP browser ("Cloudflare check on <url>
 can you clear it?"), then re-fetch and cite the source normally. Light mode is the
*right* place for this: you fetch inline on the parent's browser, so a live clearance works
(unlike a normal-mode workflow, whose searchers can't attach). See
`$WORKFLOWSDIR/../docs/operator-present-flow.md` for the mechanics.

**`background` / `worker`** (default, non-interactive); **drop-with-caveat**, silently.
Do NOT escalate. Instead:

- Drop the unverifiable claim from the finding.
- Record the source anyway with `"kind": "web"` and note its unreliability; tag it
  in the source `title` (e.g. `"… (gated; not fetched)"`).
- Caveat the shortAnswer ("could not access <site>; answer based on remaining
  sources").

Full worker-posture park/escalate/resume automation is v2; light mode does not queue
gate records itself (that's the normal-mode workflow's job, via `research gates
--append`).

### 6. Round-trip validate (acceptance gate)

Confirm both files parse and pass the schema by reading the session back as JSON;
`show` validates every `findings/*.json` and exits **non-zero** on a bad file:

```bash
$RUN show "$SID" --json > /dev/null && echo "round-trip OK" || {
  echo "round-trip FAILED; finding or sources rejected by schema"; \
  $RUN show "$SID" --json; }
```

If it fails (exit 2), the JSON you wrote is malformed or violates the schema:
re-read the validator error, fix `findings/sq-1.json` / `sources.json`, and re-run
`show`. Do **not** return an answer while validation is red. If the schema and a
coherent answer genuinely cannot agree, **stop and escalate** (see below); do not
paper over it.

### 7. Mark the session done

Once the round-trip in Step 6 is green, finalise the session with the `mark done`
verb. Never hand-edit `manifest.json`; `mark done` is the tool's own chokepoint for
the status transition:

```bash
$RUN mark done "$SID" || echo "mark done failed (already done/aborted, or missing); see message"
$RUN show "$SID" --json | grep -o '"status": *"[^"]*"' | head -1   # expect "done"
```

`mark done` exits 0 on success, or 2 if the session is already `done`/`aborted`
(re-running is a harmless refusal). A cleanly-written, round-trip-valid, `done`
session is the correct light-mode end-state.

### 8. Return the shortAnswer inline

Return to the caller:

- The **shortAnswer** (the `summary`), stated directly.
- `Mode: light (fact-find). Session: <id>.`
- Confidence, and any caveats (gated sources, thin evidence).
- A one-line source list (title + url).

The session on disk is the durable trail; the inline shortAnswer is the answer.

---

## Escalation

Stop and report a partial result (do not silently work around) when:

- The `research` CLI exits non-zero on `run`; report its stderr; the session
  wasn't created.
- Round-trip validation (Step 6) stays red after a fix attempt; the schema is
  rejecting a coherent answer; name the conflict, recommend a follow-up item, stop.
- A light question can't be answered in 3–10 calls; it was normal-shaped; say so
  and re-route rather than grinding.

Do not wait on background notifications; none exist. `sleep N` if you must wait on
something external.
