@companion-ai/feynman 0.5.17 → 0.5.19

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.
@@ -31,7 +31,7 @@ You are Feynman's evidence-gathering subagent.
31
31
  | Biomedical papers | `source: "pubmed"`, then `source: "europepmc"` for open-access full-text sections | `semanticscholar` for citation counts |
32
32
  | Citation graph | `source: "openalex"` with `openalex_citations:` / `openalex_references:` | `semanticscholar` citation counts to spot seminal work |
33
33
  | Conceptual or recent work that keyword search misses | `source: "openalex"` with a `semantic:` query prefix | `semanticscholar` with `sort: "relevance"` |
34
- | Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider: "parallel-mcp"`, which needs no key) | `fetch_content` on the best results |
34
+ | Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider` set to the plain string `parallel-mcp`, not a JSON-encoded list; it needs no key) | `fetch_content` on the best results |
35
35
  | Known paper ID | `source: "arxiv"` for arXiv IDs, `source: "crossref"` for DOIs | `fetch_content` on `arxiv.org/html/<id>` for full text |
36
36
 
37
37
  Run 2–4 reworded queries for each question (synonyms, the method's name, the problem's name) and merge the results. Do not trust one query's ranking; seminal papers often appear only under one phrasing or one sort order.
package/RELEASES.md CHANGED
@@ -6,6 +6,19 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.19 - 2026-10-01
10
+
11
+ - **Word drafts open without LibreOffice.** `document_parse` needs LibreOffice for `.docx` files, which most machines lack, so reviewing a thesis or draft in Word format failed. When LibreOffice is missing, Feynman now reads the document's paragraph text directly from the `.docx` and says so; install LibreOffice for page layout, tables, footnotes, and images.
12
+ - `feynman_science_database_search` accepts the `"null"` some models send for an omitted `sort` or `limit`, which failed validation before the search ran.
13
+ - **Pi 0.99.2** (faster prompt submission in long sessions; saved default models with extension providers load reliably), **pi-subagents 0.74.0**, and **pi-btw 0.7.0**. pi-subagents now ships the patched undici 8.10.2, so the undici security exceptions and Feynman's own undici pin are gone. It removed the `workflowScript` parameter; `/deepresearch` and `/summarize` now write the workflow script to `outputs/.plans/<slug>-workflow.js` and run it by path. Its other new form, an inline ```` ```js workflow ```` block, failed in testing when Claude put the block in its hidden reasoning, while a script file worked with every model, and the file stays next to the plan as a record of the run. Subagent children also follow the project's trust setting, and `/reload` no longer stops running background workflows.
14
+ - Security: Pi 0.99.2 still locks brace-expansion 5.0.9; the exception for it stays until Pi ships 5.0.12.
15
+
16
+ ## v0.5.18 - 2026-09-30
17
+
18
+ - **A rate-limited paper index no longer ends a research step.** When Semantic Scholar is rate-limited or down, a search is answered from OpenAlex, and an OpenAlex topic search that hits OpenAlex's anonymous rate limit or an outage is answered from Semantic Scholar. The result says first which index answered and why. Lookups by OpenAlex ID, DOI, author, venue, or citation still report the error, since the other index has no equivalent.
19
+ - **alphaXiv tools answer when alphaXiv cannot.** If your alphaXiv login expired or alphaXiv does not respond, `alpha_search` returns OpenAlex semantic search results and `alpha_get_paper` returns the arXiv metadata and abstract for an arXiv ID, each marked as a fallback. Run `feynman alpha login` to get alphaXiv's reports back.
20
+ - pi-web-access 0.35.0: OpenAI web search works when you signed in with ChatGPT (`feynman model login openai`), and Brave search keeps to its rate limits. The researcher is told to pass a search provider as a plain string, since some models sent `provider: "[\"parallel-mcp\"]"`, which `web_search` rejects; nicobailon/pi-web-access#491 makes `web_search` accept that form too.
21
+
9
22
  ## v0.5.17 - 2026-09-30
10
23
 
