@rackbops/docket-types 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * What a `web` first pass, and its reviewer, may use beyond the defaults (0.11.0, Rod's "add
3
+ * firecrawl" after comparing a research answer with a run through the Firecrawl tools): the
4
+ * Firecrawl MCP server's search and scrape. Only the web first pass gets them: a run on gathered
5
+ * pages (a `sent` request or a second pass) keeps `DEFAULT_ALLOWED_TOOLS` and its denials as in
6
+ * 0.10.0. The runner (Rackbops/docket-runner) holds the ceiling of tools a Job may name: these
7
+ * Jobs need runner 0.0.6 or later, whose ceiling admits the two names and which drops them when
8
+ * it has no Firecrawl key (the run then falls back to WebFetch). A runner older than that fails
9
+ * these Jobs (0.0.5 throws `ToolCeilingError`), so the runner is released first.
10
+ *
11
+ * The permission rules cannot see a tool's parameters, so nothing in the tool lists stops a
12
+ * scrape with a disguising proxy or of an eBay URL: `WEB_DISALLOWED_TOOLS` denies eBay to WebFetch
13
+ * only. The prompt is the control there (`firecrawlLines`, `firecrawlChecks`): `proxy: "basic"`
14
+ * always, one page at a time, and no eBay URL through any tool. The reviewer's eBay handling and
15
+ * the belt (`withBarredSites`) read the answer's addresses, so they apply whichever tool fetched
16
+ * the page.
17
+ */
18
+ export declare const FIRECRAWL_TOOLS: readonly ["mcp__firecrawl__firecrawl_search", "mcp__firecrawl__firecrawl_scrape"];
19
+ /** A `web` first pass's allowed tools (0.11.0): WebSearch, WebFetch, then `FIRECRAWL_TOOLS`. */
20
+ export declare const WEB_ALLOWED_TOOLS: readonly string[];
21
+ /** The research run's rules for the Firecrawl tools (0.11.0); a web first pass only. */
22
+ export declare function firecrawlLines(): string[];
23
+ /** The reviewer's rules for the Firecrawl tools (0.11.0); a web first pass only. */
24
+ export declare function firecrawlChecks(): string[];
25
+ /**
26
+ * BoardGameGeek's category rankings (0.11.0): the eight its own subdomains define, each a
27
+ * `familyids[0]` value on its advanced search. The ids were observed on boardgamegeek.com on
28
+ * 2026-10-18, not documented by BGG: a run that finds one wrong falls back to the category's
29
+ * own ranking page.
30
+ */
31
+ export declare const BGG_CATEGORIES: readonly {
32
+ name: string;
33
+ familyId: number;
34
+ }[];
35
+ /**
36
+ * A BGG advanced search for one category's games of one year, best rated first, with at least
37
+ * 30 ratings and no expansions: the shape observed on 2026-10-18.
38
+ */
39
+ export declare function bggCategoryUrl(familyId: number, year: number): string;
40
+ /**
41
+ * The site's own categories (0.11.0, Rod's comparison with a run that used BGG's): where the
42
+ * question asks about categories and the best source defines its own, the answer uses that
43
+ * taxonomy and says so, rather than inventing genres or saying no source defines them. A category
44
+ * with no qualifying entry is answered "none yet", with why, never skipped. Every research run.
45
+ * A first pass's summary names every category's pick in one compact run (`COMPACT_RUN`;
46
+ * a second pass, `second`, names only the categories whose pick changed): `renderAnswer` fits the DM into
47
+ * `MAX_MESSAGE_CHARS` by dropping whole lines from the end, so a category answered only in a late
48
+ * finding can fall out of the DM; the summary comes first and is kept.
49
+ */
50
+ export declare function categoryLines(second?: boolean): string[];
51
+ /**
52
+ * The compact-run sentence (0.11.0): a first pass only. A second pass's summary says what
53
+ * changed and repeats nothing, so it names only the categories whose pick changed.
54
+ */
55
+ export declare const COMPACT_RUN: string;
56
+ /** The BGG search hint a web first pass may use (0.11.0), with the observed ids. */
57
+ export declare function bggSearchHint(): string[];
58
+ /**
59
+ * The reviewer's check of the site's own categories (0.11.0): an answer that invents categories
60
+ * where the best source defines them, or skips one with no qualifying entry, is revised.
61
+ */
62
+ export declare function categoryChecks(): string[];
63
+ //# sourceMappingURL=research-web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"research-web.d.ts","sourceRoot":"","sources":["../src/research-web.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,eAAe,YAC1B,kCAAkC,EAClC,kCAAkC,CAC1B,CAAA;AAEV,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAmD,CAAA;AAElG,wFAAwF;AACxF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAUzC;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAQ1C;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EASvE,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrE;AAQD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,UAAQ,GAAG,MAAM,EAAE,CActD;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,QAGwC,CAAA;AAEhE,oFAAoF;AACpF,wBAAgB,aAAa,IAAI,MAAM,EAAE,CASxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAWzC"}
@@ -0,0 +1,136 @@
1
+ import { DEFAULT_ALLOWED_TOOLS } from "@rackbops/docket-core";
2
+ /**
3
+ * What a `web` first pass, and its reviewer, may use beyond the defaults (0.11.0, Rod's "add
4
+ * firecrawl" after comparing a research answer with a run through the Firecrawl tools): the
5
+ * Firecrawl MCP server's search and scrape. Only the web first pass gets them: a run on gathered
6
+ * pages (a `sent` request or a second pass) keeps `DEFAULT_ALLOWED_TOOLS` and its denials as in
7
+ * 0.10.0. The runner (Rackbops/docket-runner) holds the ceiling of tools a Job may name: these
8
+ * Jobs need runner 0.0.6 or later, whose ceiling admits the two names and which drops them when
9
+ * it has no Firecrawl key (the run then falls back to WebFetch). A runner older than that fails
10
+ * these Jobs (0.0.5 throws `ToolCeilingError`), so the runner is released first.
11
+ *
12
+ * The permission rules cannot see a tool's parameters, so nothing in the tool lists stops a
13
+ * scrape with a disguising proxy or of an eBay URL: `WEB_DISALLOWED_TOOLS` denies eBay to WebFetch
14
+ * only. The prompt is the control there (`firecrawlLines`, `firecrawlChecks`): `proxy: "basic"`
15
+ * always, one page at a time, and no eBay URL through any tool. The reviewer's eBay handling and
16
+ * the belt (`withBarredSites`) read the answer's addresses, so they apply whichever tool fetched
17
+ * the page.
18
+ */
19
+ export const FIRECRAWL_TOOLS = [
20
+ "mcp__firecrawl__firecrawl_search",
21
+ "mcp__firecrawl__firecrawl_scrape",
22
+ ];
23
+ /** A `web` first pass's allowed tools (0.11.0): WebSearch, WebFetch, then `FIRECRAWL_TOOLS`. */
24
+ export const WEB_ALLOWED_TOOLS = [...DEFAULT_ALLOWED_TOOLS, ...FIRECRAWL_TOOLS];
25
+ /** The research run's rules for the Firecrawl tools (0.11.0); a web first pass only. */
26
+ export function firecrawlLines() {
27
+ return [
28
+ "Read pages with WebFetch first. When a page refuses WebFetch (a 403, a block, a CAPTCHA " +
29
+ "or challenge page), read it with `firecrawl_scrape` instead. Always pass " +
30
+ '`proxy: "basic"` to it; never "stealth", "enhanced" or "auto", and nothing else that ' +
31
+ "disguises the request. Scrape one page at a time: never several pages of the same site " +
32
+ "in parallel. Never pass any eBay URL to any tool, Firecrawl included. You may use " +
33
+ "`firecrawl_search` as you would web search. If Firecrawl is refused too, the page did " +
34
+ "not open: handle it as a page that refused you, as above.",
35
+ ];
36
+ }
37
+ /** The reviewer's rules for the Firecrawl tools (0.11.0); a web first pass only. */
38
+ export function firecrawlChecks() {
39
+ return [
40
+ "- To open a source, use WebFetch first; when a page refuses it (a 403, a block, a CAPTCHA), " +
41
+ 'read it with `firecrawl_scrape`, always with `proxy: "basic"` (never "stealth", ' +
42
+ '"enhanced" or "auto", nothing that disguises the request), one page at a time, never ' +
43
+ "several pages of one site in parallel. Never pass any eBay URL to any tool. You may use " +
44
+ "`firecrawl_search` as you would web search.",
45
+ ];
46
+ }
47
+ /**
48
+ * BoardGameGeek's category rankings (0.11.0): the eight its own subdomains define, each a
49
+ * `familyids[0]` value on its advanced search. The ids were observed on boardgamegeek.com on
50
+ * 2026-10-18, not documented by BGG: a run that finds one wrong falls back to the category's
51
+ * own ranking page.
52
+ */
53
+ export const BGG_CATEGORIES = [
54
+ { name: "Strategy", familyId: 5497 },
55
+ { name: "Family", familyId: 5499 },
56
+ { name: "Thematic", familyId: 5496 },
57
+ { name: "Party", familyId: 5498 },
58
+ { name: "Abstract", familyId: 4666 },
59
+ { name: "Customizable", familyId: 4667 },
60
+ { name: "Children's", familyId: 4665 },
61
+ { name: "Wargames", familyId: 4664 },
62
+ ];
63
+ /**
64
+ * A BGG advanced search for one category's games of one year, best rated first, with at least
65
+ * 30 ratings and no expansions: the shape observed on 2026-10-18.
66
+ */
67
+ export function bggCategoryUrl(familyId, year) {
68
+ return ("https://boardgamegeek.com/search/boardgame?sort=bggrating&advsearch=1" +
69
+ `&range[yearpublished][min]=${year}&range[yearpublished][max]=${year}` +
70
+ `&range[numvoters][min]=30&nosubtypes[0]=boardgameexpansion&familyids[0]=${familyId}`);
71
+ }
72
+ /** "Strategy", "Family", ... as one list in words. */
73
+ function categoryNames() {
74
+ const names = BGG_CATEGORIES.map((c) => c.name);
75
+ return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
76
+ }
77
+ /**
78
+ * The site's own categories (0.11.0, Rod's comparison with a run that used BGG's): where the
79
+ * question asks about categories and the best source defines its own, the answer uses that
80
+ * taxonomy and says so, rather than inventing genres or saying no source defines them. A category
81
+ * with no qualifying entry is answered "none yet", with why, never skipped. Every research run.
82
+ * A first pass's summary names every category's pick in one compact run (`COMPACT_RUN`;
83
+ * a second pass, `second`, names only the categories whose pick changed): `renderAnswer` fits the DM into
84
+ * `MAX_MESSAGE_CHARS` by dropping whole lines from the end, so a category answered only in a late
85
+ * finding can fall out of the DM; the summary comes first and is kept.
86
+ */
87
+ export function categoryLines(second = false) {
88
+ return [
89
+ 'When the question asks about categories (or "each of the major" kinds of something) and ' +
90
+ "the best source defines its own categories, use that site's categories and say so" +
91
+ (second ? "" : " in the summary") +
92
+ "; do not invent genres, and do not say that no source defines them. For " +
93
+ `board games these are BoardGameGeek's ${BGG_CATEGORIES.length} category rankings: ` +
94
+ `${categoryNames()}. A category with no entry that qualifies is answered "none yet", ` +
95
+ "with the reason; never leave it out." +
96
+ (second
97
+ ? // A second pass's summary says what changed (research.ts), so no compact run here.
98
+ " In this update, the summary names any category whose pick changed."
99
+ : ` ${COMPACT_RUN}`),
100
+ ];
101
+ }
102
+ /**
103
+ * The compact-run sentence (0.11.0): a first pass only. A second pass's summary says what
104
+ * changed and repeats nothing, so it names only the categories whose pick changed.
105
+ */
106
+ export const COMPACT_RUN = "The summary itself names every category's pick in one compact run, such as \"Strategy: X; " +
107
+ "Family: Y; ...; Children's: none yet (no 2026 game has 30 ratings)\", so none is lost when " +
108
+ "a long answer is cut; the findings carry each pick's source.";
109
+ /** The BGG search hint a web first pass may use (0.11.0), with the observed ids. */
110
+ export function bggSearchHint() {
111
+ const ids = BGG_CATEGORIES.map((c) => `${c.name} ${c.familyId}`).join(", ");
112
+ return [
113
+ "A hint you may use: a BoardGameGeek advanced search shows one category for one year, such " +
114
+ `as ${bggCategoryUrl(5497, 2026)} (Strategy, 2026). ` +
115
+ `Change the year and the \`familyids[0]\` value for the category: ${ids}. These ids were ` +
116
+ "seen on the site, not documented by it: if one shows the wrong category, use that " +
117
+ "category's own ranking page.",
118
+ ];
119
+ }
120
+ /**
121
+ * The reviewer's check of the site's own categories (0.11.0): an answer that invents categories
122
+ * where the best source defines them, or skips one with no qualifying entry, is revised.
123
+ */
124
+ export function categoryChecks() {
125
+ return [
126
+ "- Where the question asks about categories and the best source defines its own (for " +
127
+ `board games, BoardGameGeek's ${BGG_CATEGORIES.length}: ${categoryNames()}), the answer ` +
128
+ "uses them and says so. An answer that invents its own categories, or says no source " +
129
+ "defines them, while the source does, is `revise`d onto the source's categories. A " +
130
+ 'category with no qualifying entry is answered "none yet" with the reason, never left ' +
131
+ "out: `revise` an answer that skips one. The summary names every category's pick in " +
132
+ 'one compact run ("Strategy: X; Family: Y; ..."), since the findings may be cut from ' +
133
+ "the DM: `revise` a summary that leaves a category to the findings alone.",
134
+ ];
135
+ }
136
+ //# sourceMappingURL=research-web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"research-web.js","sourceRoot":"","sources":["../src/research-web.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,7 @@ import { type GatheredPage, type ReadSources } from "./research-sources.js";
25
25
  * and tells the owner so, and so does a host that cannot read them.
26
26
  *
27
27
  * **A second pass on pages the run could not open** (0.9.0): a research run lists in `blocked` the
28
- * pages it needed and could not open (BoardGameGeek refuses the runner; eBay is never opened). When
28
+ * pages it needed and could not open (a BoardGameGeek page that refused it; eBay, never opened). When
29
29
  * the reviewed answer passes, the research run's own draft lists any, the request is a `web` one,
30
30
  * and the host can read gathered pages (`readSources`), the owner gets the answer now with a short
31
31
  * "Couldn't open:" list. With at least `MIN_SOURCES_WAIT_MS` (an hour) before `sourcesExpiry` (14
@@ -39,6 +39,29 @@ import { type GatheredPage, type ReadSources } from "./research-sources.js";
39
39
  * `update` beside the first pass's. With no pages, past the deadline, or on a second-pass failure,
40
40
  * the task ends with one short line: the first answer stands, uncharged when no run was made.
41
41
  *
42
+ * **Every part of the question** (0.10.0): the research run splits the question into the parts it
43
+ * needs answered before it searches, a part another depends on first, and answers each or names it
44
+ * in `uncertain`; both reviewers never approve an answer that silently skips one (`partsLines`,
45
+ * `answerChecks`, `updateChecks`). Prompt only: the schemas and the DM are as they were.
46
+ *
47
+ * **The best source** (0.10.0): the research run names each part's reference site first
48
+ * (BoardGameGeek for board games, eBay for listings and prices). A web first pass and its reviewer
49
+ * open BoardGameGeek like any site; eBay no run opens (`WEB_DISALLOWED_TOOLS`; eBay's User
50
+ * Agreement, section 3 (automated access)). Where a fuller pass can follow (`canWait`: a web first
51
+ * pass with `readSources`), a best source not opened goes in `blocked`, the reviewer adds one the
52
+ * answer leans around, and the belt (`withBarredSites`) adds a barred site the answer or its draft
53
+ * names as unread; elsewhere `blocked` is never shown, so the runs name it in `uncertain`.
54
+ *
55
+ * **Firecrawl** (0.11.0): a web first pass and its reviewer may also use the Firecrawl MCP tools
56
+ * (`WEB_ALLOWED_TOOLS`, `research-web.ts`): WebFetch first, `firecrawl_scrape` with
57
+ * `proxy: "basic"` for a page that refuses it, one page at a time, never an eBay URL. The tool
58
+ * rules cannot see parameters, so the prompt is the control for the proxy and for eBay through
59
+ * Firecrawl. Runs on gathered pages keep their tools and denials.
60
+ *
61
+ * **The site's own categories** (0.11.0): a question about categories is answered in the best
62
+ * source's own taxonomy (BoardGameGeek's eight category rankings for board games), a category
63
+ * with no qualifying entry as "none yet"; the reviewer revises an answer that invents its own.
64
+ *
42
65
  * Failures (a usage limit never reaches `finish`: the dispatcher requeues the run):
43
66
  *
44
67
  * - `schema_miss`, a malformed answer, `timeout`, `error`: retried once in the same phase, at once.
@@ -129,12 +152,22 @@ export declare function draftJson(draft: ResearchAnswer): string;
129
152
  * second pass (0.9.0): the answer already sent goes in as escaped JSON between markers, and the
130
153
  * run answers with only what the pages add or correct.
131
154
  */
132
- export declare function researchJob(config: ResearchConfig, pages?: readonly GatheredPage[], earlier?: ResearchAnswer): JobSpec;
155
+ export declare function researchJob(config: ResearchConfig, pages?: readonly GatheredPage[], earlier?: ResearchAnswer, options?: ResearchJobOptions): JobSpec;
156
+ /**
157
+ * What a prompt builder needs to know beyond the request (0.10.0). `canWait`: a fuller pass can
158
+ * follow this answer -- a `web` first pass whose host can read gathered pages -- so a best source
159
+ * the run could not open goes in `blocked`, which then starts the wait. Anywhere else `blocked` is
160
+ * never shown, so the run names the missing source in one short `uncertain` line instead.
161
+ * Ignored on gathered pages and on a second pass, which never wait.
162
+ */
163
+ export interface ResearchJobOptions {
164
+ canWait?: boolean;
165
+ }
133
166
  /**
134
167
  * The reviewer run: the question, and the draft as escaped JSON between markers; with `pages`
135
168
  * (`sources: "sent"`), the same gathered pages the research run had, to check its claims against.
136
169
  */
137
- export declare function reviewJob(config: ResearchConfig, draft: ResearchAnswer, pages?: readonly GatheredPage[], earlier?: ResearchAnswer): JobSpec;
170
+ export declare function reviewJob(config: ResearchConfig, draft: ResearchAnswer, pages?: readonly GatheredPage[], earlier?: ResearchAnswer, options?: ResearchJobOptions): JobSpec;
138
171
  /**
139
172
  * The shortest wait for pages worth starting (0.9.0): with less than this before the expiry, a
140
173
  * person has no real chance to gather pages, so the answer goes out as it is.
@@ -1 +1 @@
1
- {"version":3,"file":"research.d.ts","sourceRoot":"","sources":["../src/research.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"research.d.ts","sourceRoot":"","sources":["../src/research.ts"],"names":[],"mappings":"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"}
package/dist/research.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_ALLOWED_TOOLS, DEFAULT_DISALLOWED_TOOLS, defineTaskType, } from "@rackbops/docket-core";
2
2
  import { ANSWER_SCHEMA, clean, fitLines, fitMessage, parseAnswer, parseBlocked, parseReview, REVIEW_SCHEMA, renderAnswer, renderUpdate, } from "./research-answer.js";
3
- import { gatheredPages, pagesBlock, SENT_DISALLOWED_TOOLS, sourcesExpiry, } from "./research-sources.js";
3
+ import { gatheredPages, pagesBlock, SENT_DISALLOWED_TOOLS, sourcesExpiry, uniqueBlocked, WEB_DISALLOWED_TOOLS, withBarredSites, } from "./research-sources.js";
4
+ import { bggSearchHint, categoryChecks, categoryLines, firecrawlChecks, firecrawlLines, WEB_ALLOWED_TOOLS, } from "./research-web.js";
4
5
  /** True when `config` asks for gathered pages. */
5
6
  export function usesSentSources(config) {
6
7
  return typeof config.sources === "string" && config.sources.trim().toLowerCase() === "sent";
@@ -121,9 +122,19 @@ function earlierBlock(earlier) {
121
122
  "EARLIER>>>",
122
123
  ];
123
124
  }
124
- /** The runs' tool denials: the defaults, and BGG and eBay for a request on gathered pages. */
125
+ /**
126
+ * The runs' tool denials: the defaults, and BGG and eBay for a run on gathered pages; eBay alone
127
+ * for a web first pass (0.10.0), which opens BoardGameGeek like any site.
128
+ */
125
129
  function disallowed(sent) {
126
- return [...DEFAULT_DISALLOWED_TOOLS, ...(sent ? SENT_DISALLOWED_TOOLS : [])];
130
+ return [...DEFAULT_DISALLOWED_TOOLS, ...(sent ? SENT_DISALLOWED_TOOLS : WEB_DISALLOWED_TOOLS)];
131
+ }
132
+ /**
133
+ * The runs' allowed tools (0.11.0): a web first pass adds the Firecrawl tools; a run on gathered
134
+ * pages, or a second pass, keeps the defaults.
135
+ */
136
+ function allowed(webFirst) {
137
+ return [...(webFirst ? WEB_ALLOWED_TOOLS : DEFAULT_ALLOWED_TOOLS)];
127
138
  }
128
139
  /**
129
140
  * The research run: the spike's prompt (docket-runner spike/cases.json, case `research`), with the
@@ -131,8 +142,10 @@ function disallowed(sent) {
131
142
  * second pass (0.9.0): the answer already sent goes in as escaped JSON between markers, and the
132
143
  * run answers with only what the pages add or correct.
133
144
  */
134
- export function researchJob(config, pages, earlier) {
145
+ export function researchJob(config, pages, earlier, options = {}) {
146
+ const canWait = options.canWait === true && pages === undefined && earlier === undefined;
135
147
  const sent = pages !== undefined;
148
+ const webFirst = !sent && earlier === undefined;
136
149
  const prompt = [
137
150
  "You are researching a one-off question for a person, who will read your answer as a " +
138
151
  "Discord DM.",
@@ -149,6 +162,12 @@ export function researchJob(config, pages, earlier) {
149
162
  "",
150
163
  ]
151
164
  : []),
165
+ ...partsLines(earlier !== undefined),
166
+ ...categoryLines(earlier !== undefined),
167
+ "",
168
+ ...bestSourceLines(sent, canWait),
169
+ ...(webFirst ? ["", ...firecrawlLines(), ...bggSearchHint()] : []),
170
+ "",
152
171
  "Use web search and fetch pages to check facts. Prefer primary sources (manufacturers, " +
153
172
  "government, utilities, standards bodies). Every claim that a reader might act on needs a " +
154
173
  "source you actually opened. Say plainly where sources disagree or where you are unsure.",
@@ -157,7 +176,9 @@ export function researchJob(config, pages, earlier) {
157
176
  "`summary` (the answer in at most 150 words), `findings` (each claim with the URLs you " +
158
177
  "opened for it) and `uncertain` (where sources disagree, or what you could not confirm).",
159
178
  "If a page you needed would not open (refused, a 403, a CAPTCHA, a sign-in wall), list it in " +
160
- "`blocked` with its site, its URL if you have one, and why.",
179
+ "`blocked` with its site, its URL if you have one, and why." +
180
+ // `blocked` is shown only where a fuller pass can follow (0.10.0).
181
+ (canWait ? "" : " Name it in `uncertain` too: `blocked` is not shown."),
161
182
  ...(sent ? ["A gathered page counts as opened: cite its `url`."] : []),
162
183
  ...(earlier
163
184
  ? [
@@ -173,19 +194,82 @@ export function researchJob(config, pages, earlier) {
173
194
  return {
174
195
  prompt,
175
196
  jsonSchema: ANSWER_SCHEMA,
176
- allowedTools: [...DEFAULT_ALLOWED_TOOLS],
197
+ allowedTools: allowed(webFirst),
177
198
  disallowedTools: disallowed(sent),
178
199
  maxTurns: RESEARCH_MAX_TURNS,
179
200
  maxBudgetUsd: RESEARCH_MAX_BUDGET_USD,
180
201
  timeoutMs: RESEARCH_TIMEOUT_MS,
181
202
  };
182
203
  }
204
+ /**
205
+ * The research run's split of the question into parts (0.10.0): a question can imply more than one
206
+ * thing to find out, and one part can depend on another ("the best in each major category" needs
207
+ * the categories first). The answer covers every part, or says in `uncertain` which it could not
208
+ * settle. Prompt only: the summary and findings carry each part's answer, so the schema is as it
209
+ * was. On a second pass, a part the earlier answer left unsettled is what the pages may fill.
210
+ */
211
+ function partsLines(second) {
212
+ return [
213
+ "Before you search, split the question into the parts it needs answered, including any " +
214
+ 'list the question takes for granted. A part can depend on another: for "the best game ' +
215
+ 'of the year in each major category", first establish what the major categories are, ' +
216
+ "then find the best in each. Settle the parts in that order, and answer every one: the " +
217
+ "summary answers each part, and the findings carry the claims behind each. A part you " +
218
+ "could not settle goes in `uncertain`, naming the part and why; never leave a part out " +
219
+ "silently.",
220
+ ...(second
221
+ ? [
222
+ "A part the earlier answer left unsettled (in its `uncertain`, or not at all) that the " +
223
+ "pages settle is a new finding.",
224
+ ]
225
+ : []),
226
+ ];
227
+ }
228
+ /**
229
+ * The research run's best source (0.10.0, Rod's feedback on "I didn't check BoardGameGeek"): each
230
+ * part has a source its field treats as the reference, and the answer rests on it or says that
231
+ * the run could not open it: in `blocked` when a fuller pass can follow (`canWait`), so the
232
+ * person's browser can gather it, otherwise in one short `uncertain` line. A first pass on the
233
+ * web tries BoardGameGeek like any site (Rod, 2026-10-16); eBay no run opens. A run on gathered
234
+ * pages (`pages`: a `sent` request or a second pass) opens neither (`SENT_DISALLOWED_TOOLS`), and
235
+ * a gathered page counts as opened.
236
+ */
237
+ function bestSourceLines(pages, canWait) {
238
+ return [
239
+ "For each part, decide first which source is the best one for it: the site people in that " +
240
+ "field treat as the reference (for board games, BoardGameGeek; for listings and sold " +
241
+ "prices, eBay; otherwise the maker, the standards body or the official record). The " +
242
+ "answer should rest on it.",
243
+ (pages
244
+ ? "BoardGameGeek and eBay are not opened from this run: do not try. When the best source " +
245
+ "is one of them and no gathered page is from it, or a page refused you (a 403, a " +
246
+ "CAPTCHA or challenge page, an empty or blocked body, a sign-in wall)"
247
+ : "Open BoardGameGeek like any other site: an ordinary read, with nothing that disguises " +
248
+ "the request; a page that refuses WebFetch gets one more try through " +
249
+ "`firecrawl_scrape`, as below (0.11.0). eBay is not opened from this run: do not try it. When the best source is " +
250
+ "eBay, or its page refused you (BoardGameGeek or any other: a 403, a CAPTCHA or " +
251
+ "challenge page, an empty or blocked body, a sign-in wall)") +
252
+ (canWait
253
+ ? ", list it in `blocked` with its site, its URL only when that is the page's real " +
254
+ "address (such as BoardGameGeek's ranking or browse page), never a guessed one, and " +
255
+ "why; then answer from the best sources you could open. Never write in `summary` or " +
256
+ '`uncertain` that you "didn\'t check" it: `blocked` is where that goes, and the ' +
257
+ "person's own browser can gather it for a fuller pass."
258
+ : ", answer from the best sources you could open and name the missing source in one " +
259
+ "short `uncertain` line, such as " +
260
+ (pages
261
+ ? '"BoardGameGeek was not among the pages read"'
262
+ : '"BoardGameGeek refused the request, so its ranking is not used"') +
263
+ ": no fuller pass follows this answer, so that line is how the person learns of it."),
264
+ ];
265
+ }
183
266
  /**
184
267
  * The reviewer run: the question, and the draft as escaped JSON between markers; with `pages`
185
268
  * (`sources: "sent"`), the same gathered pages the research run had, to check its claims against.
186
269
  */
187
- export function reviewJob(config, draft, pages, earlier) {
270
+ export function reviewJob(config, draft, pages, earlier, options = {}) {
188
271
  const sent = pages !== undefined;
272
+ const canWait = options.canWait === true && !sent && earlier === undefined;
189
273
  const prompt = [
190
274
  earlier
191
275
  ? "You are reviewing an update to a research answer before it is sent to a person as a " +
@@ -215,26 +299,36 @@ export function reviewJob(config, draft, pages, earlier) {
215
299
  "gathered pages cannot be checked: move it to `uncertain`.",
216
300
  ]
217
301
  : []),
218
- ...(earlier ? updateChecks() : answerChecks()),
302
+ ...(earlier ? updateChecks() : [...answerChecks(), ...categoryChecks()]),
303
+ ...(earlier || sent ? [] : [...bestSourceChecks(canWait), ...firecrawlChecks()]),
219
304
  "",
220
- "Then give a verdict: `approve` if the draft stands as is (return it unchanged in " +
221
- "`answer`); `revise` if it stands once corrected (return the corrected answer in " +
222
- "`answer`); `reject` if editing cannot fix it. List each problem you found in " +
305
+ "Then give a verdict: `approve` if the draft stands as is and covers every part (return it " +
306
+ "unchanged in `answer`); `revise` if it stands once corrected (return the corrected " +
307
+ "answer in `answer`); `reject` if editing cannot fix it. List each problem you found in " +
223
308
  "`problems`, one line each.",
224
309
  ].join("\n");
225
310
  return {
226
311
  prompt,
227
312
  jsonSchema: REVIEW_SCHEMA,
228
- allowedTools: [...DEFAULT_ALLOWED_TOOLS],
313
+ allowedTools: allowed(!sent && earlier === undefined),
229
314
  disallowedTools: disallowed(sent),
230
315
  maxTurns: REVIEW_MAX_TURNS,
231
316
  maxBudgetUsd: REVIEW_MAX_BUDGET_USD,
232
317
  timeoutMs: REVIEW_TIMEOUT_MS,
233
318
  };
234
319
  }
235
- /** The reviewer's checks of a full answer: the spike's, unchanged. */
320
+ /**
321
+ * The reviewer's checks of a full answer: the spike's, after the parts check (0.10.0), which
322
+ * comes first so the rest is checked against what the question asked.
323
+ */
236
324
  function answerChecks() {
237
325
  return [
326
+ "- First work out the parts the question implies, including any list it takes for granted: " +
327
+ '"the best in each of the major categories" means naming the major categories, then ' +
328
+ "answering for each one. The answer covers every part, in the summary or the findings, " +
329
+ "or names it in `uncertain` with why it could not be settled. An answer that silently " +
330
+ "skips a part is never approved: `revise` it, filling the part from the sources (or " +
331
+ "naming it in `uncertain` when they cannot settle it), or `reject` it.",
238
332
  "- Open the sources behind every claim a reader might act on, and confirm each one says what " +
239
333
  "the claim says. A claim its sources do not support is corrected, moved to `uncertain`, or " +
240
334
  "removed.",
@@ -243,13 +337,51 @@ function answerChecks() {
243
337
  ];
244
338
  }
245
339
  /**
246
- * The reviewer's checks of a second pass (0.9.0): the draft is an update to the earlier answer,
247
- * so it should hold only new findings and corrections.
340
+ * The reviewer's best-source checks of a first pass on the web (0.10.0), which may open
341
+ * BoardGameGeek like the research run. An answer that leans on secondary sources while the best
342
+ * one is neither cited nor said to be missing is revised: with `canWait`, to list it in `blocked`,
343
+ * so the person's browser can gather it; otherwise to name it in one short `uncertain` line, since
344
+ * `blocked` is never shown there. A claim resting only on an eBay address moves to `uncertain`,
345
+ * whichever tool listed or fetched it (no run may open eBay; 0.11.0 names Firecrawl too). Not asked on
346
+ * gathered pages (their own checks cover BGG and eBay) nor of an update.
347
+ */
348
+ function bestSourceChecks(canWait) {
349
+ return [
350
+ "- Name the best source for each part: the site its field treats as the reference (for " +
351
+ "board games, BoardGameGeek; for listings and sold prices, eBay). If the answer leans on " +
352
+ "secondary sources while that source is neither among its findings' sources nor " +
353
+ (canWait
354
+ ? "in `blocked`, the verdict is `revise`: add it to `blocked` (its site, its URL only " +
355
+ "when that is the page's real address, such as BoardGameGeek's ranking or browse " +
356
+ "page, never a guessed one, and why) and keep the answer from the sources it has."
357
+ : "named in `uncertain`, the verdict is `revise`: add one short `uncertain` line naming " +
358
+ "it, and keep the answer from the sources it has."),
359
+ "- You may open BoardGameGeek like any site. eBay is never opened by these runs, through " +
360
+ "any tool (WebFetch, Firecrawl or another): a claim resting only on an eBay address, " +
361
+ "whichever tool listed or fetched it, is not one an answer may rest on, so move it to " +
362
+ "`uncertain`.",
363
+ ...(canWait
364
+ ? [
365
+ "- A page that refused is not read, so a claim must not say a source was checked " +
366
+ "unless a finding cites it. A line saying a source was not checked (\"I didn't " +
367
+ 'check BoardGameGeek") is no answer: move it to `blocked` on a `revise`.',
368
+ ]
369
+ : []),
370
+ ];
371
+ }
372
+ /**
373
+ * The reviewer's checks of a second pass (0.9.0; the parts check 0.10.0): the draft is an update
374
+ * to the earlier answer, so it should hold only new findings and corrections.
248
375
  */
249
376
  function updateChecks() {
250
377
  return [
251
378
  "- The draft is an update to the earlier answer: it should hold only new findings and " +
252
379
  "corrections to the earlier answer.",
380
+ "- Work out the parts the question implies, as for a full answer, including any list it " +
381
+ "takes for granted. Where the earlier answer left a part unsettled (in its `uncertain`, " +
382
+ "or not at all) and the pages settle it, the update fills it. An update that silently " +
383
+ "skips such a part is never approved: `revise` it, filling the part from the pages, or " +
384
+ "`reject` it.",
253
385
  "- Each new claim is supported by the source it cites. A claim its source does not support " +
254
386
  "is corrected, moved to `uncertain`, or removed.",
255
387
  '- A claim starting "Correction:" really corrects a claim the earlier answer made, and its ' +
@@ -354,8 +486,7 @@ function afterReview(ctx, state, output, canWait) {
354
486
  ...(f.sources[0] ? { source: f.sources[0] } : {}),
355
487
  tags: ["research", review.verdict],
356
488
  }));
357
- // The research run's own list: a reviewer cannot start a wait, nor stop one, by what it returns.
358
- const blocked = canWait ? (state.draft?.blocked ?? []) : [];
489
+ const blocked = canWait ? blockedAfterReview(state, review, answer) : [];
359
490
  if (blocked.length > 0) {
360
491
  const until = sourcesExpiry(ctx.now, deadlineOf(ctx.config)).at;
361
492
  if (until.getTime() - ctx.now.getTime() >= MIN_SOURCES_WAIT_MS) {
@@ -394,6 +525,18 @@ function afterReview(ctx, state, output, canWait) {
394
525
  summary: `${verdict}: ${n} finding(s)`,
395
526
  };
396
527
  }
528
+ /**
529
+ * The pages the first pass waits for (0.10.0): the research run's own list, then any the reviewer
530
+ * added on a revise (a reviewer cannot stop a wait, and on approve, where the stored draft is what
531
+ * goes out, it cannot start one either), one entry per page (`uniqueBlocked`), then the belt's
532
+ * (`withBarredSites`), which also reads the draft, so a reviewer that rewords "I didn't check
533
+ * BoardGameGeek" away, or moves an eBay-only claim to `uncertain`, still leaves the wait.
534
+ */
535
+ function blockedAfterReview(state, review, answer) {
536
+ const own = state.draft?.blocked ?? [];
537
+ const added = review.verdict === "revise" ? (review.answer?.blocked ?? []) : [];
538
+ return withBarredSites(answer, uniqueBlocked([...own, ...added]), state.draft);
539
+ }
397
540
  /**
398
541
  * The shortest wait for pages worth starting (0.9.0): with less than this before the expiry, a
399
542
  * person has no real chance to gather pages, so the answer goes out as it is.
@@ -583,9 +726,12 @@ export function researchType(options = {}) {
583
726
  if (state.phase === "awaiting-pages")
584
727
  return morePages(ctx, state);
585
728
  const sent = usesSentSources(ctx.config);
729
+ // A web first pass whose host can read gathered pages: a fuller pass can follow it.
730
+ const canWait = readSources !== undefined && !sent;
586
731
  if (state.phase === "review" && state.draft) {
587
- if (!sent && state.pass !== 2)
588
- return reviewJob(ctx.config, state.draft);
732
+ if (!sent && state.pass !== 2) {
733
+ return reviewJob(ctx.config, state.draft, undefined, undefined, { canWait });
734
+ }
589
735
  const got = await pagesFor(ctx, state);
590
736
  if ("outcome" in got)
591
737
  return got;
@@ -596,7 +742,7 @@ export function researchType(options = {}) {
596
742
  if (deadline && ctx.now > deadline)
597
743
  return { outcome: missed(ctx, deadline) };
598
744
  if (!sent)
599
- return researchJob(ctx.config);
745
+ return researchJob(ctx.config, undefined, undefined, { canWait });
600
746
  const got = await pagesFor(ctx, state);
601
747
  return "outcome" in got ? got : researchJob(ctx.config, got.pages);
602
748
  },