pi-workflow-engine 0.12.0 → 1.0.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.
Files changed (92) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
  6. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
  7. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  17. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
  18. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  19. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  20. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
  21. package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
  22. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  23. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  24. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  25. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  31. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  32. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  33. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  34. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  35. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  36. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  37. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  38. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
  39. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
  40. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  41. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  52. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  53. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  54. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
  55. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  56. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  57. package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
  58. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  59. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  61. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  62. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  68. package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  86. package/USAGE.md +90 -17
  87. package/package.json +1 -2
  88. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  89. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  90. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
  91. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  92. package/.pi/settings.json +0 -3
@@ -1,25 +1,24 @@
1
+ import { challengeFindings, parseChallengeArgs } from "../src/advisory-challenge.ts";
1
2
  import { Type } from "typebox";
2
3
  import {
3
- AdvisoryReportSchema,
4
- type AdvisoryCandidate,
5
- type AdvisoryVerdict,
6
- } from "../src/advisory-schema.ts";
7
- import {
8
- backfillAdvisoryFindings,
4
+ type AdvisoryVerified,
5
+ type AdvisoryLens,
6
+ concludeLensReview,
7
+ finishAdvisoryReport,
9
8
  emptyAdvisoryReport,
10
- formatEvidence,
9
+ EMPTY_LENS_REVIEW_STATS,
11
10
  formatLocation,
12
- publishVerifiedKeptProgress,
11
+ formatRankedFinding,
13
12
  runLensVerificationPipeline,
14
13
  DEFAULT_ADVISORY_TOOL_HINTS,
15
14
  DEFAULT_ADVISORY_TOOLS,
16
15
  } from "../src/workflow-advisory-utils.ts";
17
- import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
16
+ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
18
17
 
19
18
  export const meta: WorkflowMeta = {
20
19
  name: "refactor-scout",
21
20
  description: "Advisory-only refactor scout: scope → per-lens find → independent verify → synthesize safe refactor opportunities.",
22
- phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Synthesize" }],
21
+ phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
23
22
  };
24
23
 
