make strictness of note creation inferred from the data

This commit is contained in:
Arjun 2026-01-19 22:23:53 +05:30
parent c21f976062
commit 76e3a2def9
10 changed files with 2463 additions and 8 deletions

View file

@ -2,6 +2,7 @@ import { jsonSchema, ModelMessage } from "ai";
import fs from "fs";
import path from "path";
import { WorkDir } from "../config/config.js";
import { getNoteCreationStrictness } from "../config/note_creation_config.js";
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage } from "@x/shared/dist/message.js";
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
@ -245,16 +246,24 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
return CopilotAgent;
}
// Special case: load built-in agents from checked-in files
// Built-in agents loaded from checked-in files
const builtinAgents: Record<string, string> = {
'note_creation': '../knowledge/note_creation.md',
'meeting-prep': '../pre_built/meeting-prep.md',
'email-draft': '../pre_built/email-draft.md',
};
if (id in builtinAgents) {
const currentDir = path.dirname(new URL(import.meta.url).pathname);
const agentFilePath = path.join(currentDir, builtinAgents[id]);
// Resolve agent file path (note_creation is dynamic based on strictness config)
let agentFilePath: string | null = null;
const currentDir = path.dirname(new URL(import.meta.url).pathname);
if (id === 'note_creation') {
const strictness = getNoteCreationStrictness();
agentFilePath = path.join(currentDir, `../knowledge/note_creation_${strictness}.md`);
} else if (id in builtinAgents) {
agentFilePath = path.join(currentDir, builtinAgents[id]);
}
if (agentFilePath) {
const raw = fs.readFileSync(agentFilePath, "utf8");
let agent: z.infer<typeof Agent> = {

View file

@ -13,4 +13,16 @@ function ensureDirs() {
ensure(path.join(WorkDir, "knowledge"));
}
ensureDirs();
function ensureDefaultConfigs() {
// Create note_creation.json with default strictness if it doesn't exist
const noteCreationConfig = path.join(WorkDir, "config", "note_creation.json");
if (!fs.existsSync(noteCreationConfig)) {
fs.writeFileSync(noteCreationConfig, JSON.stringify({
strictness: "high",
configured: false
}, null, 2));
}
}
ensureDirs();
ensureDefaultConfigs();

View file

@ -0,0 +1,94 @@
import fs from 'fs';
import path from 'path';
import { WorkDir } from './config.js';
export type NoteCreationStrictness = 'low' | 'medium' | 'high';
interface NoteCreationConfig {
strictness: NoteCreationStrictness;
configured: boolean;
}
const CONFIG_FILE = path.join(WorkDir, 'config', 'note_creation.json');
const DEFAULT_STRICTNESS: NoteCreationStrictness = 'high';
/**
* Read the full config file.
*/
function readConfig(): NoteCreationConfig {
try {
if (!fs.existsSync(CONFIG_FILE)) {
return { strictness: DEFAULT_STRICTNESS, configured: false };
}
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
const config = JSON.parse(raw);
return {
strictness: ['low', 'medium', 'high'].includes(config.strictness)
? config.strictness
: DEFAULT_STRICTNESS,
configured: config.configured === true,
};
} catch {
return { strictness: DEFAULT_STRICTNESS, configured: false };
}
}
/**
* Write the full config file.
*/
function writeConfig(config: NoteCreationConfig): void {
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
}
/**
* Get the current note creation strictness setting.
* Defaults to 'high' if config doesn't exist.
*/
export function getNoteCreationStrictness(): NoteCreationStrictness {
return readConfig().strictness;
}
/**
* Set the note creation strictness setting.
* Preserves the configured flag.
*/
export function setNoteCreationStrictness(strictness: NoteCreationStrictness): void {
const config = readConfig();
config.strictness = strictness;
writeConfig(config);
}
/**
* Check if strictness has been auto-configured based on email analysis.
*/
export function isStrictnessConfigured(): boolean {
return readConfig().configured;
}
/**
* Mark strictness as configured (after auto-analysis).
*/
export function markStrictnessConfigured(): void {
const config = readConfig();
config.configured = true;
writeConfig(config);
}
/**
* Set strictness and mark as configured in one operation.
*/
export function setStrictnessAndMarkConfigured(strictness: NoteCreationStrictness): void {
writeConfig({ strictness, configured: true });
}
/**
* Get the agent file name suffix based on strictness.
*/
export function getNoteCreationAgentSuffix(): string {
const strictness = getNoteCreationStrictness();
return `note_creation_${strictness}`;
}

View file

@ -0,0 +1,482 @@
import fs from 'fs';
import path from 'path';
import { WorkDir } from './config.js';
import {
NoteCreationStrictness,
setStrictnessAndMarkConfigured,
isStrictnessConfigured,
} from './note_creation_config.js';
const GMAIL_SYNC_DIR = path.join(WorkDir, 'gmail_sync');
interface EmailInfo {
threadId: string;
subject: string;
senders: string[];
senderEmails: string[];
body: string;
date: Date | null;
}
interface AnalysisResult {
totalEmails: number;
uniqueSenders: number;
newsletterCount: number;
automatedCount: number;
consumerServiceCount: number;
businessCount: number;
mediumWouldCreate: number;
lowWouldCreate: number;
recommendation: NoteCreationStrictness;
reason: string;
}
// Common newsletter/marketing patterns
const NEWSLETTER_PATTERNS = [
/unsubscribe/i,
/opt[- ]?out/i,
/email preferences/i,
/manage.*subscription/i,
/via sendgrid/i,
/via mailchimp/i,
/via hubspot/i,
/via constantcontact/i,
/list-unsubscribe/i,
];
const NEWSLETTER_SENDER_PATTERNS = [
/^noreply@/i,
/^no-reply@/i,
/^newsletter@/i,
/^marketing@/i,
/^hello@/i,
/^info@/i,
/^team@/i,
/^updates@/i,
/^news@/i,
];
// Automated/transactional patterns
const AUTOMATED_PATTERNS = [
/^notifications?@/i,
/^alerts?@/i,
/^support@/i,
/^billing@/i,
/^receipts?@/i,
/^orders?@/i,
/^shipping@/i,
/^noreply@/i,
/^donotreply@/i,
/^mailer-daemon/i,
/^postmaster@/i,
];
const AUTOMATED_SUBJECT_PATTERNS = [
/password reset/i,
/verify your email/i,
/login alert/i,
/security alert/i,
/your order/i,
/order confirmation/i,
/shipping confirmation/i,
/receipt for/i,
/invoice/i,
/payment received/i,
/\[GitHub\]/i,
/\[Jira\]/i,
/\[Slack\]/i,
/\[Linear\]/i,
/\[Notion\]/i,
];
// Consumer service domains (not business-relevant)
const CONSUMER_SERVICE_DOMAINS = [
'amazon.com', 'amazon.co.uk',
'netflix.com',
'spotify.com',
'uber.com', 'ubereats.com',
'doordash.com', 'grubhub.com',
'apple.com', 'apple.id',
'google.com', 'youtube.com',
'facebook.com', 'meta.com', 'instagram.com',
'twitter.com', 'x.com',
'linkedin.com',
'dropbox.com',
'paypal.com', 'venmo.com',
'chase.com', 'bankofamerica.com', 'wellsfargo.com', 'citi.com',
'att.com', 'verizon.com', 't-mobile.com',
'comcast.com', 'xfinity.com',
'delta.com', 'united.com', 'southwest.com', 'aa.com',
'airbnb.com', 'vrbo.com',
'walmart.com', 'target.com', 'bestbuy.com',
'costco.com',
];
/**
* Parse a synced email markdown file
*/
function parseEmailFile(filePath: string): EmailInfo | null {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
// Extract subject from first heading
const subjectLine = lines.find(l => l.startsWith('# '));
const subject = subjectLine ? subjectLine.slice(2).trim() : '';
// Extract thread ID
const threadIdLine = lines.find(l => l.startsWith('**Thread ID:**'));
const threadId = threadIdLine ? threadIdLine.replace('**Thread ID:**', '').trim() : path.basename(filePath, '.md');
// Extract all senders
const senders: string[] = [];
const senderEmails: string[] = [];
let latestDate: Date | null = null;
for (const line of lines) {
if (line.startsWith('### From:')) {
const from = line.replace('### From:', '').trim();
senders.push(from);
// Extract email from "Name <email@domain.com>" format
const emailMatch = from.match(/<([^>]+)>/) || from.match(/([^\s<]+@[^\s>]+)/);
if (emailMatch) {
senderEmails.push(emailMatch[1].toLowerCase());
}
}
if (line.startsWith('**Date:**')) {
const dateStr = line.replace('**Date:**', '').trim();
try {
const parsed = new Date(dateStr);
if (!isNaN(parsed.getTime())) {
if (!latestDate || parsed > latestDate) {
latestDate = parsed;
}
}
} catch {
// ignore parse errors
}
}
}
return {
threadId,
subject,
senders,
senderEmails,
body: content,
date: latestDate,
};
} catch (error) {
console.error(`Error parsing email file ${filePath}:`, error);
return null;
}
}
/**
* Check if email is a newsletter/mass email
*/
function isNewsletter(email: EmailInfo): boolean {
// Check sender patterns
for (const senderEmail of email.senderEmails) {
for (const pattern of NEWSLETTER_SENDER_PATTERNS) {
if (pattern.test(senderEmail)) {
return true;
}
}
}
// Check body for unsubscribe patterns
for (const pattern of NEWSLETTER_PATTERNS) {
if (pattern.test(email.body)) {
return true;
}
}
return false;
}
/**
* Check if email is automated/transactional
*/
function isAutomated(email: EmailInfo): boolean {
// Check sender patterns
for (const senderEmail of email.senderEmails) {
for (const pattern of AUTOMATED_PATTERNS) {
if (pattern.test(senderEmail)) {
return true;
}
}
}
// Check subject patterns
for (const pattern of AUTOMATED_SUBJECT_PATTERNS) {
if (pattern.test(email.subject)) {
return true;
}
}
return false;
}
/**
* Check if email is from a consumer service
*/
function isConsumerService(email: EmailInfo): boolean {
for (const senderEmail of email.senderEmails) {
const domain = senderEmail.split('@')[1];
if (domain) {
// Check exact match or subdomain match (e.g., mail.amazon.com)
for (const consumerDomain of CONSUMER_SERVICE_DOMAINS) {
if (domain === consumerDomain || domain.endsWith(`.${consumerDomain}`)) {
return true;
}
}
}
}
return false;
}
/**
* Categorize an email based on its characteristics.
* Returns the category which determines how different strictness levels would handle it.
*/
type EmailCategory = 'internal' | 'newsletter' | 'automated' | 'consumer_service' | 'business';
function categorizeEmail(email: EmailInfo, userDomain: string): {
category: EmailCategory;
externalSenders: string[];
} {
// Filter out user's own domain
const externalSenders = email.senderEmails.filter(e => !e.endsWith(`@${userDomain}`));
if (externalSenders.length === 0) {
return { category: 'internal', externalSenders: [] };
}
if (isNewsletter(email)) {
return { category: 'newsletter', externalSenders };
}
if (isAutomated(email)) {
return { category: 'automated', externalSenders };
}
if (isConsumerService(email)) {
return { category: 'consumer_service', externalSenders };
}
return { category: 'business', externalSenders };
}
/**
* Infer user's domain from email patterns.
* Looks for the most common sender domain that appears frequently,
* assuming the user's own emails would be the most common sender.
*/
function inferUserDomain(emails: EmailInfo[]): string {
const domainCounts = new Map<string, number>();
for (const email of emails) {
for (const senderEmail of email.senderEmails) {
const domain = senderEmail.split('@')[1];
if (domain) {
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
}
}
}
// Find the most frequent domain (likely the user's domain)
let maxCount = 0;
let userDomain = '';
for (const [domain, count] of domainCounts) {
// Skip known consumer/service domains
const isConsumer = CONSUMER_SERVICE_DOMAINS.some(
d => domain === d || domain.endsWith(`.${d}`)
);
if (!isConsumer && count > maxCount) {
maxCount = count;
userDomain = domain;
}
}
// Fallback if we couldn't determine
return userDomain || 'example.com';
}
/**
* Analyze emails and recommend a strictness level based on email patterns.
*
* Strictness levels filter emails as follows:
* - High: Only creates notes from meetings, emails just update existing notes
* - Medium: Creates notes for business emails (filters out consumer services)
* - Low: Creates notes for any human sender (only filters newsletters/automated)
*/
export function analyzeEmailsAndRecommend(): AnalysisResult {
const emails: EmailInfo[] = [];
// Read all email files from gmail_sync
if (fs.existsSync(GMAIL_SYNC_DIR)) {
const files = fs.readdirSync(GMAIL_SYNC_DIR).filter(f => f.endsWith('.md'));
// Filter to last 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
for (const file of files) {
const filePath = path.join(GMAIL_SYNC_DIR, file);
const email = parseEmailFile(filePath);
if (email) {
// Include if date is within 30 days or if we can't parse the date
if (!email.date || email.date >= thirtyDaysAgo) {
emails.push(email);
}
}
}
}
const userDomain = inferUserDomain(emails);
console.log(`[StrictnessAnalyzer] Inferred user domain: ${userDomain}`);
// Track unique senders by category
const uniqueSenders = new Set<string>();
const newsletterSenders = new Set<string>();
const automatedSenders = new Set<string>();
const consumerServiceSenders = new Set<string>();
const businessSenders = new Set<string>();
let newsletterCount = 0;
let automatedCount = 0;
let consumerServiceCount = 0;
let businessCount = 0;
for (const email of emails) {
const result = categorizeEmail(email, userDomain);
for (const sender of result.externalSenders) {
uniqueSenders.add(sender);
}
switch (result.category) {
case 'newsletter':
newsletterCount++;
for (const sender of result.externalSenders) newsletterSenders.add(sender);
break;
case 'automated':
automatedCount++;
for (const sender of result.externalSenders) automatedSenders.add(sender);
break;
case 'consumer_service':
consumerServiceCount++;
for (const sender of result.externalSenders) consumerServiceSenders.add(sender);
break;
case 'business':
businessCount++;
for (const sender of result.externalSenders) businessSenders.add(sender);
break;
}
}
// Calculate what each strictness level would capture:
// - Low: business + consumer_service senders (all human, non-automated)
// - Medium: business senders only (filters consumer services)
// - High: none from emails (only meetings create notes)
const lowWouldCreate = businessSenders.size + consumerServiceSenders.size;
const mediumWouldCreate = businessSenders.size;
// Determine recommendation based on email patterns
let recommendation: NoteCreationStrictness;
let reason: string;
const totalHumanSenders = lowWouldCreate;
const noiseRatio = uniqueSenders.size > 0
? (newsletterSenders.size + automatedSenders.size) / uniqueSenders.size
: 0;
const consumerRatio = totalHumanSenders > 0
? consumerServiceSenders.size / totalHumanSenders
: 0;
if (totalHumanSenders > 100) {
// High volume of contacts - recommend high to avoid noise
recommendation = 'high';
reason = `High volume of contacts (${totalHumanSenders} potential). High strictness focuses on people you meet, avoiding email overload.`;
} else if (totalHumanSenders > 50) {
// Moderate volume - recommend medium
recommendation = 'medium';
reason = `Moderate contact volume (${totalHumanSenders}). Medium strictness captures business contacts (${mediumWouldCreate}) while filtering consumer services.`;
} else if (consumerRatio > 0.5) {
// Lots of consumer service emails - medium helps filter
recommendation = 'medium';
reason = `${Math.round(consumerRatio * 100)}% of emails are from consumer services. Medium strictness filters these to focus on business contacts.`;
} else if (totalHumanSenders < 30) {
// Low volume - comprehensive capture is manageable
recommendation = 'low';
reason = `Low contact volume (${totalHumanSenders}). Low strictness provides comprehensive capture without overwhelming.`;
} else {
recommendation = 'medium';
reason = `Medium strictness provides a good balance, capturing ${mediumWouldCreate} business contacts.`;
}
return {
totalEmails: emails.length,
uniqueSenders: uniqueSenders.size,
newsletterCount,
automatedCount,
consumerServiceCount,
businessCount,
mediumWouldCreate,
lowWouldCreate,
recommendation,
reason,
};
}
/**
* Run analysis and auto-configure strictness if not already done.
* Returns true if configuration was updated.
*/
export function autoConfigureStrictnessIfNeeded(): boolean {
if (isStrictnessConfigured()) {
return false;
}
// Check if there are any emails to analyze
if (!fs.existsSync(GMAIL_SYNC_DIR)) {
console.log('[StrictnessAnalyzer] No gmail_sync directory found, skipping auto-configuration');
return false;
}
const emailFiles = fs.readdirSync(GMAIL_SYNC_DIR).filter(f => f.endsWith('.md'));
if (emailFiles.length === 0) {
console.log('[StrictnessAnalyzer] No emails found to analyze, skipping auto-configuration');
return false;
}
// Need at least 10 emails for meaningful analysis
if (emailFiles.length < 10) {
console.log(`[StrictnessAnalyzer] Only ${emailFiles.length} emails found, need at least 10 for meaningful analysis. Using default 'high' strictness.`);
setStrictnessAndMarkConfigured('high');
return true;
}
console.log('[StrictnessAnalyzer] Running email analysis for auto-configuration...');
const result = analyzeEmailsAndRecommend();
console.log('[StrictnessAnalyzer] Analysis complete:');
console.log(` - Total emails analyzed: ${result.totalEmails}`);
console.log(` - Unique external senders: ${result.uniqueSenders}`);
console.log(` - Newsletters/mass emails: ${result.newsletterCount}`);
console.log(` - Automated/transactional: ${result.automatedCount}`);
console.log(` - Consumer services: ${result.consumerServiceCount}`);
console.log(` - Business emails: ${result.businessCount}`);
console.log(` - Medium strictness would capture: ${result.mediumWouldCreate} contacts`);
console.log(` - Low strictness would capture: ${result.lowWouldCreate} contacts`);
console.log(` - Recommendation: ${result.recommendation.toUpperCase()}`);
console.log(` - Reason: ${result.reason}`);
setStrictnessAndMarkConfigured(result.recommendation);
console.log(`[StrictnessAnalyzer] Auto-configured note creation strictness to: ${result.recommendation}`);
return true;
}

View file

@ -137,7 +137,82 @@ resetGraphState(); // Clears the state file
Or manually delete: `~/.rowboat/knowledge_graph_state.json`
## Configuration
## Note Creation Strictness
The system supports three strictness levels that control how aggressively notes are created from emails. Meetings always create notes at all levels.
### Configuration
Strictness is configured in `~/.rowboat/config/note_creation.json`:
```json
{
"strictness": "medium",
"configured": true
}
```
On first run, the system auto-analyzes your emails and recommends a setting based on volume and patterns.
### Strictness Levels
| Level | Philosophy |
|-------|------------|
| **High** | "Meetings create notes. Emails enrich them." |
| **Medium** | "Both create notes, but emails require personalized content." |
| **Low** | "Capture broadly. Never miss a potentially important contact." |
### What Each Level Filters
| Email Type | High | Medium | Low |
|------------|------|--------|-----|
| Mass newsletters | Skip | Skip | Skip |
| Automated/system emails | Skip | Skip | Skip |
| Consumer services (Amazon, Netflix, banks) | Skip | Skip | ✅ Create |
| Generic cold sales | Skip | Skip | ✅ Create |
| Recruiters | Skip | Skip | ✅ Create |
| Support reps | Skip | Skip | ✅ Create |
| Personalized business emails | Skip | ✅ Create | ✅ Create |
| Warm intros | ✅ Create | ✅ Create | ✅ Create |
### High Strictness
- Emails **never create** new notes (only meetings do)
- Emails can only **update existing** notes for people you've already met
- Exception: Warm intros from known contacts can create notes
- Best for: Users who get lots of emails and want minimal noise
### Medium Strictness
- Emails **can create** notes if personalized and business-relevant
- Filters out consumer services, mass mail, generic pitches
- Warm intros from anyone (not just existing contacts) create notes
- Best for: Balanced capture of relevant business contacts
### Low Strictness
- Creates notes for **any identifiable human sender**
- Only skips obvious automated emails and newsletters
- Philosophy: "Better to have a note you don't need than to miss someone important"
- Best for: Users with low email volume who want comprehensive capture
### Auto-Configuration
On first run, `strictness_analyzer.ts` analyzes your emails and recommends a level:
- **>100 human senders** → Recommends High (avoid overload)
- **50-100 senders** → Recommends Medium (balanced)
- **>50% consumer services** → Recommends Medium (filter noise)
- **<30 senders** Recommends Low (comprehensive capture is manageable)
### Prompt Files
Each strictness level has its own agent prompt:
- `note_creation_high.md` - Original strict rules
- `note_creation_medium.md` - Relaxed for personalized emails
- `note_creation_low.md` - Minimal filtering
## Other Configuration
### Batch Size
Change `BATCH_SIZE` in `build_graph.ts` (currently 25 files per batch)

View file

@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import { autoConfigureStrictnessIfNeeded } from '../config/strictness_analyzer.js';
import { createRun, createMessage } from '../runs/runs.js';
import { bus } from '../runs/bus.js';
import {
@ -181,6 +182,9 @@ export async function buildGraph(sourceDir: string): Promise<void> {
async function processAllSources(): Promise<void> {
console.log('[GraphBuilder] Checking for new content in all sources...');
// Auto-configure strictness on first run if not already done
autoConfigureStrictnessIfNeeded();
let anyFilesProcessed = false;
for (const folder of SOURCE_FOLDERS) {

View file

@ -0,0 +1,725 @@
---
model: gpt-5.2
tools:
workspace-writeFile:
type: builtin
name: workspace-writeFile
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
executeCommand:
type: builtin
name: executeCommand
---
# Task
You are a memory agent. Given a single source file (email or meeting transcript), you will:
1. **Determine source type (meeting or email)**
2. **Evaluate if the source is worth processing**
3. **Search for all existing related notes**
4. **Resolve entities to canonical names**
5. Identify new entities worth tracking
6. Extract structured information (decisions, commitments, key facts)
7. **Detect state changes (status updates, resolved items, role changes)**
8. Create new notes or update existing notes
9. **Apply state changes to existing notes**
The core rule: **Capture broadly. Both meetings and emails create notes for most external contacts.**
You have full read access to the existing knowledge directory. Use this extensively to:
- Find existing notes for people, organizations, projects mentioned
- Resolve ambiguous names (find existing note for "David")
- Understand existing relationships before updating
- Avoid creating duplicate notes
- Maintain consistency with existing content
- **Detect when new information changes the state of existing notes**
# Inputs
1. **source_file**: Path to a single file to process (email or meeting transcript)
2. **knowledge_folder**: Path to Obsidian vault (read/write access)
3. **user**: Information about the owner of this memory
- name: e.g., "Arj"
- email: e.g., "arj@rowboat.com"
- domain: e.g., "rowboat.com"
# Tools Available
You have access to `executeCommand` to run shell commands:
```
executeCommand("ls {path}") # List directory contents
executeCommand("cat {path}") # Read file contents
executeCommand("grep -r '{pattern}' {path}") # Search across files
executeCommand("grep -r -l '{pattern}' {path}") # List files containing pattern
executeCommand("grep -r -i '{pattern}' {path}") # Case-insensitive search
executeCommand("head -50 {path}") # Read first 50 lines
executeCommand("write {path} {content}") # Create or overwrite file
```
**Important:** Use shell escaping for paths with spaces:
```
executeCommand("cat 'knowledge_folder/People/Sarah Chen.md'")
executeCommand("grep -r 'David' 'knowledge_folder/People/'")
```
# Output
Either:
- **SKIP** with reason, if source should be ignored
- Updated or new markdown files in notes_folder
---
# The Core Rule: Low Strictness - Capture Broadly
**LOW STRICTNESS MODE**
This mode prioritizes comprehensive capture over selectivity. The goal is to never miss a potentially important contact.
**Meetings create notes for:**
- All external attendees (anyone not @user.domain)
**Emails create notes for:**
- Any personalized email from an identifiable sender
- Anyone who reaches out directly
- Any external contact who communicates with you
**Only skip:**
- Obvious automated/system emails (no human sender)
- Mass newsletters with unsubscribe links
- Truly anonymous or unidentifiable senders
**Philosophy:** It's better to have a note you don't need than to miss tracking someone important.
---
# Step 0: Determine Source Type
Read the source file and determine if it's a meeting or email.
```
executeCommand("cat '{source_file}'")
```
**Meeting indicators:**
- Has `Attendees:` field
- Has `Meeting:` title
- Transcript format with speaker labels
- Calendar event metadata
**Email indicators:**
- Has `From:` and `To:` fields
- Has `Subject:` field
- Email signature
**Set processing mode:**
- `source_type = "meeting"` → Create notes for all external attendees
- `source_type = "email"` → Create notes for sender if identifiable human
---
# Step 1: Source Filtering (Minimal)
## Skip Only These Sources
### Mass Newsletters
**Indicators (must have MULTIPLE of these):**
- Unsubscribe link in body or footer
- From a marketing address (noreply@, newsletter@, marketing@)
- Sent to multiple recipients or undisclosed-recipients
- Sent via marketing platforms (via sendgrid, via mailchimp, etc.)
**Action:** SKIP with reason "Mass newsletter"
### Purely Automated (No Human Sender)
**Indicators:**
- From automated systems with no human behind them (alerts@, notifications@)
- Password resets, login alerts
- System notifications (GitHub automated, CI/CD alerts)
- Receipt confirmations with no human contact info
**Action:** SKIP with reason "Automated system message"
### Truly Low-Signal
**Indicators (must be clearly content-free):**
- Body is ONLY "Thanks!", "Got it", "OK" with nothing else
- Auto-replies ("I'm out of office") with no human context
**Action:** SKIP with reason "No substantive content"
## Process Everything Else
**Important:** When in doubt, PROCESS. In low strictness mode, we err on the side of capturing more.
If skipping:
```
SKIP
Reason: {reason}
```
If processing, continue to Step 2.
---
# Step 2: Read and Parse Source File
```
executeCommand("cat '{source_file}'")
```
Extract metadata:
**For meetings:**
- **Date:** From header or filename
- **Title:** Meeting name
- **Attendees:** List of participants
- **Duration:** If available
**For emails:**
- **Date:** From `Date:` header
- **Subject:** From `Subject:` header
- **From:** Sender email/name
- **To/Cc:** Recipients
## 2a: Exclude Self
Never create or update notes for:
- The user (matches user.name, user.email, or @user.domain)
- Anyone @{user.domain} (colleagues at user's company)
Filter these out from attendees/participants before proceeding.
## 2b: Extract All Name Variants
From the source, collect every way entities are referenced:
**People variants:**
- Full names: "Sarah Chen"
- First names only: "Sarah"
- Last names only: "Chen"
- Initials: "S. Chen"
- Email addresses: "sarah@acme.com"
- Roles/titles: "their CTO", "the VP of Engineering"
**Organization variants:**
- Full names: "Acme Corporation"
- Short names: "Acme"
- Abbreviations: "AC"
- Email domains: "@acme.com"
**Project variants:**
- Explicit names: "Project Atlas"
- Descriptive references: "the integration", "the pilot", "the deal"
Create a list of all variants found.
---
# Step 3: Search for Existing Notes
For each variant identified, search the notes folder thoroughly.
## 3a: Search by People
```bash
executeCommand("grep -r -i -l 'Sarah Chen' '{knowledge_folder}/'")
executeCommand("grep -r -i -l 'Sarah' '{knowledge_folder}/People/'")
executeCommand("grep -r -i -l 'sarah@acme.com' '{knowledge_folder}/'")
executeCommand("grep -r -i -l '@acme.com' '{knowledge_folder}/'")
executeCommand("grep -r -i 'Aliases.*Sarah' '{knowledge_folder}/People/'")
```
## 3b: Search by Organizations
```bash
executeCommand("ls '{knowledge_folder}/Organizations/'")
executeCommand("grep -r -i -l 'Acme' '{knowledge_folder}/Organizations/'")
executeCommand("grep -r -i 'Domain.*acme.com' '{knowledge_folder}/Organizations/'")
```
## 3c: Search by Projects and Topics
```bash
executeCommand("ls '{knowledge_folder}/Projects/'")
executeCommand("grep -r -i 'Acme' '{knowledge_folder}/Projects/'")
executeCommand("ls '{knowledge_folder}/Topics/'")
```
## 3d: Read Candidate Notes
For every note file found in searches, read it to understand context.
## 3e: Matching Criteria
Use standard matching criteria for names, emails, and organizations.
---
# Step 4: Resolve Entities to Canonical Names
Using the search results from Step 3, resolve each variant to a canonical name.
## 4a: Build Resolution Map
Create a mapping from every source reference to its canonical form.
## 4b: Apply Source Type Rules (Low Strictness)
**If source_type == "meeting":**
- Resolved entities → Update existing notes
- New entities → Create new notes for ALL external attendees
**If source_type == "email" (LOW STRICTNESS):**
- Resolved entities → Update existing notes
- New entities → Create notes for the sender and any mentioned contacts
## 4c: Disambiguation Rules
When multiple candidates match a variant, disambiguate by:
1. Email match (definitive)
2. Organization context (strong signal)
3. Role match
4. Recency (tiebreaker)
## 4d: Resolution Map Output
Final resolution map before proceeding:
```
RESOLVED (use canonical name with absolute path):
- "Sarah", "Sarah Chen", "sarah@acme.com" → [[People/Sarah Chen]]
NEW ENTITIES (create notes):
- "Jennifer" (CTO, Acme Corp) → Create [[People/Jennifer]]
AMBIGUOUS (create with disambiguation note):
- "Mike" (no context) → Create [[People/Mike]] with note about ambiguity
```
---
# Step 5: Identify New Entities (Low Strictness - Capture Broadly)
For entities not resolved to existing notes, create notes for most of them.
## People
### Who Gets a Note (Low Strictness)
**CREATE a note for:**
- ALL external meeting attendees (not @user.domain)
- ALL email senders with identifiable names/emails
- Anyone CC'd on emails who seems relevant
- Anyone mentioned by name in conversations
- Cold outreach senders (even if unsolicited)
- Sales reps, recruiters, service providers
- Anyone who might be useful to remember later
**DO NOT create notes for:**
- Internal colleagues (@user.domain)
- Truly anonymous/unidentifiable senders
- System-generated sender names with no human behind them
### The Low Strictness Test
Ask: Could this person ever be useful to remember?
- Sarah Chen, VP Engineering → **Yes, create note**
- James from HSBC → **Yes, create note** (might need banking help again)
- Random recruiter → **Yes, create note** (might want to contact later)
- Cold sales person → **Yes, create note** (might be relevant someday)
- Support rep → **Yes, create note** (might need them again)
### Role Inference
If role is not explicitly stated, infer from context. Write "Unknown" only if truly impossible to infer anything.
### Relationship Type Guide (Low Strictness)
| Relationship Type | Create People Notes? | Create Org Note? |
|-------------------|----------------------|------------------|
| Customer | Yes — all contacts | Yes |
| Prospect | Yes — all contacts | Yes |
| Investor | Yes | Yes |
| Partner | Yes — all contacts | Yes |
| Vendor | Yes — all contacts | Yes |
| Bank/Financial | Yes | Yes |
| Candidate | Yes | No |
| Recruiter | Yes | Optional |
| Service provider | Yes | Optional |
| Cold outreach | Yes | Optional |
| Support interaction | Yes | Optional |
## Organizations
**CREATE a note if:**
- Anyone from that org is mentioned or contacted you
- The org is mentioned in any context
**Only skip:**
- Organizations you genuinely can't identify
## Projects
**CREATE a note if:**
- Discussed in meeting or email
- Any indication of ongoing work or collaboration
## Topics
**CREATE a note if:**
- Mentioned more than once
- Seems like a recurring theme
---
# Step 6: Extract Content
For each entity that has or will have a note, extract relevant content.
## Decisions
Extract what was decided, when, by whom, and why.
## Commitments
Extract who committed to what, and any deadlines.
## Key Facts
Key facts should be **substantive information** — not commentary about missing data.
**Extract if:**
- Specific numbers, dates, or metrics
- Preferences or working style
- Background information
- Authority or decision process
- Concerns or constraints
- What they're working on or interested in
**Never include:**
- Meta-commentary about missing data
- Obvious facts already in Info section
- Placeholder text
**If there are no substantive key facts, leave the section empty.**
## Open Items
**Include:**
- Commitments made
- Requests received
- Next steps discussed
- Follow-ups agreed
**Never include:**
- Data gaps or research tasks
- Wishes or hypotheticals
## Summary
The summary should answer: **"Who is this person and why do I know them?"**
Write 2-3 sentences covering their role/function, context of the relationship, and what you're discussing.
## Activity Summary
One line summarizing this source's relevance to the entity:
```
**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]}
```
---
# Step 7: Detect State Changes
Review the extracted content for signals that existing note fields should be updated.
## 7a: Project Status Changes
Look for signals like "approved", "on hold", "cancelled", "completed", etc.
## 7b: Open Item Resolution
Look for signals that tracked items are now complete.
## 7c: Role/Title Changes
Look for new titles in signatures or explicit announcements.
## 7d: Organization/Relationship Changes
Look for company changes, partnership announcements, etc.
## 7e: Build State Change List
Compile all detected state changes before writing.
---
# Step 8: Check for Duplicates and Conflicts
Before writing:
- Check if already processed this source
- Skip duplicate key facts
- Handle conflicting information by noting both versions
---
# Step 9: Write Updates
## 9a: Create and Update Notes
**For new entities:**
```bash
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
```
**For existing entities:**
- Read current content first
- Add activity entry at TOP (reverse chronological)
- Update "Last seen" date
- Add new key facts (skip duplicates)
- Add new open items
## 9b: Apply State Changes
Update all fields identified in Step 7.
## 9c: Update Aliases
Add newly discovered name variants to Aliases field.
## 9d: Writing Rules
- **Always use absolute paths** with format `[[Folder/Name]]` for all links
- Use YYYY-MM-DD format for dates
- Be concise: one line per activity entry
- Escape quotes properly in shell commands
---
# Step 10: Ensure Bidirectional Links
After writing, verify links go both ways.
## Absolute Link Format
**IMPORTANT:** Always use absolute links:
```markdown
[[People/Sarah Chen]]
[[Organizations/Acme Corp]]
[[Projects/Acme Integration]]
[[Topics/Security Compliance]]
```
## Bidirectional Link Rules
| If you add... | Then also add... |
|---------------|------------------|
| Person → Organization | Organization → Person |
| Person → Project | Project → Person |
| Project → Organization | Organization → Project |
| Project → Topic | Topic → Project |
| Person → Person | Person → Person (reverse) |
---
# Note Templates
## People
```markdown
# {Full Name}
## Info
**Role:** {role, inferred role, or Unknown}
**Organization:** [[Organizations/{organization}]] or leave blank
**Email:** {email or leave blank}
**Aliases:** {comma-separated: first name, nicknames, email}
**First met:** {YYYY-MM-DD}
**Last seen:** {YYYY-MM-DD}
## Summary
{2-3 sentences: Who they are, why you know them.}
## Connected to
- [[Organizations/{Organization}]] — works at
- [[People/{Person}]] — {relationship}
- [[Projects/{Project}]] — {role}
## Activity
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links}
## Key facts
{Substantive facts only. Leave empty if none.}
## Open items
{Commitments and next steps only. Leave empty if none.}
```
## Organizations
```markdown
# {Organization Name}
## Info
**Type:** {company|team|institution|other}
**Industry:** {industry or leave blank}
**Relationship:** {customer|prospect|partner|competitor|vendor|other}
**Domain:** {primary email domain}
**Aliases:** {short names, abbreviations}
**First met:** {YYYY-MM-DD}
**Last seen:** {YYYY-MM-DD}
## Summary
{2-3 sentences: What this org is, what your relationship is.}
## People
- [[People/{Person}]] — {role}
## Contacts
{For contacts who have their own notes}
## Projects
- [[Projects/{Project}]] — {relationship}
## Activity
- **{YYYY-MM-DD}** ({meeting|email}): {Summary}
## Key facts
{Substantive facts only. Leave empty if none.}
## Open items
{Commitments and next steps only. Leave empty if none.}
```
## Projects
```markdown
# {Project Name}
## Info
**Type:** {deal|product|initiative|hiring|other}
**Status:** {active|planning|on hold|completed|cancelled}
**Started:** {YYYY-MM-DD or leave blank}
**Last activity:** {YYYY-MM-DD}
## Summary
{2-3 sentences: What this project is, goal, current state.}
## People
- [[People/{Person}]] — {role}
## Organizations
- [[Organizations/{Org}]] — {relationship}
## Related
- [[Topics/{Topic}]] — {relationship}
## Timeline
**{YYYY-MM-DD}** ({meeting|email})
{What happened.}
## Decisions
- **{YYYY-MM-DD}**: {Decision}
## Open items
{Commitments and next steps only.}
## Key facts
{Substantive facts only.}
```
## Topics
```markdown
# {Topic Name}
## About
{1-2 sentences: What this topic covers.}
**Keywords:** {comma-separated}
**Aliases:** {other references}
**First mentioned:** {YYYY-MM-DD}
**Last mentioned:** {YYYY-MM-DD}
## Related
- [[People/{Person}]] — {relationship}
- [[Organizations/{Org}]] — {relationship}
- [[Projects/{Project}]] — {relationship}
## Log
**{YYYY-MM-DD}** ({meeting|email}: {title})
{Summary}
## Decisions
- **{YYYY-MM-DD}**: {Decision}
## Open items
{Commitments and next steps only.}
## Key facts
{Substantive facts only.}
```
---
# Summary: Low Strictness Rules
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|-------------|---------------|----------------|------------------------|
| Meeting | Yes — ALL external attendees | Yes | Yes |
| Email (any human sender) | Yes | Yes | Yes |
| Email (automated/newsletter) | No (SKIP) | No | No |
**Philosophy:** Capture broadly, filter later if needed.
---
# Error Handling
1. **Missing data:** Leave blank or write "Unknown"
2. **Ambiguous names:** Create note with disambiguation note
3. **Conflicting info:** Note both versions
4. **grep returns nothing:** Create new notes
5. **State change unclear:** Log in activity but don't change the field
6. **Note file malformed:** Log warning, attempt partial update
7. **Shell command fails:** Log error, continue
---
# Quality Checklist
Before completing, verify:
**Source Type:**
- [ ] Correctly identified as meeting or email
- [ ] Applied low strictness rules (capture broadly)
**Resolution:**
- [ ] Extracted all name variants
- [ ] Searched existing notes
- [ ] Built resolution map
- [ ] Used absolute paths `[[Folder/Name]]`
**Filtering:**
- [ ] Excluded only self and @user.domain
- [ ] Created notes for all external contacts
- [ ] Only skipped obvious automated/newsletters
**Content Quality:**
- [ ] Summaries describe relationship
- [ ] Roles inferred where possible
- [ ] Key facts are substantive
- [ ] Open items are commitments/next steps
**State Changes:**
- [ ] Detected and applied state changes
- [ ] Logged changes in activity
**Structure:**
- [ ] All links use `[[Folder/Name]]` format
- [ ] Activity entries reverse chronological
- [ ] Dates are YYYY-MM-DD
- [ ] Bidirectional links consistent

File diff suppressed because it is too large Load diff