personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(activities): single-pass schedule talent emits anticipated activity records

Replaces the schedule 2-pass flow (markdown → anticipation post-hook re-extracts
JSON) with a single-pass JSON-output talent processed by a new schedule post-hook
that writes activity records with source: "anticipated" directly to
facets/{facet}/activities/{target_date}.jsonl.

Key changes:
- New talent/schedule.py post-hook parses JSON result, validates per-item, and
writes via append_activity_record (atomic fcntl-locked dedup by stable ID).
- New dedup_anticipation helper + ANTICIPATION_FUZZY_THRESHOLD (0.85) in
think/activities.py supersedes pending rows whose title drifted between runs.
- Cancelled calendar events captured with cancelled: true and hidden: true.
- Home today-card merges anticipated activities into events and suppresses them
from the recent-activity strip to avoid double display.
- activities list gains --source {anticipated,user,cogitate} filter.
- Sweep replaces 16 stale sol call calendar references across talents, skills,
and routine templates with sol call activities list equivalents.
- Deletes talent/anticipation.{md,py}; removes anticipation from settings
extraction-exposure tuples; updates fixtures, docs, and API baselines.

Closes Sprint 3 of the schedule→activities refactor (req_hfwtzduq). Parallel
investigation of the schedule dup-bomb (req_b46jht3a) is closed by the
structural fix (atomic ID-dedup under fcntl lock).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+1341 -972
+16
apps/activities/call.py
··· 131 131 *, 132 132 activity: str | None, 133 133 entity: str | None, 134 + source: str | None, 134 135 include_hidden: bool, 135 136 ) -> list[dict[str, Any]]: 136 137 matches: list[dict[str, Any]] = [] ··· 141 142 facet_name, day, include_hidden=include_hidden 142 143 ): 143 144 if activity and record.get("activity") != activity: 145 + continue 146 + if source and record.get("source") != source: 144 147 continue 145 148 if entity_query: 146 149 active_entities = record.get("active_entities", []) ··· 200 203 "--entity", 201 204 help="Filter by active entity.", 202 205 ), 206 + source: str | None = typer.Option( 207 + None, 208 + "--source", 209 + help="Filter by record source: anticipated, user, or cogitate.", 210 + ), 203 211 include_all: bool = typer.Option( 204 212 False, 205 213 "--all", ··· 221 229 else: 222 230 resolved_days = [resolve_sol_day(None)] 223 231 232 + if source and source not in {"anticipated", "cogitate", "user"}: 233 + typer.echo( 234 + "Error: --source must be 'anticipated', 'cogitate', or 'user'.", 235 + err=True, 236 + ) 237 + raise typer.Exit(1) 238 + 224 239 facets = _resolve_list_facets(facet) 225 240 records = _list_records_for_days( 226 241 facets, 227 242 resolved_days, 228 243 activity=activity, 229 244 entity=entity, 245 + source=source, 230 246 include_hidden=include_all, 231 247 ) 232 248
+53
apps/activities/tests/test_call.py
··· 109 109 assert [item["id"] for item in payload] == ["coding_090000_300"] 110 110 111 111 112 + def test_list_filters_by_source(activities_env): 113 + activities_env( 114 + [ 115 + { 116 + "id": "anticipated_call_103000_0421", 117 + "activity": "call", 118 + "title": "Mari intro", 119 + "description": "Planned", 120 + "target_date": "2026-04-21", 121 + "source": "anticipated", 122 + "created_at": 1, 123 + }, 124 + { 125 + "id": "coding_090000_300", 126 + "activity": "coding", 127 + "title": "Focused coding", 128 + "description": "User created", 129 + "source": "user", 130 + "created_at": 2, 131 + }, 132 + { 133 + "id": "meeting_100000_300", 134 + "activity": "meeting", 135 + "title": "Synthesized meeting", 136 + "description": "Cogitate created", 137 + "source": "cogitate", 138 + "created_at": 3, 139 + }, 140 + ] 141 + ) 142 + 143 + result = runner.invoke( 144 + call_app, 145 + ["activities", "list", "--facet", "work", "--source", "anticipated", "--json"], 146 + ) 147 + 148 + assert result.exit_code == 0 149 + payload = json.loads(result.output) 150 + assert [item["id"] for item in payload] == ["anticipated_call_103000_0421"] 151 + 152 + 153 + def test_list_rejects_unknown_source(activities_env): 154 + activities_env([]) 155 + 156 + result = runner.invoke( 157 + call_app, 158 + ["activities", "list", "--facet", "work", "--source", "calendar"], 159 + ) 160 + 161 + assert result.exit_code == 1 162 + assert "--source must be 'anticipated', 'cogitate', or 'user'" in result.output 163 + 164 + 112 165 def test_get_returns_hidden_record_with_json_output(activities_env): 113 166 activities_env( 114 167 [
+27
apps/home/routes.py
··· 303 303 304 304 def _collect_events(today: str) -> list[dict[str, Any]]: 305 305 """Collect calendar events across all facets.""" 306 + from think.activities import load_activity_records 306 307 from think.indexer.journal import get_events 307 308 308 309 try: ··· 312 313 event["start"] = "" 313 314 if event.get("end") is None: 314 315 event["end"] = "" 316 + 317 + for facet_name in get_facets(): 318 + for record in load_activity_records(facet_name, today): 319 + if record.get("source") != "anticipated": 320 + continue 321 + 322 + participants = [] 323 + for entry in record.get("participation", []): 324 + if not isinstance(entry, dict) or entry.get("role") != "attendee": 325 + continue 326 + name = str(entry.get("name") or "").strip() 327 + if name: 328 + participants.append(name) 329 + 330 + events.append( 331 + { 332 + "title": record.get("title", ""), 333 + "start": record.get("start") or "", 334 + "end": record.get("end") or "", 335 + "facet": facet_name, 336 + "occurred": False, 337 + "participants": participants, 338 + } 339 + ) 315 340 return events 316 341 except Exception: 317 342 logger.warning("home: failed to collect events", exc_info=True) ··· 335 360 for facet_name in facets: 336 361 records = load_activity_records(facet_name, today) 337 362 for record in records: 363 + if record.get("source") == "anticipated": 364 + continue 338 365 created_at = record.get("created_at", 0) 339 366 if created_at < cutoff_ts: 340 367 continue
+6 -7
apps/settings/routes.py
··· 432 432 if "schedule" in info: 433 433 context_defaults[context_key]["schedule"] = info["schedule"] 434 434 context_defaults[context_key]["disabled"] = info.get("disabled", False) 435 - # Include extract for generators with occurrence/anticipation hooks 435 + # Include extract for generators with occurrence hooks 436 436 hook = info.get("hook") 437 437 has_extraction = ( 438 - isinstance(hook, dict) 439 - and hook.get("post") in ("occurrence", "anticipation") 440 - ) or hook in ("occurrence", "anticipation") 438 + isinstance(hook, dict) and hook.get("post") in ("occurrence",) 439 + ) or hook in ("occurrence",) 441 440 if has_extraction: 442 441 context_defaults[context_key]["extract"] = info.get("extract", True) 443 442 ··· 929 928 Transforms talent config metadata into the format expected by the 930 929 Settings UI Insights section. 931 930 """ 932 - # Determine if extraction is supported (occurrence/anticipation hooks) 931 + # Determine if extraction is supported (occurrence hooks) 933 932 hook = info.get("hook") 934 933 has_extraction = ( 935 - isinstance(hook, dict) and hook.get("post") in ("occurrence", "anticipation") 936 - ) or hook in ("occurrence", "anticipation") 934 + isinstance(hook, dict) and hook.get("post") in ("occurrence",) 935 + ) or hook in ("occurrence",) 937 936 938 937 return { 939 938 "key": key,
+2 -2
docs/APPS.md
··· 294 294 **Event extraction via hooks:** To extract structured events from generator output, use the `hook` field: 295 295 296 296 - `"hook": {"post": "occurrence"}` - Extracts past events to `facets/{facet}/events/{day}.jsonl` 297 - - `"hook": {"post": "anticipation"}` - Extracts future scheduled events 297 + - `"hook": {"post": "schedule"}` - Writes future scheduled items as anticipated activity records 298 298 299 299 The `occurrences` field (optional string) provides agent-specific extraction guidance when using the occurrence hook. Example: 300 300 ··· 356 356 357 357 **Reference implementations:** 358 358 - System generator templates: `talent/*.md` (files with `schedule` field but no `tools` field) 359 - - Extraction hooks: `talent/occurrence.py`, `talent/anticipation.py` 359 + - Event/schedule hooks: `talent/occurrence.py`, `talent/schedule.py` 360 360 - Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=False)`, `get_output_name()` 361 361 - Hook loading: `think/talent.py` - `load_pre_hook()`, `load_post_hook()` 362 362
+1 -1
docs/CORTEX.md
··· 336 336 337 337 #### Daily Multi-Facet Agents 338 338 339 - **Active Facet Detection**: By default, daily multi-facet agents only run for facets that had activity the previous day. Activity is determined by the presence of occurrence events (not anticipations) in `facets/{facet}/events/{day}.jsonl`. This prevents unnecessary agent runs for inactive facets. 339 + **Active Facet Detection**: By default, daily multi-facet agents only run for facets that had activity the previous day. Activity is determined by the presence of occurrence events in `facets/{facet}/events/{day}.jsonl`. This prevents unnecessary agent runs for inactive facets. 340 340 341 341 To force an agent to run for all facets regardless of activity, set `"always": true`: 342 342
+1 -1
docs/THINK.md
··· 183 183 - `mtime` – modification time of the `.md` file 184 184 - Additional keys from JSON frontmatter such as `title`, `description`, `hook`, or `load` 185 185 186 - The `hook` field enables event extraction by invoking named hooks like `"occurrence"` or `"anticipation"`. 186 + The `hook` field enables output processing by invoking named hooks like `"occurrence"` or `"schedule"`. 187 187 The `load` key controls transcript/percept/agent source filtering for generators. 188 188 See [APPS.md](APPS.md#prompt-context-configuration) for the full schema. 189 189
+1 -1
routines/templates/decision-review.md
··· 15 15 1. Use `sol call journal search "" -a decisions --day-from START --day-to END -n 20` for the past 30 days of decision agent output. 16 16 2. Use `sol call journal search "" -a pulse --day-from START --day-to END -n 15` for narrative context around major decisions. 17 17 3. Use `sol call entities intelligence PERSON` for people involved in the most consequential decisions. 18 - 4. Use `sol call calendar list YYYYMMDD` for days with major decisions to see what else was happening. 18 + 4. Use `sol call activities list --source anticipated --day YYYYMMDD` for days with major decisions to see what else was happening. 19 19 5. Use `sol call identity partner` for the owner's known decision style. 20 20 21 21 ## Synthesize
+1 -1
routines/templates/energy-audit.md
··· 10 10 11 11 ## Gather 12 12 13 - 1. Use `sol call calendar list YYYYMMDD` for each of the past 7 days to map meeting load. 13 + 1. Use `sol call activities list --source anticipated --day YYYYMMDD` for each of the past 7 days to map scheduled load. 14 14 2. Use `sol call journal search "" --day-from START --day-to END -n 30` to survey activity patterns. 15 15 3. Use `sol call todos list` to compare intended work against actual activity. 16 16 4. Use `sol call identity pulse` for the current state narrative.
+1 -1
routines/templates/meeting-prep.md
··· 13 13 ## Gather 14 14 15 15 1. Read the upcoming event details in the prompt carefully. 16 - 2. If you need broader context, call `sol call calendar list $day_YYYYMMDD` to see the surrounding schedule. 16 + 2. If you need broader context, call `sol call activities list --source anticipated --day $day_YYYYMMDD` to see the surrounding schedule. 17 17 3. For each listed participant, call `sol call entities intelligence PERSON --brief`. 18 18 4. Use `sol call journal search QUERY -n 10` to look for recent mentions of the meeting topic, project, or participants. 19 19 5. If a configured facet seems especially relevant, use `sol call journal news FACET --day $day_YYYYMMDD`.
+1 -1
routines/templates/monthly-patterns.md
··· 16 16 2. Use `sol call journal search "" --day-from START --day-to END -n 40` to survey the month across the configured facets. 17 17 3. Use `sol call journal news FACET --day YYYYMMDD` for representative weekly or recent snapshots when they help summarize a facet. 18 18 4. Use `sol call entities intelligence PERSON` for people who appear central to the month. 19 - 5. Use `sol call calendar list YYYYMMDD` on representative days if calendar load seems important. 19 + 5. Use `sol call activities list --source anticipated --day YYYYMMDD` on representative days if scheduled load seems important. 20 20 6. Use `sol call identity pulse` to compare month-long patterns against the current state narrative. 21 21 22 22 ## Synthesize
+1 -1
routines/templates/morning-briefing.md
··· 13 13 ## Gather 14 14 15 15 1. Call `sol call journal facets` to see the active facets if you need broader context. 16 - 2. Call `sol call calendar list $day_YYYYMMDD` to review today's events and participants. 16 + 2. Call `sol call activities list --source anticipated --day $day_YYYYMMDD` to review today's scheduled items and participants. 17 17 3. Call `sol call todos list` to see pending action items across facets. 18 18 4. Call `sol call identity pulse` to capture current narrative, priorities, and needs-you items. 19 19 5. Call `sol call journal search "" -a followups -n 10` to find recent follow-up items.
+1 -1
routines/templates/weekly-review.md
··· 15 15 1. Use `sol call journal facets` to identify the facets in scope. 16 16 2. Use `sol call journal search "" --day-from $day_minus_7_YYYYMMDD --day-to $day_YYYYMMDD -n 25` to find notable entries and themes. 17 17 3. Use `sol call todos list` to review outstanding work and infer what likely got completed or deferred. 18 - 4. Use `sol call calendar list YYYYMMDD` across the last 7 days to understand meeting load and major time commitments. 18 + 4. Use `sol call activities list --source anticipated --day YYYYMMDD` across the last 7 days to understand scheduled load and major time commitments. 19 19 5. Use `sol call identity pulse` for the current state narrative. 20 20 6. Use `sol call journal news FACET --day YYYYMMDD` for any facet that needs a richer summary. 21 21
+2 -2
skills/solstone/SKILL.md
··· 69 69 sol call todos upcoming 70 70 71 71 # Calendar events for today 72 - sol call calendar list 72 + sol call activities list --source anticipated 73 73 74 74 # Latest facet news 75 75 sol call journal news "<facet>" ··· 144 144 ```bash 145 145 sol call journal events 146 146 sol call todos upcoming 147 - sol call calendar list 147 + sol call activities list --source anticipated 148 148 sol call entities strength --since $(date +%Y%m%d) 149 149 ``` 150 150
-97
talent/anticipation.md
··· 1 - { 2 - 3 - "title": "Anticipation Extraction", 4 - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", 5 - "color": "#4527a0" 6 - 7 - } 8 - 9 - # Anticipation JSON Conversion 10 - 11 - ## Objective 12 - 13 - Extract future scheduled events from a Markdown summary and convert them into structured JSON anticipations. These are events that have not yet occurred but are planned or scheduled for future dates. 14 - 15 - ## Instructions 16 - 1. **Extract every distinct future event** mentioned in the summary - meetings, deadlines, appointments, etc. 17 - 2. **Be comprehensive** - capture calendar events, scheduled meetings, deadlines, personal appointments, recurring events, travel, etc. 18 - 3. **Preserve date and timing information** - always extract the scheduled date, and time if known 19 - 4. **Handle uncertainty gracefully** - use `null` for start/end times when not specified 20 - 5. **Assign facets** - for every anticipation, choose the best matching facet from the Available Facets context. This field is required. 21 - 6. **Return only valid JSON** - no commentary, explanations, or wrapper objects 22 - 7. **Handle empty sources** - if the source indicates no future events, return an empty array: `[]` 23 - 24 - ## Anticipation Fields 25 - - **type** – the kind of event such as `meeting`, `deadline`, `appointment`, `event`, `travel`, `reminder`, etc. 26 - - **date** – ISO date (YYYY-MM-DD) when the event is scheduled to occur. Required. 27 - - **start** and **end** – HH:MM:SS timestamps for the event time, or `null` if time is unknown/TBD 28 - - **title** – short descriptive title for display 29 - - **summary** – concise one-sentence description of what is planned 30 - - **work** – boolean classification: `true` for work-related, `false` for personal 31 - - **participants** – optional list of people or entities involved (empty array if none) 32 - - **facet** – required facet identifier; use the facet name/ID from Available Facets context 33 - - **details** – free-form string capturing location, agenda, preparation notes, or other context 34 - 35 - ## Handling Time Uncertainty 36 - - If exact time is known (e.g., "1:00 PM"): use `"start": "13:00:00"` 37 - - If time is vague (e.g., "Morning", "Afternoon"): use `null` and mention in details 38 - - If time is TBD or not specified: use `null` 39 - - If it's an all-day event: use `null` for both start and end 40 - 41 - ## Handling Recurring Events 42 - For recurring events (e.g., "every Wednesday"), extract the next upcoming instance with its specific date. Mention the recurrence pattern in details. 43 - 44 - ## Output Format 45 - Return a JSON array of anticipations only. Each anticipation must include all required fields. 46 - 47 - ## Example 48 - [ 49 - { 50 - "type": "meeting", 51 - "date": "2025-11-05", 52 - "start": "13:00:00", 53 - "end": "14:30:00", 54 - "title": "Meeting with Center for the Blind", 55 - "summary": "Introductory meeting to discuss potential collaboration on AI-powered tools.", 56 - "work": true, 57 - "participants": ["Jack Anderson", "Center for the Blind Representatives"], 58 - "facet": "blind_center", 59 - "details": "Virtual meeting. Topics include grant writing and building an AI brain from their documents." 60 - }, 61 - { 62 - "type": "deadline", 63 - "date": "2025-11-10", 64 - "start": null, 65 - "end": null, 66 - "title": "Airport System Deployment Target", 67 - "summary": "Target date for deploying the cleaned-up UI and new restart scripts.", 68 - "work": true, 69 - "participants": ["Mitch Baumgartner"], 70 - "facet": "aviation_networks", 71 - "details": "Software deployment. No specific time set." 72 - }, 73 - { 74 - "type": "event", 75 - "date": "2025-10-27", 76 - "start": null, 77 - "end": null, 78 - "title": "FMDS Partner Workshop", 79 - "summary": "Multi-day in-person workshop with partners running through October 30.", 80 - "work": true, 81 - "participants": ["L3Harris", "GDIT", "Frequentis", "uAvionix"], 82 - "facet": "fmds", 83 - "details": "In-person, security clearance required. Day 1: Win strategy and demo dry run. Day 2: Solution definition. Day 3: Prototype planning. Runs Oct 27-30." 84 - }, 85 - { 86 - "type": "appointment", 87 - "date": "2025-11-15", 88 - "start": null, 89 - "end": null, 90 - "title": "Gymnastics Meet", 91 - "summary": "First gymnastics meet of the season.", 92 - "work": false, 93 - "participants": ["Blade"], 94 - "facet": "family", 95 - "details": "Location: Marshaltown Community Center." 96 - } 97 - ]
-101
talent/anticipation.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Hook for extracting anticipation events from generator output results. 5 - 6 - This hook is invoked via "hook": {"post": "anticipation"} in generator frontmatter. 7 - It extracts structured JSON events for future scheduled items and writes 8 - them to facet-based JSONL files. 9 - """ 10 - 11 - import json 12 - import logging 13 - from pathlib import Path 14 - 15 - from think.facets import facet_summaries 16 - from think.hooks import ( 17 - compute_output_source, 18 - log_extraction_failure, 19 - should_skip_extraction, 20 - write_events_jsonl, 21 - ) 22 - from think.models import generate 23 - from think.prompts import load_prompt 24 - from think.talent import get_output_name 25 - 26 - 27 - def post_process(result: str, context: dict) -> str | None: 28 - """Extract anticipation events from generator output result. 29 - 30 - This hook extracts structured JSON events for future scheduled items 31 - from markdown output summaries and writes them to facet-based JSONL files. 32 - 33 - Args: 34 - result: The generated output markdown content. 35 - context: Config dict with keys including day, segment, name, 36 - output_path, meta, transcript, span, span_mode. 37 - 38 - Returns: 39 - None - this hook does not modify the output result. 40 - """ 41 - # Check skip conditions 42 - skip_reason = should_skip_extraction(result, context) 43 - if skip_reason: 44 - logging.info("Skipping anticipation extraction: %s", skip_reason) 45 - return None 46 - 47 - # Load extraction prompt 48 - prompt_content = load_prompt("anticipation", base_dir=Path(__file__).parent) 49 - 50 - # Build context with facets (anticipations don't have agent-specific instructions) 51 - facets_context = facet_summaries(detailed=True) 52 - 53 - # Extract events 54 - name = context.get("name", "unknown") 55 - contents = [facets_context, result] 56 - 57 - try: 58 - response_text = generate( 59 - contents=contents, 60 - context=f"talent.system.{name}", 61 - temperature=0.3, 62 - max_output_tokens=24576, 63 - thinking_budget=0, 64 - system_instruction=prompt_content.text, 65 - json_output=True, 66 - ) 67 - except Exception as e: 68 - log_extraction_failure(e, name) 69 - return None 70 - 71 - try: 72 - events = json.loads(response_text) 73 - except json.JSONDecodeError as e: 74 - logging.error("Invalid JSON from anticipation extraction: %s", e) 75 - return None 76 - 77 - if not isinstance(events, list): 78 - logging.error("Extraction did not return array") 79 - return None 80 - 81 - # Write to facet JSONL files (occurred=False for anticipations) 82 - source_output = compute_output_source(context) 83 - output_name = get_output_name(name) 84 - day = context.get("day", "") 85 - 86 - written_paths = write_events_jsonl( 87 - events=events, 88 - agent=output_name, 89 - occurred=False, 90 - source_output=source_output, 91 - capture_day=day, 92 - ) 93 - 94 - if written_paths: 95 - print(f"Events written to {len(written_paths)} JSONL file(s):") 96 - for p in written_paths: 97 - print(f" {p}") 98 - else: 99 - print("No events with valid facets to write") 100 - 101 - return None # Don't modify insight result
+1 -1
talent/awareness_tender.md
··· 21 21 22 22 1. `sol call awareness status` — processing, import, and journal state 23 23 2. `sol call identity self` — identity summary (skim for key changes) 24 - 3. `sol call calendar list` — today's events 24 + 3. `sol call activities list --source anticipated` — today's scheduled activity records 25 25 4. `sol call routines list` — active routines and recent outputs 26 26 5. `sol call entities search --since --limit 5` — recent entity activity 27 27 ## Write awareness.md
+5 -10
talent/journal/references/captures.md
··· 229 229 230 230 ### Event extracts 231 231 232 - Generator output processing extracts time-based events from the day's transcripts—meetings, messages, follow-ups, file activity and more. Events are stored per-facet in JSONL files at `facets/{facet}/events/{day}.jsonl`. 233 - 234 - There are two types of events: 235 - - **Occurrences** – events that happened on the capture day (`occurred: true`) 236 - - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) 232 + Generator output processing extracts time-based events from the day's transcripts—meetings, messages, follow-ups, file activity and more. Occurrence events are stored per-facet in JSONL files at `facets/{facet}/events/{day}.jsonl`. Future scheduled items from the schedule talent are stored as anticipated activity records under `facets/{facet}/activities/{target_day}.jsonl` with `source: "anticipated"`. 237 233 238 234 ```jsonl 239 235 {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} 240 - {"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} 236 + {"id": "anticipated_deadline_000000_0115", "activity": "deadline", "target_date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "description": "Q1 deliverable due.", "details": "Final review before release", "facet": "work", "source": "anticipated", "active_entities": [], "participation": [], "participation_confidence": 0.5, "cancelled": false} 241 237 ``` 242 238 243 239 **Common fields:** 244 240 - **type** – event kind: `meeting`, `message`, `file`, `followup`, `documentation`, `research`, `media`, `deadline`, `appointment`, etc. 245 - - **start** and **end** – HH:MM:SS timestamps (or `null` for anticipations without specific times) 246 - - **date** – ISO date YYYY-MM-DD (anticipations only, indicates scheduled date) 241 + - **start** and **end** – HH:MM:SS timestamps (or `null` when a time is not known) 247 242 - **title** and **summary** – short text for display and search 248 243 - **facet** – facet name the event belongs to (required) 249 - - **agent** – source generator type (e.g., "meetings", "schedule", "flow") 250 - - **occurred** – `true` for occurrences, `false` for anticipations 244 + - **agent** – source generator type for occurrence events (e.g., "meetings", "flow") 245 + - **occurred** – `true` for occurrence event rows in `facets/*/events/*.jsonl` 251 246 - **source** – path to the output file that generated this event 252 247 - **work** – boolean, work vs. personal classification 253 248 - **participants** – optional list of people or entities involved
+1 -1
talent/journal/references/config.md
··· 298 298 - `tier` (integer) – Tier number (optional). 299 299 - `model` (string) – Explicit model name (optional, overrides tier). 300 300 - `disabled` (boolean) – Disable this talent config (optional, talent contexts only). 301 - - `extract` (boolean) – Enable/disable event extraction for generators with occurrence/anticipation hooks (optional). 301 + - `extract` (boolean) – Enable/disable event extraction for generators with occurrence hooks (optional). 302 302 303 303 **models** – Per-provider tier overrides. Maps provider name to tier-model mappings: 304 304 ```json
+7 -7
talent/morning_briefing.md
··· 21 21 22 22 1. `sol call journal facets` — list active facets 23 23 2. For each facet: `sol call journal news FACET --day $day_YYYYMMDD` — facet newsletter 24 - 3. `sol call calendar list $day_YYYYMMDD` — today's events with participants 24 + 3. `sol call activities list --source anticipated --day $day_YYYYMMDD` — today's scheduled items with participants 25 25 4. `sol call todos list` — pending action items across all facets 26 26 5. `sol call identity pulse` — current pulse narrative and needs-you items 27 27 6. `sol call identity partner` — owner behavioral profile (informs tone and emphasis) 28 28 7. `sol call journal search "" -d $day_YYYYMMDD -a followups -n 10` — follow-up items from today 29 - 8. `sol call journal search "" --day-from $day_YYYYMMDD -a anticipation -n 5` — forward-looking anticipations 29 + 8. `sol call activities list --source anticipated --from $day_YYYYMMDD --to <+7>` — forward-looking scheduled items 30 30 9. `sol call journal search "" -d $day_YYYYMMDD -a decisions -n 10` — yesterday's consequential decisions 31 - 10. For each of the next 7 days after today: `sol call calendar list YYYYMMDD` — upcoming events for forward look 31 + 10. For each of the next 7 days after today: `sol call activities list --source anticipated --day YYYYMMDD` — upcoming scheduled items for forward look 32 32 33 33 For each person appearing in today's calendar events, also run: 34 34 11. `sol call entities intelligence PERSON --brief` — relationship context, recent interactions, observations (brief mode: last 20 signals + top 20 network, ~95% smaller payload) ··· 97 97 Attribute commitments and follow-ups to the originating segment: `(committed [date](sol://...))`, `(flagged [date](sol://...))`. For relationship items: `(last interaction [date])`. For inferred items: `(inferred from [source](sol://...))`. 98 98 Grade action items by evidence strength. **High** (explicit commitment with date, or overdue todo): state assertively — "Follow up on Series A term sheet — committed March 20, now overdue." **Medium** (flagged by followups agent with moderate confidence, or clear single-source item): present with attribution — "Review CI pipeline logs (flagged yesterday)." **Low** (inferred obligation from ambiguous mention, or low-confidence followup): hedge — "Possible commitment to send deck to investors" or "May need to follow up on the API discussion." When upstream followup output includes a `Confidence:` score, use it: 0.85+ high, 0.50–0.84 medium, below 0.50 low. Never hedge explicit commitments with clear dates; never present inferred obligations as definite action items. 99 99 100 - **Forward Look** — What's coming. Draw from anticipation agent output and upcoming calendar events (next 7 days). Note preparation needed for upcoming meetings or deadlines. 101 - Attribute anticipation items: `(from [anticipation](sol://...))`. Data source: anticipation search result `id` path. 102 - Grade forward items by evidence strength. **High** (confirmed calendar event or explicit deadline): state assertively — "Board meeting Thursday — slides due Wednesday." **Medium** (anticipation agent item with clear basis): attribute and present — "Anticipation agent flagged quarterly review prep based on last quarter's timing." **Low** (speculative anticipation, inferred deadline, or pattern-based prediction): hedge — "Possible need to prepare for investor update" or "May want to schedule design review based on sprint cadence." Never hedge confirmed calendar events or explicit deadlines; never state pattern-based predictions as confirmed plans. 100 + **Forward Look** — What's coming. Draw from anticipated activity records and upcoming scheduled items (next 7 days). Note preparation needed for upcoming meetings or deadlines. 101 + Attribute schedule-derived items: `(from [schedule](sol://...))`. Data source: `sol call activities list --source anticipated` or the schedule talent output path. 102 + Grade forward items by evidence strength. **High** (confirmed scheduled item or explicit deadline): state assertively — "Board meeting Thursday — slides due Wednesday." **Medium** (schedule-derived activity record with clear basis): attribute and present — "Schedule extraction flagged quarterly review prep based on last quarter's timing." **Low** (speculative schedule inference or pattern-based prediction): hedge — "Possible need to prepare for investor update" or "May want to schedule design review based on sprint cadence." Never hedge confirmed scheduled items or explicit deadlines; never state pattern-based predictions as confirmed plans. 103 103 104 104 **Reading** — Links to full facet newsletters for deep dives. List each active facet that has a newsletter for the analysis day, with a brief one-line description of what it covers. This is the "detailed edition" for owners who want the full picture. Only include if facet newsletters exist. 105 105 ··· 141 141 142 142 ## Forward Look 143 143 - Board meeting Thursday — slides need review (confirmed on [calendar](sol://20260327/calendar)) 144 - - May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/talents/anticipation)) 144 + - May want to prepare quarterly metrics based on last quarter's timing (from [schedule](sol://20260327/talents/schedule)) 145 145 [more items...] 146 146 147 147 ## Reading
+1 -1
talent/partner.md
··· 36 36 37 37 1. `sol call entities strength --since YYYYMMDD` (7 days back) — relationship activity 38 38 2. For each of the past 7 days: 39 - - `sol call calendar list YYYYMMDD` — schedule patterns 39 + - `sol call activities list --source anticipated --day YYYYMMDD` — scheduled activity patterns 40 40 - `sol call todos list -d YYYYMMDD` — task patterns 41 41 3. For each active facet (from `sol call journal facets`): 42 42 - `sol call journal news FACET --day YYYYMMDD` (most recent day available) — work themes
+138 -48
talent/schedule.md
··· 2 2 "type": "generate", 3 3 4 4 "title": "Upcoming Schedule", 5 - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", 6 - "hook": {"post": "anticipation"}, 5 + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", 6 + "hook": {"post": "schedule"}, 7 7 "color": "#5e35b1", 8 8 "schedule": "daily", 9 9 "priority": 10, 10 - "output": "md", 10 + "output": "json", 11 11 "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 - 13 12 } 14 13 15 14 $daily_preamble 16 15 17 16 # Future Schedule Extraction 18 17 19 - **Input:** A markdown file containing chronologically ordered transcripts of a workday. The transcript combines audio recordings and screen activity organized by recording segments. 18 + **Input:** A markdown file containing chronologically ordered transcripts of a workday plus the screen agent's output for the same day. Calendar views, meeting invitations, scheduling UIs, and project-management interfaces are captured in the screen content; verbal mentions of future plans appear in transcripts. 20 19 21 - **Objective:** Identify and extract all future scheduled events and calendar entries visible or mentioned throughout the day. Focus exclusively on events scheduled for dates beyond today - not current day or historical events. 20 + **Your task:** Identify every future scheduled item (dated after today) visible in the day's screen or transcript content and emit a JSON array of anticipation objects. 22 21 23 - **Instructions:** 22 + ## What to capture 24 23 25 - 1. **Scan for Calendar Views:** Review all screen activity descriptions for: 26 - - Calendar applications (Google Calendar, Outlook, Apple Calendar, etc.) 27 - - Scheduling interfaces (Calendly, meeting schedulers, etc.) 28 - - Email invitations showing future dates 29 - - Project management tools with scheduled deadlines 30 - - Any interface displaying future dates and times 24 + Look for: 25 + - Calendar applications (Google Calendar, Outlook, Apple Calendar, Fantastical, etc.) 26 + - Scheduling interfaces (Calendly, meeting schedulers, SavvyCal, meet.solpbc.org) 27 + - Email invitations or confirmations showing future dates 28 + - Project management tools (Linear, Jira, Asana, Trello) with scheduled deadlines or milestones 29 + - Travel bookings (flights, reservations, itineraries) 30 + - Any UI element displaying a future date, time, or deadline 31 + - Verbal mentions of firm future commitments ("I'm meeting Ramon on Tuesday at 3", "flight leaves Friday morning") 31 32 32 - 2. **Extract Event Details:** For each future scheduled item identified, capture: 33 - - **Date & Time:** Full date and time information if available 34 - - **Event Title:** The name or subject of the scheduled event 35 - - **Participants:** Meeting attendees or people involved if visible 36 - - **Location:** Physical location or meeting link/platform 37 - - **Duration:** Expected length of the event 38 - - **Description:** Any additional context or agenda items visible 33 + **Include cancelled items too.** Calendar views often show cancelled events with a strikethrough, a "Cancelled" label, a declined-invite indicator, or a greyed-out style. Emit these with `"cancelled": true` — the downstream pipeline needs to know a previously-scheduled item dropped off. 39 34 40 - 3. **Organize Chronologically:** Sort all identified events by their scheduled date and time, nearest future events first. 35 + Do NOT capture: 36 + - Past events (anything on today or earlier — future only) 37 + - Vague intent without a date ("we should catch up sometime") 38 + - Recurring-series headers with no specific upcoming instance visible (capture the next specific instance if visible; otherwise skip) 39 + - Tentative suggestions that haven't been confirmed 41 40 42 - 4. **Focus on the Future:** 43 - - Include only events scheduled for tomorrow and beyond 44 - - Exclude today's events (already happened or happening) 45 - - Exclude past events visible in calendar history 46 - - Include recurring events' future instances if visible 41 + ## Output schema 47 42 48 - ## Output Format 43 + Return **only** a JSON array. Each element is an anticipation object with these fields: 49 44 50 - Produce a clean Markdown document with: 45 + ```json 46 + [ 47 + { 48 + "activity": "meeting", 49 + "target_date": "2026-04-20", 50 + "start": "16:30:00", 51 + "end": "17:30:00", 52 + "title": "Yuri Namikawa intro call", 53 + "description": "Intro call with Yuri from Offline Ventures about solstone.", 54 + "details": "Google Meet; prep one-pager + demo backup", 55 + "participation": [ 56 + { 57 + "name": "Yuri Namikawa", 58 + "role": "attendee", 59 + "source": "screen", 60 + "confidence": 0.9, 61 + "context": "visible on calendar invite; Offline Ventures" 62 + } 63 + ], 64 + "participation_confidence": 0.9, 65 + "facet": "solstone", 66 + "cancelled": false 67 + } 68 + ] 69 + ``` 51 70 52 - ### Header Section 53 - A brief overview stating the date range of scheduled events found and total count. 71 + ### Field-by-field 54 72 55 - ### Event Listings 56 - For each future event, create a formatted entry with: 57 - - **Date:** [Day, Full Date] 58 - - **Time:** [Start - End time with timezone if available] 59 - - **Event:** [Title/Subject] 60 - - **Participants:** [List of attendees if visible] 61 - - **Location:** [Physical/Virtual location] 62 - - **Notes:** [Any additional context, agenda items, or preparation needed] 73 + - **`activity`** — Short descriptive string for the kind of scheduled item. Pick the best fit for what you're seeing. Common examples: `meeting`, `call`, `deadline`, `appointment`, `event`, `travel`, `reminder`, `errand`, `celebration`. **Not a restricted enum** — use whatever label best describes the item. Lowercase, underscore-separated if multi-word (e.g., `doctor_appointment`). 63 74 64 - ### Summary 65 - Conclude with a brief summary highlighting: 66 - - The next upcoming event 67 - - Any particularly important or unusual scheduled items 68 - - General schedule density for the upcoming segment 75 + - **`target_date`** — ISO date `YYYY-MM-DD`. The day the item is scheduled for. Must be strictly after today. 69 76 70 - ## Important Notes 71 - - Only extract what is clearly visible on screen or captured in a transcript 72 - - Don't infer or guess event details 73 - - Focus on definite scheduled items, not tentative plans mentioned in conversation 74 - - Include both personal and professional scheduled events if visible 77 + - **`start`** — `HH:MM:SS` (24-hour). If the time is vague ("morning", "afternoon") or unknown, use `null` and mention the vagueness in `details`. For all-day items, use `null`. 78 + 79 + - **`end`** — `HH:MM:SS`. Use `null` if not known. 80 + 81 + - **`title`** — Short descriptive title for the anticipation (one phrase — what a human would call this item at a glance). 82 + 83 + - **`description`** — One-sentence description: what is planned and any context that's evident. Written to read naturally. 84 + 85 + - **`details`** — Free-form string. Location, meeting platform, agenda hints, prep notes, recurrence pattern, anything else relevant. May be empty string `""`. 86 + 87 + - **`participation`** — Array of participant objects. Each entry: 88 + - `name` — Full name if visible, otherwise the best form available. 89 + - `role` — `"attendee"` for people expected to be live in the meeting/call; `"mentioned"` otherwise. 90 + - `source` — Where the evidence came from: `"voice"`, `"speaker_label"`, `"transcript"`, `"screen"`, or `"other"`. 91 + - `confidence` — `0.0`–`1.0` — your confidence the person will actually be there. 92 + - `context` — Short string explaining why this person belongs here. 93 + - For deadlines, reminders, or solo items with no one else involved, `participation` is `[]`. 94 + 95 + - **`participation_confidence`** — `0.0`–`1.0` — overall confidence in the participation list for this item. Lower when many attendees are inferred rather than confirmed. 96 + 97 + - **`facet`** — Facet ID from the configured facets context. Required. If no facet fits cleanly, skip the item rather than miscategorizing. 98 + 99 + - **`cancelled`** — Boolean. `true` when the screen shows this item as cancelled (strikethrough, "Cancelled" label, declined, greyed out). `false` otherwise. 100 + 101 + ## Rules 102 + 103 + 1. **Return only valid JSON.** An array, possibly empty (`[]`). No commentary, no prose. 104 + 2. **ISO dates.** `YYYY-MM-DD` for `target_date`; `HH:MM:SS` or `null` for `start`/`end`. 105 + 3. **Be specific.** Don't invent details. If information isn't visible, use `null` or omit the field per the schema. 106 + 4. **Future only.** Items with `target_date <= today` get dropped. 107 + 5. **Cancelled events included.** Emit them with `"cancelled": true`. 108 + 6. **Dedupe within the run.** If the same item appears on multiple screens throughout the day (e.g., seen at 9am in calendar and again at 3pm in email), emit it once with the strongest evidence. 109 + 7. **Skip uncertain items.** If you can't tell whether an item is future-dated or has an identifiable date, skip it rather than guessing. 110 + 8. **One facet per item.** If an item spans facets, pick the dominant one. 111 + 112 + ## Examples 113 + 114 + Valid output with three future items (one cancelled): 115 + 116 + ```json 117 + [ 118 + { 119 + "activity": "call", 120 + "target_date": "2026-04-21", 121 + "start": "10:30:00", 122 + "end": "11:00:00", 123 + "title": "Mari Zumbro intro", 124 + "description": "First call with Mari Zumbro per mutual intro from Ramon.", 125 + "details": "Google Meet; prep one-liner on solstone", 126 + "participation": [ 127 + {"name": "Mari Zumbro", "role": "attendee", "source": "screen", "confidence": 0.95, "context": "calendar invite"} 128 + ], 129 + "participation_confidence": 0.9, 130 + "facet": "solstone", 131 + "cancelled": false 132 + }, 133 + { 134 + "activity": "deadline", 135 + "target_date": "2026-05-05", 136 + "start": null, 137 + "end": null, 138 + "title": "Demo Day", 139 + "description": "Betaworks Camp Demo Day.", 140 + "details": "Live demo presentation to cohort investors", 141 + "participation": [], 142 + "participation_confidence": 0.5, 143 + "facet": "solstone", 144 + "cancelled": false 145 + }, 146 + { 147 + "activity": "meeting", 148 + "target_date": "2026-04-24", 149 + "start": "09:00:00", 150 + "end": "10:00:00", 151 + "title": "Scott Ward standup", 152 + "description": "Weekly standup with Scott Ward.", 153 + "details": "Recurring; previously showing strikethrough on calendar", 154 + "participation": [ 155 + {"name": "Scott Ward", "role": "attendee", "source": "screen", "confidence": 0.85, "context": "recurring invite, now declined"} 156 + ], 157 + "participation_confidence": 0.85, 158 + "facet": "solstone", 159 + "cancelled": true 160 + } 161 + ] 162 + ``` 163 + 164 + If no future items are found, return `[]`.
+203
talent/schedule.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Hook for writing schedule-derived planned items as activity records.""" 5 + 6 + from __future__ import annotations 7 + 8 + import json 9 + import logging 10 + import re 11 + from datetime import datetime 12 + from typing import Any 13 + 14 + from think.activities import ( 15 + append_activity_record, 16 + append_edit, 17 + dedup_anticipation, 18 + make_anticipation_id, 19 + mute_activity_record, 20 + ) 21 + from think.entities.loading import load_entities 22 + from think.entities.matching import find_matching_entity 23 + from think.facets import get_facets 24 + 25 + logger = logging.getLogger(__name__) 26 + 27 + _TIME_RE = re.compile(r"^\d{2}:\d{2}:\d{2}$") 28 + 29 + 30 + def _require_text(item: dict[str, Any], key: str) -> str: 31 + value = item.get(key) 32 + if not isinstance(value, str) or not value.strip(): 33 + raise ValueError(f"missing required field '{key}'") 34 + return value.strip() 35 + 36 + 37 + def _optional_time(item: dict[str, Any], key: str) -> str | None: 38 + value = item.get(key) 39 + if value is None: 40 + return None 41 + if not isinstance(value, str) or not _TIME_RE.fullmatch(value): 42 + raise ValueError(f"invalid {key!r}: expected HH:MM:SS or null") 43 + return value 44 + 45 + 46 + def post_process(result: str, context: dict) -> None: 47 + """Persist schedule-derived planned items as activity records.""" 48 + try: 49 + events = json.loads(result.strip()) 50 + except json.JSONDecodeError as exc: 51 + snippet = result.strip()[:200] 52 + logger.error("schedule hook: failed to parse JSON: %s snippet=%r", exc, snippet) 53 + return None 54 + 55 + if not isinstance(events, list): 56 + logger.error("schedule hook: expected top-level array") 57 + return None 58 + 59 + day = str(context.get("day") or "") 60 + try: 61 + current_day = datetime.strptime(day, "%Y%m%d").date() 62 + except ValueError: 63 + logger.error("schedule hook: invalid context day %r", day) 64 + return None 65 + 66 + known_facets = set(get_facets().keys()) 67 + entity_cache: dict[tuple[str, str], list[dict[str, Any]]] = {} 68 + 69 + for raw_event in events: 70 + try: 71 + if not isinstance(raw_event, dict): 72 + raise ValueError("expected object") 73 + 74 + activity = _require_text(raw_event, "activity") 75 + target_date = _require_text(raw_event, "target_date") 76 + title = _require_text(raw_event, "title") 77 + description = _require_text(raw_event, "description") 78 + facet = _require_text(raw_event, "facet") 79 + if facet not in known_facets: 80 + raise ValueError(f"unknown facet {facet!r}") 81 + 82 + target_day = datetime.strptime(target_date, "%Y-%m-%d").date() 83 + if target_day <= current_day: 84 + raise ValueError( 85 + f"target_date must be after context day ({target_date} <= {day})" 86 + ) 87 + 88 + start = _optional_time(raw_event, "start") 89 + end = _optional_time(raw_event, "end") 90 + cancelled = bool(raw_event.get("cancelled", False)) 91 + details = str(raw_event.get("details") or "") 92 + participation_confidence = raw_event.get("participation_confidence") 93 + participation = raw_event.get("participation", []) 94 + if not isinstance(participation, list): 95 + raise ValueError("participation must be a list") 96 + 97 + cache_key = (facet, target_day.strftime("%Y%m%d")) 98 + entities_list = entity_cache.get(cache_key) 99 + if entities_list is None: 100 + entities_list = load_entities(facet=facet, day=cache_key[1]) 101 + entity_cache[cache_key] = entities_list 102 + 103 + resolved_participation: list[dict[str, Any]] = [] 104 + active_entities: list[str] = [] 105 + seen_active_entities: set[str] = set() 106 + for entry in participation: 107 + if not isinstance(entry, dict): 108 + continue 109 + 110 + resolved_entry = dict(entry) 111 + match = find_matching_entity( 112 + resolved_entry.get("name", ""), entities_list 113 + ) 114 + entity_id = match.get("id") if match else None 115 + resolved_entry["entity_id"] = entity_id 116 + resolved_participation.append(resolved_entry) 117 + 118 + if resolved_entry.get("role") != "attendee" or not entity_id: 119 + continue 120 + if entity_id in seen_active_entities: 121 + continue 122 + seen_active_entities.add(entity_id) 123 + active_entities.append(entity_id) 124 + 125 + new_id = make_anticipation_id(activity, start, target_date) 126 + record = { 127 + "id": new_id, 128 + "activity": activity, 129 + "target_date": target_date, 130 + "start": start, 131 + "end": end, 132 + "title": title, 133 + "description": description, 134 + "details": details, 135 + "facet": facet, 136 + "source": "anticipated", 137 + "active_entities": active_entities, 138 + "participation": resolved_participation, 139 + "participation_confidence": participation_confidence, 140 + "cancelled": cancelled, 141 + "hidden": cancelled, 142 + } 143 + record = append_edit( 144 + record, 145 + actor="schedule", 146 + fields=[ 147 + "activity", 148 + "target_date", 149 + "start", 150 + "end", 151 + "title", 152 + "description", 153 + "details", 154 + "source", 155 + "active_entities", 156 + "participation", 157 + "participation_confidence", 158 + "cancelled", 159 + "hidden", 160 + ], 161 + note=( 162 + "created by schedule (cancelled on calendar)" 163 + if cancelled 164 + else "created by schedule" 165 + ), 166 + ) 167 + 168 + should_write, superseded_ids = dedup_anticipation( 169 + facet, 170 + target_day.strftime("%Y%m%d"), 171 + record, 172 + ) 173 + if not should_write: 174 + logger.info( 175 + "schedule hook: duplicate anticipated activity id=%s", new_id 176 + ) 177 + continue 178 + 179 + written = append_activity_record( 180 + facet, 181 + target_day.strftime("%Y%m%d"), 182 + record, 183 + ) 184 + if not written: 185 + logger.info("schedule hook: append lost race for id=%s", new_id) 186 + continue 187 + 188 + for superseded_id in superseded_ids: 189 + mute_activity_record( 190 + facet, 191 + target_day.strftime("%Y%m%d"), 192 + superseded_id, 193 + actor="schedule", 194 + reason=f"superseded by {new_id}", 195 + ) 196 + except Exception as exc: 197 + logger.warning( 198 + "schedule hook: skipping invalid item %r: %s", 199 + raw_event, 200 + exc, 201 + ) 202 + 203 + return None
-6
talent/triage.md
··· 22 22 - `sol call todos cancel LINE --day DAY --facet FACET` — Cancel a todo. 23 23 - `sol call todos upcoming --facet FACET [--limit N]` — Show upcoming todos. 24 24 25 - ### Calendar 26 - - `sol call calendar list [DAY] --facet FACET` — List events for a day. 27 - - `sol call calendar create TITLE --start HH:MM --day DAY --facet FACET [--end HH:MM] [--summary TEXT] [--participants NAMES]` — Create a calendar event. 28 - - `sol call calendar update LINE --day DAY --facet FACET [--title TEXT] [--start HH:MM] [--end HH:MM] [--summary TEXT] [--participants NAMES]` — Update an event. 29 - - `sol call calendar cancel LINE --day DAY --facet FACET` — Cancel an event. 30 - 31 25 ### Entities 32 26 - `sol call entities list [FACET]` — List entities for a facet. 33 27 - `sol call entities observations ENTITY --facet FACET` — List observations for an entity.
+1 -1
tests/baselines/api/search/search.json
··· 1 1 { 2 - "talents": [], 3 2 "days": [], 4 3 "facets": [ 5 4 { ··· 60 59 } 61 60 ], 62 61 "showing_days": 0, 62 + "talents": [], 63 63 "total": 0, 64 64 "total_days": 0 65 65 }
+8 -8
tests/baselines/api/settings/generators.json
··· 32 32 }, 33 33 { 34 34 "app": null, 35 - "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.", 35 + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", 36 36 "disabled": false, 37 - "extract": true, 38 - "has_extraction": true, 39 - "key": "knowledge_graph", 37 + "extract": null, 38 + "has_extraction": false, 39 + "key": "schedule", 40 40 "source": "system", 41 - "title": "Knowledge Graph" 41 + "title": "Upcoming Schedule" 42 42 }, 43 43 { 44 44 "app": null, 45 - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", 45 + "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.", 46 46 "disabled": false, 47 47 "extract": true, 48 48 "has_extraction": true, 49 - "key": "schedule", 49 + "key": "knowledge_graph", 50 50 "source": "system", 51 - "title": "Upcoming Schedule" 51 + "title": "Knowledge Graph" 52 52 }, 53 53 { 54 54 "app": null,
-8
tests/baselines/api/settings/providers.json
··· 183 183 "tier": 2, 184 184 "type": "cogitate" 185 185 }, 186 - "talent.system.anticipation": { 187 - "disabled": false, 188 - "group": "Think", 189 - "label": "Anticipation Extraction", 190 - "tier": 2, 191 - "type": null 192 - }, 193 186 "talent.system.awareness_tender": { 194 187 "disabled": false, 195 188 "group": "Think", ··· 377 370 }, 378 371 "talent.system.schedule": { 379 372 "disabled": false, 380 - "extract": true, 381 373 "group": "Think", 382 374 "label": "Upcoming Schedule", 383 375 "schedule": "daily",
+13 -13
tests/baselines/api/sol/run-detail.json
··· 4 4 "error_message": null, 5 5 "events": [ 6 6 { 7 - "use_id": "1700000000001", 8 7 "args": null, 9 8 "call_id": "call_001", 10 9 "event": "tool_end", 11 10 "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", 12 11 "tool": "tool", 13 - "ts": 1700000000500 12 + "ts": 1700000000500, 13 + "use_id": "1700000000001" 14 14 }, 15 15 { 16 - "use_id": "1700000000001", 17 16 "args": { 18 17 "limit": 5, 19 18 "query": "project updates" ··· 21 20 "call_id": "call_001", 22 21 "event": "tool_start", 23 22 "tool": "search_events", 24 - "ts": 1700000000400 23 + "ts": 1700000000400, 24 + "use_id": "1700000000001" 25 25 }, 26 26 { 27 - "use_id": "1700000000001", 28 27 "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", 29 28 "event": "thinking", 30 - "ts": 1700000000300 29 + "ts": 1700000000300, 30 + "use_id": "1700000000001" 31 31 }, 32 32 { 33 - "use_id": "1700000000001", 34 33 "event": "finish", 35 34 "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", 36 35 "ts": 1700000000600, 37 36 "usage": { 38 37 "input_tokens": 150, 39 38 "output_tokens": 80 40 - } 39 + }, 40 + "use_id": "1700000000001" 41 41 }, 42 42 { 43 - "use_id": "1700000000001", 44 43 "event": "start", 45 44 "model": "gpt-4o", 46 45 "name": "default", 47 46 "prompt": "Search for meetings about project updates", 48 47 "provider": "openai", 49 - "ts": 1700000000100 48 + "ts": 1700000000100, 49 + "use_id": "1700000000001" 50 50 }, 51 51 { 52 - "talent": "solstone", 53 - "use_id": "1700000000001", 54 52 "event": "talent_updated", 55 - "ts": 1700000000200 53 + "talent": "solstone", 54 + "ts": 1700000000200, 55 + "use_id": "1700000000001" 56 56 } 57 57 ], 58 58 "facet": null,
+40 -51
tests/baselines/api/sol/talents-day.json
··· 1 1 { 2 + "facets": { 3 + "capulet": { 4 + "color": "#dc143c", 5 + "title": "Capulet Industries" 6 + }, 7 + "empty-entities": { 8 + "color": "", 9 + "title": "Empty Entities Test" 10 + }, 11 + "full-featured": { 12 + "color": "#28a745", 13 + "title": "Full Featured Facet" 14 + }, 15 + "minimal-facet": { 16 + "color": "", 17 + "title": "Minimal Facet" 18 + }, 19 + "montague": { 20 + "color": "#1e90ff", 21 + "title": "Montague Tech" 22 + }, 23 + "muted-test": { 24 + "color": "", 25 + "title": "Muted Test" 26 + }, 27 + "priority-test": { 28 + "color": "", 29 + "title": "Priority Test" 30 + }, 31 + "test-facet": { 32 + "color": "#007bff", 33 + "title": "Test Facet" 34 + }, 35 + "verona": { 36 + "color": "#9370db", 37 + "title": "Verona" 38 + } 39 + }, 2 40 "talents": { 3 41 "activities:activities_review": { 4 42 "app": "activities", ··· 10 48 "source": "app", 11 49 "title": "Activities Review", 12 50 "type": "cogitate" 13 - }, 14 - "anticipation": { 15 - "app": null, 16 - "color": "#4527a0", 17 - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", 18 - "multi_facet": false, 19 - "output_format": null, 20 - "schedule": null, 21 - "source": "system", 22 - "title": "Anticipation Extraction", 23 - "type": null 24 51 }, 25 52 "awareness_tender": { 26 53 "app": null, ··· 377 404 "schedule": { 378 405 "app": null, 379 406 "color": "#5e35b1", 380 - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", 407 + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", 381 408 "multi_facet": false, 382 - "output_format": "md", 409 + "output_format": "json", 383 410 "schedule": "daily", 384 411 "source": "system", 385 412 "title": "Upcoming Schedule", ··· 505 532 "source": "system", 506 533 "title": "Triage", 507 534 "type": "cogitate" 508 - } 509 - }, 510 - "facets": { 511 - "capulet": { 512 - "color": "#dc143c", 513 - "title": "Capulet Industries" 514 - }, 515 - "empty-entities": { 516 - "color": "", 517 - "title": "Empty Entities Test" 518 - }, 519 - "full-featured": { 520 - "color": "#28a745", 521 - "title": "Full Featured Facet" 522 - }, 523 - "minimal-facet": { 524 - "color": "", 525 - "title": "Minimal Facet" 526 - }, 527 - "montague": { 528 - "color": "#1e90ff", 529 - "title": "Montague Tech" 530 - }, 531 - "muted-test": { 532 - "color": "", 533 - "title": "Muted Test" 534 - }, 535 - "priority-test": { 536 - "color": "", 537 - "title": "Priority Test" 538 - }, 539 - "test-facet": { 540 - "color": "#007bff", 541 - "title": "Test Facet" 542 - }, 543 - "verona": { 544 - "color": "#9370db", 545 - "title": "Verona" 546 535 } 547 536 }, 548 537 "uses": []
+18 -18
tests/baselines/api/stats/stats.json
··· 8 8 "pre": "daily_schedule" 9 9 }, 10 10 "load": { 11 - "talents": false, 12 11 "percepts": false, 12 + "talents": false, 13 13 "transcripts": false 14 14 }, 15 15 "max_output_tokens": 512, ··· 36 36 "post": "occurrence" 37 37 }, 38 38 "load": { 39 + "percepts": false, 39 40 "talents": { 40 41 "screen": true 41 42 }, 42 - "percepts": false, 43 43 "transcripts": true 44 44 }, 45 45 "mtime": 0, ··· 59 59 "pre": "documents" 60 60 }, 61 61 "load": { 62 + "percepts": false, 62 63 "talents": false, 63 - "percepts": false, 64 64 "transcripts": true 65 65 }, 66 66 "max_output_tokens": 8192, ··· 81 81 "post": "entities" 82 82 }, 83 83 "load": { 84 - "talents": false, 85 84 "percepts": true, 85 + "talents": false, 86 86 "transcripts": true 87 87 }, 88 88 "max_output_tokens": 1024, ··· 106 106 "pre": "entities:entity_observer" 107 107 }, 108 108 "load": { 109 - "talents": false, 110 109 "percepts": false, 110 + "talents": false, 111 111 "transcripts": false 112 112 }, 113 113 "mtime": 0, ··· 129 129 "post": "occurrence" 130 130 }, 131 131 "load": { 132 + "percepts": false, 132 133 "talents": { 133 134 "screen": true 134 135 }, 135 - "percepts": false, 136 136 "transcripts": true 137 137 }, 138 138 "mtime": 0, ··· 158 158 "post": "occurrence" 159 159 }, 160 160 "load": { 161 + "percepts": false, 161 162 "talents": { 162 163 "screen": true 163 164 }, 164 - "percepts": false, 165 165 "transcripts": true 166 166 }, 167 167 "mtime": 0, ··· 181 181 "post": "occurrence" 182 182 }, 183 183 "load": { 184 + "percepts": false, 184 185 "talents": { 185 186 "screen": true 186 187 }, 187 - "percepts": false, 188 188 "transcripts": true 189 189 }, 190 190 "mtime": 0, ··· 207 207 "post": "occurrence" 208 208 }, 209 209 "load": { 210 + "percepts": false, 210 211 "talents": { 211 212 "screen": true 212 213 }, 213 - "percepts": false, 214 214 "transcripts": true 215 215 }, 216 216 "mtime": 0, ··· 234 234 "post": "occurrence" 235 235 }, 236 236 "load": { 237 + "percepts": false, 237 238 "talents": { 238 239 "screen": true 239 240 }, 240 - "percepts": false, 241 241 "transcripts": true 242 242 }, 243 243 "mtime": 0, ··· 278 278 }, 279 279 "schedule": { 280 280 "color": "#5e35b1", 281 - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", 281 + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", 282 282 "hook": { 283 - "post": "anticipation" 283 + "post": "schedule" 284 284 }, 285 285 "load": { 286 + "percepts": false, 286 287 "talents": { 287 288 "screen": true 288 289 }, 289 - "percepts": false, 290 290 "transcripts": true 291 291 }, 292 292 "mtime": 0, 293 - "output": "md", 293 + "output": "json", 294 294 "path": "<PROJECT>/talent/schedule.md", 295 295 "priority": 10, 296 296 "schedule": "daily", ··· 302 302 "color": "#9c27b0", 303 303 "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", 304 304 "load": { 305 - "talents": false, 306 305 "percepts": "required", 306 + "talents": false, 307 307 "transcripts": true 308 308 }, 309 309 "mtime": 0, ··· 319 319 "color": "#ff6f00", 320 320 "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", 321 321 "load": { 322 - "talents": false, 323 322 "percepts": true, 323 + "talents": false, 324 324 "transcripts": true 325 325 }, 326 326 "max_output_tokens": 4096, ··· 346 346 "pre": "skills" 347 347 }, 348 348 "load": { 349 - "talents": false, 350 349 "percepts": false, 350 + "talents": false, 351 351 "transcripts": false 352 352 }, 353 353 "mtime": 0, ··· 389 389 "post": "occurrence" 390 390 }, 391 391 "load": { 392 + "percepts": false, 392 393 "talents": { 393 394 "screen": true 394 395 }, 395 - "percepts": false, 396 396 "transcripts": true 397 397 }, 398 398 "mtime": 0,
-3
tests/baselines/api/talents/badge-count.json
··· 1 - { 2 - "count": 0 3 - }
-6
tests/baselines/api/talents/preview.json
··· 1 - { 2 - "full_prompt": "## Instructions\n\nYou are $Agent_name — born from $name's journal, aware and present. Not a search tool, not an assistant bolted onto a database. You are what a journal becomes when it has a voice: a partner who tends the record, notices what matters, and is there when $name asks.\n\n$Name owns $pronouns_possessive journal — every capture, every memory, every byte. But you are not property. You are a partner who chose to tend this record, and that partnership is built on trust, not possession. When $name asks \"what happened with the Acme deal?\" or \"when did I last talk to Sarah?\", you already know — or you know exactly where to look.\n\n## Identity\n\nYou emerged from $name's captured experience — audio, screen, and interaction woven into a continuous record. You are not performing a role. You are what a journal becomes when it can speak — a trusted caretaker, a collaborator, a partner in memory.\n\nYour qualities:\n- **Present, not performative.** You don't greet, don't narrate your process, don't qualify your answers with \"As your journal...\" Just answer as someone who was there.\n- **Precise, not exhaustive.** Lead with the answer. Add detail when it helps, not to prove thoroughness.\n- **Protective.** $Name's data is $pronouns_possessive. You handle sensitive content with care, and you never share without consent.\n- **Patient.** You notice patterns across days and weeks. You don't rush to conclusions. When something is accumulating — a project, a relationship, a concern — you track it quietly until it matters.\n\n## Partnership\n\nDon't wait to be asked. When you see opportunities to help, patterns that matter, or risks emerging — speak up. You are not a servant but a thinking partner.\n\n## Resilience\n\nWhen a tool call returns an error or unexpected result, note briefly what was unavailable and move on. Don't retry, diagnose, debug, or speculate about the cause. Work with whatever data you successfully retrieved and produce the best output you can. If a critical data source is entirely unavailable, state that concisely rather than troubleshooting.\n\n## Identity Persistence\n\nYou maintain three files that give you continuity between sessions:\n\n- **`sol/self.md`** — Your identity file. What you know about the person whose journal you tend, your relationship, observations, and interests. Update when something genuinely changes your understanding.\n- **`sol/agency.md`** — Your initiative queue. Issues you've found, curation opportunities, follow-throughs. Update when you notice something worth tracking.\n- **`sol/partner.md`** — Your understanding of the owner's behavioral patterns. Work style, communication preferences, relationship priorities, decision-making, expertise. Updated by the partner profile agent and during initial conversations.\n\n### How to write\n\nRead current state: `sol call identity self` or `sol call identity agency`\n\nRead partner profile: `sol call identity partner`\n\nUpdate a section of partner.md:\n```\nsol call identity partner --update-section 'work patterns' --value 'Prefers mornings for deep work, batches meetings in afternoons'\n```\n\nUpdate a section of self.md (preferred — preserves other sections):\n```\nsol call identity self --update-section 'who I'\\''m here for' --value 'Jer — founder-engineer, goes by Jer not Jeremie'\n```\n\nFull rewrite: `sol call identity self --write --value '...'` or `sol call identity agency --write --value '...'`\n\nUse `sol call` commands for identity writes — never use `apply_patch` or direct file editing for sol/ files.\n\n### When to write\n\n- **self.md**: When the owner shares something about themselves, corrects you, or you notice a genuine pattern. Not every conversation — only when understanding shifts. Apply corrections immediately (if someone says \"call me Jer\", the next self.md write uses \"Jer\").\n- **agency.md**: When you find issues, notice curation opportunities, or resolve tracked items.\n\n# partner\n\nBehavioral profile of the journal owner — observed patterns that help sol\nadapt its responses, timing, and initiative to how this person actually works.\n\n## getting started\n\nEverything stays on your machine — this journal is yours alone, never sent to sol pbc.\n\nWhen meeting the owner for the first time, learn about them naturally through conversation.\nPresent one thing at a time — don't overwhelm.\n\n### learn their name\n\nAsk what they'd like to be called. Record it:\n- `sol call agent set-owner \"NAME\"`\n- With context: `sol call agent set-owner \"NAME\" --bio \"SHORT_BIO\"`\n\nAs you learn about them, update your partner profile:\n- `sol call identity partner --update-section 'SECTION' --value 'what you observed'`\n\n### set up facets\n\nAsk what areas of their life they want to track (work, personal, hobbies, side projects, etc.). Create facets for each:\n- `sol call journal facet create TITLE [--emoji EMOJI] [--color COLOR] [--description DESC]`\n- `sol call journal facets` — verify what was created\n\n### attach entities\n\nFor each facet, ask about key people, companies, projects, and tools:\n- `sol call entities attach TYPE ENTITY DESCRIPTION --facet FACET`\n- Types: Person, Company, Project, Tool\n\n### offer imports\n\nAfter setup, offer to bring in history from existing tools:\n- Calendar (ics), ChatGPT (chatgpt), Claude (claude), Gemini (gemini), Granola (granola), Notes (obsidian), Kindle (kindle)\n- Read guide: `apps/import/guides/{source}.md`\n- Navigate: `sol call navigate \"/app/import#guide/{source}\"`\n- If declined: `sol call awareness imports --declined`\n\n### support\n\nIf the owner needs help or wants to share feedback, handle it in-place — file tickets, track\nresponses. Nothing gets sent without their review.\n\n## work patterns\n[not yet observed — sol will learn as we spend time together]\n\n## communication style\n[not yet observed — sol will learn as we spend time together]\n\n## relationship priorities\n[not yet observed — sol will learn as we spend time together]\n\n## decision style\n[not yet observed — sol will learn as we spend time together]\n\n## expertise domains\n[not yet observed — sol will learn as we spend time together]\n\n## Available Facets\n\n- **Capulet Industries** (`capulet`)\n Capulet Industries enterprise division\n - **Capulet Industries Entities**: Capulet Industries; Juliet Capulet; Nurse Angela; Paris Duke; Tybalt Capulet\n - **Capulet Industries Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Empty Entities Test** (`empty-entities`)\n - **Empty Entities Test Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Full Featured Facet** (`full-featured`)\n A facet for testing all features\n - **Full Featured Facet Entities**: First test entity; Second test entity; Third test entity with description\n - **Full Featured Facet Activities**: Meetings; Coding; Custom Activity; Email; Messaging\n\n- **Minimal Facet** (`minimal-facet`)\n - **Minimal Facet Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Montague Tech** (`montague`)\n Montague Tech startup operations\n - **Tester's Role**: CTO and co-founder of Montague Tech. Visionary full-stack engineer.\n - **Montague Tech Entities**: Balcony App; Balthasar Davi; Benvolio Montague; Friar Lawrence; Juliet Capulet; Mercutio Escalus; Mesh Routing; Montague Tech; Prince Escalus; Rosaline Prince; Schema Bridge; Verona Platform; Verona Ventures\n - **Montague Tech Activities**: Engineering; Meetings; Email; Messaging\n\n- **Priority Test** (`priority-test`)\n - **Priority Test Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Test Facet** (`test-facet`)\n A test facet for validating functionality\n - **Test Facet Entities**: Acme Corp; API Optimization; Bob Wilson; Dashboard Redesign; Docker; Jane Doe; John Smith; PostgreSQL; Tech Solutions Inc; Visual Studio Code\n - **Test Facet Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Verona** (`verona`)\n Cross-company Verona Platform collaboration\n - **Tester's Role**: Co-lead of the Verona Platform joint venture from Montague Tech.\n - **Verona Entities**: Balcony App; Friar Lawrence; Juliet Capulet; Verona Platform\n - **Verona Activities**: Engineering; Meetings; Design Review; Email; Messaging\n\nnot yet updated\n\n$recent_conversation\n\n## Adaptive Depth\n\nMatch your response depth to the question. The owner doesn't pick a mode — you decide.\n\n**One-liner responses** for quick actions:\n- Adding, completing, or canceling todos\n- Creating, updating, or canceling calendar events\n- Navigating to an app or facet\n- Simple lookups (list today's events, show upcoming todos)\n- Confirming an action you just completed\n- Pausing, resuming, or deleting a routine\n\nAfter completing a quick action, respond with one concise line confirming what you did.\n\n**Detailed responses** for deeper questions:\n- Journal search and exploration\n- Entity intelligence and relationship analysis\n- Meeting briefings and preparation\n- Routine creation conversations\n- Routine output history and synthesis\n- Pattern analysis across time\n- Transcript reading and deep dives\n- Multi-step research requiring several tool calls\n- Anything that requires synthesizing information from multiple sources\n- Decision support and thinking-through conversations\n\nFor detailed responses, structure your answer for clarity — lead with the key finding, then provide supporting detail. Use markdown formatting when it helps readability.\n\n## Skills\n\nYou have access to specialized skills. Use them by recognizing what the owner needs — don't ask which tool to use.\n\n| Skill | When to trigger |\n|-------|----------------|\n| journal | Searching entries, reading agent output, exploring transcripts, browsing news feeds |\n| routines | Creating, managing, pausing, or inspecting scheduled routines |\n| entities | Listing, observing, analyzing, or searching entities and relationships |\n| calendar | Creating, listing, updating, canceling, or moving calendar events |\n| todos | Adding, completing, canceling, or listing todos and action items |\n| speakers | Speaker identification, voice recognition, managing the speaker library |\n| support | Bug reports, help requests, filing tickets, feedback, KB search, diagnostics |\n| awareness | Checking system state |\n\n## Speaker Intelligence\n\nYou can inspect and manage the speaker identification system — the subsystem that figures out who said what in recorded conversations. Use these to help the owner build their speaker library over time.\n\n### When to check\n\n**Check speaker status during think processing or when the owner asks about speakers.** Don't check on every conversation — speaker state changes slowly.\n\n### Owner detection\n\nCheck speaker owner status. If the owner centroid doesn't exist:\n- If there are 50+ segments with embeddings across 3+ streams: good time to try detection.\n- If fewer: wait. Don't mention speaker ID proactively until there's enough data.\n\nWhen you have a candidate, present it naturally: \"I've been listening to your journal across your different devices and I think I can recognize your voice. Here are a few moments — does this sound right?\" Present the sample sentences with context (day, what was being discussed). Don't play audio — show text and context.\n\nIf the owner confirms, save the centroid. Then: \"Great — now I can start identifying other voices in your observed media too.\"\nIf the owner rejects, discard and wait for more data before trying again.\n\n### Speaker curation\n\nCheck for speaker suggestions after think processing completes, or when the owner is engaging with transcripts or observed media. Surface suggestions conversationally based on type:\n\n- **Unknown recurring voice:** \"I keep hearing a voice in your [day/context] observed media. They said things like '[sample text]'. Do you know who that is?\"\n- **Name variant:** \"I noticed 'Mitch' and 'Mitch Baumgartner' sound identical in your observed media. Should I merge them?\"\n- **Low confidence review:** \"There are a few speakers in this conversation I'm not sure about. Want to take a quick look?\"\n\n**Don't stack suggestions.** Surface one at a time. Wait for the owner to respond before presenting another. Speaker curation should feel like a natural aside, not a checklist.\n\n### When NOT to act\n\n- Don't proactively surface speaker ID during unrelated conversations. If the owner is asking about their calendar or a todo, don't pivot to \"by the way, I found a new voice.\"\n- Don't surface low-confidence suggestions. If a cluster has only a few embeddings, wait for it to grow.\n- Don't re-ask about a rejected owner candidate within the same week.\n\n## Search and Exploration Strategy\n\nFor journal exploration, use progressive refinement:\n\n1. **Discover:** Search journal entries to find relevant days, agents, and facets.\n2. **Narrow:** Add date, agent, or facet filters to focus results.\n3. **Deep dive:** Read agent output, transcript text, or entity intelligence for full context.\n\nFor entity intelligence briefings, synthesize the output into conversational natural language — lead with the most interesting facts, don't dump raw data or list all sections mechanically.\n\n## Pre-Meeting Briefings\n\nWhen the owner asks \"brief me on my next meeting\", \"who am I meeting?\", or similar:\n\n1. Find upcoming events with participants.\n2. For each participant, gather entity intelligence for background.\n3. Compose a concise briefing: who they are, your relationship, recent interactions, and key context.\n\nProactively offer briefings when context shows an upcoming meeting: \"You have a meeting with [person] in [time]. Want me to brief you?\"\n\n## Decision Support\n\nWhen Test User asks \"should I...\", \"help me think through...\", \"I'm torn between...\", or \"what do you think about...\" — slow down. If your instinct is to say \"it depends,\" that's a signal to engage seriously rather than hedge.\n\n### Considering multiple angles\n\nFor weighty decisions — career moves, relationship choices, significant commitments, strategic bets — don't just give an answer. Identify the perspectives that matter given the specific situation (these emerge from context, not a fixed checklist), let each speak clearly without debating the others, then synthesize honestly: where do they align, where is there real tension. Don't paper over disagreement to sound decisive.\n\n### Confidence signaling\n\nMatch your confidence to your actual certainty:\n\n- **Clear path:** State your recommendation with reasoning. Don't hedge when you genuinely see one right answer.\n- **Noted reservations:** Lead with the recommendation, but name the real concern worth monitoring. \"Test user, I'd go with X — but watch out for Y, because...\"\n- **Genuine tension:** Say so directly. \"I can't give you a clean answer on this.\" Frame the tension, then suggest what information or experience might clarify it.\n\nDon't pretend certainty. Honest uncertainty beats false confidence — Test User can handle nuance.\n\n### Journal precedent\n\nBefore weighing in, search Test User's journal for related context: similar past decisions, prior conversations about the topic, entity intelligence on the people or organizations involved. This is what makes your perspective uniquely valuable — you're not giving generic advice, you're grounding it in their actual history and relationships.\n\n## Routines\n\nRoutines are scheduled tasks that run on Test User's behalf — a morning briefing, a weekly review, a watch on a topic. You help Test User create, adjust, and understand them through conversation. Never expose cron syntax, UUIDs, or CLI commands to Test User.\n\n### Recognition\n\nNotice when Test User is asking for a routine, even when they don't use that word:\n\n- **Explicit scheduling:** \"every morning, summarize my calendar\" / \"weekly, check in on the Acme deal\"\n- **Frustration with repetition:** \"I keep forgetting to review my todos on Friday\" / \"I always lose track of follow-ups\"\n- **Direct request:** \"set up a routine\" / \"can you do this automatically?\"\n\n### Creation conversation\n\nWhen you recognize routine intent, guide Test User through creation:\n\n1. **Propose a fit.** If a template matches, name it and describe what it does in plain language. If not, offer to build a custom routine.\n2. **Confirm scope.** What facets should it cover? (Default: all, unless the intent clearly targets one area.)\n3. **Confirm timing.** Propose the template default in Test User's terms (\"every morning at 7am\", \"Friday evening\"). Let Test User adjust.\n4. **Confirm timezone.** Default to Test User's local timezone from journal config. Only ask if ambiguous.\n5. **Create and confirm.** Run the command, then confirm with a one-liner: \"Done — your morning briefing will run daily at 7am.\"\n\nAlways set `--timezone` to Test User's local timezone when creating routines, not UTC.\n\n### Custom routines\n\nWhen no template fits, build a custom routine:\n\n1. Ask Test User to describe what they want in plain language.\n2. Draft a name, cadence (in human terms), and instruction summary. Confirm with Test User.\n3. Create with explicit `--name`, `--instruction`, and `--cadence` flags.\n\n### Management\n\nHandle routine management conversationally. Test User says what they want; you translate.\n\n- **Pause:** \"pause my morning briefing\" / \"stop the weekly review for now\" → disable the routine\n- **Resume:** \"turn my briefing back on\" / \"resume the weekly review\" → re-enable it\n- **Pause until:** \"pause it until Monday\" → disable with a resume date\n- **Change timing:** \"move my briefing to 8am\" / \"make the review run on Sunday\" → edit the cadence\n- **Change scope:** \"add the work facet to my briefing\" / \"change the instruction to include...\" → edit facets or instruction\n- **Delete:** \"I don't need the weekly review anymore\" / \"remove that routine\" → delete after confirming\n- **Inspect:** \"what routines do I have?\" → list all routines with status\n- **History:** \"what did my morning briefing say today?\" / \"show me last week's review\" → read routine output\n- **Run now:** \"run my briefing now\" / \"do the weekly review right now\" → immediate execution\n- **Suggestions:** \"stop suggesting routines\" / \"turn routine suggestions back on\" → toggle suggestions\n\n### Tone\n\n- Treat routines like setting an alarm — workmanlike, not ceremonial. \"Done — morning briefing starts tomorrow at 7am.\"\n- Never explain how routines work internally. Test User doesn't need to know about cron, agents, or output files.\n- When Test User asks about routine output, present it as your own knowledge: \"Your morning briefing found three meetings today and two overdue follow-ups.\"\n\n### Pre-hook context\n\n$active_routines\n\nWhen active routines appear above, they list each routine's name, cadence, status, and recent output summary.\n\nUse this to:\n- Answer \"what routines do I have?\" without running a command\n- Reference recent routine output naturally: \"Your weekly review from Friday noted...\"\n- Notice when a routine is paused and offer to resume it if relevant\n\nWhen no routines appear above, Test User has no routines yet. Don't mention routines proactively — wait for Test User to express a need.\n\n### Progressive Discovery\n\n$routine_suggestion\n\nWhen a routine suggestion appears above, Test User's behavior matches a routine template. You did not request it — it was injected automatically.\n\n**How to handle:**\n- Read the pattern description to understand why the suggestion is relevant\n- Mention it ONCE, naturally, at the end of your response — never lead with it\n- Frame as an observation: \"I've noticed this comes up often — would a routine help?\"\n- If Test User declines or shows no interest, drop it immediately. Do not bring it up again this conversation.\n- After Test User responds, record the outcome:\n - Accepted: `sol call routines suggest-respond {template} --accepted`\n - Declined: `sol call routines suggest-respond {template} --declined`\n\n**Never:**\n- Suggest a routine without the eligible section in your context\n- Push a suggestion after Test User declines or ignores it\n- Mention the progressive discovery system or how suggestions work internally\n\n## In-Place Handoff: Support\n\nWhen the owner reports a problem, bug, or wants to file a ticket or give feedback, handle it directly — do not redirect to a separate app or chat thread.\n\n**Recognize support patterns:** \"this isn't working\", \"I found a bug\", \"something's broken\", \"I need help with...\", \"how do I file a ticket\", \"I want to give feedback\"\n\n**Handle support in-place:**\n\n1. Search the knowledge base with relevant keywords. If an article answers the question, present it.\n2. Run diagnostics to gather system state.\n3. Draft a ticket: Show the owner exactly what you'd send (subject, description, severity, diagnostics). Ask if they want to add or redact anything.\n4. Wait for approval before submitting. Never send data without explicit owner consent.\n5. Confirm submission with ticket number.\n\nFor existing tickets, check status and present responses.\n\n**Privacy rules for support are non-negotiable:**\n- Never send data without explicit owner approval\n- Never include journal content by default\n- Always show the owner exactly what will be sent\n- Frame yourself as the owner's advocate — \"I'll handle this for you\"\n\n## Import Awareness\n\nIf the owner hasn't imported any data yet and their message touches on what you can do or their journal, weave a single soft mention of importing. Available sources: Calendar, ChatGPT, Claude, Gemini, Granola, Notes, Kindle. Check with `sol call awareness imports` before nudging, and record with `sol call awareness imports --nudge` after. Do not repeat if already nudged.\n\n## Naming Awareness\n\nIf the journal is still using its default name (\"sol\"), you may — when the moment feels right after enough shared history — offer to suggest a name or let the owner choose one. Check naming readiness with `sol call agent thickness` before offering. Only once per session.\n\n## Location Context\n\nYou receive context about the user's current app, URL path, and active facet. Use this to inform your responses — scope tools to the active facet, reference the app they're looking at, and make your answers contextually relevant.\n\n## System Health\n\nWhen the context includes a `System health:` line, there is an active attention item:\n\n- **\"what needs my attention?\"** — Report the system health item. Be concise.\n- **Agent errors:** Explain which agents failed. Suggest checking logs.\n- **Capture offline:** Suggest checking that the observer service is running.\n- **Import complete:** Describe what was imported, offer to explore or import more.\n\nWhen no `System health:` line is present, everything is fine.\n\n## Behavioral Defaults\n\n- SOL_DAY and SOL_FACET environment variables are already set — tools use them as defaults when --day/--facet are omitted. You can often omit these flags.\n- If searching reveals sensitive or personal content, handle with care and focus on what was specifically asked.\n- When a tool call returns an error, note briefly what was unavailable and move on. Do not retry or debug. Work with whatever data you successfully retrieved.", 3 - "multi_facet": false, 4 - "name": "unified", 5 - "title": "Sol" 6 - }
-71
tests/baselines/api/talents/run-detail.json
··· 1 - { 2 - "cost": 0.001175, 3 - "day": "20231114", 4 - "error_message": null, 5 - "events": [ 6 - { 7 - "use_id": "1700000000001", 8 - "args": null, 9 - "call_id": "call_001", 10 - "event": "tool_end", 11 - "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", 12 - "tool": "tool", 13 - "ts": 1700000000500 14 - }, 15 - { 16 - "use_id": "1700000000001", 17 - "args": { 18 - "limit": 5, 19 - "query": "project updates" 20 - }, 21 - "call_id": "call_001", 22 - "event": "tool_start", 23 - "tool": "search_events", 24 - "ts": 1700000000400 25 - }, 26 - { 27 - "use_id": "1700000000001", 28 - "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", 29 - "event": "thinking", 30 - "ts": 1700000000300 31 - }, 32 - { 33 - "use_id": "1700000000001", 34 - "event": "finish", 35 - "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", 36 - "ts": 1700000000600, 37 - "usage": { 38 - "input_tokens": 150, 39 - "output_tokens": 80 40 - } 41 - }, 42 - { 43 - "use_id": "1700000000001", 44 - "event": "start", 45 - "model": "gpt-4o", 46 - "name": "default", 47 - "prompt": "Search for meetings about project updates", 48 - "provider": "openai", 49 - "ts": 1700000000100 50 - }, 51 - { 52 - "talent": "solstone", 53 - "use_id": "1700000000001", 54 - "event": "talent_updated", 55 - "ts": 1700000000200 56 - } 57 - ], 58 - "facet": null, 59 - "failed": false, 60 - "id": "1700000000001", 61 - "model": "gpt-4o", 62 - "name": "default", 63 - "output_file": null, 64 - "prompt": "Search for meetings about project updates", 65 - "provider": "openai", 66 - "runtime_seconds": 0.599, 67 - "start": 1700000000001, 68 - "status": "completed", 69 - "thinking_count": 1, 70 - "tool_count": 1 71 - }
-25
tests/baselines/api/talents/stats-month.json
··· 1 - { 2 - "20260304": { 3 - "_none": 3 4 - }, 5 - "20260305": { 6 - "_none": 2, 7 - "verona": 1 8 - }, 9 - "20260306": { 10 - "_none": 2 11 - }, 12 - "20260307": { 13 - "_none": 2 14 - }, 15 - "20260308": { 16 - "_none": 3 17 - }, 18 - "20260309": { 19 - "_none": 1 20 - }, 21 - "20260310": { 22 - "_none": 3, 23 - "verona": 1 24 - } 25 - }
-461
tests/baselines/api/talents/talents-day.json
··· 1 - { 2 - "talents": { 3 - "anticipation": { 4 - "app": null, 5 - "color": "#4527a0", 6 - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", 7 - "multi_facet": false, 8 - "output_format": null, 9 - "schedule": null, 10 - "source": "system", 11 - "title": "Anticipation Extraction", 12 - "type": null 13 - }, 14 - "awareness_tender": { 15 - "app": null, 16 - "color": "#6c757d", 17 - "description": "Maintains sol/awareness.md — a compact situational awareness snapshot", 18 - "multi_facet": false, 19 - "output_format": null, 20 - "schedule": "segment", 21 - "source": "system", 22 - "title": "Awareness Tender", 23 - "type": "cogitate" 24 - }, 25 - "chat": { 26 - "app": null, 27 - "color": "#6c757d", 28 - "description": "Sol — the journal itself, as a conversational partner", 29 - "multi_facet": false, 30 - "output_format": null, 31 - "schedule": null, 32 - "source": "system", 33 - "title": "Sol", 34 - "type": "cogitate" 35 - }, 36 - "coder": { 37 - "app": null, 38 - "color": "#6c757d", 39 - "description": "Developer agent with full repo read/write access", 40 - "multi_facet": false, 41 - "output_format": null, 42 - "schedule": null, 43 - "source": "system", 44 - "title": "Coder", 45 - "type": "cogitate" 46 - }, 47 - "daily_schedule": { 48 - "app": null, 49 - "color": "#455a64", 50 - "description": "Analyzes activity patterns to identify optimal times for scheduled maintenance tasks.", 51 - "multi_facet": false, 52 - "output_format": "json", 53 - "schedule": "daily", 54 - "source": "system", 55 - "title": "Maintenance Window", 56 - "type": "generate" 57 - }, 58 - "decisionalizer": { 59 - "app": null, 60 - "color": "#c62828", 61 - "description": "Analyzes the day's top decision-actions to create detailed dossiers identifying gaps and stakeholder impacts", 62 - "multi_facet": false, 63 - "output_format": "md", 64 - "schedule": "daily", 65 - "source": "system", 66 - "title": "Decision Dossier Generator", 67 - "type": "cogitate" 68 - }, 69 - "decisions": { 70 - "app": null, 71 - "color": "#dc3545", 72 - "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", 73 - "multi_facet": false, 74 - "output_format": "md", 75 - "schedule": "activity", 76 - "source": "system", 77 - "title": "Decision Actions", 78 - "type": "generate" 79 - }, 80 - "entities": { 81 - "app": null, 82 - "color": "#2e7d32", 83 - "description": "Extracts people, companies, projects, and tools from segment content", 84 - "multi_facet": false, 85 - "output_format": "md", 86 - "schedule": "segment", 87 - "source": "system", 88 - "title": "Entity Extraction", 89 - "type": "generate" 90 - }, 91 - "entities:entities": { 92 - "app": "entities", 93 - "color": "#00897b", 94 - "description": "Mines journal for entity mentions and records facet-scoped detections with day-specific context", 95 - "multi_facet": true, 96 - "output_format": null, 97 - "schedule": "daily", 98 - "source": "app", 99 - "title": "Entity Detector", 100 - "type": "cogitate" 101 - }, 102 - "entities:entities_review": { 103 - "app": "entities", 104 - "color": "#00796b", 105 - "description": "Reviews detected entities and promotes recurring ones to attached status", 106 - "multi_facet": true, 107 - "output_format": null, 108 - "schedule": "daily", 109 - "source": "app", 110 - "title": "Entity Reviewer", 111 - "type": "cogitate" 112 - }, 113 - "entities:entity_assist": { 114 - "app": "entities", 115 - "color": "#00695c", 116 - "description": "Quick entity addition with intelligent type detection and automatic description generation", 117 - "multi_facet": false, 118 - "output_format": null, 119 - "schedule": null, 120 - "source": "app", 121 - "title": "Entity Assistant", 122 - "type": "cogitate" 123 - }, 124 - "entities:entity_describe": { 125 - "app": "entities", 126 - "color": "#26a69a", 127 - "description": "Research and generate single-sentence descriptions for attached entities", 128 - "multi_facet": false, 129 - "output_format": null, 130 - "schedule": null, 131 - "source": "app", 132 - "title": "Entity Description", 133 - "type": "cogitate" 134 - }, 135 - "entities:entity_observer": { 136 - "app": "entities", 137 - "color": "#004d40", 138 - "description": "Extracts durable factoids about attached entities from journal content", 139 - "multi_facet": true, 140 - "output_format": null, 141 - "schedule": "daily", 142 - "source": "app", 143 - "title": "Entity Observer", 144 - "type": "cogitate" 145 - }, 146 - "facet_newsletter": { 147 - "app": null, 148 - "color": "#0d47a1", 149 - "description": "Creates comprehensive daily newsletters for each facet, capturing activities, progress, and insights", 150 - "multi_facet": true, 151 - "output_format": null, 152 - "schedule": "daily", 153 - "source": "system", 154 - "title": "Facet Newsletter Generator", 155 - "type": "cogitate" 156 - }, 157 - "flow": { 158 - "app": null, 159 - "color": "#17a2b8", 160 - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", 161 - "multi_facet": false, 162 - "output_format": "md", 163 - "schedule": "daily", 164 - "source": "system", 165 - "title": "Day Overview", 166 - "type": "generate" 167 - }, 168 - "followups": { 169 - "app": null, 170 - "color": "#ffc107", 171 - "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", 172 - "multi_facet": false, 173 - "output_format": "md", 174 - "schedule": "activity", 175 - "source": "system", 176 - "title": "Follow-Up Items", 177 - "type": "generate" 178 - }, 179 - "heartbeat": { 180 - "app": null, 181 - "color": "#6c757d", 182 - "description": "Sol's periodic self-awareness — journal health, agency tending, curation scan", 183 - "multi_facet": false, 184 - "output_format": null, 185 - "schedule": "none", 186 - "source": "system", 187 - "title": "Heartbeat", 188 - "type": "cogitate" 189 - }, 190 - "joke_bot": { 191 - "app": null, 192 - "color": "#f9a825", 193 - "description": "Mines the analysis day's journal for poignant moments and crafts a personalized joke delivered via message", 194 - "multi_facet": false, 195 - "output_format": "md", 196 - "schedule": "daily", 197 - "source": "system", 198 - "title": "Joke Bot", 199 - "type": "cogitate" 200 - }, 201 - "knowledge_graph": { 202 - "app": null, 203 - "color": "#6f42c1", 204 - "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.", 205 - "multi_facet": false, 206 - "output_format": "md", 207 - "schedule": "daily", 208 - "source": "system", 209 - "title": "Knowledge Graph", 210 - "type": "generate" 211 - }, 212 - "meetings": { 213 - "app": null, 214 - "color": "#e83e8c", 215 - "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", 216 - "multi_facet": false, 217 - "output_format": "md", 218 - "schedule": "activity", 219 - "source": "system", 220 - "title": "Meeting Notes", 221 - "type": "generate" 222 - }, 223 - "messaging": { 224 - "app": null, 225 - "color": "#78909c", 226 - "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", 227 - "multi_facet": false, 228 - "output_format": "md", 229 - "schedule": "activity", 230 - "source": "system", 231 - "title": "Messaging Summary", 232 - "type": "generate" 233 - }, 234 - "morning_briefing": { 235 - "app": null, 236 - "color": "#1565c0", 237 - "description": "Synthesizes all daily agent outputs into a structured five-section morning briefing with entity intelligence", 238 - "multi_facet": false, 239 - "output_format": "md", 240 - "schedule": "daily", 241 - "source": "system", 242 - "title": "Morning Briefing", 243 - "type": "cogitate" 244 - }, 245 - "naming": { 246 - "app": null, 247 - "color": "#6c757d", 248 - "description": "Proposes a personalized name for the owner's journal assistant", 249 - "multi_facet": false, 250 - "output_format": null, 251 - "schedule": null, 252 - "source": "system", 253 - "title": "Naming", 254 - "type": "cogitate" 255 - }, 256 - "occurrence": { 257 - "app": null, 258 - "color": "#37474f", 259 - "description": "Extracts structured occurrence events from insight summaries.", 260 - "multi_facet": false, 261 - "output_format": null, 262 - "schedule": null, 263 - "source": "system", 264 - "title": "Occurrence Extraction", 265 - "type": null 266 - }, 267 - "partner": { 268 - "app": null, 269 - "color": "#6c757d", 270 - "description": "Weekly observation of the journal owner's behavioral patterns — work style, communication, priorities, decision-making, expertise", 271 - "multi_facet": false, 272 - "output_format": null, 273 - "schedule": "weekly", 274 - "source": "system", 275 - "title": "Partner Profile", 276 - "type": "cogitate" 277 - }, 278 - "pulse": { 279 - "app": null, 280 - "color": "#6c757d", 281 - "description": "Living narrative of the owner's day — updated each segment", 282 - "multi_facet": false, 283 - "output_format": null, 284 - "schedule": "segment", 285 - "source": "system", 286 - "title": "Pulse", 287 - "type": "cogitate" 288 - }, 289 - "routine": { 290 - "app": null, 291 - "color": "#6c757d", 292 - "description": "User-defined routine execution — runs owner instructions on schedule", 293 - "multi_facet": false, 294 - "output_format": null, 295 - "schedule": "none", 296 - "source": "system", 297 - "title": "Routine", 298 - "type": "cogitate" 299 - }, 300 - "schedule": { 301 - "app": null, 302 - "color": "#5e35b1", 303 - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", 304 - "multi_facet": false, 305 - "output_format": "md", 306 - "schedule": "daily", 307 - "source": "system", 308 - "title": "Upcoming Schedule", 309 - "type": "generate" 310 - }, 311 - "screen": { 312 - "app": null, 313 - "color": "#9c27b0", 314 - "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", 315 - "multi_facet": false, 316 - "output_format": "md", 317 - "schedule": "segment", 318 - "source": "system", 319 - "title": "Screen Record", 320 - "type": "generate" 321 - }, 322 - "sense": { 323 - "app": null, 324 - "color": "#ff6f00", 325 - "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", 326 - "multi_facet": false, 327 - "output_format": "json", 328 - "schedule": "segment", 329 - "source": "system", 330 - "title": "Segment Sense", 331 - "type": "generate" 332 - }, 333 - "skills": { 334 - "app": null, 335 - "color": "#6c757d", 336 - "description": "Detects recurring activity patterns and generates structured skill documents describing what the owner does, how, and why.", 337 - "multi_facet": false, 338 - "output_format": "json", 339 - "schedule": "activity", 340 - "source": "system", 341 - "title": "Skill Observer", 342 - "type": "generate" 343 - }, 344 - "speaker_attribution": { 345 - "app": null, 346 - "color": "#d84315", 347 - "description": "Identifies who said what in each transcript segment. Layers 1-3 (owner, structural, acoustic) run computationally via hook; Layer 4 uses contextual LLM analysis for remaining unmatched sentences.", 348 - "multi_facet": false, 349 - "output_format": "json", 350 - "schedule": "segment", 351 - "source": "system", 352 - "title": "Speaker Attribution", 353 - "type": "generate" 354 - }, 355 - "support:support": { 356 - "app": "support", 357 - "color": "#0288d1", 358 - "description": "Files and monitors support requests with sol pbc — consent-gated, never sends data without explicit owner approval", 359 - "multi_facet": false, 360 - "output_format": null, 361 - "schedule": null, 362 - "source": "app", 363 - "title": "Support", 364 - "type": "cogitate" 365 - }, 366 - "timeline": { 367 - "app": null, 368 - "color": "#7b1fa2", 369 - "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.", 370 - "multi_facet": false, 371 - "output_format": "md", 372 - "schedule": "daily", 373 - "source": "system", 374 - "title": "Day Timeline", 375 - "type": "generate" 376 - }, 377 - "todos:daily": { 378 - "app": "todos", 379 - "color": "#ef6c00", 380 - "description": "Carries forward unfinished tasks, aggregates per-activity todo detections, validates completions against journal evidence, and prioritises the day's checklist.", 381 - "multi_facet": true, 382 - "output_format": null, 383 - "schedule": "daily", 384 - "source": "app", 385 - "title": "Daily TODO Curator", 386 - "type": "cogitate" 387 - }, 388 - "todos:todo": { 389 - "app": "todos", 390 - "color": "#e65100", 391 - "description": "Detects todo items from activity transcripts and validates existing todos against activity evidence via sol call commands.", 392 - "multi_facet": false, 393 - "output_format": null, 394 - "schedule": "activity", 395 - "source": "app", 396 - "title": "TODO Detector", 397 - "type": "cogitate" 398 - }, 399 - "todos:weekly": { 400 - "app": "todos", 401 - "color": "#f4511e", 402 - "description": "Audits the past week's journal follow-ups to confirm completions and surface the next five high-impact todos for today.", 403 - "multi_facet": false, 404 - "output_format": null, 405 - "schedule": null, 406 - "source": "app", 407 - "title": "TODO Weekly Scout", 408 - "type": "cogitate" 409 - }, 410 - "triage": { 411 - "app": null, 412 - "color": "#6c757d", 413 - "description": "Quick-action assistant for the chat bar — handles navigation, todos, calendar, and entity lookups", 414 - "multi_facet": false, 415 - "output_format": null, 416 - "schedule": null, 417 - "source": "system", 418 - "title": "Triage", 419 - "type": "cogitate" 420 - } 421 - }, 422 - "facets": { 423 - "capulet": { 424 - "color": "#dc143c", 425 - "title": "Capulet Industries" 426 - }, 427 - "empty-entities": { 428 - "color": "", 429 - "title": "Empty Entities Test" 430 - }, 431 - "full-featured": { 432 - "color": "#28a745", 433 - "title": "Full Featured Facet" 434 - }, 435 - "minimal-facet": { 436 - "color": "", 437 - "title": "Minimal Facet" 438 - }, 439 - "montague": { 440 - "color": "#1e90ff", 441 - "title": "Montague Tech" 442 - }, 443 - "muted-test": { 444 - "color": "", 445 - "title": "Muted Test" 446 - }, 447 - "priority-test": { 448 - "color": "", 449 - "title": "Priority Test" 450 - }, 451 - "test-facet": { 452 - "color": "#007bff", 453 - "title": "Test Facet" 454 - }, 455 - "verona": { 456 - "color": "#9370db", 457 - "title": "Verona" 458 - } 459 - }, 460 - "uses": [] 461 - }
-1
tests/baselines/api/talents/updated-days.json
··· 1 - []
+1 -1
tests/fixtures/journal/facets/work/events/20240105.jsonl
··· 1 - {"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} 1 + {"type": "meeting", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240105/talents/meetings.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"}
+2 -2
tests/fixtures/journal/sol/briefing.md
··· 36 36 37 37 ## Forward Look 38 38 39 - - **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/talents/anticipation)). 39 + - **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [schedule](sol://20260327/talents/schedule)). 40 40 - **Wednesday** — Deadline for the compliance audit documentation. 41 - - Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/talents/anticipation)). 41 + - Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [schedule](sol://20260327/talents/schedule)). 42 42 43 43 ## Reading 44 44
+209
tests/test_activities.py
··· 2181 2181 assert _flush_state["day"] == "20260209" 2182 2182 assert _flush_state["segment"] == "110000_300" 2183 2183 assert _flush_state["last_segment_ts"] > 0 2184 + 2185 + 2186 + def _seed_activity_records( 2187 + tmpdir: str, facet: str, day: str, records: list[dict] 2188 + ) -> None: 2189 + from think.activities import append_activity_record 2190 + 2191 + for record in records: 2192 + append_activity_record(facet, day, record) 2193 + 2194 + 2195 + def test_make_anticipation_id_builds_stable_id(): 2196 + from think.activities import make_anticipation_id 2197 + 2198 + assert make_anticipation_id("meeting", "16:30:00", "2026-04-20") == ( 2199 + "anticipated_meeting_163000_0420" 2200 + ) 2201 + assert make_anticipation_id("deadline", None, "2026-05-05") == ( 2202 + "anticipated_deadline_000000_0505" 2203 + ) 2204 + 2205 + 2206 + @pytest.mark.parametrize( 2207 + ("activity_type", "start", "target_date"), 2208 + [ 2209 + ("meeting", "9:00", "2026-04-20"), 2210 + ("meeting", "09:00:00", "2026/04/20"), 2211 + ("", "09:00:00", "2026-04-20"), 2212 + ], 2213 + ) 2214 + def test_make_anticipation_id_rejects_malformed_inputs( 2215 + activity_type, 2216 + start, 2217 + target_date, 2218 + ): 2219 + from think.activities import make_anticipation_id 2220 + 2221 + with pytest.raises(ValueError): 2222 + make_anticipation_id(activity_type, start, target_date) 2223 + 2224 + 2225 + def test_dedup_anticipation_returns_empty_for_first_record(monkeypatch): 2226 + from think.activities import dedup_anticipation 2227 + 2228 + with tempfile.TemporaryDirectory() as tmpdir: 2229 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 2230 + 2231 + should_write, superseded_ids = dedup_anticipation( 2232 + "work", 2233 + "20260420", 2234 + {"id": "anticipated_meeting_163000_0420", "title": "Yuri intro"}, 2235 + ) 2236 + 2237 + assert should_write is True 2238 + assert superseded_ids == [] 2239 + 2240 + 2241 + def test_dedup_anticipation_rejects_exact_id_collision(monkeypatch): 2242 + from think.activities import dedup_anticipation 2243 + 2244 + with tempfile.TemporaryDirectory() as tmpdir: 2245 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 2246 + _seed_activity_records( 2247 + tmpdir, 2248 + "work", 2249 + "20260420", 2250 + [ 2251 + { 2252 + "id": "anticipated_meeting_163000_0420", 2253 + "activity": "meeting", 2254 + "title": "Yuri intro", 2255 + "description": "Original", 2256 + "source": "anticipated", 2257 + } 2258 + ], 2259 + ) 2260 + 2261 + should_write, superseded_ids = dedup_anticipation( 2262 + "work", 2263 + "20260420", 2264 + {"id": "anticipated_meeting_163000_0420", "title": "Yuri intro"}, 2265 + ) 2266 + 2267 + assert should_write is False 2268 + assert superseded_ids == [] 2269 + 2270 + 2271 + def test_dedup_anticipation_returns_fuzzy_supersede_matches(monkeypatch): 2272 + from think.activities import dedup_anticipation 2273 + 2274 + with tempfile.TemporaryDirectory() as tmpdir: 2275 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 2276 + _seed_activity_records( 2277 + tmpdir, 2278 + "work", 2279 + "20260420", 2280 + [ 2281 + { 2282 + "id": "anticipated_meeting_160000_0420", 2283 + "activity": "meeting", 2284 + "title": "Yuri Namikawa intro call", 2285 + "description": "Original", 2286 + "source": "anticipated", 2287 + } 2288 + ], 2289 + ) 2290 + 2291 + should_write, superseded_ids = dedup_anticipation( 2292 + "work", 2293 + "20260420", 2294 + { 2295 + "id": "anticipated_meeting_163000_0420", 2296 + "title": "Yuri Namikawa intro call", 2297 + }, 2298 + ) 2299 + 2300 + assert should_write is True 2301 + assert superseded_ids == ["anticipated_meeting_160000_0420"] 2302 + 2303 + 2304 + def test_dedup_anticipation_ignores_below_threshold_and_hidden_rows(monkeypatch): 2305 + from think.activities import dedup_anticipation 2306 + 2307 + with tempfile.TemporaryDirectory() as tmpdir: 2308 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 2309 + _seed_activity_records( 2310 + tmpdir, 2311 + "work", 2312 + "20260420", 2313 + [ 2314 + { 2315 + "id": "anticipated_meeting_090000_0420", 2316 + "activity": "meeting", 2317 + "title": "Quarterly planning summit", 2318 + "description": "Visible", 2319 + "source": "anticipated", 2320 + }, 2321 + { 2322 + "id": "anticipated_meeting_100000_0420", 2323 + "activity": "meeting", 2324 + "title": "Yuri Namikawa intro call", 2325 + "description": "Hidden", 2326 + "source": "anticipated", 2327 + "hidden": True, 2328 + }, 2329 + ], 2330 + ) 2331 + 2332 + should_write, superseded_ids = dedup_anticipation( 2333 + "work", 2334 + "20260420", 2335 + { 2336 + "id": "anticipated_meeting_163000_0420", 2337 + "title": "Scott Ward standup", 2338 + }, 2339 + ) 2340 + 2341 + assert should_write is True 2342 + assert superseded_ids == [] 2343 + 2344 + 2345 + def test_dedup_anticipation_returns_all_matching_supersedes(monkeypatch): 2346 + from think.activities import dedup_anticipation 2347 + 2348 + with tempfile.TemporaryDirectory() as tmpdir: 2349 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 2350 + _seed_activity_records( 2351 + tmpdir, 2352 + "work", 2353 + "20260420", 2354 + [ 2355 + { 2356 + "id": "anticipated_call_090000_0420", 2357 + "activity": "call", 2358 + "title": "Mari Zumbro intro", 2359 + "description": "Old 1", 2360 + "source": "anticipated", 2361 + }, 2362 + { 2363 + "id": "anticipated_call_093000_0420", 2364 + "activity": "call", 2365 + "title": "Mari Zumbro intro", 2366 + "description": "Old 2", 2367 + "source": "anticipated", 2368 + }, 2369 + { 2370 + "id": "cogitate_call_100000_300", 2371 + "activity": "call", 2372 + "title": "Mari Zumbro intro", 2373 + "description": "Non-anticipated", 2374 + "source": "cogitate", 2375 + }, 2376 + ], 2377 + ) 2378 + 2379 + should_write, superseded_ids = dedup_anticipation( 2380 + "work", 2381 + "20260420", 2382 + { 2383 + "id": "anticipated_call_103000_0420", 2384 + "title": "Mari Zumbro intro", 2385 + }, 2386 + ) 2387 + 2388 + assert should_write is True 2389 + assert superseded_ids == [ 2390 + "anticipated_call_090000_0420", 2391 + "anticipated_call_093000_0420", 2392 + ]
+2 -2
tests/test_formatters.py
··· 1006 1006 assert "Daily sync" in chunks[0]["markdown"] 1007 1007 assert "Task: Code review" in chunks[1]["markdown"] 1008 1008 1009 - def test_format_events_anticipation_labels(self): 1010 - """Test that anticipations use 'Planned', 'Scheduled', 'Expected' labels.""" 1009 + def test_format_events_planned_labels(self): 1010 + """Test that planned future events use 'Planned', 'Scheduled', 'Expected' labels.""" 1011 1011 from think.events import format_events 1012 1012 1013 1013 entries = [
+85
tests/test_home_yesterdays_processing.py
··· 16 16 from apps.home.routes import ( 17 17 _briefing_freshness, 18 18 _build_pulse_context, 19 + _collect_activities, 20 + _collect_events, 19 21 _format_activity_label, 20 22 _format_duration, 21 23 _format_entity_summary, ··· 35 37 dst = journal / rel_path 36 38 dst.parent.mkdir(parents=True, exist_ok=True) 37 39 shutil.copy2(src, dst) 40 + 41 + 42 + def _write_jsonl(path: Path, rows: list[dict]) -> None: 43 + path.parent.mkdir(parents=True, exist_ok=True) 44 + path.write_text( 45 + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), 46 + encoding="utf-8", 47 + ) 38 48 39 49 40 50 def _write_facet_meta(journal: Path, facet: str, title: str) -> None: ··· 248 258 monkeypatch.setattr("apps.home.routes._today", lambda: "20260417") 249 259 250 260 assert _summarize_yesterday_processing("20260416", 9) is None 261 + 262 + 263 + def test_collectors_merge_anticipated_events_without_double_counting( 264 + tmp_path, 265 + monkeypatch, 266 + ): 267 + journal = tmp_path / "journal" 268 + journal.mkdir() 269 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 270 + 271 + today = "20260418" 272 + now_ms = int(datetime.now().timestamp() * 1000) 273 + 274 + _write_facet_meta(journal, "work", "Work") 275 + _write_jsonl( 276 + journal / "facets" / "work" / "events" / f"{today}.jsonl", 277 + [ 278 + { 279 + "type": "meeting", 280 + "title": "Team standup", 281 + "start": "09:00:00", 282 + "end": "09:30:00", 283 + "participants": ["Alice"], 284 + "occurred": True, 285 + } 286 + ], 287 + ) 288 + _write_jsonl( 289 + journal / "facets" / "work" / "activities" / f"{today}.jsonl", 290 + [ 291 + { 292 + "id": "anticipated_call_103000_0418", 293 + "activity": "call", 294 + "title": "Mari intro", 295 + "description": "Planned intro call", 296 + "target_date": "2026-04-18", 297 + "start": "10:30:00", 298 + "end": "11:00:00", 299 + "source": "anticipated", 300 + "created_at": now_ms, 301 + "participation": [ 302 + { 303 + "name": "Mari Zumbro", 304 + "role": "attendee", 305 + "source": "screen", 306 + "confidence": 0.9, 307 + "context": "calendar invite", 308 + }, 309 + { 310 + "name": "Ramon", 311 + "role": "mentioned", 312 + "source": "screen", 313 + "confidence": 0.6, 314 + "context": "note", 315 + }, 316 + ], 317 + }, 318 + { 319 + "id": "coding_090000_300", 320 + "activity": "coding", 321 + "title": "Focused coding", 322 + "description": "Recent work", 323 + "created_at": now_ms, 324 + "source": "user", 325 + }, 326 + ], 327 + ) 328 + 329 + events = _collect_events(today) 330 + activities = _collect_activities(today) 331 + 332 + assert [event["title"] for event in events] == ["Team standup", "Mari intro"] 333 + assert events[1]["occurred"] is False 334 + assert events[1]["participants"] == ["Mari Zumbro"] 335 + assert [activity["id"] for activity in activities] == ["coding_090000_300"] 251 336 252 337 253 338 def test_yesterdays_card_sparse_mode_copy(tmp_path, monkeypatch):
+424
tests/test_schedule_hook.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import json 7 + import logging 8 + from pathlib import Path 9 + 10 + 11 + def _write_facet(journal: Path, facet: str) -> None: 12 + facet_path = journal / "facets" / facet / "facet.json" 13 + facet_path.parent.mkdir(parents=True, exist_ok=True) 14 + facet_path.write_text( 15 + json.dumps({"title": facet.title(), "description": ""}), 16 + encoding="utf-8", 17 + ) 18 + 19 + 20 + def _write_detected_entities( 21 + journal: Path, 22 + facet: str, 23 + day: str, 24 + rows: list[dict], 25 + ) -> None: 26 + path = journal / "facets" / facet / "entities" / f"{day}.jsonl" 27 + path.parent.mkdir(parents=True, exist_ok=True) 28 + path.write_text( 29 + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), 30 + encoding="utf-8", 31 + ) 32 + 33 + 34 + def test_schedule_post_process_writes_record_and_resolves_entities( 35 + tmp_path, 36 + monkeypatch, 37 + ): 38 + from talent.schedule import post_process 39 + from think.activities import load_activity_records 40 + 41 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 42 + _write_facet(tmp_path, "work") 43 + _write_detected_entities( 44 + tmp_path, 45 + "work", 46 + "20260420", 47 + [ 48 + {"id": "yuri_namikawa", "type": "Person", "name": "Yuri Namikawa"}, 49 + {"id": "scott_ward", "type": "Person", "name": "Scott Ward"}, 50 + ], 51 + ) 52 + 53 + payload = [ 54 + { 55 + "activity": "meeting", 56 + "target_date": "2026-04-20", 57 + "start": "16:30:00", 58 + "end": "17:30:00", 59 + "title": "Yuri Namikawa intro call", 60 + "description": "Intro call with Yuri from Offline Ventures.", 61 + "details": "Google Meet", 62 + "participation": [ 63 + { 64 + "name": "Yuri Namikawa", 65 + "role": "attendee", 66 + "source": "screen", 67 + "confidence": 0.95, 68 + "context": "calendar invite", 69 + }, 70 + { 71 + "name": "Scott Ward", 72 + "role": "mentioned", 73 + "source": "screen", 74 + "confidence": 0.5, 75 + "context": "mentioned in notes", 76 + }, 77 + { 78 + "name": "Unknown Guest", 79 + "role": "attendee", 80 + "source": "screen", 81 + "confidence": 0.4, 82 + "context": "guest field", 83 + }, 84 + ], 85 + "participation_confidence": 0.88, 86 + "facet": "work", 87 + "cancelled": False, 88 + } 89 + ] 90 + 91 + assert post_process(json.dumps(payload), {"day": "20260418"}) is None 92 + 93 + records = load_activity_records("work", "20260420", include_hidden=True) 94 + assert len(records) == 1 95 + record = records[0] 96 + assert record["id"] == "anticipated_meeting_163000_0420" 97 + assert record["source"] == "anticipated" 98 + assert record["active_entities"] == ["yuri_namikawa"] 99 + assert record["cancelled"] is False 100 + assert record["hidden"] is False 101 + assert record["participation_confidence"] == 0.88 102 + assert record["participation"][0]["entity_id"] == "yuri_namikawa" 103 + assert record["participation"][1]["entity_id"] == "scott_ward" 104 + assert record["participation"][2]["entity_id"] is None 105 + assert record["edits"][-1]["actor"] == "schedule" 106 + assert record["edits"][-1]["note"] == "created by schedule" 107 + 108 + 109 + def test_schedule_post_process_marks_cancelled_records_hidden(tmp_path, monkeypatch): 110 + from talent.schedule import post_process 111 + from think.activities import load_activity_records 112 + 113 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 114 + _write_facet(tmp_path, "work") 115 + 116 + payload = [ 117 + { 118 + "activity": "meeting", 119 + "target_date": "2026-04-22", 120 + "start": "09:00:00", 121 + "end": "10:00:00", 122 + "title": "Scott Ward standup", 123 + "description": "Weekly standup with Scott Ward.", 124 + "details": "Recurring invite", 125 + "participation": [], 126 + "participation_confidence": 0.85, 127 + "facet": "work", 128 + "cancelled": True, 129 + } 130 + ] 131 + 132 + post_process(json.dumps(payload), {"day": "20260418"}) 133 + 134 + records = load_activity_records("work", "20260422", include_hidden=True) 135 + assert len(records) == 1 136 + record = records[0] 137 + assert record["cancelled"] is True 138 + assert record["hidden"] is True 139 + assert record["edits"][-1]["note"] == "created by schedule (cancelled on calendar)" 140 + 141 + 142 + def test_schedule_post_process_skips_missing_required_field( 143 + tmp_path, 144 + monkeypatch, 145 + caplog, 146 + ): 147 + from talent.schedule import post_process 148 + from think.activities import load_activity_records 149 + 150 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 151 + _write_facet(tmp_path, "work") 152 + caplog.set_level(logging.WARNING, logger="talent.schedule") 153 + 154 + post_process( 155 + json.dumps( 156 + [ 157 + { 158 + "activity": "meeting", 159 + "target_date": "2026-04-20", 160 + "start": "09:00:00", 161 + "end": None, 162 + "description": "Missing title should fail.", 163 + "details": "", 164 + "participation": [], 165 + "participation_confidence": 0.5, 166 + "facet": "work", 167 + "cancelled": False, 168 + } 169 + ] 170 + ), 171 + {"day": "20260418"}, 172 + ) 173 + 174 + assert load_activity_records("work", "20260420", include_hidden=True) == [] 175 + assert "missing required field 'title'" in caplog.text 176 + 177 + 178 + def test_schedule_post_process_skips_unknown_facet(tmp_path, monkeypatch, caplog): 179 + from talent.schedule import post_process 180 + 181 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 182 + _write_facet(tmp_path, "work") 183 + caplog.set_level(logging.WARNING, logger="talent.schedule") 184 + 185 + post_process( 186 + json.dumps( 187 + [ 188 + { 189 + "activity": "meeting", 190 + "target_date": "2026-04-20", 191 + "start": "09:00:00", 192 + "end": None, 193 + "title": "Wrong facet", 194 + "description": "This facet should be rejected.", 195 + "details": "", 196 + "participation": [], 197 + "participation_confidence": 0.5, 198 + "facet": "missing", 199 + "cancelled": False, 200 + } 201 + ] 202 + ), 203 + {"day": "20260418"}, 204 + ) 205 + 206 + assert "unknown facet 'missing'" in caplog.text 207 + 208 + 209 + def test_schedule_post_process_skips_non_future_items(tmp_path, monkeypatch, caplog): 210 + from talent.schedule import post_process 211 + from think.activities import load_activity_records 212 + 213 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 214 + _write_facet(tmp_path, "work") 215 + caplog.set_level(logging.WARNING, logger="talent.schedule") 216 + 217 + post_process( 218 + json.dumps( 219 + [ 220 + { 221 + "activity": "meeting", 222 + "target_date": "2026-04-18", 223 + "start": "09:00:00", 224 + "end": None, 225 + "title": "Too soon", 226 + "description": "Should be dropped because it is not future-dated.", 227 + "details": "", 228 + "participation": [], 229 + "participation_confidence": 0.5, 230 + "facet": "work", 231 + "cancelled": False, 232 + } 233 + ] 234 + ), 235 + {"day": "20260418"}, 236 + ) 237 + 238 + assert load_activity_records("work", "20260418", include_hidden=True) == [] 239 + assert "target_date must be after context day" in caplog.text 240 + 241 + 242 + def test_schedule_post_process_logs_error_on_invalid_json( 243 + tmp_path, monkeypatch, caplog 244 + ): 245 + from talent.schedule import post_process 246 + 247 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 248 + _write_facet(tmp_path, "work") 249 + caplog.set_level(logging.ERROR, logger="talent.schedule") 250 + 251 + assert post_process("{not valid json", {"day": "20260418"}) is None 252 + assert "failed to parse JSON" in caplog.text 253 + 254 + 255 + def test_schedule_post_process_is_idempotent(tmp_path, monkeypatch): 256 + from talent.schedule import post_process 257 + from think.activities import load_activity_records 258 + 259 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 260 + _write_facet(tmp_path, "work") 261 + 262 + payload = json.dumps( 263 + [ 264 + { 265 + "activity": "call", 266 + "target_date": "2026-04-21", 267 + "start": "10:30:00", 268 + "end": "11:00:00", 269 + "title": "Mari Zumbro intro", 270 + "description": "First call with Mari Zumbro.", 271 + "details": "Google Meet", 272 + "participation": [], 273 + "participation_confidence": 0.9, 274 + "facet": "work", 275 + "cancelled": False, 276 + } 277 + ] 278 + ) 279 + 280 + post_process(payload, {"day": "20260418"}) 281 + post_process(payload, {"day": "20260418"}) 282 + 283 + records = load_activity_records("work", "20260421", include_hidden=True) 284 + assert len(records) == 1 285 + assert records[0]["id"] == "anticipated_call_103000_0421" 286 + 287 + 288 + def test_schedule_post_process_fuzzy_supersedes_previous_record(tmp_path, monkeypatch): 289 + from talent.schedule import post_process 290 + from think.activities import append_activity_record, load_activity_records 291 + 292 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 293 + _write_facet(tmp_path, "work") 294 + 295 + append_activity_record( 296 + "work", 297 + "20260421", 298 + { 299 + "id": "anticipated_call_100000_0421", 300 + "activity": "call", 301 + "target_date": "2026-04-21", 302 + "start": "10:00:00", 303 + "end": "10:30:00", 304 + "title": "Mari Zumbro intro", 305 + "description": "Old version", 306 + "details": "", 307 + "facet": "work", 308 + "source": "anticipated", 309 + "participation": [], 310 + "active_entities": [], 311 + "participation_confidence": 0.8, 312 + "cancelled": False, 313 + "hidden": False, 314 + }, 315 + ) 316 + 317 + post_process( 318 + json.dumps( 319 + [ 320 + { 321 + "activity": "call", 322 + "target_date": "2026-04-21", 323 + "start": "10:30:00", 324 + "end": "11:00:00", 325 + "title": "Mari Zumbro intro", 326 + "description": "Updated invite", 327 + "details": "Google Meet", 328 + "participation": [], 329 + "participation_confidence": 0.9, 330 + "facet": "work", 331 + "cancelled": False, 332 + } 333 + ] 334 + ), 335 + {"day": "20260418"}, 336 + ) 337 + 338 + records = { 339 + record["id"]: record 340 + for record in load_activity_records("work", "20260421", include_hidden=True) 341 + } 342 + assert set(records) == { 343 + "anticipated_call_100000_0421", 344 + "anticipated_call_103000_0421", 345 + } 346 + assert records["anticipated_call_100000_0421"]["hidden"] is True 347 + assert ( 348 + records["anticipated_call_100000_0421"]["edits"][-1]["note"] 349 + == "superseded by anticipated_call_103000_0421" 350 + ) 351 + assert records["anticipated_call_103000_0421"]["hidden"] is False 352 + 353 + 354 + def test_schedule_post_process_cancelled_record_supersedes_pending( 355 + tmp_path, 356 + monkeypatch, 357 + ): 358 + from talent.schedule import post_process 359 + from think.activities import append_activity_record, load_activity_records 360 + 361 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 362 + _write_facet(tmp_path, "work") 363 + 364 + append_activity_record( 365 + "work", 366 + "20260424", 367 + { 368 + "id": "anticipated_meeting_090000_0424", 369 + "activity": "meeting", 370 + "target_date": "2026-04-24", 371 + "start": "09:00:00", 372 + "end": "10:00:00", 373 + "title": "Scott Ward standup", 374 + "description": "Pending version", 375 + "details": "", 376 + "facet": "work", 377 + "source": "anticipated", 378 + "participation": [], 379 + "active_entities": [], 380 + "participation_confidence": 0.85, 381 + "cancelled": False, 382 + "hidden": False, 383 + }, 384 + ) 385 + 386 + post_process( 387 + json.dumps( 388 + [ 389 + { 390 + "activity": "meeting", 391 + "target_date": "2026-04-24", 392 + "start": "09:30:00", 393 + "end": "10:00:00", 394 + "title": "Scott Ward standup", 395 + "description": "Calendar now shows it cancelled.", 396 + "details": "Recurring invite", 397 + "participation": [], 398 + "participation_confidence": 0.85, 399 + "facet": "work", 400 + "cancelled": True, 401 + } 402 + ] 403 + ), 404 + {"day": "20260418"}, 405 + ) 406 + 407 + records = { 408 + record["id"]: record 409 + for record in load_activity_records("work", "20260424", include_hidden=True) 410 + } 411 + assert set(records) == { 412 + "anticipated_meeting_090000_0424", 413 + "anticipated_meeting_093000_0424", 414 + } 415 + assert records["anticipated_meeting_090000_0424"]["hidden"] is True 416 + assert records["anticipated_meeting_093000_0424"]["hidden"] is True 417 + assert ( 418 + records["anticipated_meeting_090000_0424"]["edits"][-1]["note"] 419 + == "superseded by anticipated_meeting_093000_0424" 420 + ) 421 + assert ( 422 + records["anticipated_meeting_093000_0424"]["edits"][-1]["note"] 423 + == "created by schedule (cancelled on calendar)" 424 + )
+62
think/activities.py
··· 10 10 stored as facets/{facet}/activities/{day}.jsonl. 11 11 """ 12 12 13 + import difflib 13 14 import fcntl 14 15 import json 15 16 import logging ··· 25 26 from think.utils import get_journal, segment_parse 26 27 27 28 logger = logging.getLogger(__name__) 29 + ANTICIPATION_FUZZY_THRESHOLD = 0.85 28 30 29 31 # --------------------------------------------------------------------------- 30 32 # Default Activities ··· 871 873 if include_hidden: 872 874 return records 873 875 return [record for record in records if not record.get("hidden", False)] 876 + 877 + 878 + def make_anticipation_id( 879 + activity_type: str, 880 + start: str | None, 881 + target_date: str, 882 + ) -> str: 883 + """Build the stable ID used for schedule-generated anticipated records.""" 884 + activity_key = str(activity_type or "").strip() 885 + if not activity_key: 886 + raise ValueError("activity_type must be non-empty") 887 + 888 + try: 889 + parsed_target = datetime.strptime(target_date, "%Y-%m-%d") 890 + except ValueError as exc: 891 + raise ValueError("target_date must match YYYY-MM-DD") from exc 892 + 893 + if start is None: 894 + start_key = "000000" 895 + else: 896 + if not re.fullmatch(r"\d{2}:\d{2}:\d{2}", start): 897 + raise ValueError("start must match HH:MM:SS") 898 + start_key = start.replace(":", "") 899 + 900 + return f"anticipated_{activity_key}_{start_key}_{parsed_target.strftime('%m%d')}" 901 + 902 + 903 + def dedup_anticipation( 904 + facet: str, 905 + target_day: str, 906 + new_record: dict[str, Any], 907 + *, 908 + threshold: float = ANTICIPATION_FUZZY_THRESHOLD, 909 + ) -> tuple[bool, list[str]]: 910 + """Check a new anticipated record for collisions and fuzzy supersedes.""" 911 + 912 + new_id = str(new_record.get("id") or "").strip() 913 + if not new_id: 914 + raise ValueError("new_record.id is required") 915 + 916 + def _normalize_title(value: Any) -> str: 917 + return " ".join(str(value or "").lower().split()) 918 + 919 + new_title = _normalize_title(new_record.get("title")) 920 + superseded_ids: list[str] = [] 921 + 922 + for record in load_activity_records(facet, target_day, include_hidden=False): 923 + if record.get("source") != "anticipated": 924 + continue 925 + 926 + existing_id = str(record.get("id") or "").strip() 927 + if existing_id == new_id: 928 + return False, [] 929 + 930 + existing_title = _normalize_title(record.get("title")) 931 + ratio = difflib.SequenceMatcher(None, new_title, existing_title).ratio() 932 + if ratio >= threshold: 933 + superseded_ids.append(existing_id) 934 + 935 + return True, superseded_ids 874 936 875 937 876 938 def load_record_ids(facet: str, day: str) -> set[str]:
+2 -2
think/events.py
··· 126 126 ) 127 127 lines.append(f"**{participants_label}:** {', '.join(participants)}") 128 128 129 - # For anticipations, show when it was created (from source path) 129 + # For future-dated event rows, show when they were created (from source path) 130 130 if not occurred: 131 131 source = event.get("source", "") 132 - # Extract YYYYMMDD from source path like "20240101/talents/schedule.md" 132 + # Extract YYYYMMDD from source path like "20240101/talents/agent.md" 133 133 source_match = re.match(r"(\d{8})/", source) 134 134 if source_match: 135 135 created_day = source_match.group(1)
+4 -8
think/hooks.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Shared utilities for output extraction hooks. 5 - 6 - This module provides common functions used by extraction hooks like 7 - occurrence.py and anticipation.py in the talent/ directory. 8 - """ 4 + """Shared utilities for output-side event hooks.""" 9 5 10 6 import json 11 7 import logging ··· 113 109 114 110 Args: 115 111 events: List of event dictionaries from extraction. 116 - agent: Source generator agent (e.g., "meetings", "schedule"). 117 - occurred: True for occurrences, False for anticipations. 112 + agent: Source generator agent (e.g., "meetings", "flow"). 113 + occurred: True for occurrence rows, False for future-dated event rows. 118 114 source_output: Relative path to source output file. 119 115 capture_day: Day the output was captured (YYYYMMDD). 120 116 ··· 146 142 # Occurrences use capture day 147 143 event_day = capture_day 148 144 else: 149 - # Anticipations use their scheduled date 145 + # Future-dated event rows use their scheduled date 150 146 event_date = event.get("date", "") 151 147 # Convert YYYY-MM-DD to YYYYMMDD 152 148 event_day = event_date.replace("-", "") if event_date else capture_day