diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 9a79168bd..80898075d 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -71,7 +71,6 @@ Each domain has exactly **one** write-owning module. No other module may call `a | Facets (`facets/*/facet.json`, `facets/*/relationships/`) | `think/facets.py` + `apps/facets/*` (if/when created) | | Observations (`observations.jsonl`) | `think/entities/observations.py` | | Activities (`facets/*/activities/*.jsonl`) | `think/activities.py` | -| Facet events (`facets/*/events/*.jsonl`) | `think/hooks.py::write_events_jsonl`, called only via declared hook contract | | Chronicle day content (`chronicle/YYYYMMDD/**`) | The capturing module (observer, importer) per its declared outputs | | Index (SQLite, `indexer/*`) | `think/indexer/*` | diff --git a/talent/decisions.md b/talent/decisions.md index 4ed93aaf7..bbe197e94 100644 --- a/talent/decisions.md +++ b/talent/decisions.md @@ -3,8 +3,6 @@ "title": "Decision Actions", "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", - "occurrences": "Create an occurrence for every decision-action observed. Include the time span, decision type, actors involved, entities affected, and impact assessment. Each occurrence should capture both the intent and enactment of the decision.", - "hook": {"post": "occurrence"}, "color": "#dc3545", "schedule": "activity", "activities": ["meeting", "call", "messaging", "email"], diff --git a/talent/flow.md b/talent/flow.md deleted file mode 100644 index 975c7a4c1..000000000 --- a/talent/flow.md +++ /dev/null @@ -1,142 +0,0 @@ -{ - "type": "generate", - - "title": "Day Overview", - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", - "occurrences": "Create an occurrence for noteworthy shifts in work rhythms or focus. Include timestamps when deep work starts or ends, or when energy levels noticeably change. Classify each as work or personal based on the surrounding context.", - "hook": {"post": "occurrence"}, - "color": "#17a2b8", - "schedule": "daily", - "priority": 10, - "output": "md", - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} -} - -$facets - -$daily_preamble - -# Workday Productivity & Flow Analysis - -## Objective - -Analyze the full workday transcript to provide deep insights into work patterns, energy management, and productivity optimization, focusing on the holistic flow and rhythm of the workday. - -## Analysis Framework - -### 1. Workday Architecture & Time Patterns -- **Work Session Analysis**: - - Map focused work blocks vs. fragmented segments - - Identify optimal session lengths for different work types - - Analyze startup/shutdown rituals and $pronouns_possessive effectiveness -- **Rhythm & Cadence**: - - Natural work cycles throughout the day - - Productive vs. recovery segments - - Time between breaks and $pronouns_possessive impact on subsequent focus -- **Context Switching Cost**: - - Frequency and depth of task switches - - Recovery time after interruptions - - Strategies used to maintain or regain focus - -### 2. Deep Work & Flow States -- **Flow Identification**: - - Segments of sustained, uninterrupted focus - - Environmental conditions that enabled flow - - Types of work that achieved flow state -- **Flow Blockers**: - - What disrupted deep work sessions - - Attempted deep work that failed and why - - Patterns in when deep work is most/least successful -- **Attention Residue**: - - Impact of incomplete tasks on subsequent work - - How well transitions between different work modes were managed - -### 3. Energy & Cognitive Load Management -- **Energy Patterns**: - - Peak performance windows and what characterized them - - Energy dips and $pronouns_possessive triggers - - Correlation between task type and energy levels -- **Cognitive Load Indicators**: - - Signs of mental fatigue (errors, slower responses, frustration) - - Complexity handling throughout the day - - Decision fatigue patterns -- **Recovery & Renewal**: - - Quality and timing of breaks - - Activities that restored vs. drained energy - - Missed opportunities for strategic renewal - -### 4. Work Style & Personal Productivity Patterns -- **Working Memory Management**: - - How information is tracked across tasks - - Use of external memory systems (notes, tabs, etc.) - - Information loss between sessions -- **Momentum Patterns**: - - How work sessions build on each other - - When momentum is gained or lost - - Impact of starting tasks vs. completing them -- **Personal Productivity Style**: - - Preference for batching vs. interleaving - - Monotropic vs. polytropic work patterns - - Procrastination patterns and triggers - -### 5. Environmental & Contextual Factors -- **Physical Environment Impact**: - - References to workspace, comfort, distractions - - Environmental changes and $pronouns_possessive effects -- **Psychological State**: - - Mood indicators throughout the day - - Stress responses and coping mechanisms - - Enthusiasm/engagement fluctuations -- **External Pressures**: - - Deadline impacts on work patterns - - How external demands shaped the day's flow - -### 6. Productivity Optimization Opportunities - -#### Work Design Improvements: -- **Time Blocking Recommendations**: Specific suggestions for structuring future days -- **Energy-Task Matching**: Aligning high-energy segments with complex work -- **Focus Protection Strategies**: Concrete ways to preserve deep work time -- **Transition Rituals**: Practices to improve task switching - -#### Systemic Improvements: -- **Workflow Bottlenecks**: Recurring friction points in work processes -- **Attention Leaks**: Where focus consistently gets diverted -- **Decision Fatigue Reduction**: Opportunities to streamline or automate decisions -- **Cognitive Load Distribution**: Better ways to spread mental effort - -#### Personal Effectiveness: -- **Leverage Points**: Small changes that could yield big productivity gains -- **Habit Stacking Opportunities**: Beneficial routines to develop -- **Energy Investment ROI**: Where effort produces best/worst returns - -### 7. Comparative Performance Analysis -- **Performance Variability**: What distinguished high vs. low productivity segments -- **Success Pattern Recognition**: Common elements in productive sequences -- **Failure Pattern Analysis**: Recurring productivity obstacles -- **Personal Best Practices**: Effective strategies observed in action - -### 8. Strategic Insights & Meta-Patterns -- **Work-Life Integration**: How personal and professional elements affected each other -- **Capacity Utilization**: Whether operating at sustainable levels -- **Growth Edge Identification**: Where pushing comfort zone vs. playing safe -- **Systemic Issues**: Patterns suggesting organizational or structural problems - -## Output Requirements - -Create a comprehensive markdown report that: -- Uses clear headers and visual hierarchy for scannability -- Includes specific timestamps for key examples -- Balances detailed analysis with actionable insights -- Provides both immediate tactics and long-term strategies -- Uses data visualization descriptions where helpful (e.g., "Energy levels would show a bell curve peaking at 10am-12pm") - -## Special Considerations - -- $Preferred often multi-tasks with background meetings while doing other work -- Consider both explicit productivity (tasks completed) and implicit productivity (relationship building, learning, thinking) -- Note the difference between "busy" and "productive" time -- Identify both successful strategies to replicate and problematic patterns to address -- Consider productivity in context of overall wellbeing and sustainability - -Remember: The goal is to reveal the hidden architecture of the workday—the rhythms, patterns, and dynamics that shape productivity—providing insights that help optimize not just what gets done, but how work flows throughout the day. diff --git a/talent/followups.md b/talent/followups.md index fc65dc6e4..46aad2afb 100644 --- a/talent/followups.md +++ b/talent/followups.md @@ -3,8 +3,6 @@ "title": "Follow-Up Items", "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", - "occurrences": "Whenever a future task or commitment is mentioned, create an occurrence with the expected action and deadline if known. Note who requested it and whether it is work or personal.", - "hook": {"post": "occurrence"}, "color": "#ffc107", "schedule": "activity", "activities": ["meeting", "call", "messaging", "email"], diff --git a/talent/knowledge_graph.md b/talent/knowledge_graph.md deleted file mode 100644 index bfa171e2a..000000000 --- a/talent/knowledge_graph.md +++ /dev/null @@ -1,67 +0,0 @@ -{ - "type": "generate", - - "title": "Knowledge Graph", - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", - "occurrences": "For each entity interaction or relationship mentioned, create an occurrence describing the connection. Include start and end times when the relationship is visible, and capture the type of link such as works-on or discusses-with.", - "hook": {"post": "occurrence"}, - "color": "#6f42c1", - "schedule": "daily", - "priority": 10, - "output": "md", - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} -} - -$facets - -$daily_preamble - -# Comprehensive Workday Knowledge Graph and Network Analysis from Transcripts - -**Input:** A markdown file containing a chronologically ordered transcript of a workday for $name. The transcript is organized by recording segments, each combining information from audio recordings and screen activity for that time period. - -**Objective:** Generate a comprehensive knowledge graph and perform a network analysis based on the provided workday transcripts. - -**Instructions:** - -1. **Entity Extraction and Profiling:** - * Identify all distinct entities. Categorize entities using appropriate types such as: `Person`, `Organization` (companies, groups, teams), `Project`, `Task`, `Concept` (abstract ideas, features, problem domains), `Tool` (software, applications, physical tools), `Location`, `Event`, and `Topic` (general subjects of discussion), or other contextually relevant categories. - * For each identified entity, provide: - * `Entity Name`: The canonical name of the entity. - * `Entity Type`: Its category from the list above. - * `First Appearance`: Time. - * `Total Engagement`: Approximately how many times was it mentioned throughout the day. - * `Context`: Provide a concise (1-2 sentence) summary of its role or context in how it was used, referencing key actions or discussions from both audio and screen data if distinguishable. - * **Concept Quality Filter:** Distinguish `Concept` from `Topic`. A Concept must be a genuinely reusable idea — a mental model, framework, strategic insight, or principle — valuable to recall in a different context. "Discussed the migration timeline" is a Topic (use `Topic` type). "Conway's Law applies to their API design" is a Concept (use `Concept` type). When extracting Concepts, capture WHY the concept matters in the `Context` field, not just that it was mentioned. - -2. **Relationship Mapping:** - * Identify and map all significant connections between entities. - * For each connection, define: - * `Source Name` - * `Target Name` - * `Relationship Type`: Use descriptive labels (e.g., `works-on`, `discusses-with`, `uses-tool-for-project`, `reports-to`, `blocked-by`, `enables`, `references-concept`, `mentions-organization`). Be prepared to infer novel relationship types if the provided examples are insufficient, and briefly justify any novel types. - -3. **Network Analysis and Insights:** - * Based on the extracted entities and relationships, identify and describe: - * `Hub Entities`: Entities with the highest number of diverse connections, acting as central points in the workday. List the top 3-5. - * `Bridge Entities`: Entities that uniquely connect disparate clusters of entities or topics that would otherwise be disconnected. - * `Orphan Idea`: Concepts, tasks, or topics mentioned but not substantially connected to other entities or followed up upon. - * `Temporal Flows`: Describe how attention and activity move through the network over time. For example, "Morning focused on Project X involving Person A and Tool Y, shifting to Concept Z discussions with Person B in the afternoon." - -4. **Output Format:** - * **Part 1: Friendly Markdown Report:** - * A list of all entities with their profiles (from step 1). - * A list of all relationships (from step 2). - * **Part 2: Narrative Analysis (Markdown):** - * A summary of the key findings from the Network Analysis (step 3). - * A qualitative description of what a visual network diagram of this day would highlight. Include specific examples of the 2-3 most interesting or unexpected connections discovered, explaining why they are noteworthy (e.g., "An interesting connection is Person A using Tool Z, typically associated with Project Q, for an ad-hoc task related to Concept R. This suggests a novel application or workaround."). - -**Key Considerations:** -* Synthesize information from all transcript content within each chunk. -* Disambiguate entities by consolidating name variants to a single canonical entity using the most complete name when the same entity is referenced by different names within the transcript (e.g., "John", "John D.", "John Doe" → use "John Doe" throughout). -* When first-name-only references are ambiguous, note the ambiguity in the entity's Context field rather than guessing identity. -* Cross-reference names against attendees and participants mentioned earlier in the transcript for spelling corrections and consistent naming. -* Infer implicit relationships where explicit statements are lacking but context strongly suggests a connection. -* Focus on the most relevant and significant entities and relationships to avoid an overly noisy graph. -* For live capture, $preferred often multi-tasks — e.g., joined on a team zoom in the background while working on an unrelated task — so different content streams may not always align. -* Take time to consider all of the nuance of the interactions from the day, deeply think through how best to prioritize the most important aspects and understandings, formulate the best approach for each step of the analysis. diff --git a/talent/meetings.md b/talent/meetings.md index 4f2fc35b7..53bf43b16 100644 --- a/talent/meetings.md +++ b/talent/meetings.md @@ -3,8 +3,6 @@ "title": "Meeting Notes", "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", - "occurrences": "Each meeting should generate an occurrence with start and end times, list of participants and a concise summary. If slides are present, mention them in the details field.", - "hook": {"post": "occurrence"}, "color": "#e83e8c", "schedule": "activity", "activities": ["meeting"], diff --git a/talent/messaging.md b/talent/messaging.md index c7a7c1809..5b512e3d1 100644 --- a/talent/messaging.md +++ b/talent/messaging.md @@ -3,8 +3,6 @@ "title": "Messaging Summary", "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", - "occurrences": "Create an occurrence for every distinct message interaction. Include the time block, app name, contacts or channels involved, whether $preferred was reading or replying, and a summary of visible content.", - "hook": {"post": "occurrence"}, "color": "#78909c", "schedule": "activity", "activities": ["messaging", "email"], diff --git a/talent/occurrence.md b/talent/occurrence.md deleted file mode 100644 index 6db5dd38c..000000000 --- a/talent/occurrence.md +++ /dev/null @@ -1,74 +0,0 @@ -{ - - "title": "Occurrence Extraction", - "description": "Extracts structured occurrence events from insight summaries.", - "color": "#37474f" - -} - -# Occurrence JSON Conversion - -## Objective - -Extract events from a Markdown summary generated from daily transcripts and convert them into structured JSON occurrences. - -## Instructions -1. **Extract every distinct event** mentioned in the summary - no matter how brief -2. **Be comprehensive** - capture meetings, messages, file activities, follow-ups, documentation work, research, media consumption, etc. -3. **Preserve timing information** when available in the source -4. **Infer missing details** reasonably when context provides clues -5. **Assign facets** - for every occurrence, always choose the best matching facet from the Available Facets context. Use the facet name/ID (e.g., `facet_name`) based on entities mentioned, subject matter, or context. This field is required. -6. **Return only valid JSON** - no commentary, explanations, or wrapper objects -7. **Handle empty sources** - if the source indicates no events occurred (e.g., "No meetings detected"), return an empty array: `[]` -8. **Separate concurrent activities** - if the owner is engaged in two unrelated activities simultaneously (e.g., a meeting on one screen and a text conversation on another), these must be separate events with separate participant lists. A person texting during a meeting is NOT a participant in that meeting. Signals of concurrent-but-unrelated activity include: different communication channels (Signal vs Webex), different subject matter, different facets. -9. **Respect facet boundaries for participants** - participants should only be listed for events within their relevant facet. A personal contact who appears in a concurrent personal activity should not be listed in a work event's participants. When the source shows interleaved activities across facets, separate them into distinct occurrences and assign participants only to the occurrence they actually belong to. - -## Occurrence Fields -- **type** – the kind of occurrence such as `meeting`, `message`, `file`, `followup`, `documentation`, `research`, `media`, etc. -- **start** and **end** – HH:MM:SS timestamps containing the occurrence (use "00:00:00" if unknown) -- **title** – short descriptive title for display -- **summary** – concise one-sentence recap of what happened -- **work** – boolean classification: `true` for work-related, `false` for personal or not work related -- **participants** – optional list of people or entities involved (empty array if none) -- **facet** – required facet identifier; use the facet name/ID from Available Facets context that best matches the occurrence based on entities, subject matter, or context -- **details** – free-form string capturing all additional context, specifics, outcomes, or other relevant information from the original document that is not covered already by the other fields - -## Output Format -Return a JSON array of occurrences only. Each occurrence must include all required fields. - -## Example -[ - { - "type": "meeting", - "start": "09:00:00", - "end": "09:30:00", - "title": "Team stand-up", - "summary": "Daily status update with the engineering team discussing sprint progress and blockers.", - "work": true, - "participants": ["$name", "Alice", "Bob"], - "facet": "work_project_alpha", - "details": "Alice reported database optimization complete ahead of schedule. Bob mentioned UI testing delays due to missing design assets. Scheduled follow-up meeting for authentication module review. Sprint velocity tracking discussed." - }, - { - "type": "message", - "start": "14:22:00", - "end": "14:22:00", - "title": "Slack message to design team", - "summary": "Requested updated mockups for the login flow redesign.", - "work": true, - "participants": ["$name", "Design Team"], - "facet": "work_project_alpha", - "details": "Specifically asked for mobile responsive versions and accessibility considerations. Mentioned deadline of end of week." - }, - { - "type": "research", - "start": "15:30:00", - "end": "16:15:00", - "title": "Authentication framework comparison", - "summary": "Researched OAuth 2.0 vs Auth0 implementation options for the new user system.", - "work": true, - "participants": [], - "facet": "work_project_alpha", - "details": "Compared security features, pricing models, and integration complexity. Created comparison spreadsheet with pros/cons. Leaning toward Auth0 for faster implementation." - } -] diff --git a/talent/occurrence.py b/talent/occurrence.py deleted file mode 100644 index 6ae807438..000000000 --- a/talent/occurrence.py +++ /dev/null @@ -1,119 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Hook for extracting occurrence events from generator output results. - -This hook is invoked via "hook": {"post": "occurrence"} in generator frontmatter. -It extracts structured JSON events from markdown summaries and writes -them to facet-based JSONL files. -""" - -import json -import logging -from pathlib import Path - -from think.facets import facet_summaries -from think.hooks import ( - compute_output_source, - log_extraction_failure, - should_skip_extraction, - write_events_jsonl, -) -from think.models import generate -from think.prompts import load_prompt -from think.talent import get_output_name - - -def post_process(result: str, context: dict) -> str | None: - """Extract occurrence events from generator output result. - - This hook extracts structured JSON events from markdown output summaries - and writes them to facet-based JSONL files. - - Args: - result: The generated output markdown content. - context: Config dict with keys including day, segment, name, - output_path, meta, transcript, span, span_mode. - - Returns: - None - this hook does not modify the output result. - """ - # Check skip conditions - skip_reason = should_skip_extraction(result, context) - if skip_reason: - logging.info("Skipping occurrence extraction: %s", skip_reason) - return None - - # Load extraction prompt - prompt_content = load_prompt("occurrence", base_dir=Path(__file__).parent) - - # Build context with facets + agent-specific instructions - facets_context = facet_summaries(detailed=True) - agent_instructions = context.get("meta", {}).get("occurrences") - if agent_instructions and isinstance(agent_instructions, str): - extra_instructions = f"{facets_context}\n\n{agent_instructions}" - else: - extra_instructions = facets_context - - # Extract events - name = context.get("name", "unknown") - contents = [extra_instructions, result] - - try: - response_text = generate( - contents=contents, - context=f"talent.system.{name}", - temperature=0.3, - max_output_tokens=24576, - thinking_budget=0, - system_instruction=prompt_content.text, - json_output=True, - ) - except Exception as e: - log_extraction_failure(e, name) - return None - - try: - events = json.loads(response_text) - except json.JSONDecodeError as e: - logging.error("Invalid JSON from occurrence extraction: %s", e) - return None - - if not isinstance(events, list): - logging.error("Extraction did not return array") - return None - - filtered_events = [] - for event in events: - if event.get("type") == "meeting" and len(event.get("participants", [])) > 25: - logging.warning( - "Dropping megameeting occurrence: title=%r agent=%s participants=%d", - event.get("title", ""), - name, - len(event.get("participants", [])), - ) - continue - filtered_events.append(event) - events = filtered_events - - # Write to facet JSONL files - source_output = compute_output_source(context) - output_name = get_output_name(name) - day = context.get("day", "") - - written_paths = write_events_jsonl( - events=events, - agent=output_name, - occurred=True, - source_output=source_output, - capture_day=day, - ) - - if written_paths: - print(f"Events written to {len(written_paths)} JSONL file(s):") - for p in written_paths: - print(f" {p}") - else: - print("No events with valid facets to write") - - return None # Don't modify insight result diff --git a/talent/timeline.md b/talent/timeline.md deleted file mode 100644 index f4bd11b2a..000000000 --- a/talent/timeline.md +++ /dev/null @@ -1,82 +0,0 @@ -{ - "type": "generate", - - "title": "Day Timeline", - "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", - "occurrences": "Create an occurrence for each hour segment, don't break down hours into any smaller segments the goal for timeline occurrences is for them to capture whatever happened within each hour of the day where there was activity.", - "hook": {"post": "occurrence"}, - "color": "#7b1fa2", - "schedule": "daily", - "priority": 10, - "output": "md", - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} - -} - -$daily_preamble - -# Comprehensive Workday Timeline Documentation - -## Objective - -Create a detailed, chronological timeline of the entire workday, documenting everything that happened with rich, descriptive detail. The transcript combines audio conversations and screen activity organized by recording segments. - -## Documentation Approach - -### Timeline Structure -- Organize by hour (e.g., 09:00-10:00) -- Within each hour, create sub-sections for every activity shift -- Include precise timestamps in HHMMSS format for all transitions -- Document parallel activities (e.g., coding while in a background meeting) -- Capture even brief activities if they represent a focus shift - -### What to Capture -For each time block, capture notes such as: -- **Primary Activity**: What was being actively worked on -- **Tools & Applications**: All software/websites being used -- **Files & Documents**: Specific files opened, edited, or referenced -- **Collaborator Context**: Mention collaborators in prose only when materially relevant to the activity; do not emit standalone lists of names -- **Content Details**: Topics discussed, code written, problems solved -- **Parallel Activities**: Background meetings, music, notifications -- **Physical Context**: Any mentions of location, movement, breaks - -### Level of Detail -- Include specific project names and file names; mention collaborators in prose only when materially relevant, never as standalone name lists -- Describe the work or activity (what code was written, what was discussed) -- Note transitions between activities, even small ones -- Capture the substance of meetings and conversations -- Include break activities (media, games, etc) -- Document both completed and attempted tasks - -## Format - -Output a nicely formatted markdown document with per-hour sections and sub-sections within each hour for every task or focus shift. Only include time segments when there was activity in the transcripts. - -## Documentation Guidelines - -1. **Be Descriptive, Not Analytical** - - Write what happened, not why or how well - - Include details that paint a picture of the day - - Avoid productivity assessments or recommendations - -2. **Capture Everything** - - Don't omit "minor" activities if they represent focus shifts - - Include failed attempts and dead ends - - Document personal moments that interrupt work - -3. **Use Precise Language** - - Specific file names, not "worked on code" - - Actual meeting topics, not "discussed project" - - Real error messages and solutions found - -4. **Maintain Chronological Integrity** - - If activities overlap, note both - - Don't group similar activities from different times - - Preserve the actual flow of the day - -5. **Rich Context** - - Include enough detail that someone could understand what was accomplished - - Note tools, files, collaborators, and resources involved in prose when materially relevant; do not emit standalone lists of names - - Capture the substance of work, not just categories - -Remember: The goal is to create a detailed historical record of the day that captures not just what activities occurred, but the rich detail of how the work actually unfolded. This timeline should serve as a comprehensive reference that could help reconstruct any part of the day's work. diff --git a/tests/baselines/api/activities/day-events.json b/tests/baselines/api/activities/day-events.json index 7f463b7d0..ca57411a9 100644 --- a/tests/baselines/api/activities/day-events.json +++ b/tests/baselines/api/activities/day-events.json @@ -1,7 +1,7 @@ [ { "agent": "flow", - "color": "#17a2b8", + "color": "#6c757d", "details": "Attended keynote on unified API gateways", "endTime": "2026-03-04T12:00:00", "facet": "montague", @@ -18,7 +18,7 @@ }, { "agent": "flow", - "color": "#17a2b8", + "color": "#6c757d", "details": "Juliet and Romeo exchanged Signal contacts", "endTime": "2026-03-04T20:00:00", "facet": "capulet", @@ -35,7 +35,7 @@ }, { "agent": "flow", - "color": "#17a2b8", + "color": "#6c757d", "details": "Standing ovation for architecture presentation", "endTime": "2026-03-04T10:00:00", "facet": "capulet", @@ -51,7 +51,7 @@ }, { "agent": "flow", - "color": "#17a2b8", + "color": "#6c757d", "details": "Tybalt confronted Romeo", "endTime": "2026-03-04T18:00:00", "facet": "montague", diff --git a/tests/baselines/api/settings/generators.json b/tests/baselines/api/settings/generators.json index 77f36134b..150da5d57 100644 --- a/tests/baselines/api/settings/generators.json +++ b/tests/baselines/api/settings/generators.json @@ -20,16 +20,6 @@ "source": "system", "title": "Maintenance Window" }, - { - "app": null, - "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", - "disabled": false, - "extract": true, - "has_extraction": true, - "key": "timeline", - "source": "system", - "title": "Day Timeline" - }, { "app": null, "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", @@ -39,26 +29,6 @@ "key": "schedule", "source": "system", "title": "Upcoming Schedule" - }, - { - "app": null, - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", - "disabled": false, - "extract": true, - "has_extraction": true, - "key": "knowledge_graph", - "source": "system", - "title": "Knowledge Graph" - }, - { - "app": null, - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", - "disabled": false, - "extract": true, - "has_extraction": true, - "key": "flow", - "source": "system", - "title": "Day Overview" } ], "segment": [ diff --git a/tests/baselines/api/settings/providers.json b/tests/baselines/api/settings/providers.json index a9939b899..e65f41039 100644 --- a/tests/baselines/api/settings/providers.json +++ b/tests/baselines/api/settings/providers.json @@ -223,7 +223,6 @@ }, "talent.system.decisions": { "disabled": false, - "extract": true, "group": "Think", "label": "Decision Actions", "schedule": "activity", @@ -253,18 +252,8 @@ "tier": 2, "type": null }, - "talent.system.flow": { - "disabled": false, - "extract": true, - "group": "Think", - "label": "Day Overview", - "schedule": "daily", - "tier": 2, - "type": "generate" - }, "talent.system.followups": { "disabled": false, - "extract": true, "group": "Think", "label": "Follow-Up Items", "schedule": "activity", @@ -287,18 +276,8 @@ "tier": 2, "type": "cogitate" }, - "talent.system.knowledge_graph": { - "disabled": false, - "extract": true, - "group": "Think", - "label": "Knowledge Graph", - "schedule": "daily", - "tier": 2, - "type": "generate" - }, "talent.system.meetings": { "disabled": false, - "extract": true, "group": "Think", "label": "Meeting Notes", "schedule": "activity", @@ -307,7 +286,6 @@ }, "talent.system.messaging": { "disabled": false, - "extract": true, "group": "Think", "label": "Messaging Summary", "schedule": "activity", @@ -329,13 +307,6 @@ "tier": 2, "type": "cogitate" }, - "talent.system.occurrence": { - "disabled": false, - "group": "Think", - "label": "Occurrence Extraction", - "tier": 2, - "type": null - }, "talent.system.participation": { "disabled": false, "group": "Think", @@ -415,15 +386,6 @@ "tier": 2, "type": null }, - "talent.system.timeline": { - "disabled": false, - "extract": true, - "group": "Think", - "label": "Day Timeline", - "schedule": "daily", - "tier": 2, - "type": "generate" - }, "talent.system.triage": { "disabled": false, "group": "Think", diff --git a/tests/baselines/api/sol/talents-day.json b/tests/baselines/api/sol/talents-day.json index e2f112b94..e90d33ab2 100644 --- a/tests/baselines/api/sol/talents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -203,17 +203,6 @@ "title": "facet_newsletter", "type": null }, - "flow": { - "app": null, - "color": "#17a2b8", - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Day Overview", - "type": "generate" - }, "followups": { "app": null, "color": "#ffc107", @@ -291,17 +280,6 @@ "title": "Joke Bot", "type": "cogitate" }, - "knowledge_graph": { - "app": null, - "color": "#6f42c1", - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Knowledge Graph", - "type": "generate" - }, "meetings": { "app": null, "color": "#e83e8c", @@ -346,17 +324,6 @@ "title": "Naming", "type": "cogitate" }, - "occurrence": { - "app": null, - "color": "#37474f", - "description": "Extracts structured occurrence events from insight summaries.", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Occurrence Extraction", - "type": null - }, "participation": { "app": null, "color": "#6c757d", @@ -478,17 +445,6 @@ "title": "test_missing_type_output_1568d299dc474aa9ba42401c7b1b75e2", "type": null }, - "timeline": { - "app": null, - "color": "#7b1fa2", - "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Day Timeline", - "type": "generate" - }, "todos:daily": { "app": "todos", "color": "#6c757d", diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index f6879a4b7..270b56579 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -32,9 +32,6 @@ ], "color": "#dc3545", "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", - "hook": { - "post": "occurrence" - }, "load": { "percepts": false, "talents": { @@ -43,7 +40,6 @@ "transcripts": true }, "mtime": 0, - "occurrences": "Create an occurrence for every decision-action observed. Include the time span, decision type, actors involved, entities affected, and impact assessment. Each occurrence should capture both the intent and enactment of the decision.", "output": "md", "path": "/talent/decisions.md", "priority": 10, @@ -122,29 +118,6 @@ "title": "Entity Observer", "type": "generate" }, - "flow": { - "color": "#17a2b8", - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", - "hook": { - "post": "occurrence" - }, - "load": { - "percepts": false, - "talents": { - "screen": true - }, - "transcripts": true - }, - "mtime": 0, - "occurrences": "Create an occurrence for noteworthy shifts in work rhythms or focus. Include timestamps when deep work starts or ends, or when energy levels noticeably change. Classify each as work or personal based on the surrounding context.", - "output": "md", - "path": "/talent/flow.md", - "priority": 10, - "schedule": "daily", - "source": "system", - "title": "Day Overview", - "type": "generate" - }, "followups": { "activities": [ "meeting", @@ -154,9 +127,6 @@ ], "color": "#ffc107", "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", - "hook": { - "post": "occurrence" - }, "load": { "percepts": false, "talents": { @@ -165,7 +135,6 @@ "transcripts": true }, "mtime": 0, - "occurrences": "Whenever a future task or commitment is mentioned, create an occurrence with the expected action and deadline if known. Note who requested it and whether it is work or personal.", "output": "md", "path": "/talent/followups.md", "priority": 10, @@ -174,38 +143,12 @@ "title": "Follow-Up Items", "type": "generate" }, - "knowledge_graph": { - "color": "#6f42c1", - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", - "hook": { - "post": "occurrence" - }, - "load": { - "percepts": false, - "talents": { - "screen": true - }, - "transcripts": true - }, - "mtime": 0, - "occurrences": "For each entity interaction or relationship mentioned, create an occurrence describing the connection. Include start and end times when the relationship is visible, and capture the type of link such as works-on or discusses-with.", - "output": "md", - "path": "/talent/knowledge_graph.md", - "priority": 10, - "schedule": "daily", - "source": "system", - "title": "Knowledge Graph", - "type": "generate" - }, "meetings": { "activities": [ "meeting" ], "color": "#e83e8c", "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", - "hook": { - "post": "occurrence" - }, "load": { "percepts": false, "talents": { @@ -214,7 +157,6 @@ "transcripts": true }, "mtime": 0, - "occurrences": "Each meeting should generate an occurrence with start and end times, list of participants and a concise summary. If slides are present, mention them in the details field.", "output": "md", "path": "/talent/meetings.md", "priority": 10, @@ -230,9 +172,6 @@ ], "color": "#78909c", "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", - "hook": { - "post": "occurrence" - }, "load": { "percepts": false, "talents": { @@ -241,7 +180,6 @@ "transcripts": true }, "mtime": 0, - "occurrences": "Create an occurrence for every distinct message interaction. Include the time block, app name, contacts or channels involved, whether $preferred was reading or replying, and a summary of visible content.", "output": "md", "path": "/talent/messaging.md", "priority": 10, @@ -381,29 +319,6 @@ "source": "system", "title": "Speaker Attribution", "type": "generate" - }, - "timeline": { - "color": "#7b1fa2", - "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", - "hook": { - "post": "occurrence" - }, - "load": { - "percepts": false, - "talents": { - "screen": true - }, - "transcripts": true - }, - "mtime": 0, - "occurrences": "Create an occurrence for each hour segment, don't break down hours into any smaller segments the goal for timeline occurrences is for them to capture whatever happened within each hour of the day where there was activity.", - "output": "md", - "path": "/talent/timeline.md", - "priority": 10, - "schedule": "daily", - "source": "system", - "title": "Day Timeline", - "type": "generate" } }, "stats": {} diff --git a/tests/test_generate_full.py b/tests/test_generate_full.py index 182482609..e8fad0d61 100644 --- a/tests/test_generate_full.py +++ b/tests/test_generate_full.py @@ -320,8 +320,8 @@ def test_named_hook_resolution(tmp_path, monkeypatch): from think.talent import load_post_hook # Config with named hook (new format) - config = {"hook": {"post": "occurrence"}} + config = {"hook": {"post": "schedule"}} hook_fn = load_post_hook(config) - # Should resolve to talent/occurrence.py and be callable + # Should resolve to talent/schedule.py and be callable assert callable(hook_fn) diff --git a/tests/test_generate_scan_day.py b/tests/test_generate_scan_day.py index f6c005bd8..bd5b588a4 100644 --- a/tests/test_generate_scan_day.py +++ b/tests/test_generate_scan_day.py @@ -18,7 +18,7 @@ def copy_day(tmp_path: Path) -> Path: copytree_tracked(src, dest) talents_dir = dest / "talents" talents_dir.mkdir(exist_ok=True) # Allow existing directory - (talents_dir / "flow.md").write_text("done") + (talents_dir / "schedule.json").write_text("[]") return dest @@ -28,10 +28,10 @@ def test_scan_day(tmp_path, monkeypatch): monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) info = mod.scan_day("20240101") - assert "talents/flow.md" in info["processed"] - assert "talents/timeline.md" in info["repairable"] + assert "talents/schedule.json" in info["processed"] + assert "talents/daily_schedule.json" in info["repairable"] - (day_dir / "talents" / "timeline.md").write_text("done") + (day_dir / "talents" / "daily_schedule.json").write_text("[]") info_after = mod.scan_day("20240101") - assert "talents/timeline.md" in info_after["processed"] - assert "talents/timeline.md" not in info_after["repairable"] + assert "talents/daily_schedule.json" in info_after["processed"] + assert "talents/daily_schedule.json" not in info_after["repairable"] diff --git a/tests/test_generators.py b/tests/test_generators.py index c09b59266..99cbeada5 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -13,13 +13,13 @@ def test_get_talent_configs_generators(): """Test that system generators are discovered with source field.""" talent = importlib.import_module("think.talent") generators = talent.get_talent_configs(type="generate") - assert "flow" in generators - info = generators["flow"] - assert os.path.basename(info["path"]) == "flow.md" + assert "schedule" in generators + info = generators["schedule"] + assert os.path.basename(info["path"]) == "schedule.md" assert isinstance(info["color"], str) assert isinstance(info["mtime"], int) assert "title" in info - assert "occurrences" in info + assert "occurrences" not in info # New: check source field assert info.get("source") == "system" diff --git a/tests/test_output_hooks.py b/tests/test_output_hooks.py index 6e94a457f..4f5ed12d5 100644 --- a/tests/test_output_hooks.py +++ b/tests/test_output_hooks.py @@ -12,13 +12,10 @@ Tests cover: import importlib import io import json -import logging import os from pathlib import Path -import talent.occurrence as occurrence from tests.conftest import copytree_tracked -from think.hooks import write_events_jsonl from think.talent import load_post_hook, load_pre_hook from think.talents import _apply_template_vars from think.utils import day_path @@ -121,8 +118,8 @@ def test_load_post_hook_no_hook_config(): def test_load_post_hook_named_resolution(): """Test that named hooks resolve to talent/{name}.py.""" - # occurrence.py exists in talent/ - config = {"hook": {"post": "occurrence"}} + # schedule.py exists in talent/ + config = {"hook": {"post": "schedule"}} hook_fn = load_post_hook(config) assert callable(hook_fn) @@ -306,286 +303,6 @@ def post_process(result, context): assert finish_events[0]["result"] == MOCK_RESULT["text"] -def test_occurrence_post_process_drops_meeting_with_26_participants( - monkeypatch, caplog -): - """Test megameeting occurrences are dropped before writing.""" - captured = {} - participants = [f"Person {i}" for i in range(26)] - - def mock_generate(**kwargs): - return json.dumps( - [ - { - "type": "meeting", - "title": "All Hands", - "summary": "Large meeting", - "work": True, - "participants": participants, - "facet": "capulet", - "details": "", - } - ] - ) - - def mock_write_events_jsonl(**kwargs): - captured.update(kwargs) - return [] - - monkeypatch.setattr(occurrence, "generate", mock_generate) - monkeypatch.setattr( - occurrence, "compute_output_source", lambda context: "source.md" - ) - monkeypatch.setattr(occurrence, "write_events_jsonl", mock_write_events_jsonl) - caplog.set_level(logging.WARNING) - - result = occurrence.post_process( - "x" * 60, - { - "name": "meetings", - "day": "20240101", - "meta": {}, - "output_path": "ignored", - }, - ) - - assert result is None - assert captured["events"] == [] - assert "Dropping megameeting occurrence" in caplog.text - assert "All Hands" in caplog.text - assert "meetings" in caplog.text - assert "26" in caplog.text - - -def test_occurrence_post_process_keeps_meeting_with_25_participants( - monkeypatch, caplog -): - """Test meetings at the participant threshold are preserved.""" - captured = {} - event = { - "type": "meeting", - "title": "Planning", - "summary": "Planning meeting", - "work": True, - "participants": [f"Person {i}" for i in range(25)], - "facet": "capulet", - "details": "", - } - - monkeypatch.setattr(occurrence, "generate", lambda **kwargs: json.dumps([event])) - monkeypatch.setattr( - occurrence, "compute_output_source", lambda context: "source.md" - ) - monkeypatch.setattr( - occurrence, - "write_events_jsonl", - lambda **kwargs: captured.update(kwargs) or [], - ) - caplog.set_level(logging.WARNING) - - result = occurrence.post_process( - "x" * 60, - { - "name": "meetings", - "day": "20240101", - "meta": {}, - "output_path": "ignored", - }, - ) - - assert result is None - assert captured["events"] == [event] - assert "Dropping megameeting occurrence" not in caplog.text - - -def test_occurrence_post_process_keeps_non_meeting_with_large_participants_list( - monkeypatch, caplog -): - """Test non-meeting events are not filtered by participant count.""" - captured = {} - event = { - "type": "message", - "title": "Inbox review", - "summary": "Reviewed messages", - "work": True, - "participants": [f"Person {i}" for i in range(100)], - "facet": "capulet", - "details": "", - } - - monkeypatch.setattr(occurrence, "generate", lambda **kwargs: json.dumps([event])) - monkeypatch.setattr( - occurrence, "compute_output_source", lambda context: "source.md" - ) - monkeypatch.setattr( - occurrence, - "write_events_jsonl", - lambda **kwargs: captured.update(kwargs) or [], - ) - caplog.set_level(logging.WARNING) - - result = occurrence.post_process( - "x" * 60, - { - "name": "timeline", - "day": "20240101", - "meta": {}, - "output_path": "ignored", - }, - ) - - assert result is None - assert captured["events"] == [event] - assert "Dropping megameeting occurrence" not in caplog.text - - -def test_write_events_jsonl_skips_trailing_comma_facet(journal_copy, caplog): - """Test invalid trailing punctuation facets are rejected.""" - caplog.set_level(logging.WARNING) - - written = write_events_jsonl( - events=[ - { - "type": "message", - "title": "Chat", - "summary": "Sent a chat", - "work": True, - "participants": [], - "facet": "kognova,", - "details": "", - } - ], - agent="timeline", - occurred=True, - source_output="20240101/agents/timeline.md", - capture_day="20240101", - ) - - assert written == [] - assert "Skipping event with unknown facet" in caplog.text - assert "kognova," in caplog.text - assert "timeline" in caplog.text - assert "20240101/agents/timeline.md" in caplog.text - assert not (journal_copy / "facets" / "kognova," / "events").exists() - - -def test_write_events_jsonl_skips_unknown_person_facet(journal_copy, caplog): - """Test unknown person-like facet names are rejected.""" - caplog.set_level(logging.WARNING) - - written = write_events_jsonl( - events=[ - { - "type": "message", - "title": "Chat", - "summary": "Sent a chat", - "work": True, - "participants": [], - "facet": "Person", - "details": "", - } - ], - agent="timeline", - occurred=True, - source_output="20240101/agents/timeline.md", - capture_day="20240101", - ) - - assert written == [] - assert "Skipping event with unknown facet" in caplog.text - assert "Person" in caplog.text - assert not (journal_copy / "facets" / "Person" / "events").exists() - - -def test_write_events_jsonl_skips_mixed_case_known_facet(journal_copy, caplog): - """Test mixed-case facet values are rejected when not exact registry matches.""" - caplog.set_level(logging.WARNING) - - written = write_events_jsonl( - events=[ - { - "type": "message", - "title": "Chat", - "summary": "Sent a chat", - "work": True, - "participants": [], - "facet": "Capulet", - "details": "", - } - ], - agent="timeline", - occurred=True, - source_output="20240101/agents/timeline.md", - capture_day="20240101", - ) - - assert written == [] - assert "Skipping event with unknown facet" in caplog.text - assert "Capulet" in caplog.text - assert not (journal_copy / "facets" / "Capulet" / "events").exists() - - -def test_write_events_jsonl_writes_valid_registry_facet(journal_copy): - """Test valid registry facets are written normally.""" - written = write_events_jsonl( - events=[ - { - "type": "message", - "title": "Chat", - "summary": "Sent a chat", - "work": True, - "participants": [], - "facet": "capulet", - "details": "", - } - ], - agent="timeline", - occurred=True, - source_output="20240101/agents/timeline.md", - capture_day="20240101", - ) - - jsonl_path = journal_copy / "facets" / "capulet" / "events" / "20240101.jsonl" - - assert written == [jsonl_path] - rows = [ - json.loads(line) - for line in jsonl_path.read_text(encoding="utf-8").splitlines() - if line - ] - assert len(rows) == 1 - assert rows[0]["facet"] == "capulet" - assert rows[0]["agent"] == "timeline" - assert rows[0]["source"] == "20240101/agents/timeline.md" - - -def test_write_events_jsonl_skips_empty_facet(journal_copy, caplog): - """Test missing facets are skipped and logged.""" - caplog.set_level(logging.WARNING) - - written = write_events_jsonl( - events=[ - { - "type": "message", - "title": "Chat", - "summary": "Sent a chat", - "work": True, - "participants": [], - "details": "", - } - ], - agent="timeline", - occurred=True, - source_output="20240101/agents/timeline.md", - capture_day="20240101", - ) - - assert written == [] - assert "Skipping event with unknown facet" in caplog.text - assert "timeline" in caplog.text - assert not (journal_copy / "facets" / "" / "events").exists() - - # ============================================================================= # Pre-hook Tests # ============================================================================= diff --git a/tests/test_stats_contract.py b/tests/test_stats_contract.py index 402873f99..59ef40a6e 100644 --- a/tests/test_stats_contract.py +++ b/tests/test_stats_contract.py @@ -82,7 +82,7 @@ def _build_journal(base_path): ) (seg2 / "audio.flac").write_bytes(b"fLaC") - (day / "talents" / "flow.md").write_text("") + (day / "talents" / "schedule.json").write_text("[]") events_dir = journal / "facets" / "work" / "events" events_dir.mkdir(parents=True) diff --git a/tests/test_talent_cli.py b/tests/test_talent_cli.py index 73f9d798d..4580f4fa1 100644 --- a/tests/test_talent_cli.py +++ b/tests/test_talent_cli.py @@ -25,7 +25,7 @@ from think.talent_cli import ( def test_collect_configs_returns_prompts(): """All configs include known system prompts.""" configs = _collect_configs(include_disabled=True) - assert "flow" in configs + assert "schedule" in configs assert "sense" in configs assert "chat" in configs @@ -36,8 +36,8 @@ def test_collect_configs_excludes_disabled_by_default(): with_disabled = _collect_configs(include_disabled=True) # include_disabled should return at least as many configs assert len(with_disabled) >= len(without) - assert "flow" in without - assert "flow" in with_disabled + assert "schedule" in without + assert "schedule" in with_disabled def test_collect_configs_filter_schedule(): @@ -128,7 +128,7 @@ def test_list_prompts_output(capsys): # Prompt names assert "activity" in output - assert "flow" in output + assert "schedule" in output # Last run column is present assert "LAST RUN" in output @@ -150,20 +150,20 @@ def test_list_prompts_disabled_shown(capsys): output = capsys.readouterr().out # all agents should appear in the listing - assert "flow" in output + assert "schedule" in output def test_show_prompt_known(capsys): """Detail view shows expected fields for a known prompt.""" - show_prompt("flow") + show_prompt("schedule") output = capsys.readouterr().out - assert "talent/flow.md" in output + assert "talent/schedule.md" in output assert "title:" in output assert "schedule:" in output assert "daily" in output assert "hook:" in output - assert "occurrence" in output + assert "schedule" in output assert "variables:" in output assert "$daily_preamble" in output assert "body:" in output @@ -200,13 +200,13 @@ def test_json_output_contains_known_prompts(capsys): records = [json.loads(x) for x in output.strip().splitlines() if x.strip()] files = {r["file"] for r in records} - assert any("flow.md" in f for f in files) + assert any("schedule.md" in f for f in files) assert any("sense.md" in f for f in files) # Check a specific record has expected fields - flow = next(r for r in records if "flow.md" in r["file"]) - assert "title" in flow - assert "schedule" in flow + schedule = next(r for r in records if "schedule.md" in r["file"]) + assert "title" in schedule + assert "schedule" in schedule def test_json_output_schedule_filter(capsys): @@ -221,14 +221,14 @@ def test_json_output_schedule_filter(capsys): def test_show_prompt_as_json(capsys): """Detail view with --json outputs single JSONL record.""" - show_prompt("flow", as_json=True) + show_prompt("schedule", as_json=True) output = capsys.readouterr().out lines = [x for x in output.strip().splitlines() if x.strip()] assert len(lines) == 1 record = json.loads(lines[0]) - assert record["file"].endswith("flow.md") + assert record["file"].endswith("schedule.md") assert "title" in record assert "schedule" in record # Should not contain expanded instruction text @@ -291,14 +291,14 @@ def test_show_prompt_context_day_format_validation(capsys): # Too short with pytest.raises(SystemExit): - show_prompt_context("flow", day="2026") + show_prompt_context("schedule", day="2026") output = capsys.readouterr().err assert "invalid --day format" in output.lower() # Non-numeric with pytest.raises(SystemExit): - show_prompt_context("flow", day="abcdefgh") + show_prompt_context("schedule", day="abcdefgh") output = capsys.readouterr().err assert "invalid --day format" in output.lower() diff --git a/think/_extraction_utils.py b/think/_extraction_utils.py new file mode 100644 index 000000000..f9b34fe70 --- /dev/null +++ b/think/_extraction_utils.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Generic helper for logging extraction (IncompleteJSONError) failures.""" + +import logging + + +def log_extraction_failure(e: Exception, name: str) -> None: + """Log enhanced diagnostics for extraction generation failures. + + Handles IncompleteJSONError specially by logging a single-line summary + with a head+tail sample and degenerate repetition detection. + + Args: + e: The exception from generate(). + name: Generator name for log context. + """ + from think.models import IncompleteJSONError + + if not isinstance(e, IncompleteJSONError): + logging.error("Extraction generation failed for %s: %s", name, e) + return + + partial = e.partial_text + length = len(partial) + + # Build single-line head+tail sample (newlines collapsed for log grep) + def _collapse(s: str) -> str: + return s.replace("\n", "\\n").replace("\r", "") + + if length <= 300: + sample = _collapse(partial) + else: + sample = f"{_collapse(partial[:150])} ... {_collapse(partial[-150:])}" + + # Repetition detection: count unique chars in last 1000 + tail = partial[-1000:] if length >= 1000 else partial + unique_count = len(set(tail)) + repetition_flag = "" + if unique_count < 20: + repetition_flag = ( + f" [POSSIBLE DEGENERATE REPETITION: " + f"{unique_count} unique chars in last {len(tail)}]" + ) + + logging.error( + "Extraction generation failed for %s: %s " + "(partial_text: %d chars, %d unique in tail%s) sample: %s", + name, + e, + length, + unique_count, + repetition_flag, + sample, + ) diff --git a/think/hooks.py b/think/hooks.py deleted file mode 100644 index 6bd78f8cf..000000000 --- a/think/hooks.py +++ /dev/null @@ -1,215 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Shared utilities for output-side event hooks.""" - -import json -import logging -import os -from pathlib import Path - -from think.facets import get_facets - -# Minimum content length for meaningful event extraction -MIN_EXTRACTION_CHARS = 50 - - -def should_skip_extraction(result: str, context: dict) -> str | None: - """Check if extraction should be skipped and return reason, or None to proceed. - - Args: - result: The generated output markdown content. - context: Hook context dict with meta and span. - - Returns: - Skip reason string if extraction should be skipped, None otherwise. - """ - meta = context.get("meta", {}) - - # Skip if extraction disabled via journal config - if meta.get("extract") is False: - return "extraction disabled via journal config" - - # Skip for JSON output (output IS the structured data) - if meta.get("output") == "json": - return "JSON output (already structured)" - - # Skip in span mode (multiple sequential segments) - if context.get("span"): - return "span mode" - - # Skip for minimal content - if len(result.strip()) < MIN_EXTRACTION_CHARS: - return f"minimal content ({len(result.strip())} chars < {MIN_EXTRACTION_CHARS})" - - return None - - -def log_extraction_failure(e: Exception, name: str) -> None: - """Log enhanced diagnostics for extraction generation failures. - - Handles IncompleteJSONError specially by logging a single-line summary - with a head+tail sample and degenerate repetition detection. - - Args: - e: The exception from generate(). - name: Generator name for log context. - """ - from think.models import IncompleteJSONError - - if not isinstance(e, IncompleteJSONError): - logging.error("Extraction generation failed for %s: %s", name, e) - return - - partial = e.partial_text - length = len(partial) - - # Build single-line head+tail sample (newlines collapsed for log grep) - def _collapse(s: str) -> str: - return s.replace("\n", "\\n").replace("\r", "") - - if length <= 300: - sample = _collapse(partial) - else: - sample = f"{_collapse(partial[:150])} ... {_collapse(partial[-150:])}" - - # Repetition detection: count unique chars in last 1000 - tail = partial[-1000:] if length >= 1000 else partial - unique_count = len(set(tail)) - repetition_flag = "" - if unique_count < 20: - repetition_flag = ( - f" [POSSIBLE DEGENERATE REPETITION: " - f"{unique_count} unique chars in last {len(tail)}]" - ) - - logging.error( - "Extraction generation failed for %s: %s " - "(partial_text: %d chars, %d unique in tail%s) sample: %s", - name, - e, - length, - unique_count, - repetition_flag, - sample, - ) - - -def write_events_jsonl( - events: list[dict], - agent: str, - occurred: bool, - source_output: str, - capture_day: str, -) -> list[Path]: - """Write events to facet-based JSONL files. - - Groups events by facet and writes each to the appropriate file: - facets/{facet}/events/{event_day}.jsonl - - Args: - events: List of event dictionaries from extraction. - agent: Source generator agent (e.g., "meetings", "flow"). - occurred: True for occurrence rows, False for future-dated event rows. - source_output: Relative path to source output file. - capture_day: Day the output was captured (YYYYMMDD). - - Returns: - List of paths to written JSONL files. - """ - from think.utils import get_journal - - journal = get_journal() - known_facets = set(get_facets().keys()) - - # Group events by (facet, event_day) - grouped: dict[tuple[str, str], list[dict]] = {} - - for event in events: - raw_facet = event.get("facet", "") - facet = raw_facet.strip().lower() - if facet not in known_facets or raw_facet != facet: - logging.warning( - "Skipping event with unknown facet: facet=%r agent=%s source=%s", - raw_facet, - agent, - source_output, - ) - continue - - # Determine the event day - if occurred: - # Occurrences use capture day - event_day = capture_day - else: - # Future-dated event rows use their scheduled date - event_date = event.get("date", "") - # Convert YYYY-MM-DD to YYYYMMDD - event_day = event_date.replace("-", "") if event_date else capture_day - - if not event_day: - continue - - key = (facet, event_day) - if key not in grouped: - grouped[key] = [] - - # Enrich event with metadata - enriched = dict(event) - enriched["agent"] = agent - enriched["occurred"] = occurred - enriched["source"] = source_output - - grouped[key].append(enriched) - - # Write each group to its JSONL file - written_paths: list[Path] = [] - - for (facet, event_day), facet_events in grouped.items(): - events_dir = Path(journal) / "facets" / facet / "events" - events_dir.mkdir(parents=True, exist_ok=True) - - jsonl_path = events_dir / f"{event_day}.jsonl" - with open(jsonl_path, "a", encoding="utf-8") as f: - for event in facet_events: - f.write(json.dumps(event, ensure_ascii=False) + "\n") - - written_paths.append(jsonl_path) - - return written_paths - - -def compute_output_source(context: dict) -> str: - """Compute relative source output path from hook context. - - Args: - context: Hook context dict with day, segment, name, output_path, meta. - - Returns: - Relative path like "20240101/talents/meetings.md". - """ - from think.talent import get_output_name - from think.utils import CHRONICLE_DIR, get_journal - - day = context.get("day", "") - output_path = context.get("output_path", "") - name = context.get("name", "unknown") - journal = get_journal() - - try: - rel = os.path.relpath(output_path, journal).replace("\\", "/") - return rel.removeprefix(f"{CHRONICLE_DIR}/") - except ValueError: - segment = context.get("segment") - output_name = get_output_name(name) - # Check for facet in meta (for multi-facet talents) - meta = context.get("meta", {}) - facet = meta.get("facet") if meta else None - filename = f"{output_name}.md" - if segment and facet: - return os.path.join(day, segment, "talents", facet, filename) - if segment: - return os.path.join(day, segment, "talents", filename) - if facet: - return os.path.join(day, "talents", facet, filename) - return os.path.join(day, "talents", filename) diff --git a/think/talents.py b/think/talents.py index 6aa8e37dd..10dbed7b5 100644 --- a/think/talents.py +++ b/think/talents.py @@ -1276,7 +1276,7 @@ async def main_async() -> None: "ts": now_ms(), } if isinstance(e, IncompleteJSONError): - from think.hooks import log_extraction_failure + from think._extraction_utils import log_extraction_failure event["partial_text_length"] = len(e.partial_text) event["partial_text_tail"] = e.partial_text[-500:]