AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Legwork

skill-dbhq-uk-legwork-skill-legwork · by dbhq-uk

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…

No reviews yet
0 installs
21 views
0.0% view→install

Install

$ agentstack add skill-dbhq-uk-legwork-skill-legwork

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-dbhq-uk-legwork-skill-legwork)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
20d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Legwork? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

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.

  1. 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.

  1. Append a ## Timeline line saying what changed.
  1. 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.

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:

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:

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: [brieftemplate.md](./templates/brieftemplate.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.

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: [reporttemplate.md](./templates/reporttemplate.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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.