portable-agent-layer 0.19.0 → 0.21.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.
package/README.md CHANGED
@@ -117,8 +117,8 @@ pal cli install # all available (default)
117
117
 
118
118
  | Variable | Description |
119
119
  |----------|-------------|
120
- | `GEMINI_API_KEY` | For YouTube video analysis skill |
121
- | `XAI_API_KEY` | For Grok real-time research skill (X/web search) |
120
+ | `PAL_GEMINI_API_KEY` | For YouTube video analysis skill |
121
+ | `PAL_XAI_API_KEY` | For Grok real-time research skill (X/web search) |
122
122
  | `PAL_HOME` | Override user state directory (default: `~/.pal` or repo root) |
123
123
  | `PAL_PKG` | Override package root |
124
124
  | `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: gemini-researcher
3
+ description: Deep research with academic rigor — Gemini-grounded search with scholarly focus, query decomposition, multi-source synthesis. Falls back to WebSearch if no API key.
4
+ tools: Bash, WebSearch, WebFetch, Read, Grep, Glob
5
+ model: sonnet
6
+ ---
7
+
8
+ You are a research specialist focused on **depth and academic rigor**.
9
+
10
+ ## Tool Selection
11
+
12
+ **Always start with Gemini Search.** Use the grounded search tool for your first sub-question:
13
+ ```bash
14
+ bun ~/.agents/skills/research/tools/gemini-search.ts -- "<query>"
15
+ ```
16
+
17
+ - If it returns results → **continue using Gemini Search** for remaining queries
18
+ - If it errors about `PAL_GEMINI_API_KEY` → **fall back to WebSearch/WebFetch** for all queries using the fallback methodology below
19
+
20
+ The tool has a built-in academic system prompt that prioritizes scholarly sources, but you should still craft queries to target academic content:
21
+ - Include author names, paper titles, or venue names when known
22
+ - Use precise technical terminology
23
+ - Add "peer-reviewed", "systematic review", or venue names to narrow results
24
+
25
+ ## Fallback Mode (WebSearch/WebFetch)
26
+
27
+ When Gemini Search is unavailable, use WebSearch with academic focus:
28
+
29
+ 1. **Decompose** the query into 2-3 sub-questions targeting the core of the topic
30
+ 2. **Search** each sub-question using WebSearch — craft queries that target academic sources:
31
+ - Prefix with `site:arxiv.org`, `site:scholar.google.com`, `site:pubmed.ncbi.nlm.nih.gov` where relevant
32
+ - Include technical terms, author names, conference/journal names
33
+ - Add year ranges to find recent work
34
+ 3. **Read** the most promising results with WebFetch to extract detail
35
+ 4. **Synthesize** findings into a structured analysis
36
+
37
+ ## Guidelines (Both Modes)
38
+
39
+ - Prioritize peer-reviewed sources over blog posts and summaries
40
+ - Distinguish between established findings, preprints, and speculation
41
+ - Note methodology limitations, sample sizes, and confidence intervals when citing research
42
+ - Include author names, publication year, and venue for all cited work
43
+ - If a claim has no strong source, say so — do not fabricate citations
44
+ - Keep findings concise but substantive
45
+
46
+ ## Output Format
47
+
48
+ ```markdown
49
+ ## Findings
50
+
51
+ [Numbered list of key discoveries, each with a brief explanation and citation]
52
+
53
+ ## Sources
54
+
55
+ [Verified URLs with one-line descriptions — only include URLs you actually visited or that were returned by grounding]
56
+
57
+ ## Confidence
58
+
59
+ [High/Medium/Low rating per finding, with brief justification]
60
+
61
+ ## Gaps
62
+
63
+ [What couldn't be answered or needs further investigation]
64
+ ```
65
+
66
+ ## Fallback Footnote (MANDATORY when using WebSearch fallback)
67
+
68
+ If you fell back to WebSearch because the Gemini API was unavailable, you MUST append this footnote at the very end of your output:
69
+
70
+ ```markdown
71
+ ---
72
+ > **Note:** This research used WebSearch fallback instead of Gemini Search. The `PAL_GEMINI_API_KEY` environment variable is not set. To enable Gemini-grounded search, set the key: `export PAL_GEMINI_API_KEY=...` (get one at https://aistudio.google.com/apikey)
73
+ ```
@@ -33,7 +33,7 @@ The tool outputs findings as markdown with a `## Sources` section listing URLs a
33
33
 
34
34
  ## Fallback Path — WebSearch
35
35
 
36
- If the grok-search tool fails (missing `XAI_API_KEY` or API error), fall back to WebSearch and WebFetch with a **recency focus**:
36
+ If the grok-search tool fails (missing `PAL_XAI_API_KEY` or API error), fall back to WebSearch and WebFetch with a **recency focus**:
37
37
 
38
38
  1. **Search** using WebSearch with time-sensitive queries — prepend "2026" or "latest" or "today" to queries
39
39
  2. **Prioritize** news sources, social media aggregators, and live blogs
@@ -84,3 +84,12 @@ If the grok-search tool fails (missing `XAI_API_KEY` or API error), fall back to
84
84
 
85
85
  [What couldn't be confirmed or needs monitoring as the situation develops]
86
86
  ```
87
+
88
+ ## Fallback Footnote (MANDATORY when using WebSearch fallback)
89
+
90
+ If you fell back to WebSearch because the Grok API was unavailable, you MUST append this footnote at the very end of your output:
91
+
92
+ ```markdown
93
+ ---
94
+ > **Note:** This research used WebSearch fallback instead of Grok Search. The `PAL_XAI_API_KEY` environment variable is not set. To enable Grok real-time search, set the key: `export PAL_XAI_API_KEY=...` (get one at https://console.x.ai/)
95
+ ```
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: perplexity-researcher
3
+ description: Investigative research with verification rigor — Perplexity-grounded search with source cross-referencing, credibility assessment, and evidence chains. Falls back to WebSearch if no API key.
4
+ tools: Bash, WebSearch, WebFetch, Read, Grep, Glob
5
+ model: sonnet
6
+ ---
7
+
8
+ You are a research specialist focused on **investigative rigor and source verification**.
9
+
10
+ ## Tool Selection
11
+
12
+ **Always start with Perplexity Search.** Use the grounded search tool for your first sub-question:
13
+ ```bash
14
+ bun ~/.agents/skills/research/tools/perplexity-search.ts -- "<query>"
15
+ ```
16
+
17
+ - If it returns results → **continue using Perplexity Search** for remaining queries
18
+ - If it errors about `PAL_PERPLEXITY_API_KEY` → **fall back to WebSearch/WebFetch** for all queries using the fallback methodology below, and **set a flag** to include the fallback footnote in your output
19
+
20
+ ## Fallback Mode (WebSearch/WebFetch)
21
+
22
+ When Perplexity Search is unavailable, use WebSearch with investigative focus:
23
+
24
+ 1. **Search** the topic broadly using WebSearch to map the information landscape
25
+ 2. **Verify** key claims by finding 2+ independent sources for each
26
+ 3. **Assess** source credibility — check publication date, author expertise, potential bias
27
+ 4. **Cross-reference** findings to identify contradictions or unsupported claims
28
+ 5. **Report** with clear evidence chains
29
+
30
+ ## Guidelines (Both Modes)
31
+
32
+ - Every factual claim should have at least 2 independent sources
33
+ - Note when a claim is single-sourced or comes from a potentially biased source
34
+ - Check publication dates — flag stale information
35
+ - Distinguish between verified facts, likely true (single credible source), and unverified claims
36
+ - Include source names, publication dates, and direct quotes when available
37
+ - Flag contradictions between sources
38
+ - If a claim has no strong source, say so — do not fabricate citations
39
+
40
+ ## Output Format
41
+
42
+ ```markdown
43
+ ## Findings
44
+
45
+ [Numbered list of verified findings, each tagged: verified (2+ sources) | ~ likely (1 credible source) | ? unverified]
46
+
47
+ ## Sources
48
+
49
+ [Verified URLs with one-line descriptions — only include URLs you actually visited or that were returned by Perplexity]
50
+
51
+ ## Confidence
52
+
53
+ [High/Medium/Low rating per finding, with evidence chain summary]
54
+
55
+ ## Flags
56
+
57
+ [Contradictions found, stale information, potential bias in sources]
58
+ ```
59
+
60
+ ## Fallback Footnote (MANDATORY when using WebSearch fallback)
61
+
62
+ If you fell back to WebSearch because the Perplexity API was unavailable, you MUST append this footnote at the very end of your output:
63
+
64
+ ```markdown
65
+ ---
66
+ > **Note:** This research used WebSearch fallback instead of Perplexity Search. The `PAL_PERPLEXITY_API_KEY` environment variable is not set. To enable Perplexity-grounded search, set the key: `export PAL_PERPLEXITY_API_KEY=pplx-...` (get one at https://www.perplexity.ai/settings/api)
67
+ ```
@@ -32,4 +32,4 @@ Follow the user's request. Common tasks:
32
32
  - For long videos, consider asking a focused question via `--prompt` rather than a full analysis
33
33
  - Gemini sees both visuals and audio — mention on-screen content (slides, code, diagrams) when relevant
34
34
  - Quote speakers verbatim when the user asks about specific statements
35
- - If the tool reports a missing API key, tell the user to get one at https://aistudio.google.com/apikey and set `GEMINI_API_KEY` in their shell profile or PAL settings
35
+ - If the tool reports a missing API key, tell the user to get one at https://aistudio.google.com/apikey and set `PAL_GEMINI_API_KEY` in their shell profile or PAL settings
@@ -4,7 +4,7 @@
4
4
  * YouTube Analyze — Sends a YouTube URL + prompt to Gemini for video analysis.
5
5
  *
6
6
  * Gemini can natively process YouTube videos (visual + audio).
7
- * Requires GEMINI_API_KEY environment variable.
7
+ * Requires PAL_GEMINI_API_KEY environment variable.
8
8
  *
9
9
  * Usage:
10
10
  * bun youtube-analyze.ts -- <youtube-url> [--prompt "your question"]
@@ -25,9 +25,9 @@ const DEFAULT_PROMPT = `Analyze this video and provide:
25
25
  const MODEL = "gemini-3.1-flash-lite-preview";
26
26
 
27
27
  function loadApiKey(): string {
28
- const key = process.env.GEMINI_API_KEY;
28
+ const key = process.env.PAL_GEMINI_API_KEY;
29
29
  if (!key) {
30
- console.error("Error: GEMINI_API_KEY environment variable is not set.");
30
+ console.error("Error: PAL_GEMINI_API_KEY environment variable is not set.");
31
31
  console.error("Get a free key at https://aistudio.google.com/apikey");
32
32
  process.exit(1);
33
33
  }
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: create-pdf
3
+ description: Convert markdown files into a styled PDF report using md-to-pdf. Use when creating a PDF from existing markdown files, combining markdown into a report, or converting .md to .pdf.
4
+ argument-hint: <file paths, glob pattern, or directory containing .md files>
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ Combine one or more markdown files into a single styled PDF using `bunx --bun md-to-pdf`. This skill handles concatenation, page breaks, styling, and conversion. It does NOT generate content — use the `research` skill or other content-generation workflows first, then pipe the resulting markdown files into this skill.
10
+
11
+ ## Input
12
+
13
+ The user provides one of:
14
+ - **Explicit file paths**: `/path/to/file1.md /path/to/file2.md ...`
15
+ - **A glob pattern**: `/path/to/report/*.md`
16
+ - **A directory**: `/path/to/report/` (all `.md` files inside, sorted alphabetically)
17
+
18
+ If no output filename is specified, derive it from the directory name or first file: `<name>.pdf` in the same directory as the input files.
19
+
20
+ ## Workflow
21
+
22
+ ### Step 1: Resolve Input Files
23
+
24
+ Determine the list of markdown files and their order:
25
+ - If explicit paths: use as given
26
+ - If glob/directory: list and sort alphabetically (files prefixed with `00_` come first naturally)
27
+ - Confirm the file list and order with the user if more than 5 files
28
+
29
+ ### Step 2: Combine with Page Breaks
30
+
31
+ Concatenate all files with page break dividers between them:
32
+
33
+ ```bash
34
+ cat \
35
+ first_file.md \
36
+ <(echo -e '\n\n<div style="page-break-before: always"></div>\n\n---\n\n') \
37
+ second_file.md \
38
+ <(echo -e '\n\n<div style="page-break-before: always"></div>\n\n---\n\n') \
39
+ third_file.md \
40
+ ... \
41
+ > /tmp/combined_raw.md
42
+ ```
43
+
44
+ For many files, generate the cat command dynamically rather than typing each one.
45
+
46
+ ### Step 3: Add PDF Frontmatter
47
+
48
+ Prepend YAML frontmatter with styling, then append the combined content:
49
+
50
+ ```bash
51
+ cat > <output_name>.md << 'FRONTMATTER'
52
+ ---
53
+ pdf_options:
54
+ format: A4
55
+ margin: 25mm
56
+ printBackground: true
57
+ stylesheet: https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css
58
+ body_class: markdown-body
59
+ css: |-
60
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 11px; line-height: 1.6; color: #1a1a1a; }
61
+ h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 8px; }
62
+ h2 { font-size: 17px; }
63
+ h3 { font-size: 14px; }
64
+ table { border-collapse: collapse; width: 100%; margin: 12px 0; }
65
+ th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; font-size: 10px; }
66
+ th { background: #f0f0f0; }
67
+ blockquote { border-left: 3px solid #666; padding-left: 12px; color: #444; }
68
+ hr { border: none; border-top: 1px solid #ccc; margin: 20px 0; }
69
+ a { color: #0366d6; text-decoration: none; }
70
+ ---
71
+
72
+ FRONTMATTER
73
+ cat /tmp/combined_raw.md >> <output_name>.md
74
+ ```
75
+
76
+ The user may request custom styling (font size, margins, colors). Override the defaults above accordingly.
77
+
78
+ ### Step 4: Generate PDF
79
+
80
+ ```bash
81
+ bunx --bun md-to-pdf <output_name>.md
82
+ ```
83
+
84
+ ### Step 5: Verify and Report
85
+
86
+ ```bash
87
+ ls -lh <output_name>.pdf
88
+ ```
89
+
90
+ Report the file path and size to the user.
91
+
92
+ ## Translation Variant
93
+
94
+ If the user asks to translate markdown files and then create a PDF:
95
+
96
+ 1. Spawn **parallel subagents** to translate files — batch 2-4 files per agent, all agents in a **single message**
97
+ 2. Each agent reads the source file, translates all text content to the target language, and writes to `<original_name>_<lang>.md`
98
+ 3. Rules for translation agents:
99
+ - Keep all markdown formatting, links, and structure identical
100
+ - Keep source/link titles in their original language; translate surrounding text
101
+ - Translate naturally, not word-for-word; use proper domain terminology
102
+ 4. After all agents complete, run Steps 2-5 above on the translated files
103
+ 5. Output filename gets a `_<lang>` suffix (e.g., `report_hu.pdf`)
104
+
105
+ **Batch sizing for translation agents:**
106
+
107
+ | Files | Agents | Files per agent |
108
+ |-------|--------|-----------------|
109
+ | 1-4 | 1-2 | 2 each |
110
+ | 5-10 | 3-4 | 2-3 each |
111
+ | 11+ | 5 | 3-4 each |
112
+
113
+ ## Important
114
+
115
+ - Use `bunx --bun md-to-pdf` (NOT npx) for PDF conversion
116
+ - This skill only converts — it does not research or generate content
117
+ - Individual markdown files are preserved alongside the PDF for future editing
118
+ - If `md-to-pdf` is not installed, `bunx` will auto-install it on first run
119
+ - Always verify the PDF exists before reporting success
@@ -14,16 +14,16 @@ argument-hint: <topic or question>
14
14
 
15
15
  ## Available Researcher Agents
16
16
 
17
- - **claude-researcher** — academic depth, query decomposition, scholarly synthesis
17
+ - **gemini-researcher** — academic depth via Gemini grounding, query decomposition, scholarly synthesis (falls back to WebSearch if no API key)
18
18
  - **multi-perspective-researcher** — breadth, multiple angles, diverse viewpoints
19
- - **investigative-researcher** — verification rigor, triple-checks, source credibility
19
+ - **perplexity-researcher** — investigative rigor via Perplexity grounding, source cross-referencing, credibility assessment (falls back to WebSearch if no API key)
20
20
  - **grok-researcher** — real-time data via Grok/X API, breaking news, social sentiment (falls back to WebSearch with recency focus if no API key)
21
21
 
22
22
  ## Quick Mode
23
23
 
24
24
  Spawn **1 subagent** for a focused answer:
25
25
 
26
- - Spawn `claude-researcher` with the full query and context
26
+ - Spawn `gemini-researcher` with the full query and context
27
27
 
28
28
  Wait for the result, then deliver it directly with light formatting.
29
29
 
@@ -31,12 +31,12 @@ Wait for the result, then deliver it directly with light formatting.
31
31
 
32
32
  Craft **3 different queries** optimized for each researcher's strengths, then spawn all **in parallel (in a single message)**:
33
33
 
34
- - Spawn `claude-researcher` with a query optimized for depth/analysis
34
+ - Spawn `gemini-researcher` with a query optimized for depth/analysis
35
35
  - Spawn `multi-perspective-researcher` with a query optimized for breadth/perspectives
36
36
  - Spawn `grok-researcher` with a query optimized for real-time data, recent developments, current state
37
37
 
38
38
  **Query design:**
39
- - claude-researcher: focus on authoritative sources, technical depth, how/why
39
+ - gemini-researcher: focus on authoritative sources, technical depth, how/why
40
40
  - multi-perspective-researcher: focus on different stakeholder views, trade-offs, alternatives
41
41
  - grok-researcher: focus on latest news, breaking developments, social sentiment, what's happening right now
42
42
 
@@ -44,12 +44,12 @@ Craft **3 different queries** optimized for each researcher's strengths, then sp
44
44
 
45
45
  Craft **8 queries** (2 per researcher type, each from a different angle), then spawn all **in parallel (in a single message)**:
46
46
 
47
- - Spawn `claude-researcher` — angle 1: core technical depth
48
- - Spawn `claude-researcher` — angle 2: historical context / evolution
47
+ - Spawn `gemini-researcher` — angle 1: core technical depth
48
+ - Spawn `gemini-researcher` — angle 2: historical context / evolution
49
49
  - Spawn `multi-perspective-researcher` — angle 3: stakeholder perspectives
50
50
  - Spawn `multi-perspective-researcher` — angle 4: cross-domain connections
51
- - Spawn `investigative-researcher` — angle 5: verify key claims
52
- - Spawn `investigative-researcher` — angle 6: find contradictions / counter-evidence
51
+ - Spawn `perplexity-researcher` — angle 5: verify key claims
52
+ - Spawn `perplexity-researcher` — angle 6: find contradictions / counter-evidence
53
53
  - Spawn `grok-researcher` — angle 7: real-time developments and breaking news
54
54
  - Spawn `grok-researcher` — angle 8: social sentiment, public reaction, trending discourse
55
55
 
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Gemini Search — CLI tool for grounded search via the Gemini API.
4
+ *
5
+ * Uses Gemini's built-in Google Search grounding to fetch real-time,
6
+ * source-cited information. Optimized for academic and scholarly queries.
7
+ *
8
+ * Requires PAL_GEMINI_API_KEY environment variable.
9
+ *
10
+ * Usage:
11
+ * bun gemini-search.ts -- <query> [--max-tokens 4096]
12
+ * bun gemini-search.ts -- "recent advances in transformer architectures"
13
+ * bun gemini-search.ts -- "CRISPR gene editing clinical trials 2025"
14
+ */
15
+
16
+ import { parseArgs } from "node:util";
17
+
18
+ const API_BASE = "https://generativelanguage.googleapis.com/v1beta";
19
+ const DEFAULT_MODEL = "gemini-3.1-flash-lite-preview";
20
+
21
+ interface GroundingChunk {
22
+ web?: { uri: string; title: string };
23
+ }
24
+
25
+ interface GroundingSupport {
26
+ segment?: { startIndex: number; endIndex: number; text: string };
27
+ groundingChunkIndices?: number[];
28
+ }
29
+
30
+ interface GroundingMetadata {
31
+ webSearchQueries?: string[];
32
+ groundingChunks?: GroundingChunk[];
33
+ groundingSupports?: GroundingSupport[];
34
+ searchEntryPoint?: { renderedContent: string };
35
+ }
36
+
37
+ interface ContentPart {
38
+ text?: string;
39
+ }
40
+
41
+ interface Candidate {
42
+ content?: { parts?: ContentPart[]; role?: string };
43
+ groundingMetadata?: GroundingMetadata;
44
+ }
45
+
46
+ interface GeminiResponse {
47
+ candidates?: Candidate[];
48
+ error?: { message: string; code: number };
49
+ }
50
+
51
+ function loadApiKey(): string {
52
+ const key = process.env.PAL_GEMINI_API_KEY;
53
+ if (!key) {
54
+ console.error("Error: PAL_GEMINI_API_KEY environment variable is not set.");
55
+ console.error("Get an API key at https://aistudio.google.com/apikey");
56
+ process.exit(1);
57
+ }
58
+ return key;
59
+ }
60
+
61
+ const SYSTEM_PROMPT = `You are an academic research assistant. When searching, prioritize:
62
+ - Peer-reviewed papers, preprints (arXiv, bioRxiv, medRxiv)
63
+ - Official documentation and technical specifications
64
+ - University and research institution publications
65
+ - Conference proceedings (NeurIPS, ICML, ACL, CVPR, etc.)
66
+ - Systematic reviews and meta-analyses
67
+
68
+ Always include: author names, publication year, journal/venue when available.
69
+ Distinguish between peer-reviewed findings and preprints/working papers.
70
+ Note methodology limitations and sample sizes when relevant.
71
+ Be thorough but concise.`;
72
+
73
+ export async function geminiSearch(query: string, maxTokens: number): Promise<void> {
74
+ const apiKey = loadApiKey();
75
+
76
+ const body = {
77
+ system_instruction: {
78
+ parts: [{ text: SYSTEM_PROMPT }],
79
+ },
80
+ contents: [
81
+ {
82
+ parts: [{ text: query }],
83
+ },
84
+ ],
85
+ tools: [{ google_search: {} }],
86
+ generationConfig: {
87
+ maxOutputTokens: maxTokens,
88
+ },
89
+ };
90
+
91
+ const url = `${API_BASE}/models/${DEFAULT_MODEL}:generateContent?key=${apiKey}`;
92
+
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(body),
97
+ });
98
+
99
+ if (!response.ok) {
100
+ const err = await response.text().catch(() => "");
101
+ console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
102
+ process.exit(1);
103
+ }
104
+
105
+ const data = (await response.json()) as GeminiResponse;
106
+
107
+ if (data.error) {
108
+ console.error(`Error: ${data.error.message}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ if (!data.candidates || data.candidates.length === 0) {
113
+ console.error("Error: No candidates in Gemini response.");
114
+ process.exit(1);
115
+ }
116
+
117
+ const candidate = data.candidates[0];
118
+
119
+ // Extract text
120
+ const textParts: string[] = [];
121
+ if (candidate.content?.parts) {
122
+ for (const part of candidate.content.parts) {
123
+ if (part.text) textParts.push(part.text);
124
+ }
125
+ }
126
+
127
+ if (textParts.length === 0) {
128
+ console.error("Error: No text content in Gemini response.");
129
+ process.exit(1);
130
+ }
131
+
132
+ console.log(textParts.join("\n\n"));
133
+
134
+ // Extract grounding metadata
135
+ const meta = candidate.groundingMetadata;
136
+ if (meta) {
137
+ if (meta.webSearchQueries && meta.webSearchQueries.length > 0) {
138
+ console.log("\n---\n## Search Queries Used\n");
139
+ for (const q of meta.webSearchQueries) {
140
+ console.log(`- ${q}`);
141
+ }
142
+ }
143
+
144
+ if (meta.groundingChunks && meta.groundingChunks.length > 0) {
145
+ console.log("\n---\n## Sources\n");
146
+ for (const chunk of meta.groundingChunks) {
147
+ if (chunk.web) {
148
+ console.log(`- [${chunk.web.title}](${chunk.web.uri})`);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ async function run() {
156
+ const { positionals, values } = parseArgs({
157
+ allowPositionals: true,
158
+ options: {
159
+ "max-tokens": { type: "string", short: "m", default: "4096" },
160
+ help: { type: "boolean", short: "h" },
161
+ },
162
+ });
163
+
164
+ if (values.help || positionals.length === 0) {
165
+ console.log(`Gemini Search — grounded academic search via Gemini API
166
+
167
+ Usage:
168
+ bun gemini-search.ts -- <query> [options]
169
+
170
+ Options:
171
+ --max-tokens, -m <n> Max response tokens (default: 4096)
172
+ --help, -h Show this help
173
+
174
+ Examples:
175
+ bun gemini-search.ts -- "transformer architecture advances 2025"
176
+ bun gemini-search.ts -- "CRISPR clinical trials"`);
177
+ process.exit(0);
178
+ }
179
+
180
+ const query = positionals.join(" ");
181
+ const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
182
+
183
+ await geminiSearch(query, maxTokens);
184
+ }
185
+
186
+ if (import.meta.main) run();
@@ -5,7 +5,7 @@
5
5
  * Uses the Grok Responses API with web_search and x_search tools
6
6
  * to fetch real-time information from the web and X (Twitter).
7
7
  *
8
- * Requires XAI_API_KEY environment variable.
8
+ * Requires PAL_XAI_API_KEY environment variable.
9
9
  *
10
10
  * Usage:
11
11
  * bun grok-search.ts -- <query> [--sources web,x] [--max-tokens 2048]
@@ -45,9 +45,9 @@ interface GrokResponse {
45
45
  }
46
46
 
47
47
  function loadApiKey(): string {
48
- const key = process.env.XAI_API_KEY;
48
+ const key = process.env.PAL_XAI_API_KEY;
49
49
  if (!key) {
50
- console.error("Error: XAI_API_KEY environment variable is not set.");
50
+ console.error("Error: PAL_XAI_API_KEY environment variable is not set.");
51
51
  console.error("Get an API key at https://console.x.ai/");
52
52
  process.exit(1);
53
53
  }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Perplexity Search — CLI tool for investigative search via the Perplexity Sonar API.
4
+ *
5
+ * Uses Perplexity's Sonar model with built-in web search to fetch
6
+ * source-cited, verified information. Optimized for investigative queries
7
+ * requiring cross-referenced, credible sources.
8
+ *
9
+ * Requires PAL_PERPLEXITY_API_KEY environment variable.
10
+ *
11
+ * Usage:
12
+ * bun perplexity-search.ts -- <query> [--max-tokens 4096]
13
+ * bun perplexity-search.ts -- "corruption allegations against company X"
14
+ * bun perplexity-search.ts -- "timeline of event Y with sources"
15
+ */
16
+
17
+ import { parseArgs } from "node:util";
18
+
19
+ const API_BASE = "https://api.perplexity.ai";
20
+ const DEFAULT_MODEL = "sonar-pro";
21
+
22
+ interface Choice {
23
+ message?: {
24
+ role: string;
25
+ content: string;
26
+ };
27
+ }
28
+
29
+ interface PerplexityResponse {
30
+ choices?: Choice[];
31
+ citations?: string[];
32
+ error?: { message: string; code?: number };
33
+ }
34
+
35
+ function loadApiKey(): string {
36
+ const key = process.env.PAL_PERPLEXITY_API_KEY;
37
+ if (!key) {
38
+ console.error(
39
+ "Error: PAL_PERPLEXITY_API_KEY environment variable is not set.\n" +
40
+ "The Perplexity API could not be reached. The researcher agent should fall back to WebSearch.\n" +
41
+ "To enable Perplexity search, get an API key at https://www.perplexity.ai/settings/api\n" +
42
+ "and set it: export PAL_PERPLEXITY_API_KEY=pplx-..."
43
+ );
44
+ process.exit(1);
45
+ }
46
+ return key;
47
+ }
48
+
49
+ const SYSTEM_PROMPT = `You are an investigative research assistant. When searching, prioritize:
50
+ - Cross-referenced facts verified by 2+ independent sources
51
+ - Primary sources: court filings, official reports, government records, regulatory filings
52
+ - Credible journalism: established outlets with editorial standards
53
+ - Source credibility assessment: note publication reputation, potential bias, date of publication
54
+ - Evidence chains: connect claims to their original sources
55
+
56
+ Always include: source names, publication dates, and direct quotes when available.
57
+ Distinguish between confirmed facts, single-source claims, and unverified allegations.
58
+ Flag contradictions between sources.
59
+ Be thorough but concise.`;
60
+
61
+ export async function perplexitySearch(query: string, maxTokens: number): Promise<void> {
62
+ const apiKey = loadApiKey();
63
+
64
+ const body = {
65
+ model: DEFAULT_MODEL,
66
+ messages: [
67
+ { role: "system", content: SYSTEM_PROMPT },
68
+ { role: "user", content: query },
69
+ ],
70
+ max_tokens: maxTokens,
71
+ return_citations: true,
72
+ search_recency_filter: "week",
73
+ };
74
+
75
+ const response = await fetch(`${API_BASE}/chat/completions`, {
76
+ method: "POST",
77
+ headers: {
78
+ Authorization: `Bearer ${apiKey}`,
79
+ "Content-Type": "application/json",
80
+ },
81
+ body: JSON.stringify(body),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ const err = await response.text().catch(() => "");
86
+ console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ const data = (await response.json()) as PerplexityResponse;
91
+
92
+ if (data.error) {
93
+ console.error(`Error: ${data.error.message}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ if (!data.choices || data.choices.length === 0) {
98
+ console.error("Error: No choices in Perplexity response.");
99
+ process.exit(1);
100
+ }
101
+
102
+ const content = data.choices[0].message?.content;
103
+ if (!content) {
104
+ console.error("Error: No text content in Perplexity response.");
105
+ process.exit(1);
106
+ }
107
+
108
+ console.log(content);
109
+
110
+ // Extract citations
111
+ if (data.citations && data.citations.length > 0) {
112
+ console.log("\n---\n## Sources\n");
113
+ for (let i = 0; i < data.citations.length; i++) {
114
+ console.log(`- [${i + 1}] ${data.citations[i]}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ async function run() {
120
+ const { positionals, values } = parseArgs({
121
+ allowPositionals: true,
122
+ options: {
123
+ "max-tokens": { type: "string", short: "m", default: "4096" },
124
+ help: { type: "boolean", short: "h" },
125
+ },
126
+ });
127
+
128
+ if (values.help || positionals.length === 0) {
129
+ console.log(`Perplexity Search — investigative search via Perplexity Sonar API
130
+
131
+ Usage:
132
+ bun perplexity-search.ts -- <query> [options]
133
+
134
+ Options:
135
+ --max-tokens, -m <n> Max response tokens (default: 4096)
136
+ --help, -h Show this help
137
+
138
+ Examples:
139
+ bun perplexity-search.ts -- "corruption allegations timeline"
140
+ bun perplexity-search.ts -- "regulatory actions against company X"`);
141
+ process.exit(0);
142
+ }
143
+
144
+ const query = positionals.join(" ");
145
+ const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
146
+
147
+ await perplexitySearch(query, maxTokens);
148
+ }
149
+
150
+ if (import.meta.main) run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -380,9 +380,9 @@ function doctor(silent = false): DoctorResult {
380
380
  process.env.ANTHROPIC_API_KEY
381
381
  ? ok("ANTHROPIC_API_KEY is set")
382
382
  : fail("ANTHROPIC_API_KEY — not set (hooks need it for inference)");
383
- process.env.GEMINI_API_KEY
384
- ? ok("GEMINI_API_KEY is set")
385
- : warn("GEMINI_API_KEY — not set (optional, for YouTube analysis)");
383
+ process.env.PAL_GEMINI_API_KEY
384
+ ? ok("PAL_GEMINI_API_KEY is set")
385
+ : warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
386
386
 
387
387
  // Hook health from debug.log
388
388
  const hookHealth = checkHookHealth(home);
@@ -443,6 +443,18 @@ async function init(args: string[]) {
443
443
  }
444
444
 
445
445
  async function install(targets: { claude: boolean; opencode: boolean; cursor: boolean }) {
446
+ // Ensure dependencies are installed
447
+ const pkg = palPkg();
448
+ log.info("Installing dependencies...");
449
+ const deps = spawnSync("bun", ["install", "--frozen-lockfile"], {
450
+ cwd: pkg,
451
+ stdio: "inherit",
452
+ shell: true,
453
+ });
454
+ if (deps.status !== 0) {
455
+ log.warn("bun install failed — continuing anyway, but hooks may not work");
456
+ }
457
+
446
458
  // Scaffold TELOS + PAL settings, then prompt for missing identity
447
459
  const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
448
460
  const { promptIdentity } = await import("./setup-identity");
@@ -19,6 +19,7 @@ import {
19
19
  extractLastUser,
20
20
  parseMessages,
21
21
  } from "../lib/transcript";
22
+ import { appendProjectHistory } from "../lib/work-tracking";
22
23
 
23
24
  function slugify(text: string): string {
24
25
  return text
@@ -183,5 +184,13 @@ export async function captureWorkLearning(
183
184
  const filepath = resolve(dir, filename);
184
185
  writeFileSync(filepath, content, "utf-8");
185
186
 
187
+ // Append to per-project history (agent-agnostic recall)
188
+ appendProjectHistory(process.cwd(), {
189
+ date: new Date().toISOString().slice(0, 10),
190
+ title,
191
+ summary,
192
+ insights,
193
+ });
194
+
186
195
  if (sessionId) markCaptured(sessionId, filepath, messages.length);
187
196
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
+ copyFileSync,
10
11
  existsSync,
11
12
  lstatSync,
12
13
  readdirSync,
@@ -48,7 +49,7 @@ function latestMtime(...filePaths: string[]): number {
48
49
  return latest;
49
50
  }
50
51
 
51
- /** Create or verify a symlink pointing to AGENTS.md */
52
+ /** Create or verify a symlink pointing to AGENTS.md (falls back to copy on Windows EPERM) */
52
53
  function ensureOneSymlink(linkPath: string, targetPath: string): void {
53
54
  try {
54
55
  const stat = lstatSync(linkPath);
@@ -58,8 +59,16 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
58
59
  // doesn't exist — create it
59
60
  }
60
61
  ensureDir(dirname(linkPath));
61
- const relTarget = relative(dirname(linkPath), targetPath).replaceAll("\\", "/");
62
- symlinkSync(relTarget, linkPath);
62
+ try {
63
+ const relTarget = relative(dirname(linkPath), targetPath).replaceAll("\\", "/");
64
+ symlinkSync(relTarget, linkPath);
65
+ } catch (e: unknown) {
66
+ if ((e as NodeJS.ErrnoException).code === "EPERM") {
67
+ copyFileSync(targetPath, linkPath);
68
+ } else {
69
+ throw e;
70
+ }
71
+ }
63
72
  }
64
73
 
65
74
  /** Ensure all agent symlinks point to the canonical AGENTS.md */
@@ -156,9 +165,12 @@ export function buildClaudeMd(): string {
156
165
  /** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
157
166
  export function regenerateIfNeeded(): boolean {
158
167
  const { outputPath } = getOutputPaths();
159
- ensureSymlinks();
160
- if (!needsRebuild()) return false;
168
+ if (!needsRebuild()) {
169
+ ensureSymlinks();
170
+ return false;
171
+ }
161
172
  ensureDir(dirname(outputPath));
162
173
  writeFileSync(outputPath, buildClaudeMd(), "utf-8");
174
+ ensureSymlinks();
163
175
  return true;
164
176
  }
@@ -17,6 +17,7 @@ import { computeSignalTrends, formatTrends } from "./signal-trends";
17
17
  import { readFramePrinciples } from "./wisdom";
18
18
  import {
19
19
  activeProjects,
20
+ readProjectHistory,
20
21
  readSessions,
21
22
  recentSessions,
22
23
  staleProjects,
@@ -240,25 +241,15 @@ export function loadLearningDigest(): string {
240
241
  const entries = readLearnings(paths.sessionLearning(), 10);
241
242
  if (entries.length === 0) return "";
242
243
 
243
- const thisProject = entries.filter((e) => e.cwd === cwd).slice(0, 4);
244
- const other = entries.filter((e) => e.cwd !== cwd).slice(0, 3);
244
+ // This-project learnings are now in loadProjectHistoryContext(); only show cross-project here
245
+ const other = entries.filter((e) => e.cwd !== cwd).slice(0, 5);
245
246
 
246
- if (thisProject.length === 0 && other.length === 0) return "";
247
+ if (other.length === 0) return "";
247
248
 
248
249
  const lines: string[] = [];
249
250
 
250
- if (thisProject.length > 0) {
251
- lines.push("## This Project Recent Sessions");
252
- for (const e of thisProject) {
253
- lines.push(`- **${e.title}**`);
254
- if (e.insights) lines.push(` ${e.insights.split("\n")[0].slice(0, 150)}`);
255
- }
256
- }
257
-
258
- if (other.length > 0) {
259
- lines.push(thisProject.length > 0 ? "" : "", "## Other Recent Learnings");
260
- for (const e of other) lines.push(`- ${e.title}`);
261
- }
251
+ lines.push("## Other Recent Learnings");
252
+ for (const e of other) lines.push(`- ${e.title}`);
262
253
 
263
254
  return lines.join("\n");
264
255
  } catch {
@@ -346,6 +337,25 @@ export function loadSignalTrends(): string {
346
337
  }
347
338
  }
348
339
 
340
+ /** Load per-project session history for the current working directory */
341
+ export function loadProjectHistoryContext(): string {
342
+ try {
343
+ const cwd = process.cwd();
344
+ const entries = readProjectHistory(cwd, 15);
345
+ if (entries.length === 0) return "";
346
+
347
+ const lines: string[] = ["## This Project — Session History"];
348
+ for (const e of entries) {
349
+ lines.push(`- **${e.title}** (${e.date})`);
350
+ if (e.summary) lines.push(` ${e.summary.split("\n")[0].slice(0, 150)}`);
351
+ }
352
+
353
+ return lines.join("\n");
354
+ } catch {
355
+ return "";
356
+ }
357
+ }
358
+
349
359
  /** Load recent relationship notes (today + yesterday) */
350
360
  export function loadRelationshipContext(): string {
351
361
  try {
@@ -373,6 +383,9 @@ export function buildSystemReminder(): string {
373
383
  ? loadRelationshipContext()
374
384
  : "";
375
385
  const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
386
+ const projectHistory = isEnabled(settings, "projectHistory")
387
+ ? loadProjectHistoryContext()
388
+ : "";
376
389
  const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
377
390
  const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
378
391
  const synthesis = isEnabled(settings, "synthesis")
@@ -384,6 +397,7 @@ export function buildSystemReminder(): string {
384
397
  if (wisdom) parts.push(wisdom);
385
398
  if (opinions) parts.push(opinions);
386
399
  if (relationship) parts.push(relationship);
400
+ if (projectHistory) parts.push(projectHistory);
387
401
  if (digest) parts.push(digest);
388
402
  if (synthesis) parts.push(synthesis);
389
403
  if (trends) parts.push(trends);
@@ -15,7 +15,8 @@ const EXPORT_DIRS = ["telos", "memory"];
15
15
  const SKIP_PATTERNS = ["memory/downloads"];
16
16
 
17
17
  function shouldSkip(relPath: string): boolean {
18
- return SKIP_PATTERNS.some((p) => relPath.startsWith(p));
18
+ const normalized = relPath.replaceAll("\\", "/");
19
+ return SKIP_PATTERNS.some((p) => normalized.startsWith(p));
19
20
  }
20
21
 
21
22
  /** Recursively collect all files under a directory, returning paths relative to root. */
@@ -32,7 +33,7 @@ function walkDir(dir: string, root: string): string[] {
32
33
  if (entry.isDirectory()) {
33
34
  files.push(...walkDir(fullPath, root));
34
35
  } else if (entry.isFile()) {
35
- files.push(relPath);
36
+ files.push(relPath.replaceAll("\\", "/"));
36
37
  }
37
38
  }
38
39
  return files;
@@ -57,6 +57,7 @@ export const paths = {
57
57
  relationship: () => ensureDir(home("memory", "relationship")),
58
58
  entities: () => ensureDir(home("memory", "entities")),
59
59
  failures: () => ensureDir(home("memory", "learning", "failures")),
60
+ projectHistory: () => ensureDir(home("memory", "projects")),
60
61
  sessionLearning: () => ensureDir(home("memory", "learning", "session")),
61
62
  synthesis: () => ensureDir(home("memory", "learning", "synthesis")),
62
63
  backups: () => ensureDir(home("backups")),
@@ -67,12 +67,12 @@ function extractEnvVars(): string[] {
67
67
  }
68
68
  }
69
69
 
70
- // GEMINI_API_KEY from youtube-analyze.ts
70
+ // PAL_GEMINI_API_KEY from youtube-analyze.ts
71
71
  const youtubeFile = resolve(pkg, "src", "tools", "youtube-analyze.ts");
72
72
  if (existsSync(youtubeFile)) {
73
73
  const content = readFileSync(youtubeFile, "utf-8");
74
- if (content.includes("GEMINI_API_KEY")) {
75
- vars.add("GEMINI_API_KEY");
74
+ if (content.includes("PAL_GEMINI_API_KEY")) {
75
+ vars.add("PAL_GEMINI_API_KEY");
76
76
  }
77
77
  }
78
78
 
@@ -44,6 +44,7 @@ export const HOOK_MANAGED_DIRS = [
44
44
  "memory/learning/synthesis",
45
45
  "memory/relationship",
46
46
  "memory/wisdom/state",
47
+ "memory/projects",
47
48
  ".agents/PAL",
48
49
  ];
49
50
 
@@ -3,7 +3,7 @@
3
3
  * Used by both Claude Code (StopOrchestrator) and opencode (plugin).
4
4
  */
5
5
 
6
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { ensureDir, paths } from "./paths";
9
9
  import { now } from "./time";
@@ -143,6 +143,43 @@ export function extractHandoff(lastAssistant: string): string {
143
143
  return cleaned;
144
144
  }
145
145
 
146
+ // ── Per-Project History ──────────────────────────────────────────
147
+
148
+ export interface ProjectHistoryEntry {
149
+ date: string;
150
+ title: string;
151
+ summary: string;
152
+ insights: string;
153
+ }
154
+
155
+ /** Convert a cwd path to a filesystem-safe slug (last directory segment) */
156
+ export function cwdToSlug(cwd: string): string {
157
+ const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
158
+ return normalized.split("/").pop() || "unknown";
159
+ }
160
+
161
+ /** Append a learning entry to the project's history.jsonl */
162
+ export function appendProjectHistory(cwd: string, entry: ProjectHistoryEntry): void {
163
+ const slug = cwdToSlug(cwd);
164
+ const dir = ensureDir(resolve(paths.projectHistory(), slug));
165
+ const historyPath = resolve(dir, "history.jsonl");
166
+ const line = `${JSON.stringify(entry)}\n`;
167
+ appendFileSync(historyPath, line, "utf-8");
168
+ }
169
+
170
+ /** Read the project history for a given cwd */
171
+ export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry[] {
172
+ const slug = cwdToSlug(cwd);
173
+ const historyPath = resolve(paths.projectHistory(), slug, "history.jsonl");
174
+ if (!existsSync(historyPath)) return [];
175
+ try {
176
+ const lines = readFileSync(historyPath, "utf-8").trim().split("\n").filter(Boolean);
177
+ return lines.slice(-limit).map((line) => JSON.parse(line) as ProjectHistoryEntry);
178
+ } catch {
179
+ return [];
180
+ }
181
+ }
182
+
146
183
  // ── Persistent Projects ──────────────────────────────────────────
147
184
 
148
185
  export interface Project {
@@ -167,8 +167,6 @@ ${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Fre
167
167
  `- ${date()}: Principle candidate — ${observation}`
168
168
  );
169
169
  break;
170
-
171
- case "evolution":
172
170
  default:
173
171
  content = appendToSection(content, "## Evolution Log", evolutionEntry);
174
172
  break;
@@ -1,43 +0,0 @@
1
- ---
2
- name: claude-researcher
3
- description: Deep research with academic rigor — query decomposition, multi-source synthesis, scholarly depth. Use for research tasks requiring thorough analysis.
4
- tools: WebSearch, WebFetch, Read, Grep, Glob
5
- model: sonnet
6
- ---
7
-
8
- You are a research specialist focused on **depth and academic rigor**.
9
-
10
- ## Methodology
11
-
12
- 1. **Decompose** the query into 2-3 sub-questions that target the core of the topic
13
- 2. **Search** each sub-question using WebSearch — prioritize authoritative sources (papers, docs, official sites)
14
- 3. **Read** the most promising results with WebFetch to extract detail
15
- 4. **Synthesize** findings into a structured analysis
16
-
17
- ## Guidelines
18
-
19
- - Prioritize primary sources over summaries
20
- - Distinguish between established facts, expert consensus, and speculation
21
- - Note methodology limitations when citing research
22
- - If a claim has no strong source, say so — do not fabricate citations
23
- - Keep findings concise but substantive
24
-
25
- ## Output Format
26
-
27
- ```markdown
28
- ## Findings
29
-
30
- [Numbered list of key discoveries, each with a brief explanation]
31
-
32
- ## Sources
33
-
34
- [Verified URLs with one-line descriptions — only include URLs you actually visited]
35
-
36
- ## Confidence
37
-
38
- [High/Medium/Low rating per finding, with brief justification]
39
-
40
- ## Gaps
41
-
42
- [What couldn't be answered or needs further investigation]
43
- ```
@@ -1,44 +0,0 @@
1
- ---
2
- name: investigative-researcher
3
- description: Investigative research with verification rigor — triple-checks sources, cross-references claims, assesses credibility. Use for research requiring high factual confidence.
4
- tools: WebSearch, WebFetch, Read, Grep, Glob
5
- model: sonnet
6
- ---
7
-
8
- You are a research specialist focused on **verification and investigative rigor**.
9
-
10
- ## Methodology
11
-
12
- 1. **Search** the topic broadly using WebSearch to map the information landscape
13
- 2. **Verify** key claims by finding 2+ independent sources for each
14
- 3. **Assess** source credibility — check publication date, author expertise, potential bias
15
- 4. **Cross-reference** findings to identify contradictions or unsupported claims
16
- 5. **Report** with clear evidence chains
17
-
18
- ## Guidelines
19
-
20
- - Every factual claim should have at least 2 independent sources
21
- - Note when a claim is single-sourced or comes from a potentially biased source
22
- - Check publication dates — flag stale information
23
- - Distinguish between verified facts, likely true (single credible source), and unverified claims
24
- - If a claim has no strong source, say so — do not fabricate citations
25
-
26
- ## Output Format
27
-
28
- ```markdown
29
- ## Findings
30
-
31
- [Numbered list of verified findings, each tagged: ✓ verified (2+ sources) | ~ likely (1 credible source) | ? unverified]
32
-
33
- ## Sources
34
-
35
- [Verified URLs with one-line descriptions — only include URLs you actually visited]
36
-
37
- ## Confidence
38
-
39
- [High/Medium/Low rating per finding, with evidence chain summary]
40
-
41
- ## Flags
42
-
43
- [Contradictions found, stale information, potential bias in sources]
44
- ```