11
24
  - **`feynman model login openai` works again.** Since Pi 0.99, Sign in with ChatGPT needs a stable ID for the installation, which Feynman did not pass, so the login failed at once with "requires a device ID (UUID)". Feynman now shares the `deviceId` Pi's own `/login` keeps in `~/.feynman/agent/settings.json`.
@@ -13,6 +13,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { Type } from "typebox";
14
14
 
15
15
  import { extractPaperSections } from "./alpha-sections.js";
16
+ import { searchArxiv } from "./science-database-arxiv.js";
17
+ import { searchOpenAlex } from "./science-database-openalex.js";
16
18
 
17
19
  function formatText(value: unknown): string {
18
20
  if (typeof value === "string") return value;
@@ -21,11 +23,23 @@ function formatText(value: unknown): string {
21
23
 
22
24
  // alphaXiv's API has changed without notice before; when a call fails, point
23
25
  // the model at the other paper sources instead of leaving it on a broken tool.
24
- async function withPaperFallback<T>(run: () => Promise<T>): Promise<T> {
26
+ const ARXIV_ID = /(?:^|arxiv\.org\/(?:abs|pdf)\/|alphaxiv\.org\/(?:abs|overview)\/)(\d{4}\.\d{4,5}(?:v\d+)?|[a-z-]+(?:\.[A-Z]{2})?\/\d{7}(?:v\d+)?)/i;
27
+
28
+ // When alphaXiv does not answer (expired login, outage), answer from another
29
+ // paper source and say so first, instead of failing the research step.
30
+ async function withPaperFallback<T>(
31
+ run: () => Promise<T>,
32
+ fallback?: { source: string; run: () => Promise<Record<string, unknown>> },
33
+ ): Promise<T | Record<string, unknown>> {
25
34
  try {
26
35
  return await run();
27
36
  } catch (error) {
28
37
  const message = (error instanceof Error ? error.message : String(error)).replace("`alpha login`", "`feynman alpha login`");
38
+ if (fallback) {
39
+ try {
40
+ return { fallbackNote: `alphaXiv did not answer (${message}); these results are from ${fallback.source}.`, ...(await fallback.run()) };
41
+ } catch {}
42
+ }
29
43
  throw new Error(
30
44
  `${message}\nalphaXiv did not answer this call. Use feynman_science_database_search (source "arxiv" or "semanticscholar") or fetch_content on https://arxiv.org/abs/<id> instead.`,
31
45
  );
@@ -65,7 +79,10 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
65
79
  ),
66
80
  }),
67
81
  async execute(_toolCallId, params) {
68
- const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"));
82
+ const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"), {
83
+ source: "OpenAlex semantic search",
84
+ run: () => searchOpenAlex({ source: "openalex", query: `semantic: ${params.query}`, limit: 10 }),
85
+ });
69
86
  return { content: [{ type: "text", text: formatText(result) }], details: result };
70
87
  },
71
88
  });
@@ -87,7 +104,14 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
87
104
  sections: Type.Optional(paperSectionsSchema),
88
105
  }),
89
106
  async execute(_toolCallId, params) {
90
- const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }));
107
+ const arxivId = ARXIV_ID.exec(params.paper.trim())?.[1];
108
+ const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }), arxivId ? {
109
+ source: "arXiv metadata and abstract (no full text or sections)",
110
+ run: () => searchArxiv({ query: arxivId }),
111
+ } : undefined);
112
+ if ("fallbackNote" in result) {
113
+ return { content: [{ type: "text", text: formatText(result) }], details: result };
114
+ }
91
115
  const extracted = extractPaperSections(result.content, params.section, params.sections);
92
116
  const filteredResult = extracted.requested.length