25
24
  const ScopeSchema = Type.Object({
@@ -29,22 +28,7 @@ const ScopeSchema = Type.Object({
29
28
  conventions: Type.Optional(Type.String({ description: "Relevant project conventions from AGENTS.md / docs." })),
30
29
  });
31
30
 
32
- interface RefactorLens {
33
- label: string;
34
- category: string;
35
- text: string;
36
- }
37
-
38
- type Candidate = AdvisoryCandidate;
39
-
40
- interface Verified extends Candidate {
41
- verdict: AdvisoryVerdict["verdict"];
42
- evidence: string[];
43
- confidence?: AdvisoryVerdict["confidence"];
44
- lens: RefactorLens;
45
- }
46
-
47
- const REFACTOR_LENSES: RefactorLens[] = [
31
+ const REFACTOR_LENSES: AdvisoryLens[] = [
48
32
  { label: "duplication", category: "duplication", text: "Repeated logic, copy-pasted structures, or near-duplicate flows that could share one clearer implementation." },
49
33
  { label: "complexity", category: "complexity", text: "Oversized functions, tangled control flow, or abstractions that make local reasoning harder than necessary." },
50
34
  { label: "type-safety", category: "type-safety", text: "Weak typing, avoidable casts, unchecked shapes, or places stronger types would prevent mistakes." },
@@ -53,25 +37,12 @@ const REFACTOR_LENSES: RefactorLens[] = [
53
37
  { label: "conventions", category: "conventions", text: "Departures from project conventions, naming, dependency rules, or local idioms." },
54
38
  ];
55
39
 
56
- const TOOLS = DEFAULT_ADVISORY_TOOLS;
57
- const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
58
40
  const PER_LENS = 5;
59
41
 
60
42
  export default async function run(api: WorkflowApi): Promise<unknown> {
61
- const { agent, parallel, pipeline, phase, log, progress, args } = api;
62
- const target = args.trim() || ".";
63
- let fileCount = 0;
64
- let rawCandidateCount = 0;
65
- let droppedCandidateCount = 0;
66
- let refutedCandidateCount = 0;
67
- const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
68
- files: fileCount,
69
- candidates: rawCandidateCount,
70
- verified,
71
- kept,
72
- dropped: droppedCandidateCount,
73
- refuted: refutedCandidateCount,
74
- });
43
+ const { agent, phase, log, progress, args } = api;
44
+ const challengeConfig = parseChallengeArgs(args);
45
+ const target = challengeConfig.args || ".";
75
46
 
76
47
  phase("Scope");
77
48
  const scope = await agent(
@@ -80,35 +51,30 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
80
51
  "Inspect repository structure, the target path or module, and relevant AGENTS.md / project docs conventions. " +
81
52
  "Return the concrete files that should be considered, a short summary, and any conventions that affect refactor advice. " +
82
53
  `This workflow will fan out across ${REFACTOR_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
83
- { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
54
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
84
55
  );
85
56
 
86
- if (!scope || scope.files.length === 0) {
87
- return emptyAdvisoryReport(
57
+ if (scope.files.length === 0) {
58
+ return finishAdvisoryReport(emptyAdvisoryReport(
88
59
  "No files were identified for refactor scouting.",
89
60
  ["Provide a target path, module, or subsystem to scout for refactor opportunities."],
90
- makeStats(0, 0),
91
- );
61
+ EMPTY_LENS_REVIEW_STATS,
62
+ ), []);
92
63
  }
93
64
 
94
- fileCount = scope.files.length;
95
- progress({ type: "counter", key: "files", label: "files", value: fileCount });
65
+ progress({ type: "counter", key: "files", label: "files", value: scope.files.length });
96
66
  progress({ type: "summary", key: "files", value: scope.files.join(", ") });
97
67
  log(`${scope.files.length} files scoped for refactor scouting`);
98
68
 
99
69
  const scopeBlock =
100
70
  `## Target\n${scope.target}\n\n## Files in scope\n${scope.files.map((file) => `- ${file}`).join("\n")}\n\n` +
101
71
  `## Summary\n${scope.summary}\n\n## Conventions\n${scope.conventions ?? "(none noted)"}\n` +
102
- (args.trim() ? `\n## User instructions (verbatim)\n${args.trim()}\n` : "");
72
+ (challengeConfig.args ? `\n## User instructions (verbatim)\n${challengeConfig.args}\n` : "");
103
73
 
104
- phase("Find");
105
74
  const pipelineResult = await runLensVerificationPipeline({
106
- api: { agent, parallel, pipeline, progress, log },
75
+ api,
107
76
  lenses: REFACTOR_LENSES,
108
- schedulingMode: "finder-barrier",
109
77
  perLens: PER_LENS,
110
- tools: TOOLS,
111
- toolHints: TOOL_HINTS,
112
78
  finderPrompt: (lens) =>
113
79
  `## Refactor-scout finder — ${lens.label}\n\n${scopeBlock}\n` +
114
80
  "This workflow is advisory-only: do not edit files and do not propose broad rewrites.\n" +
@@ -120,65 +86,29 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
120
86
  `## Refactor-scout verifier\n\n${scopeBlock}\n## Candidate\n` +
121
87
  `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
122
88
  `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
123
- "Read the relevant files and return CONFIRMED, PLAUSIBLE, or REFUTED. " +
89
+ "Read the relevant files and return CONFIRMED, PLAUSIBLE, NOT_SUBSTANTIATED, or REFUTED. " +
124
90
  "Default toward REFUTED if the opportunity is generic, too broad, not evidenced by code, or lacks a safe first step. " +
125
91
  "Evidence must quote or cite code. Structured output only.",
126
- makeVerified: (candidate, lens, judged): Verified => ({
127
- ...candidate,
128
- verdict: judged.verdict,
129
- evidence: judged.evidence,
130
- confidence: judged.confidence,
131
- lens,
132
- }),
133
92
  });
134
- rawCandidateCount += pipelineResult.rawCandidates;
135
- droppedCandidateCount += pipelineResult.dropped;
136
- refutedCandidateCount += pipelineResult.refuted;
137
- const verified = pipelineResult.verified;
138
- const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
139
- const stats = makeStats(verified.length, surviving.length);
140
- publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
141
-
142
- if (surviving.length === 0) {
143
- return emptyAdvisoryReport("No refactor opportunities survived verification.", ["Leave the scoped code unchanged unless a human reviewer has additional context."], stats);
144
- }
145
-
146
- phase("Synthesize");
147
- const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
148
- const block = ranked
149
- .map(
150
- (finding, index) =>
151
- `### [${index}] ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
152
- `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\nSafe first step: ${finding.recommendation ?? "(none supplied)"}`,
153
- )
154
- .join("\n\n");
155
-
156
- const report = await agent(
157
- `## Synthesis: final refactor-scout report\n\n${ranked.length} opportunities survived independent verification.\n\n${block}\n\n` +
93
+ const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, pipelineResult.coverage);
94
+ return concludeLensReview(api, pipelineResult, verified, {
95
+ files: scope.files.length,
96
+ rank,
97
+ formatFinding: (finding, index) => formatRankedFinding(finding, index, "Safe first step"),
98
+ empty: {
99
+ summary: "No refactor opportunities survived verification.",
100
+ nextSteps: ["Leave the scoped code unchanged unless a human reviewer has additional context."],
101
+ },
102
+ synthesisPrompt: (block, ranked) =>
103
+ `## Synthesis: final refactor-scout report\n\n${ranked.length} opportunities survived independent verification.\n\n${block}\n\n` +
158
104
  "Merge findings with the same root cause and rank highest leverage / lowest risk first. " +
159
- "Return the shared advisory report shape. Categories should come from the verified candidates. " +
160
- "Severity is maintenance or future-correctness impact. Confidence is high for CONFIRMED, medium for PLAUSIBLE unless verifier confidence says otherwise. " +
105
+ "Select findings by ID. " +
106
+ "Severity is maintenance or future-correctness impact. " +
161
107
  "Recommendations must be safe first refactor steps, not rewrites. Include concrete nextSteps for the host developer. Structured output only.",
162
- {
163
- phase: "Synthesize",
164
- label: "synthesize",
165
- tools: [],
166
- profile: "medium",
167
- resume: "read-only",
168
- schema: AdvisoryReportSchema,
169
- },
170
- );
171
-
172
- if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun the workflow with a narrower target."], stats);
173
-
174
- const findings = backfillAdvisoryFindings(report.findings, ranked, {
175
- impact: "Impact not restated by synthesis.",
176
- recommendation: "Choose a small, behavior-preserving refactor first step.",
177
108
  });
178
- return { ...report, findings, stats };
179
109
  }
180
110
 
181
- function rank(finding: Verified): number {
111
+ function rank(finding: AdvisoryVerified): number {
182
112
  const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
183
113
  const categoryRank = finding.category === "dead-code" || finding.category === "conventions" ? 2 : 0;
184
114
  return verdictRank + categoryRank;
@@ -1,4 +1,3 @@
1
- import { compactResults } from "../src/concurrency.ts";
2
1
  import {
3
2
  MAX_RESEARCH_LANES,
4
3
  ResearchLaneResultSchema,
@@ -7,18 +6,17 @@ import {
7
6
  ResearchVerificationSchema,
8
7
  type ResearchClaimCandidate,
9
8
  type ResearchReport,
10
- type ResearchVerification,
11
9
  } from "../src/research-contract.ts";
12
10
  import {
13
11
  buildClaimCandidates,
14
12
  fallbackResearchReport,
15
13
  normalizeResearchLanes,
16
- sanitizeLaneResults,
17
14
  sanitizeResearchReport,
18
15
  sanitizeVerification,
19
16
  unavailableResearchReport,
20
17
  unavailableVerification,
21
18
  } from "../src/research-evidence.ts";
19
+ import { isFatalWorkflowError } from "../src/cancellation.ts";
22
20
  import { WorkflowToolHintUnavailableError } from "../src/tool-capabilities.ts";
23
21
  import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
24
22
 
@@ -32,7 +30,7 @@ const EXTERNAL_TOOLS: string[] = [];
32
30
  const EXTERNAL_TOOL_HINTS = ["external-search"] as const;
33
31
 
34
32
  export default async function run(api: WorkflowApi): Promise<ResearchReport> {
35
- const { agent, parallel, phase, log, progress, args } = api;
33
+ const { agent, parallel, phase, log, progress, args, signal } = api;
36
34
  const question = args.trim();
37
35
  if (!question) return unavailableResearchReport("empty-question");
38
36
 
@@ -61,54 +59,54 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
61
59
  throw error;
62
60
  }
63
61
 
64
- if (!plan) return unavailableResearchReport("no-evidence");
65
62
  const lanes = normalizeResearchLanes(plan);
66
63
  if (lanes.length === 0) return unavailableResearchReport("no-evidence");
67
64
  progress({ type: "counter", key: "research.lanes", label: "research lanes", value: lanes.length });
68
65
  progress({ type: "summary", key: "research.question", value: question });
69
66
 
70
67
  phase("Gather");
71
- const gathered = compactResults(
72
- await parallel(
73
- lanes.map((lane) => async () => {
74
- const result = await agent(
75
- `Research one bounded lane using only installed external web-search, browsing, or URL-extraction tools.\n\n` +
76
- `Question: ${question}\n` +
77
- `Scope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n` +
78
- `Lane id: ${lane.id}\nLane: ${lane.title}\nObjective: ${lane.objective}\n` +
79
- `Queries:\n${lane.queries.map((query) => `- ${query}`).join("\n")}\n\n` +
80
- "Open the supporting pages instead of citing a search-results page. Prefer primary and authoritative sources; use independent sources when useful. " +
81
- "Return concrete claims with importance, whether each page supports or conflicts with the claim, a short passage or precise paraphrase, and the exact page title and URL. " +
82
- "State evidence gaps. Never invent a URL or claim that the opened page does not support. Structured output only.",
83
- {
84
- phase: "Gather",
85
- label: `gather:${lane.id}`,
86
- tools: EXTERNAL_TOOLS,
87
- toolHints: EXTERNAL_TOOL_HINTS,
88
- requireToolHints: true,
89
- profile: "small",
90
- resume: "off",
91
- schema: ResearchLaneResultSchema,
92
- },
93
- );
94
- if (!result) return null;
95
- progress({ type: "counter_delta", key: "research.evidence", label: "evidence items", delta: result.evidence.length });
96
- progress({
97
- type: "lane_item",
98
- lane: "Research lanes",
99
- title: lane.title,
100
- subtitle: `${result.evidence.length} evidence item(s)`,
101
- status: result.evidence.length > 0 ? "success" : "warning",
102
- details: result.gaps.join("; ") || lane.objective,
103
- });
104
- return result;
105
- }),
106
- ),
68
+ const gatherResults = await parallel(
69
+ lanes.map((lane) => async () => {
70
+ const result = await agent(
71
+ `Research one bounded lane using only installed external web-search, browsing, or URL-extraction tools.\n\n` +
72
+ `Question: ${question}\n` +
73
+ `Scope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n` +
74
+ `Lane: ${lane.title}\nObjective: ${lane.objective}\n` +
75
+ `Queries:\n${lane.queries.map((query) => `- ${query}`).join("\n")}\n\n` +
76
+ "Open the supporting pages instead of citing a search-results page. Prefer primary and authoritative sources; use independent sources when useful. " +
77
+ "Return concrete claims with importance, whether each page supports or conflicts with the claim, a short passage or precise paraphrase, and the exact page title and URL. " +
78
+ "State evidence gaps. Never invent a URL or claim that the opened page does not support. Structured output only.",
79
+ {
80
+ phase: "Gather",
81
+ label: `gather:${lane.id}`,
82
+ tools: EXTERNAL_TOOLS,
83
+ toolHints: EXTERNAL_TOOL_HINTS,
84
+ requireToolHints: true,
85
+ // The prompt embeds lane text planned by a web-enabled agent; never infer skills (and the read tool) from it.
86
+ skills: [],
87
+ profile: "small",
88
+ resume: "off",
89
+ schema: ResearchLaneResultSchema,
90
+ },
91
+ );
92
+ progress({ type: "counter_delta", key: "research.evidence", label: "evidence items", delta: result.evidence.length });
93
+ progress({
94
+ type: "lane_item",
95
+ lane: "Research lanes",
96
+ title: lane.title,
97
+ subtitle: `${result.evidence.length} evidence item(s)`,
98
+ status: result.evidence.length > 0 ? "success" : "warning",
99
+ details: result.gaps.join("; ") || lane.objective,
100
+ });
101
+ return result;
102
+ }),
103
+ { settled: true },
107
104
  );
105
+ const gathered = gatherResults.flatMap((result) => (result.ok ? [result.value] : []));
106
+ const failedLanes = lanes.filter((_, index) => !gatherResults[index]!.ok);
108
107
 
109
- const laneResults = sanitizeLaneResults(gathered);
110
- const candidates = buildClaimCandidates(laneResults);
111
- if (candidates.length === 0) return unavailableResearchReport("no-evidence");
108
+ const candidates = buildClaimCandidates(gathered);
109
+ if (candidates.length === 0) return unavailableResearchReport(failedLanes.length > 0 ? "lanes-failed" : "no-evidence");
112
110
  progress({ type: "counter", key: "research.claims", label: "claims to verify", value: candidates.length });
113
111
  log(`${candidates.length} bounded claim(s) selected for independent verification`);
114
112
 
@@ -121,11 +119,13 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
121
119
  tools: EXTERNAL_TOOLS,
122
120
  toolHints: EXTERNAL_TOOL_HINTS,
123
121
  requireToolHints: true,
122
+ // The prompt embeds web-derived evidence; never infer skills (and the read tool) from it.
123
+ skills: [],
124
124
  profile: "medium",
125
125
  resume: "off",
126
126
  schema: ResearchVerificationSchema,
127
127
  });
128
- return result ? sanitizeVerification(result, candidate) : null;
128
+ return sanitizeVerification(result, candidate);
129
129
  }),
130
130
  );
131
131
  const verifications = verificationResults.map((result, index) => result ?? unavailableVerification(candidates[index]!));
@@ -135,28 +135,36 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
135
135
  const synthesisInputs = verifications.filter((verification) => verification.verdict !== "REJECTED");
136
136
 
137
137
  phase("Synthesize");
138
- const synthesis = await agent(
139
- `Answer the research question using only the independently verified handoff below.\n\n` +
140
- `Question: ${question}\nScope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n\n` +
141
- `Verified claims JSON:\n${JSON.stringify(synthesisInputs)}\n\n` +
142
- "Keep SUPPORTED claims, CONFLICTED evidence, UNCERTAIN claims, and model INFERENCE in their separate fields. Exclude REJECTED claims. " +
143
- "Copy each verified claim string exactly so its citations remain bound to that claim during validation. " +
144
- "Every supported or conflicting claim must cite exact title/URL objects from its verification; do not add URLs, cite search-results pages, or turn inference into fact. " +
145
- "Answer concisely, disclose limited coverage, and provide useful next steps. Structured output only.",
146
- {
147
- phase: "Synthesize",
148
- label: "synthesize",
149
- tools: [],
150
- profile: "medium",
151
- resume: "off",
152
- schema: ResearchReportSchema,
153
- },
154
- );
138
+ let synthesis: ResearchReport;
139
+ try {
140
+ synthesis = await agent(
141
+ `Answer the research question using only the independently verified handoff below.\n\n` +
142
+ `Question: ${question}\nScope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n\n` +
143
+ `Verified claims JSON:\n${JSON.stringify(synthesisInputs)}\n\n` +
144
+ "Keep SUPPORTED claims, CONFLICTED evidence, UNCERTAIN claims, and model INFERENCE in their separate fields. Exclude REJECTED claims. " +
145
+ "Copy each verified claim string exactly so its citations remain bound to that claim during validation. " +
146
+ "Every supported or conflicting claim must cite exact title/URL objects from its verification; do not add URLs, cite search-results pages, or turn inference into fact. " +
147
+ "Answer concisely, disclose limited coverage, and provide useful next steps. Structured output only.",
148
+ {
149
+ phase: "Synthesize",
150
+ label: "synthesize",
151
+ tools: [],
152
+ // The prompt embeds web-derived claims; never infer skills (and the read tool) from them.
153
+ skills: [],
154
+ profile: "medium",
155
+ resume: "off",
156
+ schema: ResearchReportSchema,
157
+ },
158
+ );
159
+ } catch (error) {
160
+ if (isFatalWorkflowError(error, signal)) throw error;
161
+ synthesis = fallbackResearchReport(synthesisInputs);
162
+ }
155
163
 
156
- return sanitizeResearchReport(
157
- synthesis ?? fallbackResearchReport(synthesisInputs, "The model did not return a structured synthesis."),
158
- synthesisInputs,
159
- );
164
+ const report = sanitizeResearchReport(synthesis, synthesisInputs);
165
+ if (failedLanes.length === 0) return report;
166
+ const laneGap = `${failedLanes.length} of ${lanes.length} research lane(s) failed (${failedLanes.map((lane) => lane.title).join(", ")}); coverage is incomplete.`;
167
+ return { ...report, limitations: [...report.limitations, laneGap] };
160
168
  }
161
169
 
162
170
  function verificationPrompt(question: string, candidate: ResearchClaimCandidate): string {