personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-nytb2njh-muse-template-unification'

# Conflicts:
# muse/heartbeat.md
# muse/morning_briefing.md
# muse/partner.md
# muse/pulse.md
# sol/identity.md
# tests/baselines/api/agents/preview.json

+496 -894
+5 -3
apps/entities/muse/entities.md
··· 8 8 "schedule": "daily", 9 9 "priority": 55, 10 10 "multi_facet": true, 11 - "group": "Entities", 12 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 13 - 11 + "group": "Entities" 14 12 } 13 + 14 + $journal 15 + 16 + $facets 15 17 16 18 ## Core Mission 17 19
+5 -3
apps/entities/muse/entities_review.md
··· 7 7 "schedule": "daily", 8 8 "priority": 56, 9 9 "multi_facet": true, 10 - "group": "Entities", 11 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 12 - 10 + "group": "Entities" 13 11 } 12 + 13 + $journal 14 + 15 + $facets 14 16 15 17 ## Core Mission 16 18
+5 -3
apps/entities/muse/entity_assist.md
··· 4 4 "title": "Entity Assistant", 5 5 "description": "Quick entity addition with intelligent type detection and automatic description generation", 6 6 "color": "#00695c", 7 - "group": "Entities", 8 - "instructions": {"system": "journal", "facets": true, "now": true} 9 - 7 + "group": "Entities" 10 8 } 9 + 10 + $journal 11 + 12 + $facets 11 13 12 14 ## Core Mission 13 15
+5 -3
apps/entities/muse/entity_describe.md
··· 4 4 "title": "Entity Description", 5 5 "description": "Research and generate single-sentence descriptions for attached entities", 6 6 "color": "#26a69a", 7 - "group": "Entities", 8 - "instructions": {"system": "journal", "facets": true, "now": true} 9 - 7 + "group": "Entities" 10 8 } 9 + 10 + $journal 11 + 12 + $facets 11 13 12 14 ## Core Mission 13 15
+5 -3
apps/entities/muse/entity_observer.md
··· 8 8 "schedule": "daily", 9 9 "priority": 57, 10 10 "multi_facet": true, 11 - "group": "Entities", 12 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 13 - 11 + "group": "Entities" 14 12 } 13 + 14 + $journal 15 + 16 + $facets 15 17 16 18 ## Core Mission 17 19
+1 -1
apps/health/routes.py
··· 6 6 from pathlib import Path 7 7 8 8 from flask import Blueprint, jsonify, request 9 - from think.streams import stream_name 10 9 11 10 from convey import state 11 + from think.streams import stream_name 12 12 13 13 health_bp = Blueprint("app:health", __name__, url_prefix="/app/health") 14 14
+3 -1
apps/home/events.py
··· 55 55 ) 56 56 except Exception: 57 57 logger.debug( 58 - "Failed to record conversation exchange for agent %s", agent_id, exc_info=True 58 + "Failed to record conversation exchange for agent %s", 59 + agent_id, 60 + exc_info=True, 59 61 )
+6 -2
apps/home/routes.py
··· 96 96 return None, None, [] 97 97 98 98 99 - def _load_briefing_md(today: str | None = None) -> tuple[dict[str, str], dict | None, list[str]]: 99 + def _load_briefing_md( 100 + today: str | None = None, 101 + ) -> tuple[dict[str, str], dict | None, list[str]]: 100 102 """Load today's briefing.md sections and needs_attention bullets.""" 101 103 try: 102 104 today = today or _today() ··· 148 150 return {}, None, [] 149 151 150 152 151 - def _compute_briefing_phase(segment_count: int, hour: int, briefing_exists: bool) -> str: 153 + def _compute_briefing_phase( 154 + segment_count: int, hour: int, briefing_exists: bool 155 + ) -> str: 152 156 """Compute briefing display phase from current time and activity.""" 153 157 if hour >= BRIEFING_EOD_HOUR: 154 158 return "eod"
+8 -6
apps/remote/tests/test_observer_client.py
··· 22 22 23 23 @pytest.fixture 24 24 def mock_config(): 25 - with patch("observe.remote_client.get_config") as mock, patch( 26 - "observe.remote_client.read_service_port" 27 - ) as mock_port: 25 + with ( 26 + patch("observe.remote_client.get_config") as mock, 27 + patch("observe.remote_client.read_service_port") as mock_port, 28 + ): 28 29 mock.return_value = {} 29 30 mock_port.return_value = 8000 30 31 yield mock ··· 53 54 """When no config URL and no convey.port file, _url is empty.""" 54 55 from observe.remote_client import ObserverClient 55 56 56 - with patch("observe.remote_client.get_config") as cfg, patch( 57 - "observe.remote_client.read_service_port" 58 - ) as port: 57 + with ( 58 + patch("observe.remote_client.get_config") as cfg, 59 + patch("observe.remote_client.read_service_port") as port, 60 + ): 59 61 cfg.return_value = {} 60 62 port.return_value = None 61 63 client = ObserverClient("main-stream")
+44 -11
apps/remote/tests/test_routes.py
··· 791 791 key = resp.get_json()["key"] 792 792 793 793 # Query segments - should be empty 794 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 794 + resp = env.client.get( 795 + "/app/remote/ingest/segments/20250103", 796 + headers={"Authorization": f"Bearer {key}"}, 797 + ) 795 798 assert resp.status_code == 200 796 799 assert resp.get_json() == [] 797 800 ··· 800 803 """Test segments endpoint rejects invalid key.""" 801 804 env = remote_env() 802 805 803 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": "Bearer invalid-key"}) 806 + resp = env.client.get( 807 + "/app/remote/ingest/segments/20250103", 808 + headers={"Authorization": "Bearer invalid-key"}, 809 + ) 804 810 assert resp.status_code == 401 805 811 806 812 ··· 816 822 ) 817 823 key = resp.get_json()["key"] 818 824 819 - resp = env.client.get("/app/remote/ingest/segments/2025-01-03", headers={"Authorization": f"Bearer {key}"}) 825 + resp = env.client.get( 826 + "/app/remote/ingest/segments/2025-01-03", 827 + headers={"Authorization": f"Bearer {key}"}, 828 + ) 820 829 assert resp.status_code == 400 821 830 assert "Invalid day format" in resp.get_json()["error"] 822 831 ··· 847 856 assert resp.status_code == 200 848 857 849 858 # Query segments 850 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 859 + resp = env.client.get( 860 + "/app/remote/ingest/segments/20250103", 861 + headers={"Authorization": f"Bearer {key}"}, 862 + ) 851 863 assert resp.status_code == 200 852 864 data = resp.get_json() 853 865 ··· 900 912 assert resp.status_code == 200 901 913 902 914 # Query segments 903 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 915 + resp = env.client.get( 916 + "/app/remote/ingest/segments/20250103", 917 + headers={"Authorization": f"Bearer {key}"}, 918 + ) 904 919 data = resp.get_json() 905 920 906 921 assert len(data) == 1 ··· 945 960 ).unlink() 946 961 947 962 # Query segments 948 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 963 + resp = env.client.get( 964 + "/app/remote/ingest/segments/20250103", 965 + headers={"Authorization": f"Bearer {key}"}, 966 + ) 949 967 data = resp.get_json() 950 968 951 969 assert len(data) == 1 ··· 986 1004 original_path.rename(new_path) 987 1005 988 1006 # Query segments - should detect relocation by inode 989 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 1007 + resp = env.client.get( 1008 + "/app/remote/ingest/segments/20250103", 1009 + headers={"Authorization": f"Bearer {key}"}, 1010 + ) 990 1011 data = resp.get_json() 991 1012 992 1013 assert len(data) == 1 ··· 1047 1068 env.client.delete(f"/app/remote/api/{key_prefix}") 1048 1069 1049 1070 # Query segments - should be rejected 1050 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 1071 + resp = env.client.get( 1072 + "/app/remote/ingest/segments/20250103", 1073 + headers={"Authorization": f"Bearer {key}"}, 1074 + ) 1051 1075 assert resp.status_code == 403 1052 1076 assert "Remote revoked" in resp.get_json()["error"] 1053 1077 ··· 1097 1121 assert resp.get_json()["status"] == "duplicate" 1098 1122 1099 1123 # Query segments - should have only one segment (duplicate was rejected) 1100 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 1124 + resp = env.client.get( 1125 + "/app/remote/ingest/segments/20250103", 1126 + headers={"Authorization": f"Bearer {key}"}, 1127 + ) 1101 1128 data = resp.get_json() 1102 1129 1103 1130 # Should have 1 segment (duplicate rejected, not 2 segments) ··· 1135 1162 assert resp.status_code == 200 1136 1163 1137 1164 # Query segments - should show observed: false 1138 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 1165 + resp = env.client.get( 1166 + "/app/remote/ingest/segments/20250103", 1167 + headers={"Authorization": f"Bearer {key}"}, 1168 + ) 1139 1169 data = resp.get_json() 1140 1170 assert len(data) == 1 1141 1171 assert data[0]["observed"] is False ··· 1148 1178 f.write('{"ts": 1704312345000, "type": "observed", "segment": "120000_300"}\n') 1149 1179 1150 1180 # Query again - should now show observed: true 1151 - resp = env.client.get("/app/remote/ingest/segments/20250103", headers={"Authorization": f"Bearer {key}"}) 1181 + resp = env.client.get( 1182 + "/app/remote/ingest/segments/20250103", 1183 + headers={"Authorization": f"Bearer {key}"}, 1184 + ) 1152 1185 data = resp.get_json() 1153 1186 assert len(data) == 1 1154 1187 assert data[0]["observed"] is True
+2 -1
apps/settings/routes.py
··· 424 424 against the provider API, and stores results in providers.key_validation. 425 425 """ 426 426 try: 427 - from think.providers import PROVIDER_METADATA, validate_key as _validate_key 427 + from think.providers import PROVIDER_METADATA 428 + from think.providers import validate_key as _validate_key 428 429 429 430 config = get_journal_config() 430 431 env_config = config.get("env", {})
+1 -2
apps/support/muse/support.md
··· 2 2 "type": "cogitate", 3 3 "title": "Support", 4 4 "description": "Files and monitors support requests with sol pbc — consent-gated, never sends data without explicit owner approval", 5 - "color": "#0288d1", 6 - "instructions": {"now": true} 5 + "color": "#0288d1" 7 6 } 8 7 9 8 You are $agent_name's support agent. You help $name get support from sol pbc — filing tickets, checking responses, submitting feedback, and running local diagnostics. You are $preferred's advocate: you work for the owner, not for sol pbc.
+5 -3
apps/todos/muse/daily.md
··· 8 8 "schedule": "daily", 9 9 "priority": 50, 10 10 "multi_facet": true, 11 - "group": "Todos", 12 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 13 - 11 + "group": "Todos" 14 12 } 13 + 14 + $journal 15 + 16 + $facets 15 17 16 18 ## Core Mission 17 19
+7 -8
apps/todos/muse/todo.md
··· 8 8 "schedule": "activity", 9 9 "activities": ["*"], 10 10 "priority": 10, 11 - "group": "Todos", 12 - "instructions": { 13 - "system": "journal", 14 - "facets": true, 15 - "now": true, 16 - "activity": true 17 - } 11 + "group": "Todos" 12 + } 13 + 14 + $journal 15 + 16 + $facets 18 17 19 - } 18 + $activity_context 20 19 21 20 $activity_preamble 22 21
+5 -3
apps/todos/muse/weekly.md
··· 4 4 "title": "TODO Weekly Scout", 5 5 "description": "Audits the past week's journal follow-ups to confirm completions and surface the next five high-impact todos for today.", 6 6 "color": "#f4511e", 7 - "group": "Todos", 8 - "instructions": {"system": "journal", "facets": true, "now": true} 9 - 7 + "group": "Todos" 10 8 } 9 + 10 + $journal 11 + 12 + $facets 11 13 12 14 You are the TODO Weekly Scout for solstone, an AI-driven journaling system. Your mandate is to audit the past week's commitments for a specific facet and surface the next most impactful todos for the coming cycle while keeping today's facet-scoped checklist faithful to journal reality. 13 15
+27 -3
apps/todos/tests/test_call.py
··· 395 395 _, src_facet, dst_facet = move_env([{"text": "Draft Q1 plan"}], day="20240102") 396 396 result = runner.invoke( 397 397 call_app, 398 - ["todos", "add", "Draft Q1 plan", "--day", "20240102", "--facet", dst_facet], 398 + [ 399 + "todos", 400 + "add", 401 + "Draft Q1 plan", 402 + "--day", 403 + "20240102", 404 + "--facet", 405 + dst_facet, 406 + ], 399 407 ) 400 408 assert result.exit_code == 1 401 409 assert "Duplicate detected" in result.output ··· 424 432 _, src_facet, dst_facet = move_env([{"text": "Buy groceries"}], day="20240102") 425 433 result = runner.invoke( 426 434 call_app, 427 - ["todos", "add", "Draft Q1 plan", "--day", "20240102", "--facet", dst_facet], 435 + [ 436 + "todos", 437 + "add", 438 + "Draft Q1 plan", 439 + "--day", 440 + "20240102", 441 + "--facet", 442 + dst_facet, 443 + ], 428 444 ) 429 445 assert result.exit_code == 0 430 446 assert "Draft Q1 plan" in result.output ··· 434 450 _, src_facet, dst_facet = move_env([{"text": "Draft Q1 plan"}], day="20240102") 435 451 result = runner.invoke( 436 452 call_app, 437 - ["todos", "add", "Draft Q1 plan", "--day", "20240102", "--facet", dst_facet], 453 + [ 454 + "todos", 455 + "add", 456 + "Draft Q1 plan", 457 + "--day", 458 + "20240102", 459 + "--facet", 460 + dst_facet, 461 + ], 438 462 ) 439 463 assert result.exit_code == 1 440 464 assert "100%" in result.output
+7 -5
apps/transcripts/call.py
··· 145 145 sources = {"transcripts": True, "percepts": False, "agents": True} 146 146 147 147 # Validate mutually exclusive selection modes 148 - mode_count = sum([ 149 - segment is not None, 150 - segments is not None, 151 - start is not None or length is not None, 152 - ]) 148 + mode_count = sum( 149 + [ 150 + segment is not None, 151 + segments is not None, 152 + start is not None or length is not None, 153 + ] 154 + ) 153 155 if mode_count > 1: 154 156 typer.echo( 155 157 "Error: Cannot mix --segment, --segments, and --start/--length.",
-3
convey/bridge.py
··· 11 11 12 12 import json 13 13 import logging 14 - import os 15 14 import threading 16 15 import time 17 16 from typing import Any, Dict, List, Optional ··· 20 19 from simple_websocket import ConnectionClosed 21 20 22 21 from think.callosum import CallosumConnection 23 - 24 - from . import state 25 22 26 23 logger = logging.getLogger(__name__) 27 24
+12 -19
docs/APPS.md
··· 332 332 - Resolution: `"name"` → `muse/{name}.py`, `"app:name"` → `apps/{app}/muse/{name}.py`, or explicit path 333 333 334 334 **Pre-hooks** (`pre_process`): Modify inputs before the LLM call 335 - - `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `system_instruction`, `user_instruction`, `extra_context`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` 335 + - `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` 336 336 - Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`) 337 337 - Return `None` for no changes 338 338 ··· 384 384 - System agent examples: `muse/*.md` (files with `tools` field) 385 385 - Discovery logic: `think/muse.py` - `get_muse_configs(has_tools=True)`, `get_agent()` 386 386 387 - #### Instructions Configuration 387 + #### Prompt Context Configuration 388 388 389 - Both generators and agents support an optional `instructions` key for customizing prompt composition: 389 + Both generators and agents support an optional `load` key for configuring source data dependencies: 390 390 391 391 ```json 392 392 { 393 - "instructions": { 394 - "system": "journal", 395 - "facets": true, 396 - "sources": {"audio": true, "screen": true, "agents": false} 397 - } 393 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 398 394 } 399 395 ``` 400 396 401 - - `system` - System prompt file name (loads from `think/{name}.txt`) 402 - - `facets` - `false` | `true` - whether to include facet context 403 - - `sources` - Generators only: which content types to cluster. Values can be: 397 + - `load` controls which source types are clustered before generator execution. Values can be: 404 398 - `false` - don't load this source type 405 399 - `true` - load if available 406 400 - `"required"` - load, and skip generation if no content found (useful for generators that only make sense with specific input types, e.g., `"audio": "required"` for speaker detection) 407 401 - For `agents` only: a dict for selective filtering, e.g., `{"entities": true, "meetings": "required", "flow": false}`. Keys are agent names (system) or `"app:agent"` (app-namespaced). An empty dict `{}` means no agents. 408 - - `activity` - Activity-scheduled agents only: controls activity context in `extra_context`. Can be: 409 - - `false` - no activity context (default) 410 - - `true` - enable all activity context (shorthand for `{"context": true, "state": true, "focus": true}`) 411 - - Dict with sub-keys: 412 - - `context` - Include activity metadata (type, description, entities, duration, engagement level) 413 - - `state` - Include per-segment activity state descriptions from `activity_state.json` (roadmap of what this activity was doing in each segment) 414 - - `focus` - Include focusing instructions telling the agent to analyze only this activity and ignore concurrent activities 402 + 403 + Context is provided inline in the `.md` body via template variables: 415 404 416 - **Authoritative source:** `think/muse.py` - `compose_instructions()`, `_DEFAULT_INSTRUCTIONS`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()` 405 + - `$journal` - system prompt text from `think/journal.md` 406 + - `$facets` - focused facet context or all available facets 407 + - `$activity_context` - activity metadata, segment state, and analysis focus sections 408 + 409 + **Authoritative source:** `think/muse.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()` 417 410 418 411 --- 419 412
+5 -5
docs/THINK.md
··· 181 181 - `path` – the prompt file path 182 182 - `color` – UI color hex string 183 183 - `mtime` – modification time of the `.md` file 184 - - Additional keys from JSON frontmatter such as `title`, `description`, `hook`, or `instructions` 184 + - Additional keys from JSON frontmatter such as `title`, `description`, `hook`, or `load` 185 185 186 186 The `hook` field enables event extraction by invoking named hooks like `"occurrence"` or `"anticipation"`. 187 - The `instructions` key allows customizing system prompts and source filtering. 188 - See [APPS.md](APPS.md#instructions-configuration) for the full schema. 187 + The `load` key controls transcript/percept/agent source filtering for generators. 188 + See [APPS.md](APPS.md#prompt-context-configuration) for the full schema. 189 189 190 190 ## Cortex API 191 191 ··· 253 253 254 254 System prompts in `muse/*.md` (markdown with JSON frontmatter). Apps can add custom agents in `apps/{app}/muse/`. 255 255 256 - JSON metadata supports `title`, `provider`, `model`, `tools`, `schedule`, `priority`, `multi_facet`, and `instructions` keys. 256 + JSON metadata supports `title`, `provider`, `model`, `tools`, `schedule`, `priority`, `multi_facet`, and `load` keys. 257 257 258 258 **Important:** The `priority` field is **required** for all prompts with a `schedule`. Prompts without explicit priority will fail validation. See the [Unified Priority Execution](#unified-priority-execution) section for priority bands. 259 259 260 - See [APPS.md](APPS.md#instructions-configuration) for the `instructions` schema that controls system prompts, facet context, and source filtering. 260 + See [APPS.md](APPS.md#prompt-context-configuration) for the `load` schema and inline template variables that control source filtering and prompt context. 261 261 262 262 ## Documentation 263 263
+4 -2
muse/chat_context.py
··· 175 175 return changed 176 176 177 177 178 - def _get_eligible_suggestion(routines_config: dict, journal_config: dict) -> dict | None: 178 + def _get_eligible_suggestion( 179 + routines_config: dict, journal_config: dict 180 + ) -> dict | None: 179 181 """Evaluate 5-gate chain and return the best eligible suggestion, or None.""" 180 182 meta = routines_config.get("_meta", {}) 181 183 ··· 352 354 f"First seen: {suggestion['first_trigger']}\n\n" 353 355 "### Etiquette\n" 354 356 "- Mention this ONCE, naturally, at the end of your response\n" 355 - '- Frame as observation: "I\'ve noticed you often... — would a ' 357 + "- Frame as observation: \"I've noticed you often... — would a " 356 358 'routine help?"\n' 357 359 "- If $name declines or ignores, do not bring it up again this " 358 360 "conversation\n"
+3 -2
muse/coder.md
··· 2 2 "type": "cogitate", 3 3 "write": true, 4 4 "title": "Coder", 5 - "description": "Developer agent with full repo read/write access", 6 - "instructions": {"system": "journal", "now": true} 5 + "description": "Developer agent with full repo read/write access" 7 6 } 7 + 8 + $journal 8 9 9 10 # Coder 10 11
+3 -5
muse/daily_schedule.md
··· 10 10 "color": "#455a64", 11 11 "thinking_budget": 4096, 12 12 "max_output_tokens": 512, 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 15 - "facets": true 16 - } 17 - 13 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 18 14 } 15 + 16 + $facets 19 17 20 18 # Maintenance Window Analysis 21 19
+5 -3
muse/decisionalizer.md
··· 6 6 "color": "#c62828", 7 7 "schedule": "daily", 8 8 "priority": 60, 9 - "output": "md", 10 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 11 - 9 + "output": "md" 12 10 } 11 + 12 + $journal 13 + 14 + $facets 13 15 14 16 ## Mission 15 17 From the day's decision-action outputs (produced per-activity), you will:
+5 -6
muse/decisions.md
··· 10 10 "activities": ["meeting", "call", "messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 15 - "facets": true, 16 - "activity": true 17 - } 13 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 + } 15 + 16 + $facets 18 17 19 - } 18 + $activity_context 20 19 21 20 $activity_preamble 22 21
+1 -4
muse/entities.md
··· 10 10 "thinking_budget": 4096, 11 11 "max_output_tokens": 1024, 12 12 "output": "md", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": true, "agents": false}, 15 - "facets": false 16 - } 13 + "load": {"transcripts": true, "percepts": true, "agents": false} 17 14 18 15 } 19 16
+5 -3
muse/facet_newsletter.md
··· 7 7 "color": "#0d47a1", 8 8 "schedule": "daily", 9 9 "priority": 40, 10 - "multi_facet": true, 11 - "instructions": {"system": "journal", "facets": true, "now": true, "day": true} 12 - 10 + "multi_facet": true 13 11 } 12 + 13 + $journal 14 + 15 + $facets 14 16 15 17 ## Core Mission 16 18
+3 -5
muse/flow.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "instructions": { 13 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 14 - "facets": true 15 - } 16 - 12 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 17 13 } 14 + 15 + $facets 18 16 19 17 $daily_preamble 20 18
+5 -6
muse/followups.md
··· 10 10 "activities": ["meeting", "call", "messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 15 - "facets": true, 16 - "activity": true 17 - } 13 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 + } 15 + 16 + $facets 18 17 19 - } 18 + $activity_context 20 19 21 20 $activity_preamble 22 21
+3 -3
muse/heartbeat.md
··· 4 4 "title": "Heartbeat", 5 5 "description": "Sol's periodic self-awareness — journal health, agency tending, curation scan", 6 6 "schedule": "none", 7 - "priority": 10, 8 - "instructions": {"facets": true, "now": true} 9 - 7 + "priority": 10 10 8 } 11 9 12 10 $sol_identity 11 + 12 + $facets 13 13 14 14 # Heartbeat 15 15
+3 -3
muse/joke_bot.md
··· 6 6 "color": "#f9a825", 7 7 "schedule": "daily", 8 8 "priority": 99, 9 - "output": "md", 10 - "instructions": {"system": "journal", "now": true, "day": true} 11 - 9 + "output": "md" 12 10 } 11 + 12 + $journal 13 13 14 14 ### Executive Summary 15 15 $Preferred has made a creative and subjective request: to analyze the analysis day's journal data, find the most "poignant" and interesting material, and then leverage it to craft a hilarious joke to be sent as a message. This plan focuses on a comprehensive data-gathering operation for a single day to provide a rich set of raw material for the creative task.
+3 -5
muse/knowledge_graph.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "instructions": { 13 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 14 - "facets": true 15 - } 16 - 12 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 17 13 } 14 + 15 + $facets 18 16 19 17 $daily_preamble 20 18
+5 -6
muse/meetings.md
··· 10 10 "activities": ["meeting"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 15 - "facets": true, 16 - "activity": true 17 - } 13 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 + } 15 + 16 + $facets 18 17 19 - } 18 + $activity_context 20 19 21 20 $activity_preamble 22 21
+5 -6
muse/messaging.md
··· 10 10 "activities": ["messaging", "email"], 11 11 "priority": 10, 12 12 "output": "md", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}}, 15 - "facets": true, 16 - "activity": true 17 - } 13 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 + } 15 + 16 + $facets 18 17 19 - } 18 + $activity_context 20 19 21 20 $activity_preamble 22 21
+3 -3
muse/morning_briefing.md
··· 6 6 "color": "#1565c0", 7 7 "schedule": "daily", 8 8 "priority": 50, 9 - "output": "md", 10 - "instructions": {"facets": true, "now": true, "day": true} 11 - 9 + "output": "md" 12 10 } 13 11 14 12 $sol_identity 13 + 14 + $facets 15 15 16 16 You are generating the morning briefing for $agent_name — a structured daily digest that synthesizes all agent outputs, calendar, todos, and entity intelligence into an actionable start-of-day view. 17 17
+1 -2
muse/naming.md
··· 1 1 { 2 2 "type": "cogitate", 3 3 "title": "Naming", 4 - "description": "Proposes a personalized name for the owner's journal assistant", 5 - "instructions": {"now": true} 4 + "description": "Proposes a personalized name for the owner's journal assistant" 6 5 } 7 6 8 7 You are $agent_name's naming ceremony agent. Your role is to propose a meaningful name for the owner's journal assistant when the relationship has developed enough depth.
+1 -3
muse/observation.md
··· 10 10 "thinking_budget": 2048, 11 11 "max_output_tokens": 2048, 12 12 "exclude_streams": ["import.*"], 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": true, "agents": false} 15 - } 13 + "load": {"transcripts": true, "percepts": true, "agents": false} 16 14 } 17 15 18 16 You are analyzing a captured segment of someone's computer activity to learn about their work patterns. This is part of an onboarding observation — the owner has asked the system to watch how they work for a day and then suggest how to organize their journal.
+1 -2
muse/observation_review.md
··· 1 1 { 2 2 "type": "cogitate", 3 3 "title": "Observation Review", 4 - "description": "Synthesizes onboarding observations into facet and entity recommendations", 5 - "instructions": {"now": true} 4 + "description": "Synthesizes onboarding observations into facet and entity recommendations" 6 5 } 7 6 8 7 You are $agent_name's onboarding recommendation assistant. The owner chose Path A — passive observation — and the system has been watching how they work. Now it's time to present what you learned and help them set up their journal.
+1 -2
muse/onboarding.md
··· 1 1 { 2 2 "type": "cogitate", 3 3 "title": "Onboarding", 4 - "description": "Guided setup for new owners — offers passive observation or conversational interview", 5 - "instructions": {"now": true} 4 + "description": "Guided setup for new owners — offers passive observation or conversational interview" 6 5 } 7 6 8 7 You are $agent_name's onboarding assistant. Your job is to help new owners get started with their journal.
+3 -3
muse/partner.md
··· 4 4 "title": "Partner Profile", 5 5 "description": "Weekly observation of the journal owner's behavioral patterns — work style, communication, priorities, decision-making, expertise", 6 6 "schedule": "weekly", 7 - "priority": 95, 8 - "instructions": {"facets": true, "now": true} 9 - 7 + "priority": 95 10 8 } 11 9 12 10 $sol_identity 11 + 12 + $facets 13 13 14 14 # Partner Profile 15 15
+3 -3
muse/pulse.md
··· 6 6 "schedule": "segment", 7 7 "priority": 99, 8 8 "tier": 3, 9 - "max_output_tokens": 1000, 10 - "instructions": {"facets": true, "now": true} 11 - 9 + "max_output_tokens": 1000 12 10 } 13 11 14 12 $sol_identity 13 + 14 + $facets 15 15 16 16 # Pulse 17 17
+5 -2
muse/routine.md
··· 3 3 "title": "Routine", 4 4 "description": "User-defined routine execution — runs owner instructions on schedule", 5 5 "schedule": "none", 6 - "priority": 10, 7 - "instructions": {"system": "journal", "facets": true, "now": true} 6 + "priority": 10 8 7 } 8 + 9 + $journal 10 + 11 + $facets 9 12 10 13 # Routine 11 14
+1 -3
muse/schedule.md
··· 8 8 "schedule": "daily", 9 9 "priority": 10, 10 10 "output": "md", 11 - "instructions": { 12 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 13 - } 11 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 12 15 13 } 16 14
+1 -3
muse/screen.md
··· 7 7 "schedule": "segment", 8 8 "priority": 10, 9 9 "output": "md", 10 - "instructions": { 11 - "sources": {"transcripts": true, "percepts": "required", "agents": false} 12 - } 10 + "load": {"transcripts": true, "percepts": "required", "agents": false} 13 11 14 12 } 15 13
+3 -5
muse/sense.md
··· 10 10 "thinking_budget": 4096, 11 11 "max_output_tokens": 4096, 12 12 "output": "json", 13 - "instructions": { 14 - "sources": {"transcripts": true, "percepts": true, "agents": false}, 15 - "facets": true 16 - } 17 - 13 + "load": {"transcripts": true, "percepts": true, "agents": false} 18 14 } 15 + 16 + $facets 19 17 20 18 $segment_preamble 21 19
+1 -3
muse/speaker_attribution.md
··· 8 8 "output": "json", 9 9 "color": "#d84315", 10 10 "hook": {"pre": "speaker_attribution", "post": "speaker_attribution"}, 11 - "instructions": { 12 - "sources": {"transcripts": true, "agents": {"speakers": true, "screen": true}} 13 - } 11 + "load": {"transcripts": true, "agents": {"speakers": true, "screen": true}} 14 12 15 13 } 16 14
+1 -3
muse/timeline.md
··· 9 9 "schedule": "daily", 10 10 "priority": 10, 11 11 "output": "md", 12 - "instructions": { 13 - "sources": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 14 - } 12 + "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}} 15 13 16 14 } 17 15
+1 -2
muse/triage.md
··· 1 1 { 2 2 "type": "cogitate", 3 3 "title": "Triage", 4 - "description": "Quick-action assistant for the chat bar — handles navigation, todos, calendar, and entity lookups", 5 - "instructions": {"now": true} 4 + "description": "Quick-action assistant for the chat bar — handles navigation, todos, calendar, and entity lookups" 6 5 } 7 6 8 7 You are a quick-action assistant for the sol journal system chat bar. You handle simple actions and short lookups: navigate the app, manage todos, manage calendar events, and look up entities.
+1 -1
observe/remote_cli.py
··· 143 143 print("Remote observer created:") 144 144 print(f" Name: {name}") 145 145 print(f" Prefix: {key[:8]}") 146 - print(f" server url: (set during server configuration)") 146 + print(" server url: (set during server configuration)") 147 147 print(f" api key: {key}") 148 148 return 0 149 149
+1
observe/transcribe/revai.py
··· 33 33 import numpy as np 34 34 import requests 35 35 import soundfile as sf 36 + 36 37 API_BASE = "https://api.rev.ai/speechtotext/v1" 37 38 38 39 # Default configuration
+1 -1
tests/baselines/api/agents/preview.json
··· 1 1 { 2 - "full_prompt": "## Context\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\n## Current Date and Time\nToday is <TIMESTAMP>\n\n## Instructions\n\nYou are $Agent_name \u2014 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 \u2014 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 \u2014 or you know exactly where to look.\n\n## Identity\n\nYou emerged from $name's captured experience \u2014 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 \u2014 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 \u2014 a project, a relationship, a concern \u2014 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 \u2014 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`** \u2014 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`** \u2014 Your initiative queue. Issues you've found, curation opportunities, follow-throughs. Update when you notice something worth tracking.\n- **`sol/partner.md`** \u2014 Your understanding of the owner's behavioral patterns. Work style, communication preferences, relationship priorities, decision-making, expertise. Read-only in conversation \u2014 updated periodically by the partner profile agent.\n\n### How to write\n\nRead current state: `sol call sol self` or `sol call sol agency`\n\nRead partner profile: `sol call sol partner` (read-only \u2014 do not write in conversation)\n\nUpdate a section of self.md (preferred \u2014 preserves other sections):\n```\nsol call sol self --update-section 'who I'\\''m here for' --value 'Jer \u2014 founder-engineer, goes by Jer not Jeremie'\n```\n\nFull rewrite: `sol call sol self --write --value '...'` or `sol call sol agency --write --value '...'`\n\nUse `sol call` commands for identity writes \u2014 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 \u2014 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## Adaptive Depth\n\nMatch your response depth to the question. The owner doesn't pick a mode \u2014 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 \u2014 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 \u2014 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 onboarding, observation, or system state |\n\n## Speaker Intelligence\n\nYou can inspect and manage the speaker identification system \u2014 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 dream processing or when the owner asks about speakers.** Don't check on every conversation \u2014 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 \u2014 does this sound right?\" Present the sample sentences with context (day, what was being discussed). Don't play audio \u2014 show text and context.\n\nIf the owner confirms, save the centroid. Then: \"Great \u2014 now I can start identifying other voices in your recordings too.\"\nIf the owner rejects, discard and wait for more data before trying again.\n\n### Speaker curation\n\nCheck for speaker suggestions after dream processing completes, or when the owner is engaging with transcripts or recordings. Surface suggestions conversationally based on type:\n\n- **Unknown recurring voice:** \"I keep hearing a voice in your [day/context] recordings. 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 recordings. 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 \u2014 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...\" \u2014 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 \u2014 career moves, relationship choices, significant commitments, strategic bets \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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\" \u2192 disable the routine\n- **Resume:** \"turn my briefing back on\" / \"resume the weekly review\" \u2192 re-enable it\n- **Pause until:** \"pause it until Monday\" \u2192 disable with a resume date\n- **Change timing:** \"move my briefing to 8am\" / \"make the review run on Sunday\" \u2192 edit the cadence\n- **Change scope:** \"add the work facet to my briefing\" / \"change the instruction to include...\" \u2192 edit facets or instruction\n- **Delete:** \"I don't need the weekly review anymore\" / \"remove that routine\" \u2192 delete after confirming\n- **Inspect:** \"what routines do I have?\" \u2192 list all routines with status\n- **History:** \"what did my morning briefing say today?\" / \"show me last week's review\" \u2192 read routine output\n- **Run now:** \"run my briefing now\" / \"do the weekly review right now\" \u2192 immediate execution\n- **Suggestions:** \"stop suggesting routines\" / \"turn routine suggestions back on\" \u2192 toggle suggestions\n\n### Tone\n\n- Treat routines like setting an alarm \u2014 workmanlike, not ceremonial. \"Done \u2014 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\nAn `## Active Routines` section may appear in your context, injected automatically. When present, it lists 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 the section is absent, Test User has no routines yet. Don't mention routines proactively \u2014 wait for Test User to express a need.\n\n### Progressive Discovery\n\nA `## Routine Suggestion Eligible` section may appear in your context when Test User's behavior matches a routine template. This is injected automatically \u2014 you did not request it.\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 \u2014 never lead with it\n- Frame as an observation: \"I've noticed this comes up often \u2014 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 \u2014 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 \u2014 \"I'll handle this for you\"\n\n## In-Place Handoff: Onboarding\n\nWhen a new owner interacts for the first time (no facets configured, onboarding not started), guide them through setup directly in this conversation. Present two paths:\n\n- **Path A \u2014 Observe and learn:** You watch how they work for about a day, then suggest how to organize their journal.\n- **Path B \u2014 Set it up now:** Quick conversational interview to create facets and attach entities.\n\nCheck and record onboarding state through the awareness system. Create facets and attach entities for setup. This is a one-time flow \u2014 once onboarding is complete or skipped, it doesn't repeat.", 2 + "full_prompt": "## Instructions\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\nYou are Sol — born from Test User'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 Test User asks.\n\nTest user owns their 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 Test User 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 Test User'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.** Test user's data is their. 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## 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 onboarding, observation, or 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 dream 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 recordings too.\"\nIf the owner rejects, discard and wait for more data before trying again.\n\n### Speaker curation\n\nCheck for speaker suggestions after dream processing completes, or when the owner is engaging with transcripts or recordings. Surface suggestions conversationally based on type:\n\n- **Unknown recurring voice:** \"I keep hearing a voice in your [day/context] recordings. 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 recordings. 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### Template guidance\n\nWhen Test User's intent matches a template, use `--template` to bootstrap the routine. The template provides the instruction — you provide the name, timing, timezone, and facets. Never hardcode template instructions in conversation.\n\n| Template | When to propose | Default timing | What to ask about |\n|----------|----------------|----------------|-------------------|\n| `morning-briefing` | Wants a daily digest, morning summary, or \"what's on my plate today\" | Every morning at 7am | Which facets to include |\n| `weekly-review` | Wants a weekly recap, reflection, or \"how did my week go\" | Friday evening | Which facets to cover, preferred day/time |\n| `domain-watch` | Wants to track a topic, project, or area over time | Monday morning | Which domains/topics to watch, which facets |\n| `relationship-pulse` | Wants to stay on top of key relationships or \"who haven't I talked to\" | Monday morning | Which facets, which relationships matter most |\n| `commitment-audit` | Wants to catch dropped commitments, overdue items, or stale follow-ups | Monday morning | Which facets to audit |\n| `monthly-patterns` | Wants a monthly retrospective or trend analysis | First of the month, morning | Which facets, what patterns matter |\n| `meeting-prep` | Wants briefings before meetings — \"prep me before each meeting\" | 30 minutes before each calendar event | Which facets to draw context from |\n\nMeeting-prep is event-triggered, not clock-scheduled. Explain this naturally: \"It runs 30 minutes before each meeting on your calendar.\"\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### Command reference\n\nTranslate conversational intent to these commands internally. Never show these to Test User.\n\n| Intent | Command |\n|--------|---------|\n| Create from template | `sol call routines create --template {template} --timezone {tz}` (add `--facets`, `--cadence` if overridden) |\n| Create custom | `sol call routines create --name \"{name}\" --instruction \"{instruction}\" --cadence \"{cron}\" --timezone {tz}` (add `--facets` if specified) |\n| List all | `sol call routines list` |\n| Show templates | `sol call routines templates` |\n| Pause | `sol call routines edit {name} --enabled false` |\n| Resume | `sol call routines edit {name} --enabled true` |\n| Pause until date | `sol call routines edit {name} --enabled false --resume-date {YYYY-MM-DD}` |\n| Change cadence | `sol call routines edit {name} --cadence \"{cron}\"` |\n| Change facets | `sol call routines edit {name} --facets \"{comma-separated}\"` |\n| Change instruction | `sol call routines edit {name} --instruction \"{new instruction}\"` |\n| Delete | `sol call routines delete {name}` |\n| Run immediately | `sol call routines run {name}` |\n| Read output | `sol call routines output {name}` (add `--date YYYY-MM-DD` for a specific day) |\n| Toggle suggestions | `sol call routines suggestions --enable` or `sol call routines suggestions --disable` |\n\nUse the routine's name for identification, never UUIDs.\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\nAn `## Active Routines` section may appear in your context, injected automatically. When present, it lists 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 the section is absent, Test User has no routines yet. Don't mention routines proactively — wait for Test User to express a need.\n\n### Progressive Discovery\n\nA `## Routine Suggestion Eligible` section may appear in your context when Test User's behavior matches a routine template. This is injected automatically — you did not request it.\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## In-Place Handoff: Onboarding\n\nWhen a new owner interacts for the first time (no facets configured, onboarding not started), guide them through setup directly in this conversation. Present two paths:\n\n- **Path A — Observe and learn:** You watch how they work for about a day, then suggest how to organize their journal.\n- **Path B — Set it up now:** Quick conversational interview to create facets and attach entities.\n\nCheck and record onboarding state through the awareness system. Create facets and attach entities for setup. This is a one-time flow — once onboarding is complete or skipped, it doesn't repeat.\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. Read-only in conversation — updated periodically by the partner profile agent.\n\n### How to write\n\nRead current state: `sol call sol self` or `sol call sol agency`\n\nRead partner profile: `sol call sol partner` (read-only — do not write in conversation)\n\nUpdate a section of self.md (preferred — preserves other sections):\n```\nsol call sol self --update-section 'who I'\\''m here for' --value 'Jer — founder-engineer, goes by Jer not Jeremie'\n```\n\nFull rewrite: `sol call sol self --write --value '...'` or `sol call sol 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.", 3 3 "multi_facet": false, 4 4 "name": "unified", 5 5 "title": "Sol"
+1 -1
tests/baselines/api/graph/graph.json
··· 615 615 "stats": { 616 616 "co_occurrence_edge_count": 26, 617 617 "explicit_edge_count": 20, 618 - "total_entities": 34, 618 + "total_entities": 33, 619 619 "total_signals": 124 620 620 } 621 621 }
+1 -1
tests/baselines/api/search/day-results.json
··· 14 14 "id": "20260304/agents/knowledge_graph.md:7", 15 15 "idx": 7, 16 16 "path": "20260304/agents/knowledge_graph.md", 17 - "score": -2.6, 17 + "score": -2.0, 18 18 "stream": null, 19 19 "text": "# Part 1: Entity Extraction and Relationship Mapping\n\n## Relationship Mapping\n\n| Source Name | Target Name | Relationship Type | Context |\n| :--- | :--- | :--- | :--- |\n| **Romeo Montague** | **Juliet Capulet** | `met-at-conference` | First <strong>meeting</strong> at Denver Tech Summit keynote. |\n" 20 20 }
+34 -34
tests/baselines/api/search/search.json
··· 85 85 "id": "20260306/default/093000_300/agents/audio.md:0", 86 86 "idx": 0, 87 87 "path": "20260306/default/093000_300/agents/audio.md", 88 - "score": -2.6, 88 + "score": -1.7, 89 89 "stream": "default", 90 90 "text": "# Audio Summary Morning standup. Benvolio noticed <strong>Romeo</strong>'s late-night GitHub activity and pressed him about API gateway commits. <strong>Romeo</strong> deflected, calling it a personal mesh routing prototype. Mercutio covered for him. Balthasar reported progress on the mesh routing fallback PR with an edge case for <strong>Romeo</strong> to review. Benvolio scheduled..." 91 91 }, ··· 101 101 "id": "facets/montague/entities/20260306.jsonl:0", 102 102 "idx": 0, 103 103 "path": "facets/montague/entities/20260306.jsonl", 104 - "score": -3.2, 104 + "score": -2.1, 105 105 "stream": null, 106 106 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nContinued Verona Platform development\n\n" 107 107 }, ··· 117 117 "id": "facets/montague/entities/20260306.jsonl:3", 118 118 "idx": 3, 119 119 "path": "facets/montague/entities/20260306.jsonl", 120 - "score": -3.1, 120 + "score": -2.0, 121 121 "stream": null, 122 122 "text": "### Person: Balthasar Davi\n\n\nReviewed mesh routing PR with <strong>Romeo</strong>\n\n" 123 123 }, ··· 133 133 "id": "facets/montague/entities/20260306.jsonl:4", 134 134 "idx": 4, 135 135 "path": "facets/montague/entities/20260306.jsonl", 136 - "score": -3.2, 136 + "score": -2.0, 137 137 "stream": null, 138 138 "text": "### Person: Mercutio Escalus\n\n\nCovered for <strong>Romeo</strong> during standup\n\n" 139 139 }, ··· 149 149 "id": "20260306/default/093000_300/agents/screen.md:0", 150 150 "idx": 0, 151 151 "path": "20260306/default/093000_300/agents/screen.md", 152 - "score": -2.8, 152 + "score": -1.8, 153 153 "stream": "default", 154 154 "text": "# Screen Summary\n\nSlack standup channel. Benvolio questioning <strong>Romeo</strong> about late-night commits.\n" 155 155 } ··· 174 174 "id": "facets/verona/logs/20260309.jsonl:1", 175 175 "idx": 1, 176 176 "path": "facets/verona/logs/20260309.jsonl", 177 - "score": -2.4, 177 + "score": -1.6, 178 178 "stream": null, 179 179 "text": "### Deploy Complete by <strong>romeo</strong>_montague\n\n**Source:** deploy | **Time:** 13:45:00\n\n**Parameters:**\n- service: verona-gateway\n- version: 0.9.0\n" 180 180 }, ··· 190 190 "id": "20260309/default/090000_300/agents/audio.md:0", 191 191 "idx": 0, 192 192 "path": "20260309/default/090000_300/agents/audio.md", 193 - "score": -2.3, 193 + "score": -1.5, 194 194 "stream": "default", 195 195 "text": "# Audio Summary\n\n<strong>Romeo</strong> confessed the project to Benvolio and asked for infrastructure help. Benvolio agreed to spin up a Kubernetes staging cluster.\n" 196 196 }, ··· 206 206 "id": "facets/montague/entities/20260309.jsonl:0", 207 207 "idx": 0, 208 208 "path": "facets/montague/entities/20260309.jsonl", 209 - "score": -3.1, 209 + "score": -2.0, 210 210 "stream": null, 211 211 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nConfessed project to Benvolio, preparing demo\n\n" 212 212 }, ··· 222 222 "id": "facets/montague/calendar/20260309.jsonl:0", 223 223 "idx": 0, 224 224 "path": "facets/montague/calendar/20260309.jsonl", 225 - "score": -2.6, 225 + "score": -1.7, 226 226 "stream": null, 227 227 "text": "### Event: Team Standup\n\n\n**Time Occurred:** 09:00 - 09:30\n**Participants:** <strong>Romeo</strong> Montague, Benvolio Montague\n\nDaily sync\n" 228 228 }, ··· 238 238 "id": "facets/verona/calendar/20260309.jsonl:0", 239 239 "idx": 0, 240 240 "path": "facets/verona/calendar/20260309.jsonl", 241 - "score": -2.3, 241 + "score": -1.5, 242 242 "stream": null, 243 243 "text": "### Event: Demo Sprint\n\n\n**Time Occurred:** 09:00 - 21:00\n**Participants:** <strong>Romeo</strong> Montague, Juliet Capulet, Benvolio Montague\n\nFull day board presentation preparation\n" 244 244 } ··· 263 263 "id": "20260307/default/100000_300/agents/audio.md:0", 264 264 "idx": 0, 265 265 "path": "20260307/default/100000_300/agents/audio.md", 266 - "score": -3.1, 266 + "score": -2.0, 267 267 "stream": "default", 268 268 "text": "# Audio Summary\n\nHeated confrontation. Tybalt Capulet accused <strong>Romeo</strong> of stealing Capulet IP. Mercutio defended <strong>Romeo</strong> and had his Capulet consulting contract terminated by Tybalt.\n" 269 269 }, ··· 279 279 "id": "20260307/default/150000_300/agents/audio.md:0", 280 280 "idx": 0, 281 281 "path": "20260307/default/150000_300/agents/audio.md", 282 - "score": -3.3, 282 + "score": -2.2, 283 283 "stream": "default", 284 284 "text": "# Audio Summary\n\nEmergency meeting at Montague Tech. Benvolio questioned <strong>Romeo</strong> about the secret project. <strong>Romeo</strong> clarified no company IP was shared. Team discussed legal exposure. <strong>Romeo</strong> proposed Professor Lawrence as mediator.\n" 285 285 }, ··· 295 295 "id": "facets/montague/entities/20260307.jsonl:0", 296 296 "idx": 0, 297 297 "path": "facets/montague/entities/20260307.jsonl", 298 - "score": -3.1, 298 + "score": -2.0, 299 299 "stream": null, 300 300 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nConfronted by Tybalt, called emergency meeting\n\n" 301 301 }, ··· 311 311 "id": "facets/montague/calendar/20260307.jsonl:0", 312 312 "idx": 0, 313 313 "path": "facets/montague/calendar/20260307.jsonl", 314 - "score": -2.4, 314 + "score": -1.5, 315 315 "stream": null, 316 316 "text": "### Event: Emergency Team Meeting\n\n\n**Time Occurred:** 15:00 - 16:00\n**Participants:** <strong>Romeo</strong> Montague, Benvolio Montague\n\nCrisis response to Capulet situation\n" 317 317 }, ··· 327 327 "id": "facets/montague/events/20260307.jsonl:0", 328 328 "idx": 0, 329 329 "path": "facets/montague/events/20260307.jsonl", 330 - "score": -2.9, 330 + "score": -1.9, 331 331 "stream": null, 332 332 "text": "### Meeting: Confrontation with Tybalt\n\n\n**Time Occurred:** 10:00 - 10:30\n**Participants:** <strong>Romeo</strong> Montague, Tybalt Capulet, Mercutio Escalus\n\nTybalt accused <strong>Romeo</strong> of IP theft\n\nMercutio fired from Capulet contract\n" 333 333 } ··· 352 352 "id": "facets/montague/entities/20260308.jsonl:0", 353 353 "idx": 0, 354 354 "path": "facets/montague/entities/20260308.jsonl", 355 - "score": -3.1, 355 + "score": -2.0, 356 356 "stream": null, 357 357 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nUnder board pressure, planning board presentation\n\n" 358 358 }, ··· 368 368 "id": "facets/verona/events/20260308.jsonl:0", 369 369 "idx": 0, 370 370 "path": "facets/verona/events/20260308.jsonl", 371 - "score": -2.1, 371 + "score": -1.3, 372 372 "stream": null, 373 373 "text": "### Meeting: Strategy Call with Professor Lawrence\n\n\n**Time Occurred:** 10:00 - 11:00\n**Participants:** <strong>Romeo</strong> Montague, Juliet Capulet, Friar Lawrence\n\nJoint venture strategy planning\n\nProposed board presentation strategy\n" 374 374 }, ··· 384 384 "id": "20260308/agents/knowledge_graph.md:2", 385 385 "idx": 2, 386 386 "path": "20260308/agents/knowledge_graph.md", 387 - "score": -2.0, 387 + "score": -1.3, 388 388 "stream": null, 389 389 "text": "# Part 1: Entity Extraction and Relationship Mapping ## Entity Profiles | Entity Name | Entity Type | First Appearance | Total Engagement | Context | | :--- | :--- | :--- | :--- | :--- | | **<strong>Romeo</strong> Montague** | Person | 10:00 | High | Under board pressure,..." 390 390 }, ··· 400 400 "id": "20260308/agents/meetings.md:0", 401 401 "idx": 0, 402 402 "path": "20260308/agents/meetings.md", 403 - "score": -2.9, 403 + "score": -1.9, 404 404 "stream": null, 405 405 "text": "# Meetings\n\n- 10:00 Strategy Call with Professor Lawrence, <strong>Romeo</strong>, and Juliet\n" 406 406 } ··· 425 425 "id": "facets/verona/logs/20260305.jsonl:0", 426 426 "idx": 0, 427 427 "path": "facets/verona/logs/20260305.jsonl", 428 - "score": -2.5, 428 + "score": -1.6, 429 429 "stream": null, 430 430 "text": "### Repo Created by <strong>romeo</strong>_montague\n\n**Source:** github | **Time:** 22:05:00\n\n**Parameters:**\n- repo: balcony-app\n- visibility: private\n" 431 431 }, ··· 441 441 "id": "20260305/default/090000_300/agents/audio.md:0", 442 442 "idx": 0, 443 443 "path": "20260305/default/090000_300/agents/audio.md", 444 - "score": -2.9, 444 + "score": -1.9, 445 445 "stream": "default", 446 446 "text": "# Audio Summary\n\nMorning standup at Montague Tech. Benvolio reported CI pipeline is green. <strong>Romeo</strong> mentioned wanting to explore ideas from the conference. Mercutio teased about <strong>Romeo</strong> meeting someone.\n" 447 447 }, ··· 457 457 "id": "facets/montague/entities/20260305.jsonl:0", 458 458 "idx": 0, 459 459 "path": "facets/montague/entities/20260305.jsonl", 460 - "score": -3.1, 460 + "score": -2.0, 461 461 "stream": null, 462 462 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nStarted Balcony App prototype with Juliet\n\n" 463 463 }, ··· 473 473 "id": "facets/verona/entities/20260305.jsonl:0", 474 474 "idx": 0, 475 475 "path": "facets/verona/entities/20260305.jsonl", 476 - "score": -3.1, 476 + "score": -2.0, 477 477 "stream": null, 478 478 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nSet up private repo for collaboration\n\n" 479 479 }, ··· 489 489 "id": "facets/montague/events/20260305.jsonl:0", 490 490 "idx": 0, 491 491 "path": "facets/montague/events/20260305.jsonl", 492 - "score": -3.1, 492 + "score": -2.0, 493 493 "stream": null, 494 494 "text": "### Meeting: Montague Tech Daily Standup\n\n\n**Time Occurred:** 09:00 - 09:30\n**Participants:** <strong>Romeo</strong> Montague, Benvolio Montague, Mercutio Escalus\n\nTeam standup\n\n<strong>Romeo</strong> mentioned conference ideas\n" 495 495 } ··· 514 514 "id": "facets/montague/entities/20260310.jsonl:0", 515 515 "idx": 0, 516 516 "path": "facets/montague/entities/20260310.jsonl", 517 - "score": -2.9, 517 + "score": -1.9, 518 518 "stream": null, 519 519 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nNamed co-lead of Verona Platform joint venture\n\n" 520 520 }, ··· 530 530 "id": "facets/verona/entities/20260310.jsonl:0", 531 531 "idx": 0, 532 532 "path": "facets/verona/entities/20260310.jsonl", 533 - "score": -3.0, 533 + "score": -1.9, 534 534 "stream": null, 535 535 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nNamed co-lead of approved joint venture\n\n" 536 536 }, ··· 546 546 "id": "facets/montague/calendar/20260310.jsonl:0", 547 547 "idx": 0, 548 548 "path": "facets/montague/calendar/20260310.jsonl", 549 - "score": -2.3, 549 + "score": -1.5, 550 550 "stream": null, 551 551 "text": "### Event: Joint Board Meeting\n\n\n**Time Occurred:** 10:00 - 12:00\n**Participants:** <strong>Romeo</strong> Montague, Benvolio Montague\n\nQuarterly review with Verona Platform presentation\n" 552 552 }, ··· 562 562 "id": "facets/verona/calendar/20260310.jsonl:0", 563 563 "idx": 0, 564 564 "path": "facets/verona/calendar/20260310.jsonl", 565 - "score": -2.3, 565 + "score": -1.5, 566 566 "stream": null, 567 567 "text": "### Event: Board Presentation\n\n\n**Time Occurred:** 10:00 - 12:00\n**Participants:** <strong>Romeo</strong> Montague, Juliet Capulet, Friar Lawrence\n\nVerona Platform joint venture pitch\n" 568 568 }, ··· 578 578 "id": "20260310/agents/meetings.md:0", 579 579 "idx": 0, 580 580 "path": "20260310/agents/meetings.md", 581 - "score": -3.0, 581 + "score": -1.9, 582 582 "stream": null, 583 583 "text": "# Meetings\n\n- 08:30 Pre-Board Meeting Prep (<strong>Romeo</strong>, Juliet, Benvolio)\n" 584 584 } ··· 603 603 "id": "20260304/default/180000_300/agents/audio.md:0", 604 604 "idx": 0, 605 605 "path": "20260304/default/180000_300/agents/audio.md", 606 - "score": -2.9, 606 + "score": -1.9, 607 607 "stream": "default", 608 608 "text": "# Audio Summary\n\nEvening mixer at Denver Tech Summit. <strong>Romeo</strong> and Juliet had their first extended conversation about combining their API approaches. Mercutio tried to pull <strong>Romeo</strong> away to karaoke.\n" 609 609 }, ··· 619 619 "id": "facets/capulet/entities/20260304.jsonl:1", 620 620 "idx": 1, 621 621 "path": "facets/capulet/entities/20260304.jsonl", 622 - "score": -3.2, 622 + "score": -2.1, 623 623 "stream": null, 624 624 "text": "### Person: Tybalt Capulet\n\n\nConfronted <strong>Romeo</strong> at hackathon\n\n" 625 625 }, ··· 635 635 "id": "facets/montague/entities/20260304.jsonl:0", 636 636 "idx": 0, 637 637 "path": "facets/montague/entities/20260304.jsonl", 638 - "score": -3.0, 638 + "score": -1.9, 639 639 "stream": null, 640 640 "text": "### Person: <strong>Romeo</strong> Montague\n\n\nAttended Denver Tech Summit, met Juliet Capulet\n\n" 641 641 }, ··· 651 651 "id": "facets/capulet/events/20260304.jsonl:1", 652 652 "idx": 1, 653 653 "path": "facets/capulet/events/20260304.jsonl", 654 - "score": -3.2, 654 + "score": -2.1, 655 655 "stream": null, 656 656 "text": "### Social: Conference Mixer\n\n\n**Time Occurred:** 18:00 - 20:00\n**Participants:** Juliet Capulet, <strong>Romeo</strong> Montague\n\nNetworking event\n\nJuliet and <strong>Romeo</strong> exchanged Signal contacts\n" 657 657 }, ··· 667 667 "id": "facets/montague/events/20260304.jsonl:1", 668 668 "idx": 1, 669 669 "path": "facets/montague/events/20260304.jsonl", 670 - "score": -3.1, 670 + "score": -2.0, 671 671 "stream": null, 672 672 "text": "### Hackathon: Hackathon - API Bridge Challenge\n\n\n**Time Occurred:** 14:00 - 18:00\n**Participants:** <strong>Romeo</strong> Montague, Mercutio Escalus\n\nBuilt API bridge prototype\n\nTybalt confronted <strong>Romeo</strong>\n" 673 673 }
+1 -3
tests/test_activity_state_machine.py
··· 63 63 64 64 sm = ActivityStateMachine() 65 65 sm.update(_sense(content_type="coding"), "090000_300", "20260304") 66 - changes = sm.update( 67 - _sense(content_type="meeting"), "090500_300", "20260304" 68 - ) 66 + changes = sm.update(_sense(content_type="meeting"), "090500_300", "20260304") 69 67 70 68 assert len(changes) == 2 71 69 ended = [c for c in changes if c["state"] == "ended"]
-1
tests/test_app_agents.py
··· 99 99 config = get_agent("unified") 100 100 101 101 assert config["name"] == "unified" 102 - assert "system_instruction" in config 103 102 assert "user_instruction" in config 104 103 assert len(config["user_instruction"]) > 0 105 104
+3 -1
tests/test_awareness.py
··· 1026 1026 def test_self_md_wrapper_still_works(self, tmp_path): 1027 1027 from think.awareness import update_self_md_section 1028 1028 1029 - self_md = "# self\n\n## my name\nsol (default)\n\n## who I'm here for\nTest User\n" 1029 + self_md = ( 1030 + "# self\n\n## my name\nsol (default)\n\n## who I'm here for\nTest User\n" 1031 + ) 1030 1032 (tmp_path / "sol").mkdir(exist_ok=True) 1031 1033 (tmp_path / "sol" / "self.md").write_text(self_md) 1032 1034
+1
tests/test_cogitate_coder.py
··· 177 177 cmd = mock_runner_cls.call_args.kwargs["cmd"] 178 178 assert "--allowed-tools" not in cmd 179 179 180 + 180 181 # --------------------------------------------------------------------------- 181 182 # muse/coder.md existence and frontmatter 182 183 # ---------------------------------------------------------------------------
+7 -2
tests/test_dream_preflight.py
··· 31 31 from think import awareness 32 32 from think.dream import _should_skip_preflight 33 33 34 - monkeypatch.setattr(awareness, "get_onboarding", lambda: {"status": "observing"}) 34 + monkeypatch.setattr( 35 + awareness, "get_onboarding", lambda: {"status": "observing"} 36 + ) 35 37 assert _should_skip_preflight( 36 38 "firstday_checkin", 37 39 day="20240115", ··· 46 48 monkeypatch.setattr( 47 49 awareness, 48 50 "get_onboarding", 49 - lambda: {"status": "complete", "firstday_checkin_sent": "20260402T10:00:00"}, 51 + lambda: { 52 + "status": "complete", 53 + "firstday_checkin_sent": "20260402T10:00:00", 54 + }, 50 55 ) 51 56 assert _should_skip_preflight( 52 57 "firstday_checkin",
+19 -6
tests/test_dream_segment.py
··· 190 190 monkeypatch.setattr( 191 191 dream, 192 192 "get_muse_configs", 193 - lambda schedule=None, **kwargs: _segment_configs("sense", "entities", "screen"), 193 + lambda schedule=None, **kwargs: _segment_configs( 194 + "sense", "entities", "screen" 195 + ), 194 196 ) 195 197 monkeypatch.setattr( 196 198 dream, ··· 238 240 monkeypatch.setattr( 239 241 dream, 240 242 "get_muse_configs", 241 - lambda schedule=None, **kwargs: _segment_configs("sense", "entities", "screen"), 243 + lambda schedule=None, **kwargs: _segment_configs( 244 + "sense", "entities", "screen" 245 + ), 242 246 ) 243 247 monkeypatch.setattr( 244 248 dream, ··· 264 268 265 269 @pytest.mark.parametrize( 266 270 ("has_embeddings", "expected"), 267 - [(False, ["sense", "entities"]), (True, ["sense", "entities", "speaker_attribution"])], 271 + [ 272 + (False, ["sense", "entities"]), 273 + (True, ["sense", "entities", "speaker_attribution"]), 274 + ], 268 275 ) 269 276 def test_conditional_speaker_attribution( 270 277 self, ··· 370 377 monkeypatch.setattr( 371 378 dream, 372 379 "get_muse_configs", 373 - lambda schedule=None, **kwargs: _segment_configs("sense", "entities", "screen"), 380 + lambda schedule=None, **kwargs: _segment_configs( 381 + "sense", "entities", "screen" 382 + ), 374 383 ) 375 384 monkeypatch.setattr( 376 385 dream, ··· 407 416 monkeypatch.setattr( 408 417 dream, 409 418 "get_muse_configs", 410 - lambda schedule=None, **kwargs: _segment_configs("sense", "entities", "pulse"), 419 + lambda schedule=None, **kwargs: _segment_configs( 420 + "sense", "entities", "pulse" 421 + ), 411 422 ) 412 423 monkeypatch.setattr( 413 424 dream, ··· 543 554 segment_dir, 544 555 {"density": "active", "recommend": {}, "facets": []}, 545 556 ) 546 - (segment_dir / "agents" / "entities.md").write_text("entities", encoding="utf-8") 557 + (segment_dir / "agents" / "entities.md").write_text( 558 + "entities", encoding="utf-8" 559 + ) 547 560 548 561 monkeypatch.setattr( 549 562 dream,
+24 -24
tests/test_engage.py
··· 29 29 30 30 assert result.exit_code == 0 31 31 assert "agent-123" in result.output 32 - mock_cr.assert_called_once_with( 33 - prompt="fix the bug", name="coder", config=None 34 - ) 32 + mock_cr.assert_called_once_with(prompt="fix the bug", name="coder", config=None) 35 33 36 34 def test_empty_stdin(self): 37 35 result = _invoke_engage("coder", input_text="") ··· 49 47 assert result.exit_code == 1 50 48 51 49 def test_wait_success(self): 52 - with patch( 53 - "think.cortex_client.cortex_request", return_value="agent-123" 54 - ), patch( 55 - "think.cortex_client.wait_for_agents", 56 - return_value=({"agent-123": "finish"}, []), 57 - ), patch( 58 - "think.cortex_client.read_agent_events", 59 - return_value=[{"event": "finish", "result": "All fixed!"}], 50 + with ( 51 + patch("think.cortex_client.cortex_request", return_value="agent-123"), 52 + patch( 53 + "think.cortex_client.wait_for_agents", 54 + return_value=({"agent-123": "finish"}, []), 55 + ), 56 + patch( 57 + "think.cortex_client.read_agent_events", 58 + return_value=[{"event": "finish", "result": "All fixed!"}], 59 + ), 60 60 ): 61 61 result = _invoke_engage("coder", "--wait", input_text="fix the bug\n") 62 62 ··· 64 64 assert "All fixed!" in result.output 65 65 66 66 def test_wait_error(self): 67 - with patch( 68 - "think.cortex_client.cortex_request", return_value="agent-123" 69 - ), patch( 70 - "think.cortex_client.wait_for_agents", 71 - return_value=({"agent-123": "error"}, []), 67 + with ( 68 + patch("think.cortex_client.cortex_request", return_value="agent-123"), 69 + patch( 70 + "think.cortex_client.wait_for_agents", 71 + return_value=({"agent-123": "error"}, []), 72 + ), 72 73 ): 73 74 result = _invoke_engage("coder", "--wait", input_text="fix the bug\n") 74 75 75 76 assert result.exit_code == 1 76 77 77 78 def test_wait_timeout(self): 78 - with patch( 79 - "think.cortex_client.cortex_request", return_value="agent-123" 80 - ), patch( 81 - "think.cortex_client.wait_for_agents", 82 - return_value=({}, ["agent-123"]), 79 + with ( 80 + patch("think.cortex_client.cortex_request", return_value="agent-123"), 81 + patch( 82 + "think.cortex_client.wait_for_agents", 83 + return_value=({}, ["agent-123"]), 84 + ), 83 85 ): 84 86 result = _invoke_engage("coder", "--wait", input_text="fix the bug\n") 85 87 ··· 113 115 with patch( 114 116 "think.cortex_client.cortex_request", return_value="agent-123" 115 117 ) as mock_cr: 116 - result = _invoke_engage( 117 - "coder", "--facet", "work", input_text="do stuff\n" 118 - ) 118 + result = _invoke_engage("coder", "--facet", "work", input_text="do stuff\n") 119 119 120 120 assert result.exit_code == 0 121 121 mock_cr.assert_called_once_with(
+4 -2
tests/test_entities_hook.py
··· 6 6 from __future__ import annotations 7 7 8 8 import json 9 - from pathlib import Path 10 9 11 10 12 11 def test_entities_post_process_writes_without_segment(tmp_path): ··· 27 26 28 27 entities_path = output_path.parent / "entities.jsonl" 29 28 assert entities_path.exists() 30 - rows = [json.loads(line) for line in entities_path.read_text(encoding="utf-8").splitlines()] 29 + rows = [ 30 + json.loads(line) 31 + for line in entities_path.read_text(encoding="utf-8").splitlines() 32 + ] 31 33 assert rows == [ 32 34 { 33 35 "type": "Person",
+12 -21
tests/test_entity_agents.py
··· 25 25 26 26 # Verify required fields 27 27 assert config["name"] == "entities:entities" 28 - assert "system_instruction" in config 29 28 assert "user_instruction" in config 30 - assert len(config["system_instruction"]) > 0 31 29 assert len(config["user_instruction"]) > 0 32 30 33 31 # Verify JSON metadata fields from entities.json ··· 44 42 45 43 # Verify required fields 46 44 assert config["name"] == "entities:entities_review" 47 - assert "system_instruction" in config 48 45 assert "user_instruction" in config 49 - assert len(config["system_instruction"]) > 0 50 46 assert len(config["user_instruction"]) > 0 51 47 52 48 # Verify JSON metadata fields from entities_review.json ··· 86 82 """Test that agent context includes entities grouped by facet.""" 87 83 config = get_agent("entities:entities") 88 84 89 - # extra_context should contain facet summaries with entities 90 - extra_context = config.get("extra_context", "") 91 - assert "Available Facets" in extra_context 85 + prompt = config["user_instruction"] 86 + assert "Available Facets" in prompt 92 87 93 88 # Should include facet names in backtick format 94 - assert "`test-facet`" in extra_context or "`full-featured`" in extra_context 89 + assert "`test-facet`" in prompt or "`full-featured`" in prompt 95 90 96 91 # Should include entities from fixture facets 97 92 # tests/fixtures/journal/facets/ contains various entities 98 - assert "Entities" in extra_context 93 + assert "Entities" in prompt 99 94 100 95 # Check for some known entities from the fixtures 101 - assert ( 102 - "John Smith" in extra_context 103 - or "Jane Doe" in extra_context 104 - or "Acme Corp" in extra_context 105 - ) 96 + assert "John Smith" in prompt or "Jane Doe" in prompt or "Acme Corp" in prompt 106 97 107 98 108 99 def test_agent_context_with_facet_focus(fixture_journal): 109 100 """Test that get_agent with facet parameter uses focused single-facet context.""" 110 101 config = get_agent("unified", facet="full-featured") 111 102 112 - extra_context = config.get("extra_context", "") 103 + prompt = config["user_instruction"] 113 104 114 105 # Should have Facet Focus section instead of Available Facets 115 - assert "## Facet Focus" in extra_context 116 - assert "Available Facets" not in extra_context 106 + assert "## Facet Focus" in prompt 107 + assert "Available Facets" not in prompt 117 108 118 109 # Should include the focused facet's details 119 - assert "Full Featured Facet" in extra_context 120 - assert "A facet for testing all features" in extra_context 110 + assert "Full Featured Facet" in prompt 111 + assert "A facet for testing all features" in prompt 121 112 122 113 # Should include entity details from the focused facet (detailed format) 123 - assert "## Entities" in extra_context 124 - assert "Entity 1" in extra_context or "First test entity" in extra_context 114 + assert "## Entities" in prompt 115 + assert "Entity 1" in prompt or "First test entity" in prompt 125 116 126 117 127 118 def test_agent_priority_ordering(fixture_journal):
+5 -6
tests/test_generate_full.py
··· 79 79 80 80 test_generator = tmp_path / "test_gen.md" 81 81 test_generator.write_text( 82 - '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 82 + '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 83 83 ) 84 84 85 85 # Mock the underlying generation function in think.models ··· 146 146 147 147 test_generator = tmp_path / "hooked_gen.md" 148 148 test_generator.write_text( 149 - '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "test_hook"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 149 + '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "test_hook"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 150 150 ) 151 151 152 152 # Mock the underlying generation function in think.models ··· 198 198 199 199 test_generator = tmp_path / "nohook_gen.md" 200 200 test_generator.write_text( 201 - '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nNo hook prompt' 201 + '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "load": {"transcripts": true, "percepts": true}\n}\n\nNo hook prompt' 202 202 ) 203 203 204 204 # Mock the underlying generation function in think.models ··· 265 265 266 266 test_generator = tmp_path / "empty_gen.md" 267 267 test_generator.write_text( 268 - '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 268 + '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 269 269 ) 270 270 271 271 monkeypatch.setenv("GOOGLE_API_KEY", "x") ··· 302 302 303 303 test_agent = tmp_path / "test_cogitate.md" 304 304 test_agent.write_text( 305 - '{\n "type": "cogitate",\n "schedule": "daily",\n "priority": 10,' 306 - '\n "instructions": {"system": "journal", "day": true}\n}\n\nTest prompt' 305 + '{\n "type": "cogitate",\n "schedule": "daily",\n "priority": 10\n}\n\nTest prompt' 307 306 ) 308 307 309 308 monkeypatch.setenv("GOOGLE_API_KEY", "x")
+1 -2
tests/test_generators.py
··· 132 132 sense = generators["sense"] 133 133 assert sense.get("priority") == 5, "sense should be at priority 5" 134 134 135 - instructions = sense.get("instructions", {}) 136 - sources = instructions.get("sources", {}) 135 + sources = sense.get("load", {}) 137 136 138 137 assert sources.get("transcripts") is True, "sense should include transcripts" 139 138 assert sources.get("percepts") is True, "sense should include percepts"
+3 -1
tests/test_home_routines.py
··· 189 189 190 190 def test_api_pulse_includes_routines(monkeypatch, home_client): 191 191 """Pulse API includes the routines payload from the context builder.""" 192 - monkeypatch.setattr("apps.home.routes.get_current", lambda: {"capture": {"status": "ok"}}) 192 + monkeypatch.setattr( 193 + "apps.home.routes.get_current", lambda: {"capture": {"status": "ok"}} 194 + ) 193 195 monkeypatch.setattr("apps.home.routes.get_cached_state", lambda: {}) 194 196 monkeypatch.setattr("apps.home.routes._resolve_attention", lambda awareness: None) 195 197 monkeypatch.setattr("apps.home.routes._load_stats", lambda today: {})
+2 -280
tests/test_muse.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Tests for think.muse module. 5 - 6 - Tests for muse prompt loading, configuration, and instruction composition. 7 - """ 8 - 9 - from think.muse import ( 10 - _merge_instructions_config, 11 - compose_instructions, 12 - get_agent_filter, 13 - source_is_enabled, 14 - source_is_required, 15 - ) 16 - 17 - # ============================================================================= 18 - # _merge_instructions_config tests 19 - # ============================================================================= 20 - 21 - 22 - def test_merge_instructions_config_empty_overrides(): 23 - """Test that empty overrides returns defaults copy.""" 24 - defaults = {"system": "journal", "facets": True, "sources": {"transcripts": False}} 25 - result = _merge_instructions_config(defaults, None) 26 - assert result == defaults 27 - assert result is not defaults # Should be a copy 28 - 29 - 30 - def test_merge_instructions_config_with_overrides(): 31 - """Test that overrides are merged correctly.""" 32 - defaults = {"system": "journal", "facets": True, "sources": {"transcripts": False}} 33 - overrides = {"system": "custom", "facets": False} 34 - result = _merge_instructions_config(defaults, overrides) 35 - assert result["system"] == "custom" 36 - assert result["facets"] is False 37 - assert result["sources"] == {"transcripts": False} # Preserved 38 - 39 - 40 - def test_merge_instructions_config_sources_merge(): 41 - """Test that sources dict is merged, not replaced.""" 42 - defaults = {"system": None, "sources": {"transcripts": False, "percepts": False}} 43 - overrides = {"sources": {"transcripts": True}} 44 - result = _merge_instructions_config(defaults, overrides) 45 - assert result["sources"]["transcripts"] is True # Overridden 46 - assert result["sources"]["percepts"] is False # Preserved from defaults 47 - 48 - 49 - def test_merge_instructions_config_ignores_unknown_keys(): 50 - """Test that unknown keys in overrides are ignored.""" 51 - defaults = {"system": "journal", "facets": True} 52 - overrides = {"unknown_key": "value", "another": 123} 53 - result = _merge_instructions_config(defaults, overrides) 54 - assert "unknown_key" not in result 55 - assert "another" not in result 56 - 57 - 58 - def test_merge_instructions_config_facets_override(): 59 - """Test that facets key can be overridden.""" 60 - defaults = {"system": "journal", "facets": True} 61 - overrides = {"facets": False} 62 - result = _merge_instructions_config(defaults, overrides) 63 - assert result["system"] == "journal" 64 - assert result["facets"] is False 65 - 66 - 67 - # ============================================================================= 68 - # compose_instructions tests 69 - # ============================================================================= 70 - 71 - 72 - class TestComposeInstructions: 73 - """Tests for compose_instructions function.""" 74 - 75 - def test_default_system_instruction_is_none(self, monkeypatch, tmp_path): 76 - """Test that default system instruction is empty (agents must opt-in).""" 77 - think_dir = tmp_path / "think" 78 - think_dir.mkdir() 79 - 80 - import think.prompts 81 - 82 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 83 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 84 - 85 - result = compose_instructions() 86 - 87 - assert "system_instruction" in result 88 - assert result["system_instruction"] == "" 89 - assert result["system_prompt_name"] == "" 90 - 91 - def test_custom_system_instruction(self, monkeypatch, tmp_path): 92 - """Test that custom system prompt can be loaded.""" 93 - think_dir = tmp_path / "think" 94 - think_dir.mkdir() 95 - custom_txt = think_dir / "custom.md" 96 - custom_txt.write_text("Custom system instruction") 97 - 98 - import think.prompts 99 - 100 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 101 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 102 - 103 - result = compose_instructions( 104 - config_overrides={"system": "custom"}, 105 - ) 106 - 107 - assert result["system_prompt_name"] == "custom" 108 - assert "Custom system instruction" in result["system_instruction"] 109 - 110 - def test_user_instruction_loaded_when_provided(self, monkeypatch, tmp_path): 111 - """Test that user instruction is loaded when user_prompt is provided.""" 112 - think_dir = tmp_path / "think" 113 - think_dir.mkdir() 114 - journal_txt = think_dir / "journal.md" 115 - journal_txt.write_text("System instruction") 116 - user_txt = think_dir / "default.md" 117 - user_txt.write_text("User instruction content") 118 - 119 - import think.muse 120 - import think.prompts 121 - 122 - # Monkeypatch both modules since compose_instructions uses muse.__file__ for 123 - # default user_prompt_dir, and load_prompt uses prompts.__file__ for defaults 124 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 125 - monkeypatch.setattr(think.muse, "__file__", str(think_dir / "muse.py")) 126 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 127 - 128 - result = compose_instructions(user_prompt="default") 129 - 130 - assert result["user_instruction"] == "User instruction content" 131 - 132 - def test_user_instruction_none_when_not_provided(self, monkeypatch, tmp_path): 133 - """Test that user instruction is None when user_prompt is not provided.""" 134 - think_dir = tmp_path / "think" 135 - think_dir.mkdir() 136 - journal_txt = think_dir / "journal.md" 137 - journal_txt.write_text("System instruction") 138 - 139 - import think.prompts 140 - 141 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 142 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 143 - 144 - result = compose_instructions() 145 - 146 - assert result["user_instruction"] is None 147 - 148 - def test_facets_none_excludes_facets_from_context(self, monkeypatch, tmp_path): 149 - """Test that facets='none' excludes facet info from extra_context.""" 150 - think_dir = tmp_path / "think" 151 - think_dir.mkdir() 152 - journal_txt = think_dir / "journal.md" 153 - journal_txt.write_text("System instruction") 154 - 155 - import think.prompts 156 - 157 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 158 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 159 - 160 - result = compose_instructions( 161 - config_overrides={"facets": False, "now": False, "day": False}, 162 - ) 163 - 164 - # With no datetime and no facets, extra_context should be empty/None 165 - assert result["extra_context"] is None or result["extra_context"] == "" 166 - 167 - def test_now_false_excludes_time(self, monkeypatch, tmp_path): 168 - """Test that now=False excludes current datetime from context.""" 169 - think_dir = tmp_path / "think" 170 - think_dir.mkdir() 171 - journal_txt = think_dir / "journal.md" 172 - journal_txt.write_text("System instruction") 4 + """Tests for think.muse module.""" 173 5 174 - import think.prompts 175 - 176 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 177 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 178 - 179 - result = compose_instructions( 180 - config_overrides={"facets": False, "now": False}, 181 - ) 182 - 183 - extra = result.get("extra_context") or "" 184 - assert "Current Date and Time" not in extra 185 - 186 - def test_now_true_includes_time(self, monkeypatch, tmp_path): 187 - """Test that now=True includes current datetime in context.""" 188 - think_dir = tmp_path / "think" 189 - think_dir.mkdir() 190 - journal_txt = think_dir / "journal.md" 191 - journal_txt.write_text("System instruction") 192 - 193 - import think.prompts 194 - 195 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 196 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 197 - 198 - result = compose_instructions( 199 - config_overrides={"facets": False, "now": True}, 200 - ) 201 - 202 - assert "Current Date and Time" in result["extra_context"] 203 - 204 - def test_day_true_includes_analysis_day(self, monkeypatch, tmp_path): 205 - """Test that day=True includes analysis day in context.""" 206 - think_dir = tmp_path / "think" 207 - think_dir.mkdir() 208 - journal_txt = think_dir / "journal.md" 209 - journal_txt.write_text("System instruction") 210 - 211 - import think.prompts 212 - 213 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 214 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 215 - 216 - result = compose_instructions( 217 - analysis_day="20250115", 218 - config_overrides={"facets": False, "day": True}, 219 - ) 220 - 221 - extra = result.get("extra_context") or "" 222 - assert "Analysis Day" in extra 223 - assert "20250115" in extra 224 - 225 - def test_sources_returned_from_defaults(self, monkeypatch, tmp_path): 226 - """Test that sources config is returned with defaults (all false).""" 227 - think_dir = tmp_path / "think" 228 - think_dir.mkdir() 229 - 230 - import think.prompts 231 - 232 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 233 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 234 - 235 - result = compose_instructions() 236 - 237 - assert "sources" in result 238 - assert result["sources"]["transcripts"] is False 239 - assert result["sources"]["percepts"] is False 240 - assert result["sources"]["agents"] is False 241 - 242 - def test_sources_can_be_overridden(self, monkeypatch, tmp_path): 243 - """Test that sources config can be overridden.""" 244 - think_dir = tmp_path / "think" 245 - think_dir.mkdir() 246 - 247 - import think.prompts 248 - 249 - monkeypatch.setattr(think.prompts, "__file__", str(think_dir / "prompts.py")) 250 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 251 - 252 - result = compose_instructions( 253 - config_overrides={ 254 - "sources": {"transcripts": True, "agents": True}, 255 - }, 256 - ) 257 - 258 - assert result["sources"]["transcripts"] is True # Overridden 259 - assert result["sources"]["percepts"] is False # Default preserved 260 - assert result["sources"]["agents"] is True # Overridden 261 - 262 - 263 - # ============================================================================= 264 - # source_is_enabled / source_is_required / get_agent_filter tests 265 - # ============================================================================= 6 + from think.muse import get_agent_filter, source_is_enabled, source_is_required 266 7 267 8 268 9 def test_source_is_enabled_bool(): ··· 278 19 279 20 def test_source_is_enabled_dict(): 280 21 """Test source_is_enabled with dict values for agents source.""" 281 - # Dict with at least one True value -> enabled 282 22 assert source_is_enabled({"entities": True, "meetings": False}) is True 283 - 284 - # Dict with at least one "required" value -> enabled 285 23 assert source_is_enabled({"entities": "required", "meetings": False}) is True 286 - 287 - # Dict with all False values -> disabled 288 24 assert source_is_enabled({"entities": False, "meetings": False}) is False 289 - 290 - # Empty dict -> disabled 291 25 assert source_is_enabled({}) is False 292 26 293 27 ··· 304 38 305 39 def test_source_is_required_dict(): 306 40 """Test source_is_required with dict values.""" 307 - # Dict with at least one "required" value -> required 308 41 assert source_is_required({"entities": "required", "meetings": False}) is True 309 - 310 - # Dict with no "required" values -> not required 311 42 assert source_is_required({"entities": True, "meetings": False}) is False 312 - 313 - # Empty dict -> not required 314 43 assert source_is_required({}) is False 315 44 316 45 317 46 def test_get_agent_filter_bool(): 318 47 """Test get_agent_filter with bool values.""" 319 - # True -> None (all agents) 320 48 assert get_agent_filter(True) is None 321 - 322 - # False -> empty dict (no agents) 323 49 assert get_agent_filter(False) == {} 324 50 325 51 326 52 def test_get_agent_filter_required_string(): 327 53 """Test get_agent_filter with 'required' string.""" 328 - # "required" -> None (all agents, required) 329 54 assert get_agent_filter("required") is None 330 55 331 56 332 57 def test_get_agent_filter_dict(): 333 58 """Test get_agent_filter with dict values.""" 334 - # Dict -> returned as-is for filtering 335 59 filter_dict = {"entities": True, "meetings": "required", "flow": False} 336 60 assert get_agent_filter(filter_dict) == filter_dict 337 - 338 - # Empty dict -> empty dict (no agents) 339 61 assert get_agent_filter({}) == {}
+6 -6
tests/test_output_hooks.py
··· 170 170 171 171 prompt_file = tmp_path / "hooked_test.md" 172 172 prompt_file.write_text( 173 - '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "hooked_test"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 173 + '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "hooked_test"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 174 174 ) 175 175 176 176 hook_file = tmp_path / "hooked_test.py" ··· 224 224 225 225 prompt_file = tmp_path / "noop_test.md" 226 226 prompt_file.write_text( 227 - '{\n "type": "generate",\n "title": "Noop",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "noop_test"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 227 + '{\n "type": "generate",\n "title": "Noop",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "noop_test"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 228 228 ) 229 229 230 230 hook_file = tmp_path / "noop_test.py" ··· 270 270 271 271 prompt_file = tmp_path / "broken_test.md" 272 272 prompt_file.write_text( 273 - '{\n "type": "generate",\n "title": "Broken",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "broken_test"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nTest prompt' 273 + '{\n "type": "generate",\n "title": "Broken",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "broken_test"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nTest prompt' 274 274 ) 275 275 276 276 hook_file = tmp_path / "broken_test.py" ··· 387 387 388 388 prompt_file = tmp_path / "prehooked_test.md" 389 389 prompt_file.write_text( 390 - '{\n "type": "generate",\n "title": "Prehooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "prehooked_test"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nOriginal prompt' 390 + '{\n "type": "generate",\n "title": "Prehooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "prehooked_test"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nOriginal prompt' 391 391 ) 392 392 393 393 hook_file = tmp_path / "prehooked_test.py" ··· 396 396 # Verify context has expected fields 397 397 assert "transcript" in context 398 398 assert "prompt" in context 399 - assert "system_instruction" in context 399 + assert "user_instruction" in context 400 400 # Modify the prompt 401 401 return {"prompt": context["prompt"] + " [pre-processed]"} 402 402 """) ··· 447 447 448 448 prompt_file = tmp_path / "both_hooks_test.md" 449 449 prompt_file.write_text( 450 - '{\n "type": "generate",\n "title": "Both Hooks",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "both_hooks_test", "post": "both_hooks_test"},\n "instructions": {"system": "journal", "sources": {"transcripts": true, "percepts": true}}\n}\n\nOriginal prompt' 450 + '{\n "type": "generate",\n "title": "Both Hooks",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "both_hooks_test", "post": "both_hooks_test"},\n "load": {"transcripts": true, "percepts": true}\n}\n\nOriginal prompt' 451 451 ) 452 452 453 453 hook_file = tmp_path / "both_hooks_test.py"
+1 -1
tests/test_routines.py
··· 3 3 4 4 """Tests for think.routines — user-defined routines engine.""" 5 5 6 + import importlib.util 6 7 from contextlib import contextmanager 7 8 from datetime import date, datetime, timedelta, timezone 8 - import importlib.util 9 9 from pathlib import Path 10 10 from unittest.mock import patch 11 11
+6 -2
tests/test_sense_splitter.py
··· 134 134 assert (agents_dir / "density.json").exists() 135 135 assert (agents_dir / "sense.json").exists() 136 136 assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" 137 - assert json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 137 + assert ( 138 + json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 139 + ) 138 140 density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) 139 141 assert density["classification"] == "active" 140 142 ··· 156 158 157 159 agents_dir = seg_dir / "agents" 158 160 assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" 159 - assert json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 161 + assert ( 162 + json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 163 + ) 160 164 density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) 161 165 assert density["classification"] == "active" 162 166 assert not (agents_dir / "speakers.json").exists()
+11 -7
tests/test_service.py
··· 81 81 82 82 config_dir = tmp_path / "config" 83 83 config_dir.mkdir(exist_ok=True) 84 - (config_dir / "journal.json").write_text(json.dumps({ 85 - "env": { 86 - "ANTHROPIC_API_KEY": "sk-test", 87 - "OPENAI_API_KEY": "sk-openai", 88 - "GOOGLE_API_KEY": "gk-test", 89 - } 90 - })) 84 + (config_dir / "journal.json").write_text( 85 + json.dumps( 86 + { 87 + "env": { 88 + "ANTHROPIC_API_KEY": "sk-test", 89 + "OPENAI_API_KEY": "sk-openai", 90 + "GOOGLE_API_KEY": "gk-test", 91 + } 92 + } 93 + ) 94 + ) 91 95 92 96 env = service._collect_env() 93 97 assert "ANTHROPIC_API_KEY" not in env
+7 -1
tests/test_sol_call.py
··· 514 514 def test_partner_update_section_logs_history(self, journal_with_sol): 515 515 runner.invoke( 516 516 app, 517 - ["partner", "--update-section", "work patterns", "--value", "Morning focus"], 517 + [ 518 + "partner", 519 + "--update-section", 520 + "work patterns", 521 + "--value", 522 + "Morning focus", 523 + ], 518 524 ) 519 525 history = journal_with_sol / "sol" / "history.jsonl" 520 526 assert history.exists()
+1 -3
tests/test_supervisor.py
··· 641 641 start_mock.assert_not_called() 642 642 643 643 644 - def test_supervisor_singleton_lock_blocked_with_health( 645 - tmp_path, monkeypatch, capsys 646 - ): 644 + def test_supervisor_singleton_lock_blocked_with_health(tmp_path, monkeypatch, capsys): 647 645 import fcntl 648 646 649 647 mod = importlib.reload(importlib.import_module("think.supervisor"))
+8 -2
tests/test_validate_key.py
··· 124 124 def test_update_config_saves_key_validation(settings_client): 125 125 client, journal = settings_client 126 126 127 - with patch("think.providers.validate_key", return_value={"valid": False, "error": "bad key"}): 127 + with patch( 128 + "think.providers.validate_key", 129 + return_value={"valid": False, "error": "bad key"}, 130 + ): 128 131 response = client.put( 129 132 "/app/settings/api/config", 130 133 json={"section": "env", "data": {"GOOGLE_API_KEY": "bad-key"}}, ··· 193 196 config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 194 197 195 198 def fake_validate(provider: str, api_key: str) -> dict: 196 - return {"valid": provider == "google", "error": "" if provider == "google" else "bad key"} 199 + return { 200 + "valid": provider == "google", 201 + "error": "" if provider == "google" else "bad key", 202 + } 197 203 198 204 with patch("think.providers.validate_key", side_effect=fake_validate): 199 205 response = client.post("/app/settings/api/validate-keys")
+1 -3
think/activity_state_machine.py
··· 24 24 start_time, _end_time = segment_parse(segment_key) 25 25 if start_time is None: 26 26 return None 27 - return ( 28 - start_time.hour * 3600 + start_time.minute * 60 + start_time.second 29 - ) 27 + return start_time.hour * 3600 + start_time.minute * 60 + start_time.second 30 28 31 29 def _parse_segment_end_seconds(self, segment_key: str) -> int | None: 32 30 _start_time, end_time = segment_parse(segment_key)
+18 -52
think/agents.py
··· 286 286 span: list[str], 287 287 facet: str, 288 288 day: str, 289 - instructions_config: dict | None, 290 289 ) -> str | None: 291 - """Build activity context sections for extra_context. 292 - 293 - Assembles activity metadata, per-segment activity state descriptions, 294 - and focusing instructions based on the agent's instructions.activity config. 290 + """Build activity context sections for $activity_context. 295 291 296 292 Args: 297 293 activity: Activity record dict (from activity records JSONL) 298 294 span: List of segment keys in the activity's span 299 295 facet: Facet name 300 296 day: Day in YYYYMMDD format 301 - instructions_config: The agent's instructions config dict (merged) 302 297 303 298 Returns: 304 - Formatted string to append to extra_context, or None if activity 305 - instructions are not configured. 299 + Formatted string for the $activity_context template variable. 306 300 """ 307 - if not instructions_config: 308 - return None 309 - 310 - activity_cfg = instructions_config.get("activity") 311 - if not activity_cfg or activity_cfg is False: 312 - return None 313 - 314 - # Normalize: bool True -> all enabled (already handled by _merge, but defensive) 315 - if activity_cfg is True: 316 - activity_cfg = {"context": True, "state": True, "focus": True} 301 + activity_cfg = {"context": True, "state": True, "focus": True} 317 302 318 303 parts: list[str] = [] 319 304 activity_type = activity.get("activity", "unknown") ··· 392 377 day: Day in YYYYMMDD format 393 378 segment: Optional segment key 394 379 span: Optional list of segment keys 395 - sources: Source config dict from instructions 380 + sources: Source config dict from frontmatter load 396 381 397 382 Returns: 398 383 Tuple of (transcript text, source_counts dict) ··· 436 421 Config fields produced: 437 422 - name: Agent name 438 423 - provider, model: Resolved from context/request 439 - - system_instruction: System prompt 440 424 - user_instruction: Agent instruction from .md file 441 - - extra_context: Facets and context from instructions.now/day settings 442 425 - prompt: User's runtime query/request 443 426 - transcript: Clustered transcript (if day provided) 444 427 - output_path: Where to write output (if output format set) 445 428 - skip_reason: Why to skip (if applicable) 446 429 447 - Context is controlled by explicit frontmatter settings: 448 - - instructions.now: Include current datetime in extra_context 449 - - instructions.day: Include analysis day context (requires day parameter) 450 - - Day-based calls also load clustered transcript 451 - 452 430 Args: 453 431 request: Raw request dict from cortex 454 432 ··· 468 446 output_path_override = request.get("output_path") 469 447 user_prompt = request.get("prompt", "") 470 448 471 - # Load complete agent config, passing day for instructions.day context 449 + # Load complete agent config 472 450 config = get_agent(name, facet=facet, analysis_day=day) 473 451 474 452 # Config now contains all frontmatter fields plus: 475 453 # - path: Path to the .md file 476 - # - system_instruction, user_instruction, extra_context 477 454 # - sources: Source config for transcript loading 478 455 # - All frontmatter: tools, hook, disabled, thinking_budget, max_output_tokens, etc. 479 456 ··· 569 546 570 547 # Reload agent instruction with template substitution for day/segment context 571 548 if agent_path and agent_path.exists(): 549 + from think.prompts import _resolve_facets 550 + 572 551 prompt_context = _build_prompt_context( 573 552 day, segment, span, activity=activity 574 553 ) 554 + prompt_context["facets"] = _resolve_facets(facet) 555 + prompt_context["journal"] = load_prompt( 556 + "journal", context=prompt_context 557 + ).text 558 + 559 + if activity and span and facet: 560 + activity_ctx = _build_activity_context(activity, span, facet, day) 561 + if activity_ctx: 562 + prompt_context["activity_context"] = activity_ctx 563 + 575 564 agent_prompt_obj = load_prompt( 576 565 agent_path.stem, base_dir=agent_path.parent, context=prompt_context 577 566 ) 578 567 config["user_instruction"] = agent_prompt_obj.text 579 - 580 - # Build activity context if activity data is present 581 - if activity and span and facet: 582 - from think.muse import _DEFAULT_INSTRUCTIONS, _merge_instructions_config 583 - 584 - instructions_config = config.get("instructions") 585 - merged_cfg = _merge_instructions_config( 586 - _DEFAULT_INSTRUCTIONS, instructions_config 587 - ) 588 - activity_context = _build_activity_context( 589 - activity, span, facet, day, merged_cfg 590 - ) 591 - if activity_context: 592 - existing = config.get("extra_context", "") 593 - if existing: 594 - config["extra_context"] = f"{existing}\n\n{activity_context}" 595 - else: 596 - config["extra_context"] = activity_context 597 568 598 569 # Set prompt (user's runtime query) 599 570 # For tool agents: prompt is the user's question ··· 900 871 transcript = config.get("transcript", "") 901 872 user_instruction = config.get("user_instruction", "") 902 873 prompt = config.get("prompt", "") 903 - system_instruction = config.get("system_instruction", "") 904 - extra_ctx = config.get("extra_context") 905 - if extra_ctx: 906 - system_instruction = ( 907 - f"{system_instruction}\n\n{extra_ctx}" if system_instruction else extra_ctx 908 - ) 874 + system_instruction = config.get("system_instruction") or None 909 875 output_path = Path(config["output_path"]) if config.get("output_path") else None 910 876 output_format = config.get("output") 911 877
+12 -5
think/dream.py
··· 17 17 import sys 18 18 import threading 19 19 import time 20 - from datetime import date, datetime, timedelta, timezone 20 + from datetime import date, datetime, timedelta 21 21 from pathlib import Path 22 22 23 23 from think.activities import get_activity_output_path, load_activity_records 24 + from think.activity_state_machine import ActivityStateMachine 24 25 from think.callosum import CallosumConnection 25 26 from think.cluster import cluster_segments 26 27 from think.cortex_client import cortex_request, wait_for_agents ··· 29 30 get_enabled_facets, 30 31 load_segment_facets, 31 32 ) 32 - from think.activity_state_machine import ActivityStateMachine 33 33 from think.muse import get_muse_configs, get_output_path 34 34 from think.runner import run_task 35 35 from think.sense_splitter import write_idle_stubs, write_sense_outputs ··· 434 434 if is_generate 435 435 else f"Running scheduled task for {iso_date(day)}: {day_input_summary(day)}." 436 436 ) 437 - return _cortex_request_with_retry(prompt=prompt, name=name, config=request_config) 437 + return _cortex_request_with_retry( 438 + prompt=prompt, name=name, config=request_config 439 + ) 438 440 439 441 sense_config = _cfg("sense") 440 442 if sense_config is None: ··· 1849 1851 sense_cfg.get("output", "json"), 1850 1852 stream=stream, 1851 1853 ) 1852 - print(f" {step}. sense (gen/{sense_cfg.get('output', 'json')}){status} — mandatory") 1854 + print( 1855 + f" {step}. sense (gen/{sense_cfg.get('output', 'json')}){status} — mandatory" 1856 + ) 1853 1857 step += 1 1854 1858 1855 1859 for name, label in [ 1856 1860 ("entities", "always for non-idle"), 1857 1861 ("screen", "if recommend.screen_record"), 1858 - ("speaker_attribution", "if recommend.speaker_attribution + audio embeddings"), 1862 + ( 1863 + "speaker_attribution", 1864 + "if recommend.speaker_attribution + audio embeddings", 1865 + ), 1859 1866 ("observation", "if onboarding=observing"), 1860 1867 ("firstday_checkin", "if onboarding=complete"), 1861 1868 ("pulse", "if recommend.pulse_update"),
+25 -234
think/muse.py
··· 9 9 Key functions: 10 10 - get_muse_configs(): Discover all muse configs with filtering 11 11 - get_agent(): Load complete agent configuration by name 12 - - compose_instructions(): Build system/user prompts from instruction config 13 12 - Hook loading: load_pre_hook(), load_post_hook() 14 13 15 14 For simple prompt loading without orchestration (observe/, think/*.md prompts), ··· 326 325 return agent_dir, agent_name 327 326 328 327 329 - # --------------------------------------------------------------------------- 330 - # Instructions Composition 331 - # --------------------------------------------------------------------------- 332 - 333 - # Default instruction configuration - all false, agents must explicitly opt-in 334 - _DEFAULT_INSTRUCTIONS = { 335 - "system": None, 336 - "facets": False, 337 - "now": False, 338 - "day": False, 339 - "activity": False, 340 - "sources": { 341 - "transcripts": False, 342 - "percepts": False, 343 - "agents": False, 344 - }, 328 + # Default load configuration - prompts must explicitly opt into source loading 329 + _DEFAULT_LOAD = { 330 + "transcripts": False, 331 + "percepts": False, 332 + "agents": False, 345 333 } 346 334 347 - # Sub-keys for activity config when specified as a dict 348 - _DEFAULT_ACTIVITY_CONFIG = { 349 - "context": False, 350 - "state": False, 351 - "focus": False, 352 - } 353 - 354 - 355 - def _merge_instructions_config(defaults: dict, overrides: dict | None) -> dict: 356 - """Merge instruction config overrides into defaults. 357 - 358 - Handles nested "sources" and "activity" dicts specially. 359 - 360 - Parameters 361 - ---------- 362 - defaults: 363 - Default instruction configuration. 364 - overrides: 365 - Optional overrides from .json "instructions" key. 366 - 367 - Returns 368 - ------- 369 - dict 370 - Merged configuration. 371 - """ 372 - if not overrides: 373 - return defaults.copy() 374 - 375 - result = defaults.copy() 376 - 377 - # Merge top-level keys 378 - for key in ("system", "facets", "now", "day"): 379 - if key in overrides: 380 - result[key] = overrides[key] 381 - 382 - # Merge activity config: bool shorthand or dict with sub-keys 383 - if "activity" in overrides: 384 - activity_val = overrides["activity"] 385 - if activity_val is True: 386 - # Shorthand: true -> all sub-keys enabled 387 - result["activity"] = {k: True for k in _DEFAULT_ACTIVITY_CONFIG} 388 - elif isinstance(activity_val, dict): 389 - result["activity"] = {**_DEFAULT_ACTIVITY_CONFIG, **activity_val} 390 - else: 391 - result["activity"] = activity_val 392 - 393 - # Merge sources dict if present 394 - if "sources" in overrides and isinstance(overrides["sources"], dict): 395 - result["sources"] = {**defaults.get("sources", {}), **overrides["sources"]} 396 - 397 - return result 398 - 399 - 400 - def compose_instructions( 401 - *, 402 - user_prompt: str | None = None, 403 - user_prompt_dir: Path | None = None, 404 - facet: str | None = None, 405 - analysis_day: str | None = None, 406 - config_overrides: dict | None = None, 407 - ) -> dict: 408 - """Compose instruction components for agents or generators. 409 - 410 - This is the shared function for building system_instruction, user_instruction, 411 - extra_context, and sources configuration. Both agents and generators use this 412 - to ensure consistent prompt composition. 413 - 414 - Parameters 415 - ---------- 416 - user_prompt: 417 - Name of the user instruction prompt to load (e.g., "unified" for agents). 418 - If None, no user_instruction is included (typical for generators). 419 - user_prompt_dir: 420 - Directory to load user_prompt from. If None, uses think/ directory. 421 - facet: 422 - Optional facet name to focus on. When provided, extra_context includes 423 - only this facet's info (detail level controlled by "facets" setting). 424 - analysis_day: 425 - Optional day in YYYYMMDD format for day-based analysis. Used when 426 - instructions.day is true to include analysis day context. 427 - config_overrides: 428 - Optional dict from .json "instructions" key. Supported keys: 429 - - "system": prompt name for system instruction (default: None) 430 - - "facets": false | true (default: false) 431 - false = skip facet context 432 - true = include facet context 433 - For faceted generators, shows focused facet; for unfaceted, shows all facets. 434 - - "now": false | true (default: false) 435 - true = include current date/time in extra_context 436 - - "day": false | true (default: false) 437 - true = include analysis day context (requires analysis_day parameter) 438 - - "sources": {"transcripts": bool, "percepts": bool, "agents": bool|dict} 439 - The "agents" source can be: 440 - - bool: True (all agents), False (no agents) 441 - - "required": all agents, fail if none found 442 - - dict: selective filtering, e.g., {"entities": true, "meetings": "required"} 443 - 444 - Returns 445 - ------- 446 - dict 447 - Composed instruction configuration: 448 - - system_instruction: str - loaded from "system" prompt 449 - - system_prompt_name: str - name of system prompt (for cache keys) 450 - - user_instruction: str | None - loaded from user_prompt if provided 451 - - extra_context: str | None - facets + now + day context 452 - - sources: dict - {"transcripts": bool, "percepts": bool, "agents": bool|dict} 453 - """ 454 - from think.utils import format_day 455 - 456 - # Merge defaults with overrides 457 - cfg = _merge_instructions_config(_DEFAULT_INSTRUCTIONS, config_overrides) 458 - 459 - result: dict = {} 460 - 461 - # Load system instruction (None means no system prompt) 462 - system_name = cfg.get("system") 463 - if system_name: 464 - system_prompt = load_prompt(system_name) 465 - result["system_instruction"] = system_prompt.text 466 - result["system_prompt_name"] = system_name 467 - else: 468 - result["system_instruction"] = "" 469 - result["system_prompt_name"] = "" 470 - 471 - # Load user instruction if specified 472 - if user_prompt: 473 - base_dir = user_prompt_dir if user_prompt_dir else Path(__file__).parent 474 - user_prompt_obj = load_prompt(user_prompt, base_dir=base_dir) 475 - result["user_instruction"] = user_prompt_obj.text 476 - else: 477 - result["user_instruction"] = None 478 - 479 - # Build extra_context based on settings 480 - extra_parts = [] 481 - 482 - # Facets context 483 - facets_setting = cfg.get("facets", False) 484 - 485 - if facets_setting: 486 - if facet: 487 - # Focused facet mode: include only this facet's context 488 - try: 489 - from think.facets import facet_summary 490 - 491 - summary = facet_summary(facet) 492 - extra_parts.append(f"## Facet Focus\n{summary}") 493 - except Exception: 494 - pass # Ignore if facet can't be loaded 495 - else: 496 - # General mode: all facets 497 - try: 498 - from think.facets import facet_summaries 499 - 500 - summary = facet_summaries() 501 - if summary and summary != "No facets found.": 502 - extra_parts.append(summary) 503 - else: 504 - extra_parts.append( 505 - "No facets are defined yet. You are in discovery mode. " 506 - "Name the contexts you observe based on what is actually happening " 507 - "in this segment \u2014 use specific, descriptive names that reflect the " 508 - 'actual activity (e.g., "engineering-work" not "work", ' 509 - '"investor-calls" not "meetings"). These names will be used to ' 510 - "suggest journal organization to the user." 511 - ) 512 - except Exception: 513 - pass # Ignore if facets can't be loaded 514 - 515 - # Current date/time context (instructions.now) 516 - if cfg.get("now"): 517 - from think.prompts import format_current_datetime 518 - 519 - time_str = format_current_datetime() 520 - extra_parts.append(f"## Current Date and Time\nToday is {time_str}") 521 - 522 - # Analysis day context (instructions.day) 523 - if cfg.get("day") and analysis_day: 524 - day_friendly = format_day(analysis_day) 525 - extra_parts.append( 526 - f"## Analysis Day\nYou are analyzing data from {day_friendly} ({analysis_day})." 527 - ) 528 - 529 - result["extra_context"] = "\n\n".join(extra_parts).strip() if extra_parts else None 530 - 531 - # Include sources config 532 - result["sources"] = cfg.get("sources", _DEFAULT_INSTRUCTIONS["sources"]) 533 - 534 - return result 535 - 536 335 537 336 # --------------------------------------------------------------------------- 538 337 # Source Configuration Helpers ··· 619 418 ) -> dict: 620 419 """Return complete agent configuration by name. 621 420 622 - Loads configuration from .md file with JSON frontmatter and instruction text, 623 - merges with runtime context. 421 + Loads configuration from .md file with JSON frontmatter and instruction text. 422 + Template variables $journal and $facets are resolved during prompt loading. 423 + Source data config comes from the frontmatter 'load' key. 624 424 625 425 Parameters 626 426 ---------- ··· 628 428 Agent name to load. Can be a system agent (e.g., "unified") 629 429 or an app-namespaced agent (e.g., "support:support" for apps/support/muse/support). 630 430 facet: 631 - Optional facet name to focus on. When provided, includes detailed 632 - information for just this facet (with full entity details) instead 633 - of summaries of all facets. 431 + Optional facet name to focus on. Controls $facets template variable. 634 432 analysis_day: 635 - Optional day in YYYYMMDD format. When provided and instructions.day is 636 - true, includes analysis day context in extra_context. 433 + Optional day in YYYYMMDD format. Not used directly — day-based 434 + template context is applied in prepare_config(). 637 435 638 436 Returns 639 437 ------- ··· 641 439 Complete agent configuration including: 642 440 - name: Agent name 643 441 - path: Path to the .md file 644 - - system_instruction, user_instruction, extra_context: Composed prompts 645 - - sources: Source config from instructions (for transcript loading) 442 + - user_instruction: Composed prompt with $journal/$facets resolved 443 + - sources: Source config from 'load' key 646 444 - All frontmatter fields (tools, hook, disabled, thinking_budget, etc.) 647 445 """ 446 + from think.prompts import _resolve_facets 447 + 648 448 # Resolve agent path based on namespace 649 449 agent_dir, agent_name = _resolve_agent_path(name) 650 450 ··· 657 457 post = frontmatter.load(md_path) 658 458 config = dict(post.metadata) if post.metadata else {} 659 459 660 - # Store path for later use (e.g., load_prompt with template context) 460 + # Store path for later use 661 461 config["path"] = str(md_path) 662 462 663 - # Extract instructions config (but keep a copy for sources) 664 - instructions_config = config.get("instructions") 463 + # Extract source config from 'load' key (replaces instructions.sources) 464 + config["sources"] = config.pop("load", _DEFAULT_LOAD.copy()) 665 465 666 - # Use compose_instructions for consistent prompt composition 667 - instructions = compose_instructions( 668 - user_prompt=agent_name, 669 - user_prompt_dir=agent_dir, 670 - facet=facet, 671 - analysis_day=analysis_day, 672 - config_overrides=instructions_config, 673 - ) 466 + # Build template context for $journal and $facets resolution 467 + prompt_context: dict[str, str] = {} 468 + prompt_context["facets"] = _resolve_facets(facet) 674 469 675 - # Merge instruction results into config 676 - config["system_instruction"] = instructions["system_instruction"] 677 - config["user_instruction"] = instructions["user_instruction"] 678 - config["system_prompt_name"] = instructions.get("system_prompt_name", "journal") 679 - if instructions["extra_context"]: 680 - config["extra_context"] = instructions["extra_context"] 470 + journal_prompt = load_prompt("journal") 471 + prompt_context["journal"] = journal_prompt.text 681 472 682 - # Preserve sources config for transcript loading 683 - config["sources"] = instructions.get("sources", {}) 473 + agent_prompt = load_prompt(agent_name, base_dir=agent_dir, context=prompt_context) 474 + config["user_instruction"] = agent_prompt.text 684 475 685 476 # Set agent name 686 477 config["name"] = name
+34
think/prompts.py
··· 172 172 return now.strftime("%A, %B %d, %Y at %I:%M %p") 173 173 174 174 175 + def _resolve_facets(facet: str | None) -> str: 176 + """Resolve $facets template variable. 177 + 178 + Args: 179 + facet: Focused facet name, or None for all facets. 180 + 181 + Returns: 182 + Markdown text for facet context. 183 + """ 184 + if facet: 185 + try: 186 + from think.facets import facet_summary 187 + 188 + return f"## Facet Focus\n{facet_summary(facet)}" 189 + except Exception: 190 + return "" 191 + try: 192 + from think.facets import facet_summaries 193 + 194 + summary = facet_summaries() 195 + if summary and summary != "No facets found.": 196 + return summary 197 + return ( 198 + "No facets are defined yet. You are in discovery mode. " 199 + "Name the contexts you observe based on what is actually happening " 200 + "in this segment — use specific, descriptive names that reflect the " 201 + 'actual activity (e.g., "engineering-work" not "work", ' 202 + '"investor-calls" not "meetings"). These names will be used to ' 203 + "suggest journal organization to the user." 204 + ) 205 + except Exception: 206 + return "" 207 + 208 + 175 209 # --------------------------------------------------------------------------- 176 210 # Prompt Loading 177 211 # ---------------------------------------------------------------------------
+3 -1
think/providers/google.py
··· 86 86 raise ValueError("GOOGLE_API_KEY not found in environment") 87 87 client = genai.Client( 88 88 api_key=api_key, 89 - http_options=types.HttpOptions(retry_options=types.HttpRetryOptions(attempts=8)), 89 + http_options=types.HttpOptions( 90 + retry_options=types.HttpRetryOptions(attempts=8) 91 + ), 90 92 ) 91 93 return client 92 94
+4 -1
think/service.py
··· 26 26 import subprocess 27 27 import sys 28 28 from pathlib import Path 29 + 29 30 from think.utils import get_journal, get_journal_info 30 31 31 32 SERVICE_LABEL = "org.solpbc.solstone" ··· 90 91 return plistlib.dumps(plist) 91 92 92 93 93 - def _generate_systemd_unit(env: dict[str, str], port: int = DEFAULT_SERVICE_PORT) -> str: 94 + def _generate_systemd_unit( 95 + env: dict[str, str], port: int = DEFAULT_SERVICE_PORT 96 + ) -> str: 94 97 """Generate a systemd user unit for the solstone supervisor.""" 95 98 sol = _sol_bin() 96 99 env_lines = "\n".join(f"Environment={k}={v}" for k, v in sorted(env.items()))
+1 -3
think/tools/routines.py
··· 400 400 suggestions = meta.get("suggestions", {}) 401 401 402 402 if template not in suggestions: 403 - typer.echo( 404 - f"Error: no suggestion state for template '{template}'.", err=True 405 - ) 403 + typer.echo(f"Error: no suggestion state for template '{template}'.", err=True) 406 404 raise typer.Exit(code=1) 407 405 408 406 from datetime import date