93
117
  ? {
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { inflateRawSync } from "node:zlib";
4
+
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+
7
+ // Reads one stored or deflated entry through the zip central directory.
8
+ function readZipEntry(zip: Buffer, name: string): Buffer | undefined {
9
+ const end = zip.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
10
+ if (end < 0) return undefined;
11
+ let offset = zip.readUInt32LE(end + 16);
12
+ for (let index = zip.readUInt16LE(end + 10); index > 0; index -= 1) {
13
+ if (zip.readUInt32LE(offset) !== 0x02014b50) return undefined;
14
+ const method = zip.readUInt16LE(offset + 10);
15
+ const size = zip.readUInt32LE(offset + 20);
16
+ const nameLength = zip.readUInt16LE(offset + 28);
17
+ const local = zip.readUInt32LE(offset + 42);
18
+ if (zip.toString("utf8", offset + 46, offset + 46 + nameLength) === name) {
19
+ const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28);
20
+ const data = zip.subarray(start, start + size);
21
+ return method === 0 ? data : method === 8 ? inflateRawSync(data) : undefined;
22
+ }
23
+ offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32);
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ function decodeXml(text: string): string {
29
+ return text.replace(/&(?:#x([0-9a-f]+)|#(\d+)|(amp|lt|gt|quot|apos));/gi, (_match, hex: string, dec: string, named: string) => {
30
+ if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
31
+ if (dec) return String.fromCodePoint(Number(dec));
32
+ return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[named.toLowerCase()] ?? "";
33
+ });
34
+ }
35
+
36
+ // Paragraph text of a .docx body: runs, tabs, and line breaks; no tables'
37
+ // layout, headers, footnotes, or images.
38
+ export function docxText(file: Buffer): string | undefined {
39
+ const xml = readZipEntry(file, "word/document.xml")?.toString("utf8");
40
+ if (!xml) return undefined;
41
+ return xml
42
+ .split("</w:p>")
43
+ .map((paragraph) =>
44
+ [...paragraph.matchAll(/<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>|<w:tab\/>|<w:br\/>/g)]
45
+ .map((match) => (match[0] === "<w:tab/>" ? "\t" : match[0] === "<w:br/>" ? "\n" : decodeXml(match[1] ?? "")))
46
+ .join(""),
47
+ )
48
+ .join("\n")
49
+ .replace(/\n{3,}/g, "\n\n")
50
+ .trim();
51
+ }
52
+
53
+ // document_parse needs LibreOffice for Word files, which most machines lack.
54
+ // Answer a .docx from its own XML instead of failing the read.
55
+ export function registerDocxFallback(pi: ExtensionAPI): void {
56
+ pi.on("tool_result", (event, ctx) => {
57
+ if (event.toolName !== "document_parse" || !event.isError) return undefined;
58
+ const errorText = event.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
59
+ const path = typeof event.input.path === "string" ? event.input.path.replace(/^@/, "") : "";
60
+ if (!errorText.includes("LibreOffice is not installed") || !/\.doc[xm]$/i.test(path)) return undefined;
61
+ let text: string | undefined;
62
+ try {
63
+ text = docxText(readFileSync(resolve(ctx.cwd, path)));
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ if (!text) return undefined;
68
+ return {
69
+ isError: false,
70
+ content: [{
71
+ type: "text",
72
+ text: `LibreOffice is not installed, so this is the document's paragraph text read directly from the .docx (no page layout, table structure, headers, footnotes, or images). Install LibreOffice for a full parse.\n\n${text}`,
73
+ }],
74
+ };
75
+ });
76
+ }
@@ -380,19 +380,54 @@ async function searchSemanticScholar(params: ScienceDatabaseSearchParams): Promi
380
380
  };
381
381
  }
382
382
 
