@agwab/pi-workflow 0.1.0 → 0.1.2

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.
Files changed (58) hide show
  1. package/README.md +14 -3
  2. package/agents/researcher.md +17 -7
  3. package/dist/artifact-graph-runtime.js +1 -0
  4. package/dist/compiler.js +2 -2
  5. package/dist/dynamic-generated-task-runtime.js +4 -3
  6. package/dist/dynamic-runtime-bundle.js +3 -2
  7. package/dist/extension.js +40 -1
  8. package/dist/subagent-backend.js +82 -27
  9. package/dist/tool-metadata.d.ts +1 -0
  10. package/dist/tool-metadata.js +13 -1
  11. package/dist/workflow-artifact-extension.js +3 -2
  12. package/dist/workflow-artifact-tool.js +84 -4
  13. package/dist/workflow-web-source-extension.d.ts +43 -0
  14. package/dist/workflow-web-source-extension.js +1194 -0
  15. package/dist/workflow-web-source.d.ts +171 -0
  16. package/dist/workflow-web-source.js +897 -0
  17. package/docs/usage.md +32 -45
  18. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  19. package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
  20. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
  21. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
  22. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
  23. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
  24. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
  25. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
  26. package/package.json +3 -4
  27. package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
  28. package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
  29. package/src/artifact-graph-runtime.ts +1 -0
  30. package/src/compiler.ts +2 -1
  31. package/src/dynamic-generated-task-runtime.ts +4 -2
  32. package/src/dynamic-runtime-bundle.ts +3 -2
  33. package/src/extension.ts +46 -1
  34. package/src/subagent-backend.ts +121 -37
  35. package/src/tool-metadata.ts +22 -1
  36. package/src/workflow-artifact-extension.ts +3 -2
  37. package/src/workflow-artifact-tool.ts +96 -4
  38. package/src/workflow-web-source-extension.ts +1411 -0
  39. package/src/workflow-web-source.ts +1171 -0
  40. package/workflows/README.md +1 -1
  41. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
  42. package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
  43. package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
  44. package/workflows/deep-research/helpers/render-executive.mjs +571 -198
  45. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
  46. package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
  47. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
  48. package/workflows/deep-research/spec.json +36 -21
  49. package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
  50. package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
  51. package/workflows/deep-review/spec.json +22 -1
  52. package/docs/release.md +0 -89
  53. package/node_modules/@pondwader/socks5-server/.DS_Store +0 -0
  54. package/node_modules/commander/.DS_Store +0 -0
  55. package/node_modules/jiti/.DS_Store +0 -0
  56. package/node_modules/node-forge/.DS_Store +0 -0
  57. package/node_modules/shell-quote/.DS_Store +0 -0
  58. package/node_modules/zod/.DS_Store +0 -0
@@ -1,7 +1,7 @@
1
- // Deterministic executive renderer for deep-research-compact-v2.
1
+ // Deterministic evidence-backed renderer for deep-research.
2
2
  //
3
3
  // Input: final-audit.control.json from the full deep-research final stage.
4
- // Output: compact executiveMarkdown in control plus an executive.md sidecar.
4
+ // Output: a parent-facing research report in executiveMarkdown plus sidecars.
5
5
  //
6
6
  // This intentionally treats final-audit.control.json as the source of truth and
7
7
  // renders a bounded view. It does not re-verify or invent evidence.
@@ -20,6 +20,35 @@ function asArray(value) {
20
20
  return Array.isArray(value) ? value : [];
21
21
  }
22
22
 
