# Legwork

> Use when the user needs research that settles a decision - market validation, build-or-buy, competitor and pricing scans, "is there demand for X", "where should we publish this", "what does the incumbent actually do". Produces a cited findings memo or report where every claim states how well it is supported. Triggers on "legwork", "deep research", "research report", "compare X vs Y", "should we b…

- **Type:** Skill
- **Install:** `agentstack add skill-dbhq-uk-legwork-skill-legwork`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [dbhq-uk](https://agentstack.voostack.com/s/dbhq-uk)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [dbhq-uk](https://github.com/dbhq-uk)
- **Source:** https://github.com/dbhq-uk/legwork-skill/tree/main/skills/legwork
- **Website:** https://dbhq.uk

## Install

```sh
agentstack add skill-dbhq-uk-legwork-skill-legwork
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Legwork

## What this is for

Decision research. Not academic research. The question is always some version of
"what should we do about X", and the deliverable is judged on whether it is
usable, not on whether it is exhaustive.

That shapes everything below. A claim is judged by whether its source is the
right **kind** of thing for that claim, not by whether its domain is respectable.
Three sources agreeing only counts if they could have disagreed. And a run that
cannot answer the question says so rather than producing hedged length.

**Autonomy principle.** Infer what you need from context, pick a level, and
start. Announce the level in one line and proceed; do not ask the user to choose.
They can redirect mid-run, which costs far less than a blocking question on every
request. Stop only for a critical error or an incomprehensible query.

## Levels

Depth raises rigour. It never raises length.

| | quick | standard | deep |
|---|---|---|---|
| Frame | Decision plus 2-3 sub-questions | Plus named falsifiers | Plus second-order angles |
| Gather | SERP snippets; fetch only to pin a figure | Direct-fetch the top sources per finding | A primary source for every finding |
| Challenge | Independence grouping only | One disconfirming search per finding | Per-finding disconfirming pass plus an origin audit |
| Format | brief | brief or report | report |
| Gate level | `--level quick` | `--level standard` | `--level deep` |
| Rough time | 3-5 min | 8-12 min | 20-40 min |

Default is **standard**. Use the level the user names (`quick`, `standard`,
`deep`, or an equivalent like "quick scan" or "go deep"), else
`$LEGWORK_DEFAULT_MODE`, else standard. **"Deep research" on its own is an
invocation phrase for this skill, not a request for deep level** - fall through
to the default.

Escalate silently by one level if scoping reveals the question is materially
higher-stakes than the request implied, and say so in the same opening line.

Opening line, then straight into Phase 1:

> Running **standard** (~8-12 min). Say "deep" for primary sources and a disconfirming pass.

## Pipeline

Four phases. Full instructions in [methodology.md](./reference/methodology.md).

1. **Frame** - name the decision, the sub-questions that would settle it, and what evidence would change the answer.
2. **Gather** - retrieve against each sub-question, logging every fetch.
3. **Challenge** - hunt the disconfirming case; group sources by independence.
4. **Write** - assemble the deliverable, then gate it.

Phases 2 and 3 interleave per finding rather than running as strict gates.

## Before you start

**Anchor the date.** Run `date -u +%Y-%m-%d` and use that string for the run, in
the output folder name, and in every search query that could return dated
material. Never rely on your own sense of what year it is, and never let a
subagent work it out for itself.

**Check whether this run has already been done.** Read the index first. It is a
dispatcher: one row per past run, and the one-liner column exists so you can
decide whether to open a report without paying to open it.

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/index.py list --base "$OUTPUT_BASE"
```

Then pick one of four paths, and say which in your opening line:

| The index says | Do this |
|---|---|
| A run covers this and is not stale | Answer from it. Read that report, not the web. |
| A run covers this but is flagged stale | **Refresh** it |
| A run is close but answers a different question | New run, and cross-reference it |
| Nothing matches | New run |

**Answering from a prior run is a success, not a shortcut.** Open the report, read
the findings that bear on the question, and answer with their confidence bands
intact. Say plainly that it comes from a run of a given date. Re-running research
that was already done and still holds is the waste this index exists to prevent.

### Refreshing rather than re-running

A refresh **updates the existing report in place** - same folder, same file. Do
not create a second folder: two folders describing one question is how a reader
ends up acting on whichever they happened to open.

1. Resume the fetch log rather than starting a new one. Everything in it is
   already paid for.

   ```bash
   python3 ${CLAUDE_SKILL_DIR}/scripts/sources.py resume --tsv "$OUT/$BASE.tsv"
   ```

   That prints the angles already worked, the pages already fetched, and which of
   them still carry no quote. Work the uncovered angles and re-verify the claims
   that decide the answer; do not refetch what is already recorded and current.

2. Move every claim that is now wrong into `## Superseded` with the date and the
   reason. **Never delete a claim silently.** Someone may have acted on it, and a
   future run needs to know this ground has been covered. If the same answer has
   now been overturned twice, say so loudly - that is the strongest signal in the
   document that the question is unstable.

3. Append a `## Timeline` line saying what changed.

4. Update the index row.

### Filing the run

Every run ends in the index, including one that could not answer - "we looked and
found nothing" is exactly what a future session needs to not look again.

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/index.py add --base "$OUTPUT_BASE" \
  --folder "$BASE" --topic "Outlook triage for small practices" --level deep \
  --one-liner "No native triage below E5; the gap is real but narrow"
```

The one-liner says what the run **concluded**, not what it was about. "Notes on
the plugin gallery" is useless six months from now; "No submission route exists,
install is by URL or not at all" answers the question on its own. The gate warns
when a run is missing from an index that exists.

## Retrieval policy

**Built-in `WebSearch` and `WebFetch` first.** Free, no setup, no per-request
billing.

**Bright Data (`bd_search.py`) only when that fails.** It is the unblocker, never
the default retriever:

- `WebFetch` fails on a page you genuinely need (bot-blocked, paywalled, JS-heavy) -> `bd_search.py "" -m scrape --json`
- Reddit threads, where both `WebFetch` and the Unlocker zone are blocked -> `bd_search.py "" -m reddit --json` (billed per record; top threads only)
- Geo-specific or vertical SERP that `WebSearch` cannot express -> `--country XX`, `-m news`

No separate spend rule is needed per level. Deep attempts more primary sources,
more of those attempts get blocked, so paid usage rises on its own.

On exit code 2 (auth or quota), tell the user to run `brightdata login`. Do not
retry.

**Cap what you pull back.** Pass `--max-chars 8000` on scrape calls unless you
have a specific reason to need more; the wrapper's own default is 20000. A
research run reads dozens of pages and almost none of them need 20,000 characters
in context to yield the sentence you are after.

### Whose sources you may use

Research is what an outsider could establish. That boundary matters in both
directions.

**Never present the user's own records as a finding.** If the question is about
the user's own company, product or market position, do not reach into their
private accounts - their registrar, their billing, their inbox, their internal
files - and report back what you found there as though it were discovered. They
already know it. It is circular, it inflates the apparent evidence, and it
disguises how little an outsider can actually see. Search for the public
equivalent and report what an outsider would find, including nothing.

**Do use exclusive access the user has given you, on third parties.** A paid
subscription, a private dataset or an internal database the user has explicitly
offered is a genuine advantage when researching competitors, suppliers or a
market. Use it, log it as a source like any other, and note in Limitations that
the finding rests on access the reader may not have.

When an entity turns out to have no public footprint at all, that is the answer.
List what you checked, say that existence or scale could not be verified from
outside, and do not fill the gap from privileged access. It is the "could not
answer" shape applied to one entity rather than the whole question.

## The fetch log

Every retrieval, at every level, appends one row:

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/sources.py log \
  --tsv "$OUT/$BASE.tsv" \
  --url "https://acme.example/pricing" \
  --kind vendor_pricing \
  --angle "what does the incumbent charge" \
  --via webfetch \
  --date 2026-07-01 \
  --title "Pricing" \
  --quote "Team plan: 30 US dollars per user per month, billed annually." \
  --text-file /tmp/page.txt
```

`--angle` is the sub-question this retrieval was answering, and it is the most
important field in the file. Legwork's own fan-out inflates the number of sources
behind a finding, so a source count measures our effort rather than
corroboration. The angle is the layer we do not amplify, so corroboration is
counted there. **Record the angle honestly** - reusing one angle string across a
whole run silently destroys the check.

`--quote` is the sentence that made the source worth citing, verbatim, truncated
at 300 characters. **Record one for every source you intend to cite.** Around half
of all findings carry no figure at all, so without a quote those findings are
backed by nothing but proof that somebody opened the page - and the gate will say
so. It is also the only part of the evidence that survives the page changing or
going dead six months from now.

`--text-file` extracts the numeric tokens from a fetched page so the gate can
later confirm that a figure you quote actually appeared on a page you opened. No
page text beyond the quote is stored.

`--via` records how the page was reached: `websearch`, `webfetch`, `brightdata`,
`api` for a structured endpoint queried directly, `local` for evidence read from
disk, `mcp` for a connected tool. Log the transport you actually used. Re-fetching
a page through a different one to make it loggable distorts the trail rather than
recording it. Local evidence is cited with a `file://` locator and counts as a
single party however many files it spans.

Run `sources.py kinds` for the source kinds and which claims they suit.

## Subagents

Retrieval is the one phase worth parallelising. Brief them from
[subagent-brief.md](./reference/subagent-brief.md), which carries the template
verbatim and the reasons each line is in it.

- **A subagent has zero context.** It cannot see this skill, the conversation or
  the decision. Everything it needs goes in the brief, including today's literal
  date and how much effort the angle is worth.
- **Match the model to the shape of the angle.** Snippet gathering and pinning a
  known figure run fine on a cheap model - pass the override explicitly, never let
  one inherit the session model by accident. But **deep-level primary-source work,
  and anything that rebuilds an enumeration, stays on the orchestrator's model**:
  small models drop rows when a task means opening a dozen pages and keeping every
  figure exact, and they report success while doing it.
- **They return structured evidence, never prose.** One JSON object per source:
  `{url, kind, angle, date, title, quote}`, then a required `{gaps: [...]}` object
  saying what they searched for and did not find. An empty gaps list on a
  non-trivial angle means the negative case was never looked for.
- **Never paste a subagent's transcript into your synthesis.** Take its structured
  return, check the angle string came back unchanged, log each row with
  `sources.py log`, and work from the log.
- **The orchestrator keeps scoping, challenge and synthesis.** Those are
  judgement, and they stay on the main model.

One subagent per search angle is the natural split, and it keeps the angle
attribution honest because each agent only ever writes its own angle.

## Scripts

All stdlib-only. No virtualenv. Any `python3` >= 3.9.

| Script | Purpose |
|---|---|
| `sources.py kinds \| log \| score` | Source-kind vocabulary, the fetch log, fitness scoring per claim kind |
| `sources.py stale --claim-kind K` | Which logged sources have gone off, on that claim kind's half-life |
| `sources.py resume` | What a previous run already fetched, so a re-run skips it |
| `independence.py groups \| check` | Collapse sources into independent voices; count angle-aware corroboration |
| `independence.py portfolio` | Source concentration across the whole run, not within one finding |
| `index.py add \| list` | The research index - file a run, find a past one, spot stale ones |
| `matrix.py check --report PATH` | Completeness of a comparison matrix |
| `check.py --report PATH --level LEVEL` | The shippability gate |
| `finish.py --report PATH --level LEVEL` | Gate, staleness sweep and filing in one call |
| `bd_search.py "" -m MODE --json` | Bright Data retrieval fallback |

## Output

Resolve the output base once at the start of the run:

```bash
OUTPUT_BASE="${LEGWORK_OUTPUT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/docs/research}"
BASE="[Topic]_Research_$(date +%Y%m%d)"
OUT="$OUTPUT_BASE/$BASE"; mkdir -p "$OUT"
```

The folder and every file in it share one base name, so they group and sort
together, and `index.md` at the base is the dispatcher across all of them:

```
docs/research/
  index.md
  Outlook_Email_SaaS_Research_20260728/
    Outlook_Email_SaaS_Research_20260728.md
    Outlook_Email_SaaS_Research_20260728.tsv
```

The date in the folder name is when the run was **created**. A refresh keeps that
name and records the new date in `## Timeline` and in the index, so the folder
stays a stable address rather than multiplying.

Supporting documents keep their own descriptive names inside the folder.

**Markdown only.** No HTML, no PDF.

### The document

**brief** (quick, and standard when the question is small) - 800 to 2,500 words.
Template: [brief_template.md](./templates/brief_template.md).

**A comparison across three or more named options adds a matrix.** "Which of
these should we use" is a grid question, and prose alone loses the grid. Add a
`## Comparison matrix` section: one row per option, one column per field that
would decide it. The matrix carries the data, the findings still carry the
argument, and neither repeats the other.

One agent per option is the natural fan-out, filling the same field list. Every
cell must say something - a claim, or `[unknown]` - because a blank cell reads as
"no" when it means "we never found out", and that is the commonest way a
comparison misleads. A row that is entirely `[unknown]` still belongs in the
table: it records that the option was examined and came back empty, which is
exactly the option a reader would otherwise assume was overlooked.

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/matrix.py check --report "$OUT/$BASE.md"
```

Table cells are not sentences, so none of the gate's sentence-level checks can
see inside them. This is what stops a grid of confident-looking values citing
nothing from passing a gate that would reject the same claim written as prose.

**report** (deep, and standard when the question warrants it) - Executive
Summary, Introduction, Findings, Synthesis, Limitations, Recommendations,
Bibliography. No word target: stop when the question is answered.
Template: [report_template.md](./templates/report_template.md).

Two lines are mandatory in both formats:

**The receipt**, italic, directly under the H1, so the weight of the document is
visible before reading it:

> *deep · 6 angles · 14 primary sources (9 via Bright Data) · 7 disconfirming searches · 2 findings downgraded, 1 dropped below floor*

**A confidence line** as the first line of every finding:

> **Confidence: Strong** - the vendor's own pricing page, plus two independent user reports from separate searches.

Bands: **Strong** needs primary-tier evidence and corroboration of 2 or more.
**Moderate** needs corroboration of at least 1. **Weak** is commentary only, or
everything tracing to one origin. Anything below that does not ship as a find

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [dbhq-uk](https://github.com/dbhq-uk)
- **Source:** [dbhq-uk/legwork-skill](https://github.com/dbhq-uk/legwork-skill)
- **License:** MIT
- **Homepage:** https://dbhq.uk

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-dbhq-uk-legwork-skill-legwork
- Seller: https://agentstack.voostack.com/s/dbhq-uk
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