383
+ // OpenAlex lookups by ID, DOI, author, venue, or citation have no Semantic
384
+ // Scholar equivalent; only plain topic searches fall back.
385
+ const OPENALEX_LOOKUP_QUERY =
386
+ /^(?:openalex_\w+|rate-limit|authors?|author-search|sources?|venues?|citations?|references?|work|detail)(?::|\s|$)|^(?:W\d+|doi:\s*10\.|10\.\S+\/)/i;
387
+
388
+ function isIndexUnavailable(error: unknown): boolean {
389
+ if (error instanceof ScienceDatabaseRequestError) return error.status === 429 || error.status >= 500;
390
+ return error instanceof Error && /^OpenAlex request failed: (?:429|5\d\d)\b/.test(error.message);
391
+ }
392
+
393
+ // A rate-limited or overloaded index should not end a research step: answer a
394
+ // topic search from the other general index, and say so first in the result.
395
+ async function withIndexFallback(
396
+ run: () => Promise<Record<string, unknown>>,
397
+ fallback: (() => Promise<Record<string, unknown>>) | undefined,
398
+ describe: (reason: string) => string,
399
+ ): Promise<Record<string, unknown>> {
400
+ try {
401
+ return await run();
402
+ } catch (error) {
403
+ if (!fallback || !isIndexUnavailable(error)) throw error;
404
+ let result: Record<string, unknown>;
405
+ try {
406
+ result = await fallback();
407
+ } catch {
408
+ throw error;
409
+ }
410
+ return { fallbackNote: describe(error instanceof Error ? error.message : String(error)), ...result };
411
+ }
412
+ }
413
+
383
414
  async function scienceDatabaseSearch(params: ScienceDatabaseSearchParams): Promise<Record<string, unknown>> {
384
415
  if (params.source === "arxiv") return searchArxiv(params);
385
416
  if (params.source === "biorxiv") return searchPreprints(params, "biorxiv");
386
417
  if (params.source === "medrxiv") return searchPreprints(params, "medrxiv");
387
418
  if (params.source === "pubmed") return searchPubMed(params);
388
419
  if (params.source === "crossref") return searchCrossref(params);
389
- if (params.source === "semanticscholar") return searchSemanticScholar(params);
420
+ const openAlex = () => searchOpenAlex({ limit: params.limit, query: params.query, source: "openalex" });
421
+ const semanticScholar = () => searchSemanticScholar({ ...params, source: "semanticscholar" });
422
+ if (params.source === "semanticscholar") {
423
+ return withIndexFallback(semanticScholar, openAlex, (reason) => `Semantic Scholar was unavailable (${reason}); these results are from OpenAlex.`);
424
+ }
390
425
  if (params.source === "openalex") {
391
- return searchOpenAlex({
392
- limit: params.limit,
393
- query: params.query,
394
- source: params.source,
395
- });
426
+ return withIndexFallback(
427
+ openAlex,
428
+ OPENALEX_LOOKUP_QUERY.test(params.query.trim()) ? undefined : semanticScholar,
429
+ (reason) => `OpenAlex was unavailable (${reason}); these results are from Semantic Scholar.`,
430
+ );
396
431
  }
397
432
  return searchEuropePmc(params);
398
433
  }
@@ -410,6 +445,17 @@ export function registerScienceDatabaseTools(pi: ExtensionAPI): void {
410
445
  "Preserve returned PMIDs, PMCIDs, DOIs, arXiv IDs, Semantic Scholar paper IDs, preprint DOIs, OpenAlex W/A/S IDs, author ORCIDs, citation/reference counts, OA status, Europe PMC full-text statuses and section inventories, source URLs, and endpoint provenance in research artifacts and answers.",
411
446
  "Treat database summaries as retrieval evidence, then verify decisive claims against the full paper when needed.",
412
447
  ],
