@selesai/code 0.1.19 → 0.2.1

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 (157) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -0
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +8 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/skills.d.ts.map +1 -1
  23. package/dist/core/skills.js +25 -11
  24. package/dist/core/skills.js.map +1 -1
  25. package/dist/defaults/models.json +143 -39
  26. package/dist/defaults/settings.json +2 -29
  27. package/dist/extensions/package.json +5 -4
  28. package/dist/extensions/pi-intercom/index.ts +12 -5
  29. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  30. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  31. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  32. package/dist/extensions/pi-subagents/README.md +12 -5
  33. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  34. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  35. package/dist/extensions/pi-subagents/package.json +8 -5
  36. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  37. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  39. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  40. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  41. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  42. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  43. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  44. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  45. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  46. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  48. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  51. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  52. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  53. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  54. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  56. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  57. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  58. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  59. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  60. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  62. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  63. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  66. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  68. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  69. package/dist/extensions/pi-web-agent/package.json +31 -0
  70. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  71. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  72. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  73. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  74. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  75. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  76. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  77. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  78. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  82. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  94. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  101. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  102. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  103. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  104. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  108. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  109. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  110. package/dist/extensions/ponytail/index.js +189 -0
  111. package/dist/extensions/ponytail/package.json +8 -0
  112. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  113. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  114. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  115. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  116. package/dist/extensions/question/constants.ts +38 -0
  117. package/dist/extensions/question/dialog-adapter.ts +74 -0
  118. package/dist/extensions/question/helpers.ts +102 -0
  119. package/dist/extensions/question/index.ts +671 -0
  120. package/dist/extensions/question/navigation.ts +14 -0
  121. package/dist/extensions/question/package.json +18 -0
  122. package/dist/extensions/question/question-list.ts +498 -0
  123. package/dist/extensions/question/row-layout.ts +162 -0
  124. package/dist/extensions/question/schemas.ts +69 -0
  125. package/dist/extensions/question/selection-mode.ts +49 -0
  126. package/dist/extensions/question/shortcuts.ts +53 -0
  127. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  128. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  129. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  130. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  131. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  132. package/dist/extensions/question/tui-adapter.ts +33 -0
  133. package/dist/extensions/question/types.ts +102 -0
  134. package/dist/extensions/question/ui-protocol.ts +46 -0
  135. package/dist/extensions/tokenin-onboarding.ts +265 -2
  136. package/dist/extensions/web-agent-onboarding.ts +222 -0
  137. package/dist/extensions/workflow/adapter.ts +588 -0
  138. package/dist/extensions/workflow/extension.ts +24 -0
  139. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  140. package/dist/extensions/workflow/modes/quick.ts +138 -0
  141. package/dist/extensions/workflow/package.json +17 -0
  142. package/dist/extensions/workflow/state-machine.ts +386 -0
  143. package/dist/index.d.ts +1 -1
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -1
  146. package/dist/index.js.map +1 -1
  147. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.js +57 -13
  150. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  151. package/dist/utils/mime.d.ts.map +1 -1
  152. package/dist/utils/mime.js +24 -1
  153. package/dist/utils/mime.js.map +1 -1
  154. package/docs/docs.json +4 -0
  155. package/docs/workflows.md +206 -0
  156. package/package.json +5 -1
  157. package/README.md +0 -198