23
+ function isRecord(value) {
24
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
25
+ }
26
+
27
+ function flattenItems(value) {
28
+ if (Array.isArray(value)) return value.flatMap((item) => flattenItems(item));
29
+ if (typeof value === "string") return value.trim() ? [value] : [];
30
+ if (!isRecord(value)) return [];
31
+ const renderFields = [
32
+ "gap",
33
+ "finding",
34
+ "claim",
35
+ "note",
36
+ "whyItMatters",
37
+ "parentImpact",
38
+ "recommendation",
39
+ "action",
40
+ "step",
41
+ ];
42
+ if (
43
+ renderFields.some(
44
+ (field) => typeof value[field] === "string" && value[field].trim(),
45
+ )
46
+ ) {
47
+ return [value];
48
+ }
49
+ return Object.values(value).flatMap((item) => flattenItems(item));
50
+ }
51
+
23
52
  function words(text) {
24
53
  return (
25
54
  String(text ?? "")
@@ -39,6 +68,21 @@ function cleanText(value) {
39
68
  .trim();
40
69
  }
41
70
 
71
+ function escapeTableCell(value) {
72
+ return cleanText(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
73
+ }
74
+
75
+ function stringifyItem(item) {
76
+ if (typeof item === "string") return cleanText(item) || "(empty string)";
77
+ try {
78
+ const json = JSON.stringify(item);
79
+ if (json) return cleanText(json);
80
+ } catch {
81
+ // Fall through to String below.
82
+ }
83
+ return cleanText(String(item)) || "(empty item)";
84
+ }
85
+
42
86
  function truncateWords(text, maxWords) {
43
87
  const items = words(text);
44
88
  if (items.length <= maxWords) return cleanText(text);
@@ -56,85 +100,111 @@ function hostOf(url) {
56
100
  }
57
101
  }
58
102
 
59
- function collectUrls(value, urls = []) {
60
- if (typeof value === "string") {
61
- for (const match of value.matchAll(/https?:\/\/[^\s)\]}"`]+/g))
62
- urls.push(match[0].replace(/[.,;:]+$/, ""));
63
- return urls;
103
+ function normalizeUrl(url) {
104
+ if (typeof url !== "string") return null;
105
+ const trimmed = url.trim().replace(/[.,;:]+$/, "");
106
+ if (!/^https?:\/\//i.test(trimmed)) return null;
107
+ try {
108
+ const parsed = new URL(trimmed);
109
+ parsed.hash = "";
110
+ return parsed.toString();
111
+ } catch {
112
+ return trimmed;
64
113
  }
114
+ }
115
+
116
+ function collectStructuredUrls(value, urls = []) {
117
+ if (!value || typeof value !== "object") return urls;
65
118
  if (Array.isArray(value)) {
66
- for (const item of value) collectUrls(item, urls);
119
+ for (const item of value) collectStructuredUrls(item, urls);
67
120
  return urls;
68
121
  }
69
- if (value && typeof value === "object") {
70
- for (const item of Object.values(value)) collectUrls(item, urls);
122
+ for (const [key, item] of Object.entries(value)) {
123
+ if (
124
+ /^(sourceUrls?|evidenceUrls?|urls?|url|uri|href|links?|references?|refs?|basis|sources)$/i.test(
125
+ key,
126
+ )
127
+ ) {
128
+ for (const candidate of asArray(item).length ? item : [item]) {
129
+ const normalized = normalizeUrl(candidate);
130
+ if (normalized) urls.push(normalized);
131
+ else if (candidate && typeof candidate === "object") {
132
+ collectStructuredUrls(candidate, urls);
133
+ }
134
+ }
135
+ continue;
136
+ }
137
+ if (item && typeof item === "object") collectStructuredUrls(item, urls);
71
138
  }
72
139
  return urls;
73
140
  }
74
141
 
75
- function collectSourceLocators(value, locators = [], fieldName = "") {
76
- if (typeof value === "string") {
77
- const urls = collectUrls(value, []);
78
- if (urls.length > 0) locators.push(...urls);
79
- else if (
80
- /^(sourceUrls?|sourceRefs?|sourcePaths?|urls?|paths?)$/i.test(fieldName)
81
- ) {
82
- const trimmed = value.trim();
83
- if (trimmed) locators.push(trimmed);
84
- }
85
- return locators;
86
- }
87
- if (Array.isArray(value)) {
88
- for (const item of value) collectSourceLocators(item, locators, fieldName);
89
- return locators;
90
- }
91
- if (value && typeof value === "object") {
92
- for (const [key, item] of Object.entries(value)) {
93
- collectSourceLocators(item, locators, key);
142
+ function uniqueStructuredUrls(...values) {
143
+ const out = [];
144
+ const seen = new Set();
145
+ for (const value of values) {
146
+ for (const url of collectStructuredUrls(value, [])) {
147
+ if (seen.has(url)) continue;
148
+ seen.add(url);
149
+ out.push(url);
94
150
  }
95
151
  }
96
- return locators;
152
+ return out;
97
153
  }
98
154
 
99
- function itemText(item, kind) {
100
- if (typeof item === "string") return cleanText(item);
101
- if (!item || typeof item !== "object") return "";
102
- const fields =
103
- kind === "recommendation"
104
- ? ["recommendation", "step", "action", "note", "finding", "gap"]
105
- : kind === "gap"
106
- ? ["gap", "note", "finding", "whyItMatters", "parentImpact"]
107
- : ["finding", "summary", "bestValue", "recommendation", "step", "note"];
155
+ function urlsOf(item, limit = 3) {
156
+ return uniqueStructuredUrls(item).slice(0, limit);
157
+ }
158
+
159
+ function markdownLinkList(urls, maxItems = 3) {
160
+ return urls
161
+ .slice(0, maxItems)
162
+ .map((url) => `[${hostOf(url)}](${url})`)
163
+ .join(", ");
164
+ }
165
+
166
+ function itemText(item, fields, fallback = "") {
167
+ if (typeof item === "string") return cleanText(item) || fallback;
168
+ if (!item || typeof item !== "object") return fallback;
108
169
  for (const field of fields) {
109
- if (typeof item[field] === "string" && item[field].trim())
170
+ if (typeof item[field] === "string" && item[field].trim()) {
110
171
  return cleanText(item[field]);
172
+ }
111
173
  }
112
- return cleanText(JSON.stringify(item));
174
+ return fallback;
113
175
  }
114
176
 
115
- function sourceSuffix(item, state, options) {
116
- const urls = [...new Set(collectUrls(item))].filter(Boolean);
117
- const selected = [];
118
- for (const url of urls) {
119
- if (state.urls.size >= options.maxUrls) break;
120
- if (state.urls.has(url)) continue;
121
- state.urls.add(url);
122
- selected.push(url);
123
- if (selected.length >= 1) break; // one URL per bullet keeps memo compact
124
- }
125
- if (selected.length === 0) return "";
126
- return ` (${selected.map((url) => hostOf(url)).join(", ")}: ${selected.join(" ")})`;
177
+ function evidenceStatusOf(item) {
178
+ if (!item || typeof item !== "object") return "not specified";
179
+ return cleanText(
180
+ item.evidenceStatus ??
181
+ item.status ??
182
+ item.confidence ??
183
+ item.sourceQuality ??
184
+ "not specified",
185
+ );
127
186
  }
128
187
 
129
- function bulletLines(items, kind, limit, state, options, perItemWords = 34) {
130
- const out = [];
131
- for (const item of asArray(items)) {
132
- if (out.length >= limit) break;
133
- const text = truncateWords(itemText(item, kind), perItemWords);
134
- if (!text) continue;
135
- out.push(`- ${text}${sourceSuffix(item, state, options)}`);
136
- }
137
- return out;
188
+ function confidenceOf(item) {
189
+ if (!item || typeof item !== "object") return "";
190
+ return cleanText(item.confidence ?? item.evidenceStatus ?? "");
191
+ }
192
+
193
+ function finiteNumber(value) {
194
+ const parsed = Number(value);
195
+ return Number.isFinite(parsed) ? parsed : undefined;
196
+ }
197
+
198
+ function coverageCounts(coverage, fallback) {
199
+ if (!coverage || typeof coverage !== "object") return null;
200
+ return {
201
+ total: finiteNumber(coverage.verificationCandidates) ?? fallback.total,
202
+ verified: finiteNumber(coverage.verified) ?? fallback.verified,
203
+ partially_supported:
204
+ finiteNumber(coverage.partiallySupported) ?? fallback.partially_supported,
205
+ unsupported: finiteNumber(coverage.unsupported) ?? fallback.unsupported,
206
+ conflicting: finiteNumber(coverage.conflicting) ?? fallback.conflicting,
207
+ };
138
208
  }
139
209
 
140
210
  function claimCounts(control) {
@@ -150,163 +220,455 @@ function claimCounts(control) {
150
220
  const status = claim?.status;
151
221
  if (status && Object.hasOwn(counts, status)) counts[status] += 1;
152
222
  }
153
- const coverage = control?.finalReport?.coverageSummary;
154
- if (coverage && typeof coverage === "object") {
155
- return {
156
- total:
157
- Number(coverage.verificationCandidates ?? counts.total) || counts.total,
158
- verified: Number(coverage.verified ?? counts.verified) || counts.verified,
159
- partially_supported:
160
- Number(coverage.partiallySupported ?? counts.partially_supported) ||
161
- counts.partially_supported,
162
- unsupported:
163
- Number(coverage.unsupported ?? counts.unsupported) ||
164
- counts.unsupported,
165
- conflicting:
166
- Number(coverage.conflicting ?? counts.conflicting) ||
167
- counts.conflicting,
168
- };
223
+ const coverage = coverageCounts(
224
+ control?.finalReport?.coverageSummary,
225
+ counts,
226
+ );
227
+ if (claims.length === 0 && coverage) return coverage;
228
+ if (!coverage) return counts;
229
+
230
+ const mismatches = [];
231
+ for (const key of [
232
+ "total",
233
+ "verified",
234
+ "partially_supported",
235
+ "unsupported",
236
+ "conflicting",
237
+ ]) {
238
+ if (coverage[key] !== counts[key]) {
239
+ mismatches.push({
240
+ field: key,
241
+ claimVerdictIndex: counts[key],
242
+ coverageSummary: coverage[key],
243
+ });
244
+ }
169
245
  }
170
- return counts;
246
+ return mismatches.length > 0
247
+ ? { ...counts, coverageSummaryMismatch: mismatches }
248
+ : counts;
171
249
  }
172
250
 
173
- function renderExecutiveMarkdown(control, options) {
174
- const report = control?.finalReport ?? {};
175
- const state = { urls: new Set() };
176
- const maxWords = options.maxWords;
177
- const counts = claimCounts(control);
178
- const factSlots = asArray(report.factSlotCoverage);
179
- const filledSlots = factSlots.filter(
180
- (slot) => slot?.status === "filled",
181
- ).length;
182
- const partialSlots = factSlots.filter(
183
- (slot) => slot?.status === "partial",
184
- ).length;
185
- const missingSlots = factSlots.filter((slot) =>
186
- ["missing", "gap", "conflicting"].includes(slot?.status),
187
- ).length;
188
-
189
- const sections = [];
190
- sections.push("# Executive summary");
191
- sections.push("");
192
- sections.push(
193
- `**Bottom line:** ${truncateWords(report.summary ?? control.digest ?? "Research completed with audited evidence.", 85)}`,
194
- );
195
- sections.push("");
251
+ function factSlotSummary(factSlots) {
252
+ return {
253
+ total: factSlots.length,
254
+ filled: factSlots.filter((slot) => slot?.status === "filled").length,
255
+ partial: factSlots.filter((slot) => slot?.status === "partial").length,
256
+ missingOrConflicting: factSlots.filter((slot) =>
257
+ ["missing", "gap", "conflicting"].includes(slot?.status),
258
+ ).length,
259
+ };
260
+ }
196
261
 
197
- const findings = bulletLines(
198
- report.mainFindings,
199
- "finding",
200
- options.maxFindings,
201
- state,
202
- options,
203
- );
204
- if (findings.length) {
205
- sections.push("**Top findings**");
206
- sections.push(...findings);
207
- sections.push("");
262
+ function statusRank(item) {
263
+ const status =
264
+ `${item?.evidenceStatus ?? item?.status ?? item?.confidence ?? ""}`.toLowerCase();
265
+ if (
266
+ status.includes("missing") ||
267
+ status.includes("gap") ||
268
+ status.includes("conflict")
269
+ ) {
270
+ return 0;
208
271
  }
272
+ if (status.includes("unsupported")) return 1;
273
+ if (status.includes("partial")) return 2;
274
+ if (status.includes("verified") && !status.includes("partial")) return 3;
275
+ if (status.includes("filled") || status.includes("high")) return 4;
276
+ return 5;
277
+ }
209
278
 
210
- const recommendations = bulletLines(
211
- report.recommendations?.length ? report.recommendations : report.actionPlan,
212
- "recommendation",
213
- options.maxRecommendations,
214
- state,
215
- options,
216
- 32,
217
- );
218
- if (recommendations.length) {
219
- sections.push("**Recommended next steps**");
220
- sections.push(...recommendations);
221
- sections.push("");
222
- }
279
+ function sortedFactSlots(report) {
280
+ return asArray(report.factSlotCoverage)
281
+ .slice()
282
+ .sort(
283
+ (a, b) =>
284
+ statusRank(a) - statusRank(b) ||
285
+ cleanText(a?.slotId ?? a?.label).localeCompare(
286
+ cleanText(b?.slotId ?? b?.label),
287
+ ),
288
+ );
289
+ }
223
290
 
224
- const caveatItems = [
225
- ...asArray(report.caveatedFindings),
226
- ...asArray(report.remainingGaps),
227
- ...asArray(report.parentDecisionNotes),
291
+ function renderEvidenceStrength(report) {
292
+ const slots = sortedFactSlots(report);
293
+ const rows = slots.map((slot) => {
294
+ const area = escapeTableCell(
295
+ slot.label ?? slot.slotId ?? slot.bestValue ?? "Evidence area",
296
+ );
297
+ const status = escapeTableCell(evidenceStatusOf(slot));
298
+ const evidence = escapeTableCell(
299
+ markdownLinkList(urlsOf(slot, 2), 2) || "—",
300
+ );
301
+ const impact = escapeTableCell(
302
+ slot.parentImpact ?? slot.whyItMatters ?? slot.notes ?? "",
303
+ );
304
+ return `| ${area || "Evidence area"} | ${status || "—"} | ${evidence} | ${impact || "—"} |`;
305
+ });
306
+ if (rows.length === 0) return [];
307
+ return [
308
+ "## Evidence strength",
309
+ "",
310
+ "| Area | Status | Evidence | Why it matters |",
311
+ "|---|---|---|---|",
312
+ ...rows,
313
+ "",
228
314
  ];
229
- const gaps = bulletLines(
230
- caveatItems,
231
- "gap",
232
- options.maxGaps,
233
- state,
234
- options,
235
- 30,
315
+ }
316
+
317
+ function mainFindingEntries(report) {
318
+ return asArray(report.mainFindings).map((item) => ({
319
+ item,
320
+ text: itemText(
321
+ item,
322
+ ["finding", "summary", "bestValue", "claim"],
323
+ stringifyItem(item),
324
+ ),
325
+ }));
326
+ }
327
+
328
+ function recommendationEntries(report) {
329
+ return asArray(report.recommendations).map((item) => ({
330
+ item,
331
+ text: itemText(
332
+ item,
333
+ ["recommendation", "action", "step", "note"],
334
+ stringifyItem(item),
335
+ ),
336
+ }));
337
+ }
338
+
339
+ function actionEntries(report) {
340
+ return asArray(report.actionPlan).map((item) => ({
341
+ item,
342
+ text:
343
+ itemText(item, ["action", "recommendation", "note"]) ||
344
+ (typeof item?.step === "string" && cleanText(item.step)) ||
345
+ stringifyItem(item),
346
+ }));
347
+ }
348
+
349
+ function renderMainFindings(report) {
350
+ const findings = mainFindingEntries(report);
351
+ if (findings.length === 0) return [];
352
+ const out = ["## Main findings", ""];
353
+ findings.forEach(({ item: finding, text }, index) => {
354
+ const status = evidenceStatusOf(finding);
355
+ const confidence = confidenceOf(finding);
356
+ const urls = markdownLinkList(urlsOf(finding, 4), 4);
357
+ out.push(`### ${index + 1}. ${text}`);
358
+ out.push("");
359
+ out.push(
360
+ `Evidence status: **${status || "not specified"}**${confidence && confidence !== status ? ` \nConfidence: **${confidence}**` : ""}`,
361
+ );
362
+ if (urls) out.push(`Sources: ${urls}`);
363
+ const explanation = itemText(finding, [
364
+ "rationale",
365
+ "explanation",
366
+ "details",
367
+ "notes",
368
+ ]);
369
+ if (explanation && explanation !== text) out.push("", explanation);
370
+ out.push("");
371
+ });
372
+ return out;
373
+ }
374
+
375
+ function renderRecommendations(report) {
376
+ const recommendations = recommendationEntries(report);
377
+ if (recommendations.length === 0) return [];
378
+ const out = ["## Recommendations", ""];
379
+ recommendations.forEach(({ item, text }, index) => {
380
+ const status = evidenceStatusOf(item);
381
+ const urls = markdownLinkList(urlsOf(item, 4), 4);
382
+ out.push(`${index + 1}. **${text}**`);
383
+ out.push(` - Evidence status: ${status || "not specified"}`);
384
+ if (urls) out.push(` - Sources: ${urls}`);
385
+ out.push("");
386
+ });
387
+ return out;
388
+ }
389
+
390
+ function renderActionPlan(report) {
391
+ const actions = actionEntries(report);
392
+ if (actions.length === 0) return [];
393
+ const out = ["## Action plan", ""];
394
+ actions.forEach(({ item, text }, index) => {
395
+ const numericStep = Number(item?.step);
396
+ const step = Number.isFinite(numericStep) ? numericStep : index + 1;
397
+ const urls = markdownLinkList(urlsOf(item, 3), 3);
398
+ const evidence = evidenceStatusOf(item);
399
+ out.push(`${step}. ${text}`);
400
+ if (evidence && evidence !== "not specified")
401
+ out.push(` - Evidence: ${evidence}`);
402
+ if (urls) out.push(` - Sources: ${urls}`);
403
+ out.push("");
404
+ });
405
+ return out;
406
+ }
407
+
408
+ function caveatText(item) {
409
+ return itemText(
410
+ item,
411
+ ["gap", "finding", "claim", "note", "whyItMatters", "parentImpact"],
412
+ stringifyItem(item),
236
413
  );
237
- if (gaps.length) {
238
- sections.push("**Key caveats / gaps**");
239
- sections.push(...gaps);
240
- sections.push("");
414
+ }
415
+
416
+ function caveatCategories(report) {
417
+ return [
418
+ { kind: "Gap", items: flattenItems(report.remainingGaps) },
419
+ { kind: "Unsupported", items: flattenItems(report.notableUnsupportedClaims) },
420
+ { kind: "Contested", items: flattenItems(report.contestedAreas) },
421
+ { kind: "Caveat", items: flattenItems(report.caveatedFindings) },
422
+ { kind: "Unverified lead", items: flattenItems(report.unverifiedButRelevant) },
423
+ { kind: "Decision note", items: flattenItems(report.parentDecisionNotes) },
424
+ ]
425
+ .map((category) => ({
426
+ kind: category.kind,
427
+ entries: category.items
428
+ .map((item) => ({ item, text: caveatText(item) }))
429
+ .filter((entry) => entry.text),
430
+ }))
431
+ .filter((category) => category.entries.length > 0);
432
+ }
433
+
434
+ function selectCaveats(report) {
435
+ const categories = caveatCategories(report);
436
+ const selected = [];
437
+ for (const category of categories) {
438
+ for (const entry of category.entries) {
439
+ selected.push({ kind: category.kind, ...entry });
440
+ }
241
441
  }
442
+ return {
443
+ selected,
444
+ total: selected.length,
445
+ };
446
+ }
447
+
448
+ function renderCaveats(report) {
449
+ const selection = selectCaveats(report);
450
+ if (selection.total === 0) return [];
451
+ const out = ["## Caveats and remaining gaps", ""];
452
+ for (const { kind, item, text } of selection.selected) {
453
+ const urls = markdownLinkList(urlsOf(item, 3), 3);
454
+ out.push(`- **${kind}:** ${text}${urls ? ` (${urls})` : ""}`);
455
+ }
456
+ out.push("");
457
+ return out;
458
+ }
242
459
 
243
- sections.push(
244
- `**Audit trail:** Full evidence remains in \`final-audit.control.json\`: ${counts.verified} verified, ${counts.partially_supported} partially supported, ${counts.unsupported} unsupported, ${counts.conflicting} conflicting claims; fact slots ${filledSlots} filled, ${partialSlots} partial, ${missingSlots} missing/conflicting.`,
460
+ function renderSourceIndex(sourceIndex) {
461
+ if (sourceIndex.length === 0) return [];
462
+ const grouped = new Map();
463
+ for (const url of sourceIndex) {
464
+ const host = hostOf(url);
465
+ if (!grouped.has(host)) grouped.set(host, []);
466
+ grouped.get(host).push(url);
467
+ }
468
+ const out = ["## Source index", ""];
469
+ for (const [host, urls] of grouped) {
470
+ out.push(
471
+ `- **${host}**: ${urls.map((url) => `[${url}](${url})`).join(", ")}`,
472
+ );
473
+ }
474
+ out.push("");
475
+ return out;
476
+ }
477
+
478
+ function renderAuditSummary(control, claimSummary, slots) {
479
+ const coverage = control?.finalReport?.coverageSummary ?? {};
480
+ const mismatches = asArray(claimSummary.coverageSummaryMismatch);
481
+ return [
482
+ "## Audit summary",
483
+ "",
484
+ `- Claims: ${claimSummary.verified} verified, ${claimSummary.partially_supported} partially supported, ${claimSummary.unsupported} unsupported, ${claimSummary.conflicting} conflicting.`,
485
+ `- Fact slots: ${slots.filled} filled, ${slots.partial} partial, ${slots.missingOrConflicting} missing/conflicting, ${slots.total} total.`,
486
+ ...(mismatches.length > 0
487
+ ? [
488
+ `- Coverage summary mismatch: displayed claim counts come from \`claimVerdictIndex\`; model coverageSummary disagreed on ${mismatches
489
+ .map((mismatch) => mismatch.field)
490
+ .join(", ")}.`,
491
+ ]
492
+ : []),
493
+ ...(coverage.researchQuestions != null
494
+ ? [`- Research questions: ${coverage.researchQuestions}.`]
495
+ : []),
496
+ "- Audit artifact: `final-audit.control.json`.",
497
+ "",
498
+ ];
499
+ }
500
+
501
+ function renderWarnings(sectionCounts) {
502
+ const checks = [
503
+ ["findings", "renderedFindings", "findings"],
504
+ ["recommendations", "renderedRecommendations", "recommendations"],
505
+ ["actionItems", "renderedActionItems", "action items"],
506
+ ["caveatsAndGaps", "renderedCaveatsAndGaps", "caveats/gaps"],
507
+ ["factSlots", "renderedFactSlots", "fact slots"],
508
+ ["sourceUrls", "renderedSourceUrls", "source URLs"],
509
+ ];
510
+ return checks
511
+ .filter(([totalKey, renderedKey]) => {
512
+ const total = Number(sectionCounts[totalKey] ?? 0);
513
+ const rendered = Number(sectionCounts[renderedKey] ?? 0);
514
+ return total !== rendered;
515
+ })
516
+ .map(([totalKey, renderedKey, label]) => ({
517
+ section: totalKey,
518
+ label,
519
+ total: sectionCounts[totalKey],
520
+ rendered: sectionCounts[renderedKey],
521
+ }));
522
+ }
523
+
524
+ function renderResearchMarkdown(control, options = {}) {
525
+ const report = control?.finalReport ?? {};
526
+ const claimSummary = claimCounts(control);
527
+ const factSlots = sortedFactSlots(report);
528
+ const slots = factSlotSummary(asArray(report.factSlotCoverage));
529
+ const findings = mainFindingEntries(report);
530
+ const recommendations = recommendationEntries(report);
531
+ const actions = actionEntries(report);
532
+ const caveats = selectCaveats(report);
533
+ const allSourceIndex = uniqueStructuredUrls(
534
+ report.factSlotCoverage,
535
+ report.mainFindings,
536
+ report.recommendations,
537
+ report.actionPlan,
538
+ report.caveatedFindings,
539
+ report.contestedAreas,
540
+ report.notableUnsupportedClaims,
541
+ report.remainingGaps,
542
+ report.parentDecisionNotes,
543
+ report.unverifiedButRelevant,
544
+ control?.claimVerdictIndex?.claims,
245
545
  );
546
+ const maxUrls = Number.isFinite(Number(options.maxUrls))
547
+ ? Math.max(0, Number(options.maxUrls))
548
+ : Infinity;
549
+ const sourceIndex = Number.isFinite(maxUrls)
550
+ ? allSourceIndex.slice(0, maxUrls)
551
+ : allSourceIndex;
552
+ const sectionCounts = {
553
+ findings: asArray(report.mainFindings).length,
554
+ renderedFindings: findings.length,
555
+ recommendations: asArray(report.recommendations).length,
556
+ renderedRecommendations: recommendations.length,
557
+ actionItems: asArray(report.actionPlan).length,
558
+ renderedActionItems: actions.length,
559
+ caveatsAndGaps:
560
+ flattenItems(report.remainingGaps).length +
561
+ flattenItems(report.notableUnsupportedClaims).length +
562
+ flattenItems(report.contestedAreas).length +
563
+ flattenItems(report.caveatedFindings).length +
564
+ flattenItems(report.unverifiedButRelevant).length +
565
+ flattenItems(report.parentDecisionNotes).length,
566
+ renderedCaveatsAndGaps: caveats.selected.length,
567
+ factSlots: asArray(report.factSlotCoverage).length,
568
+ renderedFactSlots: factSlots.length,
569
+ sourceUrls: allSourceIndex.length,
570
+ renderedSourceUrls: sourceIndex.length,
571
+ };
572
+ const warnings = renderWarnings(sectionCounts);
246
573
 
247
- let markdown = sections
574
+ const sections = [
575
+ "# Research report",
576
+ "",
577
+ "## Bottom line",
578
+ "",
579
+ cleanText(
580
+ report.summary ??
581
+ control.digest ??
582
+ "Research completed with audited evidence.",
583
+ ),
584
+ "",
585
+ ...renderEvidenceStrength(report),
586
+ ...renderMainFindings(report),
587
+ ...renderRecommendations(report),
588
+ ...renderActionPlan(report),
589
+ ...renderCaveats(report),
590
+ ...renderSourceIndex(sourceIndex),
591
+ ...renderAuditSummary(control, claimSummary, slots),
592
+ ];
593
+
594
+ const markdown = sections
248
595
  .join("\n")
249
596
  .replace(/\n{3,}/g, "\n\n")
250
597
  .trim();
251
- let truncated = false;
252
- if (countWords(markdown) > maxWords) {
253
- truncated = true;
254
- markdown = truncateWords(markdown, maxWords);
255
- }
256
- for (const locator of [...new Set(collectSourceLocators(control))]) {
257
- if (state.urls.size >= options.maxUrls) break;
258
- state.urls.add(locator);
259
- }
260
598
  return {
261
599
  markdown,
262
- truncated,
263
- sourceUrls: [...state.urls],
264
- counts,
265
- factSlots: {
266
- total: factSlots.length,
267
- filled: filledSlots,
268
- partial: partialSlots,
269
- missingOrConflicting: missingSlots,
270
- },
600
+ sourceIndex,
601
+ allSourceIndex,
602
+ claimSummary,
603
+ factSlotSummary: slots,
604
+ sectionCounts,
605
+ renderWarnings: warnings,
271
606
  };
272
607
  }
273
608
 
274
- export default async function renderExecutive({
275
- sources,
276
- options = {},
277
- context = {},
278
- }) {
609
+ function stripLeadingHeading(markdown) {
610
+ return String(markdown ?? "").replace(/^#\s+[^\n]+\n*/i, "");
611
+ }
612
+
613
+ export default async function renderExecutive({ sources, options = {}, context = {} }) {
279
614
  const control =
280
615
  findSource(sources, "final-audit") ??
281
616
  sources?.[Object.keys(sources ?? {})[0]];
282
- const opts = {
283
- maxWords: Number(options.maxWords ?? 600),
284
- maxUrls: Number(options.maxUrls ?? 5),
285
- maxFindings: Number(options.maxFindings ?? 3),
286
- maxRecommendations: Number(options.maxRecommendations ?? 3),
287
- maxGaps: Number(options.maxGaps ?? 2),
288
- };
289
617
  if (!control || typeof control !== "object") {
290
618
  return {
291
619
  schema: "deep-research-executive-render-v1",
292
- digest: "Executive rendering failed: missing final-audit control source.",
620
+ digest:
621
+ "Research report rendering failed: missing final-audit control source.",
293
622
  status: "blocked",
294
623
  blockers: ["missing final-audit control source"],
295
624
  executiveMarkdown: "",
625
+ reportMarkdown: "",
296
626
  wordCount: 0,
297
627
  sourceUrlCount: 0,
298
- gates: { maxWords: opts.maxWords, maxUrls: opts.maxUrls, passed: false },
628
+ renderWarnings: [],
629
+ gates: {
630
+ renderedAllStructuredItems: false,
631
+ passed: false,
632
+ },
299
633
  };
300
634
  }
301
635
 
302
- const rendered = renderExecutiveMarkdown(control, opts);
303
- const wordCount = countWords(rendered.markdown);
304
- const sourceUrlCount = rendered.sourceUrls.length;
305
- const passed = wordCount <= opts.maxWords && sourceUrlCount <= opts.maxUrls;
636
+ const opts = {
637
+ maxWords: Number.isFinite(Number(options.maxWords))
638
+ ? Math.max(0, Number(options.maxWords))
639
+ : Infinity,
640
+ maxUrls: Number.isFinite(Number(options.maxUrls))
641
+ ? Math.max(0, Number(options.maxUrls))
642
+ : Infinity,
643
+ maxFindings: Number.isFinite(Number(options.maxFindings))
644
+ ? Math.max(0, Number(options.maxFindings))
645
+ : undefined,
646
+ maxRecommendations: Number.isFinite(Number(options.maxRecommendations))
647
+ ? Math.max(0, Number(options.maxRecommendations))
648
+ : undefined,
649
+ maxGaps: Number.isFinite(Number(options.maxGaps))
650
+ ? Math.max(0, Number(options.maxGaps))
651
+ : undefined,
652
+ };
653
+ const rendered = renderResearchMarkdown(control, opts);
654
+ let markdown = rendered.markdown;
655
+ let truncated = false;
656
+ if (Number.isFinite(opts.maxWords) && countWords(markdown) > opts.maxWords) {
657
+ truncated = true;
658
+ markdown = truncateWords(markdown, opts.maxWords);
659
+ }
660
+ const wordCount = countWords(markdown);
661
+ const sourceUrlCount = rendered.sourceIndex.length;
662
+ const substantiveRenderWarnings = rendered.renderWarnings.filter(
663
+ (warning) => warning.section !== "sourceUrls",
664
+ );
665
+ const renderedAllStructuredItems = substantiveRenderWarnings.length === 0;
666
+ const truncatedWithOpenGaps =
667
+ truncated && Number(rendered.sectionCounts.caveatsAndGaps ?? 0) > 0;
668
+ const passed = renderedAllStructuredItems && !truncatedWithOpenGaps;
306
669
 
307
- // Best-effort sidecar for local inspection. The control field is still the
308
- // authoritative workflow artifact; this file is a convenience view.
309
- let sidecarPath;
670
+ let executiveSidecarPath;
671
+ let reportSidecarPath;
310
672
  try {
311
673
  if (context.cwd && context.runId && context.taskId) {
312
674
  const taskDir = join(
@@ -318,36 +680,47 @@ export default async function renderExecutive({
318
680
  context.taskId,
319
681
  );
320
682
  await mkdir(taskDir, { recursive: true });
321
- sidecarPath = join(taskDir, "executive.md");
322
- await writeFile(sidecarPath, `${rendered.markdown}\n`, "utf8");
683
+ executiveSidecarPath = join(taskDir, "executive.md");
684
+ reportSidecarPath = join(taskDir, "report.md");
685
+ await writeFile(executiveSidecarPath, `${markdown}\n`, "utf8");
686
+ await writeFile(reportSidecarPath, `${markdown}\n`, "utf8");
323
687
  }
324
688
  } catch {
325
- // Sidecar is non-authoritative; keep control output deterministic.
689
+ // Sidecars are non-authoritative; keep control output deterministic.
326
690
  }
327
691
 
328
692
  return {
329
693
  schema: "deep-research-executive-render-v1",
330
- digest: truncateWords(
331
- rendered.markdown.replace(/^# Executive summary\s*/i, ""),
332
- 45,
333
- ),
694
+ digest: truncateWords(stripLeadingHeading(markdown), 45),
334
695
  status: passed ? "passed" : "failed",
335
- executiveMarkdown: rendered.markdown,
696
+ renderMode: "evidence-backed-report",
697
+ executiveMarkdown: markdown,
698
+ reportMarkdown: markdown,
336
699
  wordCount,
337
700
  sourceUrlCount,
338
- sourceUrls: rendered.sourceUrls,
339
- claimSummary: rendered.counts,
340
- factSlotSummary: rendered.factSlots,
701
+ totalSourceUrlCount: rendered.allSourceIndex.length,
702
+ sourceUrls: rendered.sourceIndex,
703
+ sourceIndex: rendered.sourceIndex.map((url) => ({
704
+ url,
705
+ host: hostOf(url),
706
+ })),
707
+ claimSummary: rendered.claimSummary,
708
+ factSlotSummary: rendered.factSlotSummary,
709
+ sectionCounts: rendered.sectionCounts,
710
+ renderWarnings: rendered.renderWarnings,
341
711
  gates: {
342
- maxWords: opts.maxWords,
343
- maxUrls: opts.maxUrls,
712
+ renderedAllStructuredItems,
713
+ maxWords: Number.isFinite(opts.maxWords) ? opts.maxWords : null,
714
+ maxUrls: Number.isFinite(opts.maxUrls) ? opts.maxUrls : null,
344
715
  maxFindings: opts.maxFindings,
345
716
  maxRecommendations: opts.maxRecommendations,
346
717
  maxGaps: opts.maxGaps,
347
- truncated: rendered.truncated,
718
+ truncated,
719
+ truncatedWithOpenGaps,
348
720
  passed,
349
721
  },
350
722
  auditArtifact: "final-audit.control.json",
351
- ...(sidecarPath ? { sidecarPath } : {}),
723
+ ...(executiveSidecarPath ? { sidecarPath: "executive.md" } : {}),
724
+ ...(reportSidecarPath ? { reportSidecarPath: "report.md" } : {}),
352
725
  };
353
726
  }