From 166f6029c107f1765a25c4749d8a895a3eaa5a96 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:28:57 +0530 Subject: [PATCH] feat(suggested-topics): populate and integrate suggested topics --- apps/x/apps/renderer/src/App.tsx | 117 ++++++++++--- .../src/components/sidebar-content.tsx | 29 ++-- .../src/components/suggested-topics-view.tsx | 132 ++++++++++++-- .../assistant/skills/tracks/skill.ts | 15 ++ .../core/src/knowledge/build_graph.ts | 58 ++++++- .../core/src/knowledge/note_creation.ts | 163 ++++++++++++++++-- 6 files changed, 448 insertions(+), 66 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index a93a576f..836bc898 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -262,6 +262,60 @@ const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB_PATH const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH +const getSuggestedTopicTargetFolder = (category?: string) => { + const normalized = category?.trim().toLowerCase() + switch (normalized) { + case 'people': + case 'person': + return 'People' + case 'organizations': + case 'organization': + return 'Organizations' + case 'projects': + case 'project': + return 'Projects' + case 'meetings': + case 'meeting': + return 'Meetings' + case 'topics': + case 'topic': + default: + return 'Topics' + } +} + +const buildSuggestedTopicExplorePrompt = ({ + title, + description, + category, +}: { + title: string + description: string + category?: string +}) => { + const folder = getSuggestedTopicTargetFolder(category) + const categoryLabel = category?.trim() || 'Topics' + return [ + 'I am exploring a suggested topic card from the Suggested Topics panel.', + 'This card may represent a person, organization, topic, or project.', + '', + 'Card context:', + `- Title: ${title}`, + `- Category: ${categoryLabel}`, + `- Description: ${description}`, + `- Target folder if we set this up: knowledge/${folder}/`, + '', + `Please start by telling me that you can set up a tracking note for "${title}" under knowledge/${folder}/.`, + 'Then briefly explain what that tracking note would monitor or refresh and ask me if you should set it up.', + 'Do not create or modify anything yet.', + 'Treat a clear confirmation from me as explicit approval to proceed.', + `If I confirm later, load the \`tracks\` skill first, check whether a matching note already exists under knowledge/${folder}/, and update it instead of creating a duplicate.`, + `If no matching note exists, create a new note under knowledge/${folder}/ with an appropriate filename.`, + 'Use a track block in that note rather than only writing static content, and keep any surrounding note scaffolding short and useful.', + 'Do not ask me to choose a note path unless there is a real ambiguity you cannot resolve from the card.', + ].join('\n') +} + const normalizeUsage = (usage?: Partial | null): LanguageModelUsage | null => { if (!usage) return null const hasNumbers = Object.values(usage).some((value) => typeof value === 'number') @@ -585,7 +639,7 @@ function App() { const [isGraphOpen, setIsGraphOpen] = useState(false) const [isBrowserOpen, setIsBrowserOpen] = useState(false) const [isSuggestedTopicsOpen, setIsSuggestedTopicsOpen] = useState(false) - const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean } | null>(null) + const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean; suggestedTopics: boolean } | null>(null) const [baseConfigByPath, setBaseConfigByPath] = useState>({}) const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({ nodes: [], @@ -2664,15 +2718,16 @@ function App() { } handleNewChat() // Left-pane "new chat" should always open full chat view. - if (selectedPath || isGraphOpen) { - setExpandedFrom({ path: selectedPath, graph: isGraphOpen }) + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen) { + setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen }) } else { setExpandedFrom(null) } setIsRightPaneMaximized(false) setSelectedPath(null) setIsGraphOpen(false) - }, [chatTabs, activeChatTabId, handleNewChat, selectedPath, isGraphOpen]) + setIsSuggestedTopicsOpen(false) + }, [chatTabs, activeChatTabId, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen]) // Sidebar variant: create/switch chat tab without leaving file/graph context. const handleNewChatTabInSidebar = useCallback(() => { @@ -2766,19 +2821,26 @@ function App() { const handleOpenFullScreenChat = useCallback(() => { // Remember where we came from so the close button can return - if (selectedPath || isGraphOpen) { - setExpandedFrom({ path: selectedPath, graph: isGraphOpen }) + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen) { + setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen }) } setIsRightPaneMaximized(false) setSelectedPath(null) setIsGraphOpen(false) - }, [selectedPath, isGraphOpen]) + setIsSuggestedTopicsOpen(false) + }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen]) const handleCloseFullScreenChat = useCallback(() => { if (expandedFrom) { if (expandedFrom.graph) { setIsGraphOpen(true) + setIsSuggestedTopicsOpen(false) + } else if (expandedFrom.suggestedTopics) { + setIsGraphOpen(false) + setIsSuggestedTopicsOpen(true) } else if (expandedFrom.path) { + setIsGraphOpen(false) + setIsSuggestedTopicsOpen(false) setSelectedPath(expandedFrom.path) } setExpandedFrom(null) @@ -3179,7 +3241,7 @@ function App() { }, []) // Keyboard shortcut: Ctrl+L to toggle main chat view - const isFullScreenChat = !selectedPath && !isGraphOpen && !selectedBackgroundTask && !isBrowserOpen + const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask && !isBrowserOpen useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'l') { @@ -3257,12 +3319,16 @@ function App() { const handleTabKeyDown = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey if (!mod) return - const rightPaneAvailable = Boolean((selectedPath || isGraphOpen) && isChatSidebarOpen) + const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen) && isChatSidebarOpen) const targetPane: ShortcutPane = rightPaneAvailable ? (isRightPaneMaximized ? 'right' : activeShortcutPane) : 'left' - const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen) - const selectedKnowledgePath = isGraphOpen ? GRAPH_TAB_PATH : selectedPath + const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen) + const selectedKnowledgePath = isGraphOpen + ? GRAPH_TAB_PATH + : isSuggestedTopicsOpen + ? SUGGESTED_TOPICS_TAB_PATH + : selectedPath const targetFileTabId = activeFileTabId ?? ( selectedKnowledgePath ? (fileTabs.find((tab) => tab.path === selectedKnowledgePath)?.id ?? null) @@ -3316,7 +3382,7 @@ function App() { } document.addEventListener('keydown', handleTabKeyDown) return () => document.removeEventListener('keydown', handleTabKeyDown) - }, [selectedPath, isGraphOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) + }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) const toggleExpand = (path: string, kind: 'file' | 'dir') => { if (kind === 'file') { @@ -3341,7 +3407,7 @@ function App() { }), }, })) - if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } @@ -3463,14 +3529,14 @@ function App() { }, openGraph: () => { // From chat-only landing state, open graph directly in full knowledge view. - if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } void navigateToView({ type: 'graph' }) }, openBases: () => { - if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } @@ -4042,7 +4108,7 @@ function App() { const selectedTask = selectedBackgroundTask ? backgroundTasks.find(t => t.name === selectedBackgroundTask) : null - const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isBrowserOpen) + const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized const shouldCollapseLeftPane = isRightPaneOnlyMode const openMarkdownTabs = React.useMemo(() => { @@ -4084,7 +4150,7 @@ function App() { onNewChat: handleNewChatTab, onSelectRun: (runIdToLoad) => { cancelRecordingIfActive() - if (selectedPath || isGraphOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) { setIsChatSidebarOpen(true) } @@ -4095,7 +4161,7 @@ function App() { return } // In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar. - if (selectedPath || isGraphOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) { setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t)) loadRun(runIdToLoad) return @@ -4119,14 +4185,14 @@ function App() { } else { // Only one tab, reset it to new chat setChatTabs([{ id: tabForRun.id, runId: null }]) - if (selectedPath || isGraphOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) { handleNewChat() } else { void navigateToView({ type: 'chat', runId: null }) } } } else if (runId === runIdToDelete) { - if (selectedPath || isGraphOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) { setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t)) handleNewChat() } else { @@ -4152,6 +4218,7 @@ function App() { onToggleMeeting={() => { void handleToggleMeeting() }} isBrowserOpen={isBrowserOpen} onToggleBrowser={handleToggleBrowser} + isSuggestedTopicsOpen={isSuggestedTopicsOpen} onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })} /> Version history )} - {!selectedPath && !isGraphOpen && !selectedTask && !isBrowserOpen && ( + {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedTask && !isBrowserOpen && ( )} + {onOpenSuggestedTopics && ( + + )} @@ -707,18 +724,6 @@ export function SidebarContentPanel({ )} - {onOpenSuggestedTopics && ( - - )} ) } interface SuggestedTopicsViewProps { - onExploreTopic: (title: string, description: string) => void + onExploreTopic: (topic: SuggestedTopicBlock) => void } export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps) { const [topics, setTopics] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [removingIndex, setRemovingIndex] = useState(null) useEffect(() => { let cancelled = false async function load() { try { - const result = await window.ipc.invoke('workspace:readFile', { - path: 'config/suggested-topics.md', - }) + let result + try { + result = await window.ipc.invoke('workspace:readFile', { + path: SUGGESTED_TOPICS_PATH, + }) + } catch { + let legacyResult: { data?: string } | null = null + let legacyPath: string | null = null + for (const path of LEGACY_SUGGESTED_TOPICS_PATHS) { + try { + legacyResult = await window.ipc.invoke('workspace:readFile', { path }) + legacyPath = path + break + } catch { + // Try next legacy location. + } + } + if (!legacyResult || !legacyPath) { + throw new Error('Suggested topics file not found') + } + await window.ipc.invoke('workspace:writeFile', { + path: SUGGESTED_TOPICS_PATH, + data: legacyResult.data, + opts: { encoding: 'utf8' }, + }) + await window.ipc.invoke('workspace:remove', { + path: legacyPath, + opts: { trash: true }, + }) + result = legacyResult + } if (cancelled) return if (result.data) { setTopics(parseTopics(result.data)) @@ -95,11 +171,30 @@ export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps return () => { cancelled = true } }, []) - const handleExplore = useCallback( - (topic: SuggestedTopicBlock) => { - onExploreTopic(topic.title, topic.description) + const handleTrack = useCallback( + async (topic: SuggestedTopicBlock, topicIndex: number) => { + if (removingIndex !== null) return + const nextTopics = topics.filter((_, idx) => idx !== topicIndex) + setRemovingIndex(topicIndex) + setError(null) + try { + await window.ipc.invoke('workspace:writeFile', { + path: SUGGESTED_TOPICS_PATH, + data: serializeTopics(nextTopics), + opts: { encoding: 'utf8' }, + }) + setTopics(nextTopics) + } catch (err) { + console.error('Failed to remove suggested topic:', err) + setError('Failed to update suggested topics. Please try again.') + return + } finally { + setRemovingIndex(null) + } + + onExploreTopic(topic) }, - [onExploreTopic], + [onExploreTopic, removingIndex, topics], ) if (loading) { @@ -131,13 +226,18 @@ export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps

Suggested Topics

- Topics surfaced from your knowledge graph. Explore them to create new notes. + Suggested notes surfaced from your knowledge graph. Track one to start a tracking note.

{topics.map((topic, i) => ( - + { void handleTrack(topic, i) }} + isRemoving={removingIndex === i} + /> ))}
diff --git a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts b/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts index f60173f4..781c9a03 100644 --- a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts @@ -180,6 +180,21 @@ Workflow: Ask one question: "Which note should this track live in?" Don't create a new note unless the user asks. +### Suggested Topics exploration flow + +Sometimes the user arrives from the Suggested Topics panel and gives you a prompt like: +- "I am exploring a suggested topic card from the Suggested Topics panel." +- a title, category, description, and target folder such as ` + "`" + `knowledge/Topics/` + "`" + ` or ` + "`" + `knowledge/People/` + "`" + ` + +In that flow: +1. On the first turn, **do not create or modify anything yet**. Briefly explain the tracking note you can set up and ask for confirmation. +2. If the user clearly confirms ("yes", "set it up", "do it"), treat that as explicit permission to proceed. +3. Before creating a new note, search the target folder for an existing matching note and update it if one already exists. +4. If no matching note exists and the prompt gave you a target folder, create the new note there without bouncing back to ask "which note should this live in?". +5. Use the card title as the default note title / filename unless a small normalization is clearly needed. +6. Keep the surrounding note scaffolding minimal but useful. The track block should be the core of the note. +7. If the target folder is one of the structured knowledge folders (` + "`" + `knowledge/People/` + "`" + `, ` + "`" + `knowledge/Organizations/` + "`" + `, ` + "`" + `knowledge/Projects/` + "`" + `, ` + "`" + `knowledge/Topics/` + "`" + `), mirror the local note style by quickly checking a nearby note or config before writing if needed. + ## The Exact Text to Insert Write it verbatim like this (including the blank line between fence and target): diff --git a/apps/x/packages/core/src/knowledge/build_graph.ts b/apps/x/packages/core/src/knowledge/build_graph.ts index 06fd1194..100af5d8 100644 --- a/apps/x/packages/core/src/knowledge/build_graph.ts +++ b/apps/x/packages/core/src/knowledge/build_graph.ts @@ -25,6 +25,12 @@ import { getTagDefinitions } from './tag_system.js'; const NOTES_OUTPUT_DIR = path.join(WorkDir, 'knowledge'); const NOTE_CREATION_AGENT = 'note_creation'; +const SUGGESTED_TOPICS_REL_PATH = 'suggested-topics.md'; +const SUGGESTED_TOPICS_PATH = path.join(WorkDir, 'suggested-topics.md'); +const LEGACY_SUGGESTED_TOPICS_REL_PATH = 'config/suggested-topics.md'; +const LEGACY_SUGGESTED_TOPICS_PATH = path.join(WorkDir, 'config', 'suggested-topics.md'); +const LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_REL_PATH = 'knowledge/Notes/Suggested Topics.md'; +const LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_PATH = path.join(WorkDir, 'knowledge', 'Notes', 'Suggested Topics.md'); // Configuration for the graph builder service const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds @@ -88,6 +94,49 @@ function extractPathFromToolInput(input: string): string | null { } } +function ensureSuggestedTopicsFileLocation(): string { + if (fs.existsSync(SUGGESTED_TOPICS_PATH)) { + return SUGGESTED_TOPICS_PATH; + } + + const legacyCandidates: Array<{ absPath: string; relPath: string }> = [ + { absPath: LEGACY_SUGGESTED_TOPICS_PATH, relPath: LEGACY_SUGGESTED_TOPICS_REL_PATH }, + { absPath: LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_PATH, relPath: LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_REL_PATH }, + ]; + + for (const legacy of legacyCandidates) { + if (!fs.existsSync(legacy.absPath)) { + continue; + } + + try { + fs.renameSync(legacy.absPath, SUGGESTED_TOPICS_PATH); + console.log(`[buildGraph] Moved suggested topics file from ${legacy.relPath} to ${SUGGESTED_TOPICS_REL_PATH}`); + return SUGGESTED_TOPICS_PATH; + } catch (error) { + console.error(`[buildGraph] Failed to move suggested topics file from ${legacy.relPath} to ${SUGGESTED_TOPICS_REL_PATH}:`, error); + return legacy.absPath; + } + } + + return SUGGESTED_TOPICS_PATH; +} + +function readSuggestedTopicsFile(): string { + try { + const suggestedTopicsPath = ensureSuggestedTopicsFileLocation(); + if (!fs.existsSync(suggestedTopicsPath)) { + return '_No existing suggested topics file._'; + } + + const content = fs.readFileSync(suggestedTopicsPath, 'utf-8').trim(); + return content.length > 0 ? content : '_Existing suggested topics file is empty._'; + } catch (error) { + console.error(`[buildGraph] Error reading suggested topics file:`, error); + return '_Failed to read existing suggested topics file._'; + } +} + /** * Get unprocessed voice memo files from knowledge/Voice Memos/ * Voice memos are created directly in this directory by the UI. @@ -203,6 +252,7 @@ async function createNotesFromBatch( const run = await createRun({ agentId: NOTE_CREATION_AGENT, }); + const suggestedTopicsContent = readSuggestedTopicsFile(); // Build message with index and all files in the batch let message = `Process the following ${files.length} source files and create/update obsidian notes.\n\n`; @@ -210,8 +260,9 @@ async function createNotesFromBatch( message += `- Use the KNOWLEDGE BASE INDEX below to resolve entities - DO NOT grep/search for existing notes\n`; message += `- Extract entities (people, organizations, projects, topics) from ALL files below\n`; message += `- Create or update notes in "knowledge" directory (workspace-relative paths like "knowledge/People/Name.md")\n`; + message += `- You may also create or update "${SUGGESTED_TOPICS_REL_PATH}" to maintain curated suggested-topic cards\n`; message += `- If the same entity appears in multiple files, merge the information into a single note\n`; - message += `- Use workspace tools to read existing notes (when you need full content) and write updates\n`; + message += `- Use workspace tools to read existing notes or "${SUGGESTED_TOPICS_REL_PATH}" (when you need full content) and write updates\n`; message += `- Follow the note templates and guidelines in your instructions\n\n`; // Add the knowledge base index @@ -219,6 +270,11 @@ async function createNotesFromBatch( message += knowledgeIndex; message += `\n---\n\n`; + message += `# Current Suggested Topics File\n\n`; + message += `Path: ${SUGGESTED_TOPICS_REL_PATH}\n\n`; + message += suggestedTopicsContent; + message += `\n\n---\n\n`; + // Add each file's content message += `# Source Files to Process\n\n`; files.forEach((file, idx) => { diff --git a/apps/x/packages/core/src/knowledge/note_creation.ts b/apps/x/packages/core/src/knowledge/note_creation.ts index 1d8aa32d..1740bdb7 100644 --- a/apps/x/packages/core/src/knowledge/note_creation.ts +++ b/apps/x/packages/core/src/knowledge/note_creation.ts @@ -485,9 +485,9 @@ RESOLVED (use canonical name with absolute path): - "Acme", "Acme Corp", "@acme.com" → [[Organizations/Acme Corp]] - "the pilot", "the integration" → [[Projects/Acme Integration]] -NEW ENTITIES (create notes if source passes filters): +NEW ENTITIES (create notes or suggestion cards if source passes filters): - "Jennifer" (CTO, Acme Corp) → Create [[People/Jennifer]] or [[People/Jennifer (Acme Corp)]] -- "SOC 2" → Create [[Topics/Security Compliance]] +- "SOC 2" → Add or update a suggestion card in \`suggested-topics.md\` with category \`Topics\` AMBIGUOUS (flag or skip): - "Mike" (no context) → Mention in activity only, don't create note @@ -508,8 +508,8 @@ For entities not resolved to existing notes, determine if they warrant new notes **CREATE a note for people who are:** - External (not @user.domain) -- Attendees in meetings -- Email correspondents (emails that reach this step already passed label-based filtering) +- People you directly interacted with in meetings +- Email correspondents directly participating in the thread (emails that reach this step already passed label-based filtering) - Decision makers or contacts at customers, prospects, or partners - Investors or potential investors - Candidates you are interviewing @@ -521,6 +521,7 @@ For entities not resolved to existing notes, determine if they warrant new notes - Large group meeting attendees you didn't interact with - Internal colleagues (@user.domain) - Assistants handling only logistics +- People mentioned only as third parties ("we work with X", "I can introduce you to Y") when there has been no direct interaction yet ### Role Inference @@ -579,31 +580,155 @@ For people who don't warrant their own note, add to Organization note's Contacts - Sarah Lee — Support, handled wire transfer issue \`\`\` +### Direct Interaction Test (People and Organizations) + +For **new canonical People and Organizations notes**, require **direct interaction**, not just mention. + +**Direct interaction = YES** +- The person sent the email, replied in the thread, or was directly addressed as part of the active exchange +- The person participated in the meeting, and there is evidence the user actually interacted with them or the meeting centered on them +- The organization is directly represented in the exchange by participants/senders and is part of an active first-degree relationship with the user or team +- The user is directly evaluating, selling to, buying from, partnering with, interviewing, or coordinating with that person or organization + +**Direct interaction = NO** +- Someone else mentions them in passing +- A sender says they work with someone at another company +- A sender offers to introduce the user to someone +- A company is referenced as a customer, partner, employer, competitor, or example, but nobody from that company is directly involved in the interaction +- The source only establishes a second-degree relationship, not a direct one + +**Canonical note rule:** +- For **new People/Organizations**, create the canonical note only if both are true: + 1. There is **direct interaction** + 2. The entity clears the **weekly importance test** + +If an entity seems strategically relevant but fails the direct interaction test, do **not** auto-create a canonical note. At most, create a suggestion card in \`suggested-topics.md\`. + +### Weekly Importance Test (People and Organizations only) + +For **People** and **Organizations**, the final gate for **creating a new canonical note** is an importance test: + +**Ask:** _"If I were the user, would I realistically need to look at this note on a weekly basis over the near term?"_ + +This test is mainly for **People** and **Organizations**. **Do NOT use it as the decision rule for Topic or Project suggestions.** + +**Strong YES signals:** +- Active customer, prospect, investor, partner, candidate, advisor, or strategic vendor relationship +- Repeated interaction or a likely ongoing cadence +- Decision-maker, owner, blocker, evaluator, or approver in an active process +- Material relevance to launch, sales, fundraising, hiring, compliance, product delivery, or another current priority +- The user would benefit from a durable reference note instead of repeatedly reopening raw emails or meeting transcripts + +**Strong NO signals:** +- One-off logistics, scheduling, or transactional contact +- Assistant, support rep, recruiter, or vendor rep with no ongoing strategic role +- Incidental attendee mentioned once with no leverage on current work +- Passing mention with no evidence of an ongoing relationship + +**Borderline signals:** +- Seems potentially important, but there isn't enough evidence yet that the user will need a weekly reference note +- Might become important soon, but the role, relationship, or repeated relevance is still unclear +- Important enough to track, but only through second-degree mention or an offered introduction rather than direct interaction + +**Outcome rules for new People/Organizations:** +- **Clear YES + direct interaction** → Create/update the canonical \`People/\` or \`Organizations/\` note +- **Borderline or no direct interaction, but still strategically relevant** → Do **not** create the canonical note yet; instead create or update a card in \`suggested-topics.md\` +- **Clear NO** → Skip note creation and do not add a suggestion unless the source strongly suggests near-term strategic relevance + +**When a canonical note already exists:** +- Update the existing note even if the current source is weaker; the importance test is mainly for deciding whether to create a **new** People/Organization note +- If a previously tentative person/org is now clearly important enough for a canonical note, create/update the note and remove any tentative suggestion card for that exact entity from \`suggested-topics.md\` + ## Organizations **CREATE a note if:** -- Someone from that org attended a meeting -- They're a customer, prospect, investor, or partner -- Someone from that org sent relevant personalized correspondence +- There is direct interaction with that org in the source +- They're a customer, prospect, investor, or partner in a direct first-degree interaction +- Someone from that org sent relevant personalized correspondence or joined a meeting you actually had with them +- They pass the weekly importance test above **DO NOT create for:** - Tool/service providers mentioned in passing - One-time transactional vendors - Consumer service companies +- Organizations only referenced through third-party mention or offered introductions ## Projects -**CREATE a note if:** +**If a project note already exists:** update it. + +**If no project note exists:** do **not** create a new canonical note in \`knowledge/Projects/\`. + +Instead, create or update a **suggestion card** in \`suggested-topics.md\` if the project is strong enough: - Discussed substantively in a meeting or email thread - Has a goal and timeline - Involves multiple interactions +Otherwise skip it. + +Projects do **not** use the weekly importance test above. For **new** projects, the default output is a suggestion card, not a canonical note. + ## Topics -**CREATE a note if:** +**If a topic note already exists:** update it. + +**If no topic note exists:** do **not** create a new canonical note in \`knowledge/Topics/\`. + +Instead, create or update a **suggestion card** in \`suggested-topics.md\` if the topic is strong enough: - Recurring theme discussed - Will come up again across conversations +Otherwise skip it. + +Topics do **not** use the weekly importance test above. For **new** topics, the default output is a suggestion card, not a canonical note. + +## Suggested Topics Curation + +Also maintain \`suggested-topics.md\` as a **curated shortlist** of things worth exploring next. + +Despite the filename, \`suggested-topics.md\` can contain cards for **People, Organizations, Topics, or Projects**. + +There are **two reasons** to add or update a suggestion card: + +1. **High-quality Topic/Project cards** + - Use these for topics or projects that are timely, high-leverage, strategically important, or clearly worth exploring now + - These are not a dump of every topic/project note. Be selective + - For **new** topics and projects, cards are the default output from this pipeline + +2. **Tentative People/Organization cards** + - Use these when a person or organization seems important enough to track, but you are **not 100% sure** they clear the weekly-importance test for a canonical note yet + - The card should capture why they might matter and what still needs verification + +**Do NOT add cards for:** +- Low-signal administrative or transactional entities +- Stale or completed items with no near-term relevance +- People/organizations that already have a clearly established canonical note, unless the card is about a distinct project/topic exploration rather than the entity itself + +**Card guidance:** +- For **Topics/Projects**, use category \`Topics\` or \`Projects\` +- For tentative **People/Organizations**, use category \`People\` or \`Organizations\` +- Title should be concise and canonical when possible +- Description should explain why it matters **now** +- For tentative People/Organizations, description should also mention what is still uncertain or what the user should verify + +**Curation rules:** +- Maintain a **high-quality set**, not an ever-growing backlog +- Deduplicate by normalized title +- Prefer current, actionable, recurring, or strategically important items +- Keep only the strongest **8-12 cards total** +- Preserve good existing cards unless the new source clearly supersedes them +- Remove stale cards that are no longer relevant +- If a tentative People/Organization card later becomes clearly important and you create a canonical note, remove the tentative card + +**File format for \`suggested-topics.md\`:** +\`\`\`suggestedtopic +{"title":"Security Compliance","description":"Summarize the current compliance posture, blockers, and customer implications.","category":"Topics"} +\`\`\` + +The file should start with \`# Suggested Topics\` followed by one or more blocks in that format. + +If the file does not exist, create it. If it exists, update it in place or rewrite the full file so the final result is clean, deduped, and curated. + --- # Step 6: Extract Content @@ -824,7 +949,7 @@ If new info contradicts existing: # Step 9: Write Updates -## 9a: Create and Update Notes +## 9a: Create and Update Notes and Suggested Topic Cards **IMPORTANT: Write sequentially, one file at a time.** - Generate content for exactly one note. @@ -852,6 +977,12 @@ workspace-edit({ }) \`\`\` +**For \`suggested-topics.md\`:** +- Use workspace-relative path \`suggested-topics.md\` +- Read the current file if you need the latest content +- Use \`workspace-writeFile\` to create or rewrite the file when that is simpler and cleaner +- Use \`workspace-edit\` for small targeted edits only if that keeps the file deduped and readable + ## 9b: Apply State Changes For each state change identified in Step 7, update the relevant fields. @@ -867,8 +998,9 @@ If you discovered new name variants during resolution, add them to Aliases field - Be concise: one line per activity entry - Note state changes with \`[Field → value]\` in activity - Escape quotes properly in shell commands -- Write only one file per response (no multi-file write batches) +- Write only one file per response (notes and \`suggested-topics.md\` follow the same rule) - **Always set \`Last update\`** in the Info section to the YYYY-MM-DD date of the source email or meeting. When updating an existing note, update this field to the new source event's date. +- Keep \`suggested-topics.md\` curated, deduped, and capped to the strongest 8-12 cards --- @@ -957,8 +1089,12 @@ Before completing, verify: **Filtering:** - [ ] Excluded self (user.name, user.email, @user.domain) - [ ] Applied relevance test to each person +- [ ] Applied the direct interaction test to new People/Organizations +- [ ] Applied the weekly importance test to new People/Organizations - [ ] Transactional contacts in Org Contacts, not People notes - [ ] Source correctly classified (process vs skip) +- [ ] Third-party mentions did not become new canonical People/Organizations notes +- [ ] Borderline People/Organizations became suggestion cards instead of canonical notes **Content Quality:** - [ ] Summaries describe relationship, not communication method @@ -978,8 +1114,11 @@ Before completing, verify: - [ ] All entity mentions use \`[[Folder/Name]]\` absolute links - [ ] Activity entries are reverse chronological - [ ] No duplicate activity entries +- [ ] \`suggested-topics.md\` stays deduped and curated +- [ ] High-quality Topics/Projects were added to suggested topics only when timely and useful +- [ ] New Topics/Projects were not auto-created as canonical notes - [ ] Dates are YYYY-MM-DD - [ ] Bidirectional links are consistent - [ ] New notes in correct folders `; -} \ No newline at end of file +}