@@ -0,0 +1,67 @@
1
+ import { extractReadableContentSafely } from '../extract/readability.js';
2
+ import type { WebFetchResponse } from '../types.js';
3
+
4
+ function looksLikeScriptShell(html: string): boolean {
5
+ const lower = html.toLowerCase();
6
+ return lower.includes('<script') && (lower.includes('id="app"') || lower.includes('id="root"'));
7
+ }
8
+
9
+ function isWeakHttpContent(options: { html: string; title?: string; text: string }): boolean {
10
+ const normalizedText = options.text.replace(/\s+/g, ' ').trim();
11
+ const normalizedHtml = options.html.replace(/\s+/g, ' ').trim();
12
+ const textLength = normalizedText.length;
13
+ const htmlLength = normalizedHtml.length;
14
+ const hasGenericShellMarker = /enable javascript|javascript required|please turn on javascript/i.test(
15
+ options.html
16
+ );
17
+ const veryShortBody = textLength > 0 && textLength < 120;
18
+ const lowDensity = htmlLength > 0 && textLength / htmlLength < 0.02;
19
+
20
+ return veryShortBody && (lowDensity || hasGenericShellMarker);
21
+ }
22
+
23
+ export function createHttpFetcher({
24
+ fetchImpl = fetch
25
+ }: {
26
+ fetchImpl?: typeof fetch;
27
+ } = {}) {
28
+ return async function httpFetch(url: string): Promise<WebFetchResponse> {
29
+ const response = await fetchImpl(url);
30
+ const contentType = response.headers.get('content-type') ?? '';
31
+
32
+ if (!contentType.includes('text/html')) {
33
+ return {
34
+ status: 'unsupported',
35
+ url: response.url,
36
+ metadata: { method: 'http', cacheHit: false, contentType }
37
+ };
38
+ }
39
+
40
+ const html = await response.text();
41
+ const extraction = extractReadableContentSafely(html);
42
+ const content = extraction.content;
43
+
44
+ if (
45
+ looksLikeScriptShell(html) ||
46
+ content.text.length < 40 ||
47
+ isWeakHttpContent({ html, title: content.title, text: content.text })
48
+ ) {
49
+ return {
50
+ status: 'needs_headless',
51
+ url: response.url,
52
+ metadata: { method: 'http', cacheHit: false, contentType },
53
+ error: {
54
+ code: 'WEAK_EXTRACTION',
55
+ message: 'HTTP extraction was not reliable enough.'
56
+ }
57
+ };
58
+ }
59
+
60
+ return {
61
+ status: 'ok',
62
+ url: response.url,
63
+ content,
64
+ metadata: { method: 'http', cacheHit: false, contentType, truncated: content.text.length >= 4000 }
65
+ };
66
+ };
67
+ }
@@ -0,0 +1,60 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+ import type { EvidenceCaveatReason } from './evidence-quality.js';
3
+
4
+ function normalizeSummary(summary: string) {
5
+ return summary.replace(/\s+/g, ' ').trim();
6
+ }
7
+
8
+ function sentenceForReason(reason: EvidenceCaveatReason): string {
9
+ switch (reason) {
10
+ case 'community-only':
11
+ return 'the strongest readable sources were mostly community/practical context';
12
+ case 'low-diversity':
13
+ return 'the source set was narrow';
14
+ case 'unreadable-direct-source':
15
+ return 'one or more linked sources could not be read reliably';
16
+ case 'unreadable-thread-source':
17
+ return 'one or more thread sources could not be read reliably';
18
+ case 'possible-conflict':
19
+ return 'readable sources include cautionary or possibly conflicting guidance';
20
+ case 'bot-check':
21
+ return 'some candidate sources showed bot-check or security verification pages';
22
+ }
23
+ }
24
+
25
+ function joinReasons(reasons: string[]) {
26
+ if (reasons.length === 0) return '';
27
+ if (reasons.length === 1) return reasons[0];
28
+ return `${reasons.slice(0, -1).join(', ')}, and ${reasons.at(-1)}`;
29
+ }
30
+
31
+ function caveatText(partial: boolean, caveatReasons: EvidenceCaveatReason[] = []) {
32
+ if (!partial) return undefined;
33
+ const specificReasons = caveatReasons.map(sentenceForReason);
34
+ if (specificReasons.length > 0) {
35
+ return `Evidence is partial: ${joinReasons(specificReasons)}.`;
36
+ }
37
+ return 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.';
38
+ }
39
+
40
+ export function synthesizeAnswer({
41
+ evidence,
42
+ partial,
43
+ caveatReasons = []
44
+ }: {
45
+ evidence: ResearchEvidence[];
46
+ partial: boolean;
47
+ caveatReasons?: EvidenceCaveatReason[];
48
+ }) {
49
+ const findings = evidence.slice(0, 5).map((item) => {
50
+ const summary = normalizeSummary(item.summary);
51
+ return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
52
+ ? `Community/practical context: ${summary}`
53
+ : summary;
54
+ });
55
+
56
+ return {
57
+ findings,
58
+ caveat: caveatText(partial, caveatReasons)
59
+ };
60
+ }
@@ -0,0 +1,50 @@
1
+ import type { SearchResult } from '../types.js';
2
+ import { classifySourceProfile } from './source-profile.js';
3
+
4
+ function wantsDiscussionSources(query = '') {
5
+ return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
6
+ }
7
+
8
+ function candidateScore(result: SearchResult, query?: string) {
9
+ const url = result.url.toLowerCase();
10
+ const profile = classifySourceProfile(result.url);
11
+ const wantsThreads = wantsDiscussionSources(query);
12
+
13
+ if (profile.kind === 'official-docs') return 0;
14
+ if (profile.kind === 'official-api') return 1;
15
+
16
+ if (wantsThreads) {
17
+ if (profile.kind === 'forum-thread') return 2;
18
+ if (profile.kind === 'issue-thread') return 3;
19
+ if (url.includes('github.com/')) return 4;
20
+ if (profile.kind === 'package-page') return 6;
21
+ return 5;
22
+ }
23
+
24
+ if (profile.kind === 'issue-thread') return 2;
25
+ if (url.includes('github.com/')) return 3;
26
+ if (profile.kind === 'package-page') return 5;
27
+ return 4;
28
+ }
29
+
30
+ export function selectCandidates({
31
+ query,
32
+ results,
33
+ seenUrls,
34
+ maxCandidates
35
+ }: {
36
+ query?: string;
37
+ results: SearchResult[];
38
+ seenUrls: Set<string>;
39
+ maxCandidates: number;
40
+ }) {
41
+ const deduped = new Map<string, SearchResult>();
42
+ for (const result of results) {
43
+ if (seenUrls.has(result.url)) continue;
44
+ if (!deduped.has(result.url)) deduped.set(result.url, result);
45
+ }
46
+
47
+ return [...deduped.values()]
48
+ .sort((left, right) => candidateScore(left, query) - candidateScore(right, query))
49
+ .slice(0, maxCandidates);
50
+ }
@@ -0,0 +1,52 @@
1
+ const TRACKING_PARAMS = new Set([
2
+ 'utm_source',
3
+ 'utm_medium',
4
+ 'utm_campaign',
5
+ 'utm_term',
6
+ 'utm_content',
7
+ 'utm_name',
8
+ 'fbclid',
9
+ 'gclid'
10
+ ]);
11
+
12
+ function stripTrailingPunctuation(raw: string): string {
13
+ let next = raw.trim();
14
+
15
+ while (/[),.;!?\]]$/.test(next)) {
16
+ const last = next.at(-1);
17
+ if (last === ')' && next.includes('(') && next.lastIndexOf('(') > next.lastIndexOf(')')) break;
18
+ next = next.slice(0, -1);
19
+ }
20
+
21
+ return next;
22
+ }
23
+
24
+ function normalizeDirectUrl(raw: string): string | undefined {
25
+ try {
26
+ const url = new URL(stripTrailingPunctuation(raw));
27
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined;
28
+
29
+ for (const key of [...url.searchParams.keys()]) {
30
+ if (TRACKING_PARAMS.has(key.toLowerCase())) {
31
+ url.searchParams.delete(key);
32
+ }
33
+ }
34
+
35
+ url.hash = '';
36
+ return url.toString().replace(/\/$/, '');
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ export function extractDirectUrls(query: string): string[] {
43
+ const matches = query.match(/https?:\/\/\S+/gi) ?? [];
44
+ const urls = new Set<string>();
45
+
46
+ for (const match of matches) {
47
+ const normalized = normalizeDirectUrl(match);
48
+ if (normalized) urls.add(normalized);
49
+ }
50
+
51
+ return [...urls];
52
+ }
@@ -0,0 +1,105 @@
1
+ import type { ResearchEvidence, ResearchGap, ResearchLowValueOutcome } from './research-types.js';
2
+
3
+ export type EvidenceCaveatReason =
4
+ | 'community-only'
5
+ | 'low-diversity'
6
+ | 'unreadable-direct-source'
7
+ | 'unreadable-thread-source'
8
+ | 'possible-conflict'
9
+ | 'bot-check';
10
+
11
+ export type EvidenceQualityReport = {
12
+ counts: {
13
+ total: number;
14
+ official: number;
15
+ community: number;
16
+ thread: number;
17
+ packagePage: number;
18
+ distinctHosts: number;
19
+ };
20
+ flags: {
21
+ hasOfficialEvidence: boolean;
22
+ hasOnlyCommunityEvidence: boolean;
23
+ hasLowDiversity: boolean;
24
+ hasUnreadableDirectSource: boolean;
25
+ hasUnreadableThreadSource: boolean;
26
+ hasPossibleConflict: boolean;
27
+ };
28
+ caveatReasons: EvidenceCaveatReason[];
29
+ };
30
+
31
+ function hostname(url: string) {
32
+ try {
33
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, '');
34
+ } catch {
35
+ return url.toLowerCase();
36
+ }
37
+ }
38
+
39
+ function hasConflictMarkers(evidence: ResearchEvidence[]) {
40
+ const combined = evidence
41
+ .flatMap((item) => [item.summary, ...item.supports])
42
+ .join('\n')
43
+ .toLowerCase();
44
+
45
+ const caution = /\bdeprecated\b|not recommended|use at your own risk|should not/.test(combined);
46
+ const positiveText = combined.replace(/not recommended/g, '');
47
+ const positive = /\brecommended\b/.test(positiveText);
48
+ return positive && caution;
49
+ }
50
+
51
+ function addReason(reasons: EvidenceCaveatReason[], reason: EvidenceCaveatReason, enabled: boolean) {
52
+ if (enabled && !reasons.includes(reason)) reasons.push(reason);
53
+ }
54
+
55
+ export function analyzeEvidenceQuality({
56
+ evidence,
57
+ gaps,
58
+ lowValueOutcomes
59
+ }: {
60
+ evidence: ResearchEvidence[];
61
+ gaps: ResearchGap[];
62
+ lowValueOutcomes: ResearchLowValueOutcome[];
63
+ }): EvidenceQualityReport {
64
+ const official = evidence.filter((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api').length;
65
+ const community = evidence.filter((item) => item.sourceKind === 'community').length;
66
+ const thread = evidence.filter((item) => item.sourceKind === 'issue-thread' || item.sourceKind === 'official-discussion').length;
67
+ const packagePage = evidence.filter((item) => item.sourceKind === 'package-page').length;
68
+ const distinctHosts = new Set(evidence.map((item) => hostname(item.url))).size;
69
+
70
+ const hasOfficialEvidence = official > 0;
71
+ const hasOnlyCommunityEvidence = evidence.length > 0 && official === 0;
72
+ const hasLowDiversity = evidence.length > 1 && distinctHosts <= 1;
73
+ const hasUnreadableDirectSource = gaps.some((gap) => /Direct URL could not be read reliably/i.test(gap.message));
74
+ const hasUnreadableThreadSource = gaps.some((gap) => /Thread source could not be read reliably/i.test(gap.message));
75
+ const hasPossibleConflict = hasConflictMarkers(evidence);
76
+ const hasBotCheck = lowValueOutcomes.some((outcome) => outcome.kind === 'bot-check');
77
+
78
+ const caveatReasons: EvidenceCaveatReason[] = [];
79
+ addReason(caveatReasons, 'community-only', hasOnlyCommunityEvidence);
80
+ addReason(caveatReasons, 'low-diversity', hasLowDiversity);
81
+ addReason(caveatReasons, 'unreadable-direct-source', hasUnreadableDirectSource);
82
+ addReason(caveatReasons, 'unreadable-thread-source', hasUnreadableThreadSource);
83
+ addReason(caveatReasons, 'possible-conflict', hasPossibleConflict);
84
+ addReason(caveatReasons, 'bot-check', hasBotCheck);
85
+
86
+ return {
87
+ counts: {
88
+ total: evidence.length,
89
+ official,
90
+ community,
91
+ thread,
92
+ packagePage,
93
+ distinctHosts
94
+ },
95
+ flags: {
96
+ hasOfficialEvidence,
97
+ hasOnlyCommunityEvidence,
98
+ hasLowDiversity,
99
+ hasUnreadableDirectSource,
100
+ hasUnreadableThreadSource,
101
+ hasPossibleConflict
102
+ },
103
+ caveatReasons
104
+ };
105
+ }
@@ -0,0 +1,45 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+
3
+ function sourceRank(sourceKind: ResearchEvidence['sourceKind']) {
4
+ switch (sourceKind) {
5
+ case 'official-docs':
6
+ return 0;
7
+ case 'official-api':
8
+ return 1;
9
+ case 'official-discussion':
10
+ return 2;
11
+ case 'issue-thread':
12
+ return 3;
13
+ case 'community':
14
+ return 4;
15
+ case 'package-page':
16
+ return 5;
17
+ default:
18
+ return 6;
19
+ }
20
+ }
21
+
22
+ export function rankEvidence(evidence: ResearchEvidence[]) {
23
+ const bestByUrl = new Map<string, ResearchEvidence>();
24
+ for (const item of evidence) {
25
+ const current = bestByUrl.get(item.url);
26
+ if (!current || sourceRank(item.sourceKind) < sourceRank(current.sourceKind)) {
27
+ bestByUrl.set(item.url, item);
28
+ }
29
+ }
30
+
31
+ return [...bestByUrl.values()].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
32
+ }
33
+
34
+ export function hasOfficialEvidence(evidence: ResearchEvidence[]) {
35
+ return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
36
+ }
37
+
38
+ export function strongEvidenceCount(evidence: ResearchEvidence[]) {
39
+ return evidence.filter(
40
+ (item) =>
41
+ item.sourceKind === 'official-docs' ||
42
+ item.sourceKind === 'official-api' ||
43
+ item.sourceKind === 'official-discussion'
44
+ ).length;
45
+ }
@@ -0,0 +1,28 @@
1
+ import { createBackendSet } from '../backends/factory.js';
2
+ import type { BackendConfig } from '../backends/config.js';
3
+ import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
4
+ import { createResearchOrchestrator } from './research-orchestrator.js';
5
+ import { createResearchWorker } from './research-worker.js';
6
+
7
+ export function createResearchWorkflow({
8
+ backendConfig,
9
+ search,
10
+ fetchPage,
11
+ headlessFetch
12
+ }: {
13
+ backendConfig?: BackendConfig;
14
+ search?: (input: { query: string }) => Promise<WebSearchResponse>;
15
+ fetchPage?: (input: { url: string }) => Promise<WebFetchResponse>;
16
+ headlessFetch?: (input: { url: string }) => Promise<WebFetchHeadlessResponse>;
17
+ } = {}) {
18
+ const backends = createBackendSet(backendConfig);
19
+ const resolvedSearch = search ?? backends.search;
20
+ const resolvedFetchPage = fetchPage ?? backends.fetchPage;
21
+ const resolvedHeadlessFetch = headlessFetch ?? backends.headlessFetch;
22
+ const worker = createResearchWorker({ search: resolvedSearch, fetchPage: resolvedFetchPage });
23
+ return createResearchOrchestrator({
24
+ worker,
25
+ fetchDirect: resolvedFetchPage,
26
+ headlessFetch: resolvedHeadlessFetch
27
+ });
28
+ }
@@ -0,0 +1,47 @@
1
+ export type QueryPlanInput = {
2
+ originalQuery: string;
3
+ passIndex: number;
4
+ previousQueries: string[];
5
+ gaps: string[];
6
+ };
7
+
8
+ function normalizeWhitespace(value: string) {
9
+ return value.replace(/\s+/g, ' ').trim();
10
+ }
11
+
12
+ function extractImportantWords(query: string) {
13
+ return normalizeWhitespace(query)
14
+ .replace(/\b(find|current|tell|me|how|to|the|and|with|for|in|a|an)\b/gi, ' ')
15
+ .replace(/\s+/g, ' ')
16
+ .trim();
17
+ }
18
+
19
+ function officialSiteQuery(query: string) {
20
+ const lower = query.toLowerCase();
21
+ const terms = extractImportantWords(query);
22
+ if (lower.includes('vitest')) return `site:vitest.dev ${terms}`;
23
+ if (lower.includes('playwright')) return `site:playwright.dev ${terms}`;
24
+ if (lower.includes('microsoft edge') || lower.includes('edge')) return `site:learn.microsoft.com ${terms}`;
25
+ return `${terms} official docs`;
26
+ }
27
+
28
+ function implementationQuery(query: string) {
29
+ return `site:github.com ${extractImportantWords(query)}`;
30
+ }
31
+
32
+ export function planSearchQueries(input: QueryPlanInput): string[] {
33
+ const planned =
34
+ input.passIndex === 0
35
+ ? [input.originalQuery]
36
+ : [
37
+ officialSiteQuery(input.originalQuery),
38
+ implementationQuery(input.originalQuery),
39
+ `${extractImportantWords(input.originalQuery)} discussion`
40
+ ];
41
+
42
+ const previous = new Set(input.previousQueries.map(normalizeWhitespace));
43
+ return planned
44
+ .map(normalizeWhitespace)
45
+ .filter((query) => query && !previous.has(query))
46
+ .slice(0, 3);
47
+ }