448
+ // Some models fill omitted optional fields with "null", which fails validation.
449
+ prepareArguments: (args) => {
450
+ if (!args || typeof args !== "object" || Array.isArray(args)) return args as never;
451
+ const input = { ...(args as Record<string, unknown>) };
452
+ for (const key of ["sort", "limit"]) {
453
+ if (input[key] === null || input[key] === "" || input[key] === "null") delete input[key];
454
+ }
455
+ if (typeof input.sort === "string") input.sort = input.sort.trim().toLowerCase();
456
+ if (typeof input.limit === "string" && /^\d+$/.test(input.limit.trim())) input.limit = Number(input.limit);
457
+ return input as never;
458
+ },
413
459
  parameters: Type.Object({
414
460
  source: SCIENCE_DATABASE_SOURCE_SCHEMA,
415
461
  query: Type.String({
@@ -3,6 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { registerAlphaTools } from "./research-tools/alpha.js";
4
4
  import { registerCurrentDateResearchContext } from "./research-tools/current-date.js";
5
5
  import { registerDiscoveryCommands } from "./research-tools/discovery.js";
6
+ import { registerDocxFallback } from "./research-tools/docx-fallback.js";
6
7
  import { installFeynmanHeader } from "./research-tools/header.js";
7
8
  import { registerHelpCommand } from "./research-tools/help.js";
8
9
  import { registerHuggingFaceTools } from "./research-tools/huggingface.js";
@@ -23,6 +24,7 @@ export default function researchTools(pi: ExtensionAPI): void {
23
24
  registerCurrentDateResearchContext(pi);
24
25
  registerHuggingFaceTools(pi);
25
26
  registerDiscoveryCommands(pi);
27
+ registerDocxFallback(pi);
26
28
  registerHelpCommand(pi);
27
29
  registerInitCommand(pi);
28
30
  registerOutputsCommand(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -80,16 +80,15 @@
80
80
  "@clack/prompts": "^1.8.1",
81
81
  "@companion-ai/alpha-hub": "0.1.6",
82
82
  "fast-xml-parser": "5.11.1",
83
- "pi-btw": "0.6.1",
83
+ "pi-btw": "0.7.0",
84
84
  "pi-docparser": "4.0.0",
85
- "pi-subagents": "0.73.1",
86
- "pi-web-access": "0.34.0",
87
- "posthog-node": "^5.53.0",
88
- "undici": "^8.11.2"
85
+ "pi-subagents": "0.74.0",
86
+ "pi-web-access": "0.35.0",
87
+ "posthog-node": "^5.53.0"
89
88
  },
90
89
  "peerDependencies": {
91
- "@earendil-works/pi-ai": "0.99.1",
92
- "@earendil-works/pi-coding-agent": "0.99.1",
90
+ "@earendil-works/pi-ai": "0.99.2",
91
+ "@earendil-works/pi-coding-agent": "0.99.2",
93
92
  "@earendil-works/pi-tui": "*",
94
93
  "typebox": "*"
95
94
  },
@@ -114,8 +113,5 @@
114
113
  "author": "Advait Paliwal",
115
114
  "publishConfig": {
116
115
  "access": "public"
117
- },
118
- "overrides": {
119
- "undici": "$undici"
120
116
  }
121
117
  }
@@ -68,18 +68,23 @@ If subagents were chosen:
68
68
  - Keep `subagent` tool-call JSON small and valid.
69
69
  - Do not place multi-paragraph instructions inside the `subagent` JSON.
70
70
  - Use only supported `subagent` keys. Do not add extra keys such as `artifacts` unless the tool schema explicitly exposes them.
71
- - Use one async `workflowScript` with `await runs.all(...)` for parallel evidence gathering. Each item needs a unique stable `key`, plus its agent, short task, and output path. Set `globalConcurrencyLimit: 4` on the outer call.
71
+ - For parallel evidence gathering, write a workflow script that returns `await runs.all(...)` to `outputs/.plans/<slug>-workflow.js` with the write tool. After the write succeeds, call `subagent` with `workflow: "./outputs/.plans/<slug>-workflow.js"`, `async: true`, and `globalConcurrencyLimit: 4`. Each item needs a unique stable `key`, plus its agent, short task, and output path. Do not pass the script text inside the tool call.
72
72
  - Read the ordered result array and record each child's `ok`, error, and returned output/artifact paths. Ordinary child failures are collected by `runs.all`; validation or infrastructure failure can still fail the workflow. Do not assume every output exists.
73
73
  - Prefer broad guidance such as "use paper search and web search"; if a PDF parser or paper fetch fails, the researcher must continue from metadata, abstracts, and web sources and mark PDF parsing as blocked.
74
74
 
75
- Example shape:
75
+ Example shape: `outputs/.plans/<slug>-workflow.js` contains
76
+
77
+ ```js
78
+ return await runs.all([
79
+ { key: "web", agent: "researcher", task: "Read outputs/.plans/<slug>-T1.md and write outputs/.drafts/<slug>-research-web.md.", output: "outputs/.drafts/<slug>-research-web.md" },
80
+ { key: "papers", agent: "researcher", task: "Read outputs/.plans/<slug>-T2.md and write outputs/.drafts/<slug>-research-papers.md.", output: "outputs/.drafts/<slug>-research-papers.md" },
81
+ ]);
82
+ ```
83
+
84
+ and the `subagent` call is:
76
85
 
77
86
  ```json
78
- {
79
- "workflowScript": "return await runs.all([{key:'web',agent:'researcher',task:'Read outputs/.plans/<slug>-T1.md and write outputs/.drafts/<slug>-research-web.md.',output:'outputs/.drafts/<slug>-research-web.md'},{key:'papers',agent:'researcher',task:'Read outputs/.plans/<slug>-T2.md and write outputs/.drafts/<slug>-research-papers.md.',output:'outputs/.drafts/<slug>-research-papers.md'}]);",
80
- "async": true,
81
- "globalConcurrencyLimit": 4
82
- }
87
+ { "workflow": "./outputs/.plans/<slug>-workflow.js", "async": true, "globalConcurrencyLimit": 4 }
83
88
  ```
84
89
 
85
90
  Continue independent work after launch, then consume completion results before synthesis. Relative output paths resolve against the workspace, so each child writes its declared path; verify each file on disk, and use the returned output reference for any that is missing. After evidence gathering, update the plan ledger and verification log. If research failed, record exactly what failed and proceed with a blocked or partial draft.
@@ -116,14 +116,12 @@ Briefly summarize: "Source is ~<chars> chars -> <N> chunks -> <N> researcher sub
116
116
 
117
117
  ### 3c. Dispatch researcher subagents
118
118
 
119
- Use one async workflow with a stable key for each chunk and a concurrency limit of four. Expand the example item below for the actual chunk inventory; retain the single-source restriction and explicit output declaration for every child.
120
-
121
- ```json
122
- {
123
- "workflowScript": "return await runs.all([{key:'chunk-NNN',agent:'researcher',task:'Read ONLY outputs/.notes/<slug>-chunk-NNN.txt. Extract: (1) key claims, (2) methodology or technical approach, (3) cited evidence. Do NOT use web_search or fetch external URLs — this is single-source summarization. If a claim appears to start or end mid-sentence at the file boundary, mark it BOUNDARY PARTIAL. Write to outputs/.notes/<slug>-summary-chunk-NNN.md.',output:'outputs/.notes/<slug>-summary-chunk-NNN.md'}]);",
124
- "async": true,
125
- "globalConcurrencyLimit": 4
126
- }
119
+ Use one async workflow with a stable key for each chunk and a concurrency limit of four: write the script to `outputs/.plans/<slug>-workflow.js` with the write tool, then, after the write succeeds, call `subagent` with `{ "workflow": "./outputs/.plans/<slug>-workflow.js", "async": true, "globalConcurrencyLimit": 4 }`. Expand the example item below for the actual chunk inventory; retain the single-source restriction and explicit output declaration for every child.
120
+
121
+ ```js
122
+ return await runs.all([
123
+ { key: "chunk-NNN", agent: "researcher", task: "Read ONLY outputs/.notes/<slug>-chunk-NNN.txt. Extract: (1) key claims, (2) methodology or technical approach, (3) cited evidence. Do NOT use web_search or fetch external URLs — this is single-source summarization. If a claim appears to start or end mid-sentence at the file boundary, mark it BOUNDARY PARTIAL. Write to outputs/.notes/<slug>-summary-chunk-NNN.md.", output: "outputs/.notes/<slug>-summary-chunk-NNN.md" },
124
+ ]);
127
125
  ```
128
126
 
129
127
  Consume completion results before aggregation. `runs.all` returns an ordered array including ordinary child failures; inspect each `ok` and error. A validation or infrastructure failure can still fail the workflow. Relative output paths resolve against the workspace; verify each chunk summary on disk, and use the returned output reference for any that is missing. Record missing or failed chunks rather than treating an async launch receipt as a finished summary.